diff --git a/src/aosm/HISTORY.rst b/src/aosm/HISTORY.rst index c87db19e277..903bb1b9a37 100644 --- a/src/aosm/HISTORY.rst +++ b/src/aosm/HISTORY.rst @@ -4,7 +4,19 @@ Release History =============== Unreleased + +1.0.0b8 +++++++++ + +1.0.0b7 +++++++++ +* Fixed: customLocation missing from Nexus +* Fixed: helm charts not uploading correctly + +++++++++ +1.0.0b6 ++++++++ +* Added Nexus support 1.0.0b5 ++++++++ diff --git a/src/aosm/azext_aosm/_params.py b/src/aosm/azext_aosm/_params.py index 5a445c40aac..b36b014ee02 100644 --- a/src/aosm/azext_aosm/_params.py +++ b/src/aosm/azext_aosm/_params.py @@ -7,12 +7,13 @@ from azure.cli.core import AzCommandsLoader from .common.constants import ( - ARTIFACT_UPLOAD, - BICEP_PUBLISH, CNF, + VNF, + VNF_NEXUS, + BICEP_PUBLISH, + ARTIFACT_UPLOAD, HELM_TEMPLATE, IMAGE_UPLOAD, - VNF, ) @@ -23,7 +24,7 @@ def load_arguments(self: AzCommandsLoader, _): get_three_state_flag, ) - definition_type = get_enum_type([VNF, CNF]) + definition_type = get_enum_type([VNF, CNF, VNF_NEXUS]) nf_skip_steps = get_enum_type( [BICEP_PUBLISH, ARTIFACT_UPLOAD, IMAGE_UPLOAD, HELM_TEMPLATE] ) @@ -36,7 +37,7 @@ def load_arguments(self: AzCommandsLoader, _): c.argument( "definition_type", arg_type=definition_type, - help="Type of AOSM definition.", + help="Type of AOSM definition to be published.", required=True, ) c.argument( diff --git a/src/aosm/azext_aosm/build_processors/arm_processor.py b/src/aosm/azext_aosm/build_processors/arm_processor.py index 66e9145307a..9d5fa569a1c 100644 --- a/src/aosm/azext_aosm/build_processors/arm_processor.py +++ b/src/aosm/azext_aosm/build_processors/arm_processor.py @@ -4,34 +4,41 @@ # -------------------------------------------------------------------------------------------- import json +from pathlib import Path from abc import abstractmethod from typing import List, Tuple, final from knack.log import get_logger from azext_aosm.build_processors.base_processor import BaseInputProcessor -from azext_aosm.common.artifact import BaseArtifact, LocalFileACRArtifact +from azext_aosm.common.artifact import (BaseArtifact, LocalFileACRArtifact) from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder from azext_aosm.inputs.arm_template_input import ArmTemplateInput from azext_aosm.vendored_sdks.models import ( - ApplicationEnablement, - ArmResourceDefinitionResourceElementTemplate, + ArtifactType, + AzureOperatorNexusNetworkFunctionArmTemplateApplication, + ApplicationEnablement, ArmResourceDefinitionResourceElementTemplate, ArmResourceDefinitionResourceElementTemplateDetails, ArmTemplateArtifactProfile, ArmTemplateMappingRuleProfile, AzureCoreArmTemplateArtifactProfile, + NetworkFunctionApplication, NSDArtifactProfile, + ResourceElementTemplate, TemplateType, AzureOperatorNexusArtifactType, + AzureOperatorNexusArmTemplateDeployMappingRuleProfile, AzureOperatorNexusArmTemplateArtifactProfile, AzureCoreArmTemplateDeployMappingRuleProfile, AzureCoreArtifactType, AzureCoreNetworkFunctionArmTemplateApplication, DependsOnProfile, ManifestArtifactFormat, - NetworkFunctionApplication, - NSDArtifactProfile, ReferencedResource, - ResourceElementTemplate, - TemplateType, ) +from azext_aosm.common.constants import ( + VNF_OUTPUT_FOLDER_FILENAME, + NF_DEFINITION_FOLDER_NAME, + TEMPLATE_PARAMETERS_FILENAME) + + logger = get_logger(__name__) @@ -71,7 +78,7 @@ def get_artifact_manifest_list(self) -> List[ManifestArtifactFormat]: return [ ManifestArtifactFormat( artifact_name=self.input_artifact.artifact_name, - artifact_type=AzureCoreArtifactType.ARM_TEMPLATE.value, + artifact_type=ArtifactType.ARM_TEMPLATE.value, artifact_version=self.input_artifact.artifact_version, ) ] @@ -93,7 +100,7 @@ def get_artifact_details( [ LocalFileACRArtifact( artifact_name=self.input_artifact.artifact_name, - artifact_type=AzureCoreArtifactType.ARM_TEMPLATE.value, + artifact_type=ArtifactType.ARM_TEMPLATE.value, artifact_version=self.input_artifact.artifact_version, file_path=self.input_artifact.template_path, ) @@ -105,22 +112,15 @@ def get_artifact_details( def generate_nf_application(self) -> NetworkFunctionApplication: return self.generate_nfvi_specific_nf_application() - def generate_artifact_profile(self) -> AzureCoreArmTemplateArtifactProfile: - artifact_profile = ArmTemplateArtifactProfile( - template_name=self.input_artifact.artifact_name, - template_version=self.input_artifact.artifact_version, - ) - return AzureCoreArmTemplateArtifactProfile( - artifact_store=ReferencedResource(id=""), - template_artifact_profile=artifact_profile, - ) - @abstractmethod def generate_nfvi_specific_nf_application(self): pass def generate_resource_element_template(self) -> ResourceElementTemplate: - """Generate the resource element template.""" + """Generate the resource element template. + + Note: There is no Nexus specific RET + """ parameter_values = self.generate_values_mappings( self.input_artifact.get_schema(), self.input_artifact.get_defaults(), True ) @@ -143,11 +143,31 @@ def generate_resource_element_template(self) -> ResourceElementTemplate: ), ) + def generate_parameters_file(self) -> LocalFileBuilder: + """Generate parameters file.""" + mapping_rule_profile = self._generate_mapping_rule_profile() + params = ( + mapping_rule_profile.template_mapping_rule_profile.template_parameters + ) + logger.info( + "Created parameters file for arm template." + ) + return LocalFileBuilder( + Path( + VNF_OUTPUT_FOLDER_FILENAME, + NF_DEFINITION_FOLDER_NAME, + self.input_artifact.artifact_name + '-' + TEMPLATE_PARAMETERS_FILENAME, + ), + json.dumps(json.loads(params), indent=4), + ) + + @abstractmethod + def _generate_mapping_rule_profile(self): + pass + class AzureCoreArmBuildProcessor(BaseArmBuildProcessor): - """ - This class represents an ARM template processor for Azure Core. - """ + """This class represents an ARM template processor for Azure Core.""" def generate_nfvi_specific_nf_application( self, @@ -158,7 +178,7 @@ def generate_nfvi_specific_nf_application( install_depends_on=[], uninstall_depends_on=[], update_depends_on=[] ), artifact_type=AzureCoreArtifactType.ARM_TEMPLATE, - artifact_profile=self.generate_artifact_profile(), + artifact_profile=self._generate_artifact_profile(), deploy_parameters_mapping_rule_profile=self._generate_mapping_rule_profile(), ) @@ -178,11 +198,55 @@ def _generate_mapping_rule_profile( template_mapping_rule_profile=mapping_profile, ) + def _generate_artifact_profile(self) -> AzureCoreArmTemplateArtifactProfile: + artifact_profile = ArmTemplateArtifactProfile( + template_name=self.input_artifact.artifact_name, + template_version=self.input_artifact.artifact_version, + ) + return AzureCoreArmTemplateArtifactProfile( + artifact_store=ReferencedResource(id=""), + template_artifact_profile=artifact_profile, + ) + class NexusArmBuildProcessor(BaseArmBuildProcessor): """ - Not implemented yet. This class represents a processor for generating ARM templates specific to Nexus. + This class represents a processor for generating ARM templates specific to Nexus. """ + def generate_nfvi_specific_nf_application( + self, + ) -> AzureOperatorNexusNetworkFunctionArmTemplateApplication: + return AzureOperatorNexusNetworkFunctionArmTemplateApplication( + name=self.name, + depends_on_profile=DependsOnProfile(install_depends_on=[], + uninstall_depends_on=[], update_depends_on=[]), + artifact_type=AzureOperatorNexusArtifactType.ARM_TEMPLATE, + artifact_profile=self._generate_artifact_profile(), + deploy_parameters_mapping_rule_profile=self._generate_mapping_rule_profile(), + ) - def generate_nfvi_specific_nf_application(self): - return NotImplementedError + def _generate_mapping_rule_profile( + self, + ) -> AzureOperatorNexusArmTemplateDeployMappingRuleProfile: + template_parameters = self.generate_values_mappings( + self.input_artifact.get_schema(), self.input_artifact.get_defaults() + ) + + mapping_profile = ArmTemplateMappingRuleProfile( + template_parameters=json.dumps(template_parameters) + ) + + return AzureOperatorNexusArmTemplateDeployMappingRuleProfile( + application_enablement=ApplicationEnablement.ENABLED, + template_mapping_rule_profile=mapping_profile, + ) + + def _generate_artifact_profile(self) -> AzureOperatorNexusArmTemplateArtifactProfile: + artifact_profile = ArmTemplateArtifactProfile( + template_name=self.input_artifact.artifact_name, + template_version=self.input_artifact.artifact_version, + ) + return AzureOperatorNexusArmTemplateArtifactProfile( + artifact_store=ReferencedResource(id=""), + template_artifact_profile=artifact_profile, + ) diff --git a/src/aosm/azext_aosm/build_processors/base_processor.py b/src/aosm/azext_aosm/build_processors/base_processor.py index 02076b56e1a..82de0a04a8e 100644 --- a/src/aosm/azext_aosm/build_processors/base_processor.py +++ b/src/aosm/azext_aosm/build_processors/base_processor.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Tuple from knack.log import get_logger - +from azure.cli.core.azclierror import InvalidArgumentValueError from azext_aosm.common.artifact import BaseArtifact from azext_aosm.common.constants import CGS_NAME from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder @@ -98,8 +98,7 @@ def generate_params_schema(self) -> Dict[str, Any]: ) params_schema = json.loads(base_params_schema) - # print(json.dumps(self.input_artifact.get_schema(), indent=4)) - # print(json.dumps(self.input_artifact.get_defaults(), indent=4)) + self._generate_schema( params_schema[self.name], self.input_artifact.get_schema(), @@ -185,6 +184,20 @@ def generate_values_mappings( # Loop through each property in the schema. for subschema_name, subschema in schema["properties"].items(): + + if "type" not in subschema: + if ["oneOf", "anyOf"] in subschema: + raise InvalidArgumentValueError( + f"The subschema '{subschema_name}' does not contain a type.\n" + "It contains 'anyOf' or 'oneOf' logic, which is not valid for AOSM.\n" + "Please remove this from your values.schema.json and provide a concrete type " + "or remove the schema and the CLI will generate a generic schema." + ) + raise InvalidArgumentValueError( + f"The subschema {subschema_name} does not contain a type. This is a required field.\n" + "Please fix your values.schema.json or remove the schema and the CLI will generate a " + "generic schema." + ) # If the property is not in the values, and is required, add it to the values. if ( "required" in schema diff --git a/src/aosm/azext_aosm/build_processors/nexus_image_processor.py b/src/aosm/azext_aosm/build_processors/nexus_image_processor.py new file mode 100644 index 00000000000..83406289188 --- /dev/null +++ b/src/aosm/azext_aosm/build_processors/nexus_image_processor.py @@ -0,0 +1,180 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +from pathlib import Path +from typing import List, Tuple + +from knack.log import get_logger + +from azext_aosm.build_processors.base_processor import BaseInputProcessor +from azext_aosm.common.artifact import (BaseArtifact, + RemoteACRArtifact) +from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder +from azext_aosm.inputs.nexus_image_input import NexusImageFileInput +from azext_aosm.vendored_sdks.models import ( + AzureOperatorNexusNetworkFunctionImageApplication, + AzureOperatorNexusImageArtifactProfile, + AzureOperatorNexusImageDeployMappingRuleProfile, + ImageArtifactProfile, ImageMappingRuleProfile, + ApplicationEnablement, ArtifactType, + DependsOnProfile, + ManifestArtifactFormat, ReferencedResource, ResourceElementTemplate, +) +from azext_aosm.common.constants import ( + VNF_OUTPUT_FOLDER_FILENAME, + NF_DEFINITION_FOLDER_NAME, + NEXUS_IMAGE_PARAMETERS_FILENAME) + +logger = get_logger(__name__) + + +class NexusImageProcessor(BaseInputProcessor): + """ + A class for processing Nexus image inputs. + + :param name: The name of the artifact. + :type name: str + :param input_artifact: The input artifact. + :type input_artifact: NexusImageFileInput + """ + + def __init__(self, name: str, input_artifact: NexusImageFileInput): + super().__init__(name, input_artifact) + self.input_artifact: NexusImageFileInput = input_artifact + + def get_artifact_manifest_list(self) -> List[ManifestArtifactFormat]: + """ + Get the list of artifacts for the artifact manifest. + + :return: A list of artifacts for the artifact manifest. + :rtype: List[ManifestArtifactFormat] + """ + logger.info("Getting artifact manifest list for Nexus image input.") + return [ + ManifestArtifactFormat( + artifact_name=self.input_artifact.artifact_name, + artifact_type=ArtifactType.IMAGE_FILE.value, + artifact_version=self.input_artifact.artifact_version, + ) + ] + + def get_artifact_details( + self, + ) -> Tuple[List[BaseArtifact], List[LocalFileBuilder]]: + """ + Get the artifact details for publishing. + + :return: A tuple containing the list of artifacts and the list of local file builders. + :rtype: Tuple[List[BaseArtifact], List[LocalFileBuilder]] + """ + logger.info("Getting artifact details for Nexus image input.") + artifacts: List[BaseArtifact] = [] + file_builders: List[LocalFileBuilder] = [] + + # We only support remote ACR artifacts for container images + artifacts.append( + RemoteACRArtifact( + artifact_name=self.input_artifact.artifact_name, + artifact_type=ArtifactType.IMAGE_FILE.value, + artifact_version=self.input_artifact.artifact_version, + source_registry=self.input_artifact.source_acr_registry, + source_registry_namespace="", + ) + ) + return artifacts, file_builders + + def generate_nf_application(self) -> AzureOperatorNexusNetworkFunctionImageApplication: + """ + Generate the NF application. + + :return: The NF application. + :rtype: AzureOperatorNexusNetworkFunctionImageApplication + """ + logger.info("Generating NF application for Nexus image input.") + + return AzureOperatorNexusNetworkFunctionImageApplication( + name=self.name, + depends_on_profile=DependsOnProfile(install_depends_on=[], + uninstall_depends_on=[], update_depends_on=[]), + artifact_profile=self._generate_artifact_profile(), + deploy_parameters_mapping_rule_profile=self._generate_mapping_rule_profile(), + ) + + def generate_resource_element_template(self) -> ResourceElementTemplate: + """ + Generate the resource element template. + + :raises NotImplementedError: NSDs do not support deployment of Nexus images. + """ + raise NotImplementedError("NSDs do not support deployment of Nexus images directly, " + "they must be provided in the NF.") + + def _generate_artifact_profile(self) -> AzureOperatorNexusImageArtifactProfile: + """ + Generate the artifact profile. + + :return: The artifact profile. + :rtype: AzureOperatorNexusImageArtifactProfile + """ + logger.debug("Generating artifact profile for Nexus image input.") + artifact_profile = ImageArtifactProfile( + image_name=self.input_artifact.artifact_name, + image_version=self.input_artifact.artifact_version, + ) + + return AzureOperatorNexusImageArtifactProfile( + artifact_store=ReferencedResource(id=""), + image_artifact_profile=artifact_profile, + ) + + def _generate_mapping_rule_profile( + self, + ) -> AzureOperatorNexusImageDeployMappingRuleProfile: + """ + Generate the mapping rule profile. + + :return: The mapping rule profile. + :rtype: AzureOperatorNexusImageDeployMappingRuleProfile + """ + logger.debug("Generating mapping rule profile for Nexus image input.") + user_configuration = self.generate_values_mappings( + self.input_artifact.get_schema(), self.input_artifact.get_defaults() + ) + + mapping = ImageMappingRuleProfile( + user_configuration=json.dumps(user_configuration), + ) + + return AzureOperatorNexusImageDeployMappingRuleProfile( + application_enablement=ApplicationEnablement.ENABLED, + image_mapping_rule_profile=mapping, + ) + + def generate_parameters_file(self) -> LocalFileBuilder: + """ Generate parameters file. """ + mapping_rule_profile = self._generate_mapping_rule_profile() + if ( + mapping_rule_profile.image_mapping_rule_profile + and mapping_rule_profile.image_mapping_rule_profile.user_configuration + ): + params = ( + mapping_rule_profile.image_mapping_rule_profile.user_configuration + ) + logger.info( + "Created parameters file for Nexus image." + ) + # We still want to create an empty params file, + # otherwise the nf definition bicep will refer to files that don't exist + else: + params = '{}' + return LocalFileBuilder( + Path( + VNF_OUTPUT_FOLDER_FILENAME, + NF_DEFINITION_FOLDER_NAME, + self.input_artifact.artifact_name + '-' + NEXUS_IMAGE_PARAMETERS_FILENAME, + ), + json.dumps(json.loads(params), indent=4), + ) diff --git a/src/aosm/azext_aosm/build_processors/nfd_processor.py b/src/aosm/azext_aosm/build_processors/nfd_processor.py index 23f9311dcbc..37bb56f5493 100644 --- a/src/aosm/azext_aosm/build_processors/nfd_processor.py +++ b/src/aosm/azext_aosm/build_processors/nfd_processor.py @@ -8,38 +8,22 @@ from typing import Any, Dict, List, Tuple from knack.log import get_logger - +from azure.cli.core.azclierror import ResourceNotFoundError from azext_aosm.build_processors.base_processor import BaseInputProcessor -from azext_aosm.common.artifact import BaseArtifact, LocalFileACRArtifact -from azext_aosm.common.constants import NSD_OUTPUT_FOLDER_FILENAME +from azext_aosm.common.artifact import (BaseArtifact, LocalFileACRArtifact) from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder from azext_aosm.inputs.nfd_input import NFDInput from azext_aosm.vendored_sdks.models import ( - ArmResourceDefinitionResourceElementTemplate, - ArtifactType, - DependsOnProfile, - ManifestArtifactFormat, - NetworkFunctionApplication, -) -from azext_aosm.vendored_sdks.models import ( - NetworkFunctionDefinitionResourceElementTemplateDetails as NFDResourceElementTemplate, -) -from azext_aosm.vendored_sdks.models import ( - NSDArtifactProfile, - ReferencedResource, - TemplateType, -) - + ArmResourceDefinitionResourceElementTemplate, ArtifactType, + DependsOnProfile, ManifestArtifactFormat, NetworkFunctionApplication, + NetworkFunctionDefinitionResourceElementTemplateDetails as + NFDResourceElementTemplate, NSDArtifactProfile, + ReferencedResource, TemplateType, ContainerizedNetworkFunctionDefinitionVersion, + VirtualNetworkFunctionDefinitionVersion) +from azext_aosm.common.constants import NSD_OUTPUT_FOLDER_FILENAME, NSD_NF_TEMPLATE_FILENAME, NSD_TEMPLATE_FOLDER_NAME +from azext_aosm.common.utils import render_bicep_contents_from_j2, get_template_path logger = get_logger(__name__) -NF_BICEP_TEMPLATE_PATH = ( - Path(__file__).parent.parent / "common" / "templates" / "nf_template.bicep" -) - -NF_BICEP_TEMPLATE_PATH = ( - Path(__file__).parent.parent / "common" / "templates" / "nf_template.bicep" -) - class NFDProcessor(BaseInputProcessor): """ @@ -85,17 +69,44 @@ def get_artifact_details( # Path is relative to NSD_OUTPUT_FOLDER_FILENAME as this artifact is stored in the NSD output folder artifact_details = LocalFileACRArtifact( artifact_name=self.input_artifact.artifact_name, - artifact_type=ArtifactType.OCI_ARTIFACT.value, + artifact_type=ArtifactType.ARM_TEMPLATE.value, artifact_version=self.input_artifact.artifact_version, file_path=self.input_artifact.arm_template_output_path.relative_to( Path(NSD_OUTPUT_FOLDER_FILENAME) ), ) + template_path = get_template_path(NSD_TEMPLATE_FOLDER_NAME, NSD_NF_TEMPLATE_FILENAME) + + # This horrendous if statement is required because: + # - the 'properties' and 'network_function_template' attributes are optional + # - the isinstance check is because the base NetworkFunctionDefinitionVersionPropertiesFormat class + # doesn't define the network_function_template attribute, even though both subclasses do. + # Not switching to EAFP style because mypy doesn't account for `except AttributeError` (for good reason). + # Similar test required in the NFD input, but we can't deduplicate the code because mypy doesn't + # propagate type narrowing from isinstance(). + if ( + self.input_artifact.network_function_definition.properties + and isinstance( + self.input_artifact.network_function_definition.properties, + ( + ContainerizedNetworkFunctionDefinitionVersion, + VirtualNetworkFunctionDefinitionVersion, + ), + ) + and self.input_artifact.network_function_definition.properties.network_function_template + ): + params = { + "nfvi_type": + self.input_artifact.network_function_definition.properties.network_function_template.nfvi_type + } + else: + raise ResourceNotFoundError("The NFDV provided has no nfvi type.") + bicep_contents = render_bicep_contents_from_j2(template_path, params) # Create a local file builder for the ARM template file_builder = LocalFileBuilder( self.input_artifact.arm_template_output_path, - NF_BICEP_TEMPLATE_PATH.read_text(), + bicep_contents, ) return [artifact_details], [file_builder] diff --git a/src/aosm/azext_aosm/build_processors/vhd_processor.py b/src/aosm/azext_aosm/build_processors/vhd_processor.py index 6e636416488..9458b585a67 100644 --- a/src/aosm/azext_aosm/build_processors/vhd_processor.py +++ b/src/aosm/azext_aosm/build_processors/vhd_processor.py @@ -2,7 +2,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- - +from pathlib import Path import json from typing import List, Tuple @@ -12,7 +12,7 @@ from azext_aosm.common.artifact import ( BaseArtifact, BlobStorageAccountArtifact, - LocalFileStorageAccountArtifact, + LocalFileStorageAccountArtifact ) from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder from azext_aosm.inputs.vhd_file_input import VHDFileInput @@ -29,6 +29,10 @@ VhdImageArtifactProfile, VhdImageMappingRuleProfile, ) +from azext_aosm.common.constants import ( + VNF_OUTPUT_FOLDER_FILENAME, + NF_DEFINITION_FOLDER_NAME, + VHD_PARAMETERS_FILENAME) logger = get_logger(__name__) @@ -132,7 +136,8 @@ def generate_resource_element_template(self) -> ResourceElementTemplate: :raises NotImplementedError: NSDs do not support deployment of VHDs. """ - raise NotImplementedError("NSDs do not support deployment of VHDs.") + raise NotImplementedError("NSDs do not support deployment of VHDs directly, " + "they must be provided in the NF.") def _generate_artifact_profile(self) -> AzureCoreVhdImageArtifactProfile: """ @@ -174,3 +179,27 @@ def _generate_mapping_rule_profile( application_enablement=ApplicationEnablement.ENABLED, vhd_image_mapping_rule_profile=mapping, ) + + def generate_parameters_file(self) -> LocalFileBuilder: + """ Generate parameters file. """ + mapping_rule_profile = self._generate_mapping_rule_profile() + if (mapping_rule_profile.vhd_image_mapping_rule_profile + and mapping_rule_profile.vhd_image_mapping_rule_profile.user_configuration): + params = ( + mapping_rule_profile.vhd_image_mapping_rule_profile.user_configuration + ) + # We still want to create an empty params file, + # otherwise the nf definition bicep will refer to files that don't exist + else: + params = '{}' + logger.info( + "Created parameters file for Nexus image." + ) + return LocalFileBuilder( + Path( + VNF_OUTPUT_FOLDER_FILENAME, + NF_DEFINITION_FOLDER_NAME, + VHD_PARAMETERS_FILENAME, + ), + json.dumps(json.loads(params), indent=4), + ) diff --git a/src/aosm/azext_aosm/cli_handlers/onboarding_base_handler.py b/src/aosm/azext_aosm/cli_handlers/onboarding_base_handler.py index 3e9fcb3f9e1..0ebb3db8f3d 100644 --- a/src/aosm/azext_aosm/cli_handlers/onboarding_base_handler.py +++ b/src/aosm/azext_aosm/cli_handlers/onboarding_base_handler.py @@ -8,9 +8,9 @@ from abc import ABC, abstractmethod from dataclasses import fields, is_dataclass from pathlib import Path -from typing import Optional, Union - -from azure.cli.core.azclierror import UnclassifiedUserFault +from typing import Optional +from json.decoder import JSONDecodeError +from azure.cli.core.azclierror import InvalidArgumentValueError, UnclassifiedUserFault from jinja2 import StrictUndefined, Template from knack.log import get_logger @@ -35,34 +35,40 @@ class OnboardingBaseCLIHandler(ABC): """Abstract base class for CLI handlers.""" + config: OnboardingBaseInputConfig | BaseCommonParametersConfig + def __init__( self, - provided_input_path: Optional[Path] = None, + config_file_path: Optional[Path] = None, + all_deploy_params_file_path: Optional[Path] = None, aosm_client: Optional[HybridNetworkManagementClient] = None, skip: Optional[str] = None, ): """Initialize the CLI handler.""" self.aosm_client = aosm_client self.skip = skip - # If config file provided (for build, publish and delete) - if provided_input_path: - # Explicitly define types - self.config: Union[OnboardingBaseInputConfig, BaseCommonParametersConfig] - provided_input_path = Path(provided_input_path) - # If config file is the input.jsonc for build command - if provided_input_path.suffix == ".jsonc": - config_dict = self._read_input_config_from_file(provided_input_path) + # If input.jsonc file provided (therefore if build command run) + if config_file_path: + config_dict = self._read_input_config_from_file(config_file_path) + try: self.config = self._get_input_config(config_dict) - # Validate config before getting processor list, - # in case error with input artifacts i.e helm package - self.config.validate() - self.processors = self._get_processor_list() - # If config file is the all parameters json file for publish/delete - elif provided_input_path.suffix == ".json": - self.config = self._get_params_config(provided_input_path) - else: - raise UnclassifiedUserFault("Invalid input") - # TODO: Change this to work with publish? + except TypeError as e: + raise InvalidArgumentValueError( + "The input file provided contains an incorrect input.\n" + f"Please fix the problem parameter:\n{e}") from e + # Validate config before getting processor list, + # in case error with input artifacts i.e helm package + self.config.validate() + self.processors = self._get_processor_list() + # If all_deploy.parameters.json file provided (therefore if publish/delete command run) + elif all_deploy_params_file_path: + try: + self.config = self._get_params_config(all_deploy_params_file_path) + except TypeError as e: + raise InvalidArgumentValueError( + "The all_deploy.parameters.json in the folder " + "provided contains an incorrect input.\nPlease check if you have provided " + f"the correct folder for the definition/design type:\n{e}") from e # If no config file provided (for generate-config) else: self.config = self._get_input_config() @@ -175,11 +181,16 @@ def _read_input_config_from_file(input_json_path: Path) -> dict: Returns config as dictionary. """ - lines = input_json_path.read_text().splitlines() - lines = [line for line in lines if not line.strip().startswith("//")] - config_dict = json.loads("".join(lines)) - - return config_dict + try: + lines = input_json_path.read_text().splitlines() + lines = [line for line in lines if not line.strip().startswith("//")] + config_dict = json.loads("".join(lines)) + return config_dict + except FileNotFoundError as e: + raise UnclassifiedUserFault(f"Invalid config file provided.\nError: {e} ") from e + except JSONDecodeError as e: + raise UnclassifiedUserFault("Invalid JSON found in the config file provided.\n" + f"Error: {e} ") from e @staticmethod def _render_base_bicep_contents(template_path): @@ -193,50 +204,6 @@ def _render_base_bicep_contents(template_path): bicep_contents: str = template.render() return bicep_contents - @staticmethod - def _render_definition_bicep_contents(template_path: Path, params): - """Write the definition bicep file from given template.""" - with open(template_path, "r", encoding="UTF-8") as f: - template: Template = Template( - f.read(), - undefined=StrictUndefined, - ) - - bicep_contents: str = template.render(params) - return bicep_contents - - @staticmethod - def _render_manifest_bicep_contents( - template_path: Path, - acr_artifact_list: list, - sa_artifact_list: Optional[list] = None, - ): - """Write the manifest bicep file from given template. - - Returns bicep content as string - """ - with open(template_path, "r", encoding="UTF-8") as f: - template: Template = Template( - f.read(), - undefined=StrictUndefined, - ) - - bicep_contents: str = template.render( - acr_artifacts=acr_artifact_list, sa_artifacts=sa_artifact_list - ) - return bicep_contents - - @staticmethod - def _get_template_path(definition_type: str, template_name: str) -> Path: - """Get the path to a template.""" - return ( - Path(__file__).parent.parent - / "common" - / "templates" - / definition_type - / template_name - ) - def _serialize(self, dataclass, indent_count=1): """ Convert a dataclass instance to a JSONC string. diff --git a/src/aosm/azext_aosm/cli_handlers/onboarding_cnf_handler.py b/src/aosm/azext_aosm/cli_handlers/onboarding_cnf_handler.py index 793e1ecc2c2..1b0ea8a487c 100644 --- a/src/aosm/azext_aosm/cli_handlers/onboarding_cnf_handler.py +++ b/src/aosm/azext_aosm/cli_handlers/onboarding_cnf_handler.py @@ -51,6 +51,7 @@ from azext_aosm.inputs.helm_chart_input import HelmChartInput from .onboarding_nfd_base_handler import OnboardingNFDBaseCLIHandler +from azext_aosm.common.utils import render_bicep_contents_from_j2, get_template_path logger = get_logger(__name__) yaml_processor = ruamel.yaml.YAML(typ="safe", pure=True) @@ -60,6 +61,8 @@ class OnboardingCNFCLIHandler(OnboardingNFDBaseCLIHandler): """CLI handler for publishing NFDs.""" + config: OnboardingCNFInputConfig + @property def default_config_file_name(self) -> str: """Get the default configuration file name.""" @@ -129,7 +132,7 @@ def _validate_helm_template(self): if validation_errors: # Create an error file using a j2 template - error_output_template_path = self._get_template_path( + error_output_template_path = get_template_path( CNF_TEMPLATE_FOLDER_NAME, CNF_HELM_VALIDATION_ERRORS_TEMPLATE_FILENAME ) @@ -167,20 +170,20 @@ def pre_validate_build(self): if self.skip != HELM_TEMPLATE: self._validate_helm_template() - def build_base_bicep(self): + def build_base_bicep(self) -> BicepDefinitionElementBuilder: """Build the base bicep file.""" # Build manifest bicep contents, with j2 template - template_path = self._get_template_path( + template_path = get_template_path( CNF_TEMPLATE_FOLDER_NAME, CNF_BASE_TEMPLATE_FILENAME ) - bicep_contents = self._render_base_bicep_contents(template_path) + bicep_contents = render_bicep_contents_from_j2(template_path, {}) # Create Bicep element with base contents bicep_file = BicepDefinitionElementBuilder( Path(CNF_OUTPUT_FOLDER_FILENAME, BASE_FOLDER_NAME), bicep_contents ) return bicep_file - def build_manifest_bicep(self): + def build_manifest_bicep(self) -> BicepDefinitionElementBuilder: """Build the manifest bicep file.""" artifact_list = [] logger.info("Creating artifact manifest bicep") @@ -196,12 +199,16 @@ def build_manifest_bicep(self): artifact_list, ) # Build manifest bicep contents, with j2 template - template_path = self._get_template_path( + template_path = get_template_path( CNF_TEMPLATE_FOLDER_NAME, CNF_MANIFEST_TEMPLATE_FILENAME ) - bicep_contents = self._render_manifest_bicep_contents( - template_path, artifact_list - ) + + params = { + "acr_artifacts": artifact_list, + "sa_artifacts": [] + } + bicep_contents = render_bicep_contents_from_j2(template_path, params) + # Create Bicep element with manifest contents bicep_file = BicepDefinitionElementBuilder( Path(CNF_OUTPUT_FOLDER_FILENAME, MANIFEST_FOLDER_NAME), bicep_contents @@ -209,7 +216,7 @@ def build_manifest_bicep(self): return bicep_file - def build_artifact_list(self): + def build_artifact_list(self) -> ArtifactDefinitionElementBuilder: """Build the artifact list.""" artifact_list = [] # For each helm package, get list of artifacts and combine @@ -228,7 +235,7 @@ def build_artifact_list(self): Path(CNF_OUTPUT_FOLDER_FILENAME, ARTIFACT_LIST_FILENAME), artifact_list ) - def build_resource_bicep(self): + def build_resource_bicep(self) -> BicepDefinitionElementBuilder: """Build the resource bicep file.""" logger.info("Creating artifacts list for artifacts.json") nf_application_list = [] @@ -259,7 +266,7 @@ def build_resource_bicep(self): mappings_files.append(mapping_file) # Create bicep contents using cnf defintion j2 template - template_path = self._get_template_path( + template_path = get_template_path( CNF_TEMPLATE_FOLDER_NAME, CNF_DEFINITION_TEMPLATE_FILENAME ) @@ -267,7 +274,7 @@ def build_resource_bicep(self): "acr_nf_applications": nf_application_list, "deployment_parameters_file": DEPLOYMENT_PARAMETERS_FILENAME, } - bicep_contents = self._render_definition_bicep_contents(template_path, params) + bicep_contents = render_bicep_contents_from_j2(template_path, params) # Create a bicep element + add its supporting mapping files bicep_file = BicepDefinitionElementBuilder( @@ -284,14 +291,14 @@ def build_resource_bicep(self): ) return bicep_file - def build_all_parameters_json(self): + def build_all_parameters_json(self) -> JSONDefinitionElementBuilder: """Build the all parameters json file.""" params_content = { "location": self.config.location, "publisherName": self.config.publisher_name, "publisherResourceGroupName": self.config.publisher_resource_group_name, "acrArtifactStoreName": self.config.acr_artifact_store_name, - "acrManifestName": self.config.acr_artifact_store_name + "-manifest", + "acrManifestName": self.config.acr_manifest_name, "nfDefinitionGroup": self.config.nf_name, "nfDefinitionVersion": self.config.version, } diff --git a/src/aosm/azext_aosm/cli_handlers/onboarding_core_vnf_handler.py b/src/aosm/azext_aosm/cli_handlers/onboarding_core_vnf_handler.py new file mode 100644 index 00000000000..89ab6d9ff2b --- /dev/null +++ b/src/aosm/azext_aosm/cli_handlers/onboarding_core_vnf_handler.py @@ -0,0 +1,192 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from __future__ import annotations + +import json +from pathlib import Path +from typing import Dict, Any, List, Tuple, Optional +from knack.log import get_logger + +from azext_aosm.build_processors.arm_processor import AzureCoreArmBuildProcessor +from azext_aosm.build_processors.vhd_processor import VHDProcessor +from azext_aosm.build_processors.base_processor import BaseInputProcessor +from azext_aosm.common.constants import ( + BASE_FOLDER_NAME, + VNF_CORE_BASE_TEMPLATE_FILENAME, + VNF_TEMPLATE_FOLDER_NAME, + VNF_OUTPUT_FOLDER_FILENAME, + DEPLOYMENT_PARAMETERS_FILENAME, + VHD_PARAMETERS_FILENAME, + TEMPLATE_PARAMETERS_FILENAME +) +from azext_aosm.configuration_models.onboarding_vnf_input_config import ( + OnboardingCoreVNFInputConfig, +) +from azext_aosm.configuration_models.common_parameters_config import ( + CoreVNFCommonParametersConfig, +) +from azext_aosm.definition_folder.builder.bicep_builder import ( + BicepDefinitionElementBuilder, +) +from azext_aosm.definition_folder.builder.json_builder import ( + JSONDefinitionElementBuilder, +) +from azext_aosm.inputs.arm_template_input import ArmTemplateInput +from azext_aosm.inputs.vhd_file_input import VHDFileInput +from azext_aosm.common.utils import render_bicep_contents_from_j2, get_template_path + +from .onboarding_vnf_handler import OnboardingVNFCLIHandler +logger = get_logger(__name__) + + +class OnboardingCoreVNFCLIHandler(OnboardingVNFCLIHandler): + """CLI handler for publishing NFDs.""" + + config: OnboardingCoreVNFInputConfig + + def _get_input_config( + self, input_config: Optional[dict] = None + ) -> OnboardingCoreVNFInputConfig: + """Get the configuration for the command.""" + if input_config is None: + input_config = {} + return OnboardingCoreVNFInputConfig(**input_config) + + def _get_params_config( + self, config_file: Path + ) -> CoreVNFCommonParametersConfig: + """Get the configuration for the command.""" + with open(config_file, "r", encoding="utf-8") as _file: + params_dict = json.load(_file) + if params_dict is None: + params_dict = {} + return CoreVNFCommonParametersConfig(**params_dict) + + def _get_processor_list(self) -> List[AzureCoreArmBuildProcessor | VHDProcessor]: + """Get the list of processors.""" + processor_list: List[AzureCoreArmBuildProcessor | VHDProcessor] = [] + # for each arm template, instantiate arm processor + for arm_template in self.config.arm_templates: + arm_input = ArmTemplateInput( + artifact_name=arm_template.artifact_name, + artifact_version=arm_template.version, + default_config=None, + template_path=Path(arm_template.file_path).absolute(), + ) + processor_list.append( + AzureCoreArmBuildProcessor(arm_input.artifact_name, arm_input) + ) + + # Instantiate vhd processor + if not self.config.vhd.artifact_name: + self.config.vhd.artifact_name = self.config.nf_name + "-vhd" + vhd_processor = VHDProcessor( + name=self.config.vhd.artifact_name, + input_artifact=VHDFileInput( + artifact_name=self.config.vhd.artifact_name, + artifact_version=self.config.vhd.version, + default_config=self._get_default_config(self.config.vhd), + file_path=Path(self.config.vhd.file_path).absolute(), + blob_sas_uri=self.config.vhd.blob_sas_url, + ), + ) + processor_list.append(vhd_processor) + return processor_list + + def build_base_bicep(self) -> BicepDefinitionElementBuilder: + """Build the base bicep file.""" + # Build manifest bicep contents, with j2 template + template_path = get_template_path( + VNF_TEMPLATE_FOLDER_NAME, VNF_CORE_BASE_TEMPLATE_FILENAME + ) + bicep_contents = render_bicep_contents_from_j2(template_path, {}) + # Create Bicep element with manifest contents + bicep_file = BicepDefinitionElementBuilder( + Path(VNF_OUTPUT_FOLDER_FILENAME, BASE_FOLDER_NAME), bicep_contents + ) + return bicep_file + + def build_all_parameters_json(self) -> JSONDefinitionElementBuilder: + """Create object for all_parameters.json.""" + params_content = { + "location": self.config.location, + "publisherName": self.config.publisher_name, + "publisherResourceGroupName": self.config.publisher_resource_group_name, + "acrArtifactStoreName": self.config.acr_artifact_store_name, + "saArtifactStoreName": self.config.blob_artifact_store_name, + "acrManifestName": self.config.acr_manifest_name, + "saManifestName": self.config.sa_manifest_name, + "nfDefinitionGroup": self.config.nf_name, + "nfDefinitionVersion": self.config.version + } + base_file = JSONDefinitionElementBuilder( + Path(VNF_OUTPUT_FOLDER_FILENAME), json.dumps(params_content, indent=4) + ) + return base_file + + def _get_default_config(self, vhd) -> Dict[str, Any]: + """Get default VHD config for Azure Core VNF.""" + default_config = {} + if vhd.image_disk_size_GB: + default_config.update({"image_disk_size_GB": vhd.image_disk_size_GB}) + if vhd.image_hyper_v_generation: + default_config.update( + {"image_hyper_v_generation": vhd.image_hyper_v_generation} + ) + else: + # Default to V1 if not specified + default_config.update({"image_hyper_v_generation": "V1"}) + if vhd.image_api_version: + default_config.update({"image_api_version": vhd.image_api_version}) + return default_config + + def _generate_type_specific_nf_application(self, processor) -> Tuple[List, List]: + """Generate the type specific nf application.""" + arm_nf = [] + image_nf = [] + nf_application = processor.generate_nf_application() + + if isinstance(processor, AzureCoreArmBuildProcessor): + arm_nf.append(nf_application) + elif isinstance(processor, VHDProcessor): + image_nf.append(nf_application) + else: + raise TypeError(f"Type: {type(processor)} is not valid") + logger.debug("Created nf application %s", nf_application.name) + return (arm_nf, image_nf) + + def _generate_type_specific_artifact_manifest(self, processor): + """Generate the type specific artifact manifest list.""" + arm_artifacts = [] + sa_artifacts = [] + + if isinstance(processor, AzureCoreArmBuildProcessor): + arm_artifacts = processor.get_artifact_manifest_list() + logger.debug( + "Created list of artifacts from %s arm template(s) provided: %s", + len(self.config.arm_templates), + arm_artifacts, + ) + elif isinstance(processor, VHDProcessor): + sa_artifacts = processor.get_artifact_manifest_list() + logger.debug( + "Created list of artifacts from vhd image provided: %s", + sa_artifacts, + ) + + return (arm_artifacts, sa_artifacts) + + def _get_nfd_template_params( + self, arm_nf_application_list, image_nf_application_list) -> Dict[str, Any]: + """Get the nfd template params.""" + return { + "nfvi_type": 'AzureCore', + "acr_nf_applications": arm_nf_application_list, + "sa_nf_applications": image_nf_application_list, + "nexus_image_nf_applications": [], + "deployment_parameters_file": DEPLOYMENT_PARAMETERS_FILENAME, + "vhd_parameters_file": VHD_PARAMETERS_FILENAME, + "template_parameters_file": TEMPLATE_PARAMETERS_FILENAME + } diff --git a/src/aosm/azext_aosm/cli_handlers/onboarding_nexus_vnf_handler.py b/src/aosm/azext_aosm/cli_handlers/onboarding_nexus_vnf_handler.py new file mode 100644 index 00000000000..9be8928cb87 --- /dev/null +++ b/src/aosm/azext_aosm/cli_handlers/onboarding_nexus_vnf_handler.py @@ -0,0 +1,157 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from __future__ import annotations + +import json +from pathlib import Path +from typing import Dict, Any, List, Optional +from knack.log import get_logger + +from azext_aosm.build_processors.arm_processor import NexusArmBuildProcessor +from azext_aosm.build_processors.nexus_image_processor import NexusImageProcessor +from azext_aosm.build_processors.base_processor import BaseInputProcessor +from azext_aosm.common.constants import ( + BASE_FOLDER_NAME, + VNF_TEMPLATE_FOLDER_NAME, + VNF_OUTPUT_FOLDER_FILENAME, + DEPLOYMENT_PARAMETERS_FILENAME, + NEXUS_IMAGE_PARAMETERS_FILENAME, + TEMPLATE_PARAMETERS_FILENAME, + VNF_NEXUS_BASE_TEMPLATE_FILENAME, +) +from azext_aosm.configuration_models.onboarding_vnf_input_config import ( + OnboardingNexusVNFInputConfig, +) +from azext_aosm.configuration_models.common_parameters_config import ( + NexusVNFCommonParametersConfig, +) +from azext_aosm.definition_folder.builder.bicep_builder import ( + BicepDefinitionElementBuilder, +) +from azext_aosm.definition_folder.builder.json_builder import ( + JSONDefinitionElementBuilder, +) +from azext_aosm.inputs.arm_template_input import ArmTemplateInput +from azext_aosm.inputs.nexus_image_input import NexusImageFileInput +from .onboarding_vnf_handler import OnboardingVNFCLIHandler +from azext_aosm.common.utils import render_bicep_contents_from_j2, get_template_path, split_image_path + +logger = get_logger(__name__) + + +class OnboardingNexusVNFCLIHandler(OnboardingVNFCLIHandler): + """CLI handler for publishing NFDs.""" + + config: OnboardingNexusVNFInputConfig + + def _get_input_config( + self, input_config: Optional[dict] = None + ) -> OnboardingNexusVNFInputConfig: + """Get the configuration for the command.""" + if input_config is None: + input_config = {} + return OnboardingNexusVNFInputConfig(**input_config) + + def _get_params_config( + self, config_file: Path + ) -> NexusVNFCommonParametersConfig: + """Get the configuration for the command.""" + with open(config_file, "r", encoding="utf-8") as _file: + params_dict = json.load(_file) + if params_dict is None: + params_dict = {} + return NexusVNFCommonParametersConfig(**params_dict) + + def _get_processor_list(self) -> List[NexusArmBuildProcessor | NexusImageProcessor]: + processor_list: List[NexusArmBuildProcessor | NexusImageProcessor] = [] + # for each arm template, instantiate arm processor + for arm_template in self.config.arm_templates: + arm_input = ArmTemplateInput( + artifact_name=arm_template.artifact_name, + artifact_version=arm_template.version, + default_config=None, + template_path=Path(arm_template.file_path).absolute(), + ) + processor_list.append( + NexusArmBuildProcessor(arm_input.artifact_name, arm_input) + ) + # For each image, instantiate image processor + for image in self.config.images: + (source_acr_registry, name, version) = split_image_path(image) + image_input = NexusImageFileInput( + artifact_name=name, + artifact_version=version, + default_config=None, + source_acr_registry=source_acr_registry, + ) + processor_list.append( + NexusImageProcessor(image_input.artifact_name, image_input) + ) + + return processor_list + + def build_base_bicep(self) -> BicepDefinitionElementBuilder: + """Build the base bicep file.""" + # Build manifest bicep contents, with j2 template + template_path = get_template_path( + VNF_TEMPLATE_FOLDER_NAME, VNF_NEXUS_BASE_TEMPLATE_FILENAME + ) + bicep_contents = render_bicep_contents_from_j2(template_path, {}) + # Create Bicep element with manifest contents + bicep_file = BicepDefinitionElementBuilder( + Path(VNF_OUTPUT_FOLDER_FILENAME, BASE_FOLDER_NAME), bicep_contents + ) + return bicep_file + + def build_all_parameters_json(self) -> JSONDefinitionElementBuilder: + """Build the all parameters json file.""" + params_content = { + "location": self.config.location, + "publisherName": self.config.publisher_name, + "publisherResourceGroupName": self.config.publisher_resource_group_name, + "acrArtifactStoreName": self.config.acr_artifact_store_name, + "acrManifestName": self.config.acr_manifest_name, + "nfDefinitionGroup": self.config.nf_name, + "nfDefinitionVersion": self.config.version + } + base_file = JSONDefinitionElementBuilder( + Path(VNF_OUTPUT_FOLDER_FILENAME), json.dumps(params_content, indent=4) + ) + return base_file + + def _generate_type_specific_nf_application(self, processor) -> "tuple[list, list]": + """Generate the type specific nf application.""" + arm_nf = [] + image_nf = [] + nf_application = processor.generate_nf_application() + + if isinstance(processor, NexusArmBuildProcessor): + arm_nf.append(nf_application) + elif isinstance(processor, NexusImageProcessor): + image_nf.append(nf_application) + else: + raise TypeError(f"Type: {type(processor)} is not valid") + logger.debug("Created nf application %s", nf_application.name) + return (arm_nf, image_nf) + + def _generate_type_specific_artifact_manifest(self, processor): + """Generate the type specific artifact manifest list""" + arm_manifest = processor.get_artifact_manifest_list() + sa_manifest = [] + + return (arm_manifest, sa_manifest) + + def _get_nfd_template_params( + self, arm_nf_application_list, image_nf_application_list) -> Dict[str, Any]: + """Get the nfd template params.""" + return { + "nfvi_type": 'AzureOperatorNexus', + "acr_nf_applications": arm_nf_application_list, + "sa_nf_applications": [], + "nexus_image_nf_applications": image_nf_application_list, + "deployment_parameters_file": DEPLOYMENT_PARAMETERS_FILENAME, + "template_parameters_file": TEMPLATE_PARAMETERS_FILENAME, + "image_parameters_file": NEXUS_IMAGE_PARAMETERS_FILENAME + } diff --git a/src/aosm/azext_aosm/cli_handlers/onboarding_nsd_handler.py b/src/aosm/azext_aosm/cli_handlers/onboarding_nsd_handler.py index 66322e758b2..f0b30f9fb7b 100644 --- a/src/aosm/azext_aosm/cli_handlers/onboarding_nsd_handler.py +++ b/src/aosm/azext_aosm/cli_handlers/onboarding_nsd_handler.py @@ -9,9 +9,8 @@ from typing import Optional from knack.log import get_logger - -from azext_aosm.build_processors.arm_processor import AzureCoreArmBuildProcessor from azext_aosm.build_processors.nfd_processor import NFDProcessor +from azext_aosm.build_processors.arm_processor import AzureCoreArmBuildProcessor from azext_aosm.cli_handlers.onboarding_nfd_base_handler import OnboardingBaseCLIHandler from azext_aosm.common.constants import ( # NSD_DEFINITION_TEMPLATE_FILENAME, ARTIFACT_LIST_FILENAME, @@ -47,15 +46,19 @@ from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder from azext_aosm.inputs.arm_template_input import ArmTemplateInput from azext_aosm.inputs.nfd_input import NFDInput -from azext_aosm.vendored_sdks import HybridNetworkManagementClient from azext_aosm.vendored_sdks.models import NetworkFunctionDefinitionVersion - +from azext_aosm.common.utils import render_bicep_contents_from_j2, get_template_path +from azext_aosm.vendored_sdks import HybridNetworkManagementClient +from azext_aosm.configuration_models.common_input import ArmTemplatePropertiesConfig +from azext_aosm.configuration_models.onboarding_nsd_input_config import NetworkFunctionPropertiesConfig logger = get_logger(__name__) class OnboardingNSDCLIHandler(OnboardingBaseCLIHandler): """CLI handler for publishing NFDs.""" + config: OnboardingNSDInputConfig + @property def default_config_file_name(self) -> str: """Get the default configuration file name.""" @@ -86,11 +89,12 @@ def _get_params_config(self, config_file: Path) -> NSDCommonParametersConfig: params_dict = {} return NSDCommonParametersConfig(**params_dict) - def _get_processor_list(self): - processor_list = [] + def _get_processor_list(self) -> list: + processor_list: list[AzureCoreArmBuildProcessor | NFDProcessor] = [] # for each resource element template, instantiate processor for resource_element in self.config.resource_element_templates: if resource_element.resource_element_type == "ArmTemplate": + assert isinstance(resource_element.properties, ArmTemplatePropertiesConfig) arm_input = ArmTemplateInput( artifact_name=resource_element.properties.artifact_name, artifact_version=resource_element.properties.version, @@ -104,6 +108,7 @@ def _get_processor_list(self): AzureCoreArmBuildProcessor(arm_input.artifact_name, arm_input) ) elif resource_element.resource_element_type == "NF": + assert isinstance(resource_element.properties, NetworkFunctionPropertiesConfig) # TODO: change artifact name and version to the nfd name and version or justify why it was this # in the first place # AC4 note: I couldn't find a reference in the old code, but this @@ -138,20 +143,20 @@ def _get_processor_list(self): ) return processor_list - def build_base_bicep(self): + def build_base_bicep(self) -> BicepDefinitionElementBuilder: """Build the base bicep file.""" # Build base bicep contents, with bicep template - template_path = self._get_template_path( + template_path = get_template_path( NSD_TEMPLATE_FOLDER_NAME, NSD_BASE_TEMPLATE_FILENAME ) - bicep_contents = self._render_base_bicep_contents(template_path) + bicep_contents = render_bicep_contents_from_j2(template_path, {}) # Create Bicep element with manifest contents bicep_file = BicepDefinitionElementBuilder( Path(NSD_OUTPUT_FOLDER_FILENAME, BASE_FOLDER_NAME), bicep_contents ) return bicep_file - def build_manifest_bicep(self): + def build_manifest_bicep(self) -> BicepDefinitionElementBuilder: """Build the manifest bicep file.""" artifact_list = [] for processor in self.processors: @@ -160,19 +165,21 @@ def build_manifest_bicep(self): "Created list of artifacts from resource element(s) provided: %s", artifact_list, ) - template_path = self._get_template_path( + template_path = get_template_path( NSD_TEMPLATE_FOLDER_NAME, NSD_MANIFEST_TEMPLATE_FILENAME ) - bicep_contents = self._render_manifest_bicep_contents( - template_path, artifact_list - ) + params = { + "acr_artifacts": artifact_list, + "sa_artifacts": [] + } + bicep_contents = render_bicep_contents_from_j2(template_path, params) bicep_file = BicepDefinitionElementBuilder( Path(NSD_OUTPUT_FOLDER_FILENAME, MANIFEST_FOLDER_NAME), bicep_contents ) return bicep_file - def build_artifact_list(self): + def build_artifact_list(self) -> ArtifactDefinitionElementBuilder: """Build the artifact list.""" # Build artifact list for ArmTemplates artifact_list = [] @@ -192,9 +199,8 @@ def build_artifact_list(self): return artifact_file - def build_resource_bicep(self): + def build_resource_bicep(self) -> BicepDefinitionElementBuilder: """Build the resource bicep file.""" - bicep_contents = {} schema_properties = {} nf_names = [] ret_list = [] @@ -225,12 +231,13 @@ def build_resource_bicep(self): # List of NF RET names, for adding to required part of CGS nf_names.append(processor.name) - template_path = self._get_template_path( + template_path = get_template_path( NSD_TEMPLATE_FOLDER_NAME, NSD_DEFINITION_TEMPLATE_FILENAME ) params = { "nsdv_description": self.config.nsdv_description, + "nfvi_type": self.config.nfvi_type, "cgs_name": CGS_NAME, "nfvi_site_name": self.nfvi_site_name, "nf_rets": ret_list, @@ -239,7 +246,9 @@ def build_resource_bicep(self): "template_parameters_file": TEMPLATE_PARAMETERS_FILENAME, } - bicep_contents = self._render_definition_bicep_contents(template_path, params) + bicep_contents = render_bicep_contents_from_j2( + template_path, params + ) # Generate the nsd bicep file bicep_file = BicepDefinitionElementBuilder( Path(NSD_OUTPUT_FOLDER_FILENAME, NSD_DEFINITION_FOLDER_NAME), bicep_contents @@ -256,14 +265,14 @@ def build_resource_bicep(self): return bicep_file - def build_all_parameters_json(self): - # TODO: add common params for build resource bicep + def build_all_parameters_json(self) -> JSONDefinitionElementBuilder: + """Build all parameters json.""" params_content = { "location": self.config.location, "publisherName": self.config.publisher_name, "publisherResourceGroupName": self.config.publisher_resource_group_name, "acrArtifactStoreName": self.config.acr_artifact_store_name, - "acrManifestName": self.config.acr_artifact_store_name + "-manifest", + "acrManifestName": self.config.acr_manifest_name, "nsDesignGroup": self.config.nsd_name, "nsDesignVersion": self.config.nsd_version, "nfviSiteName": self.nfvi_site_name, diff --git a/src/aosm/azext_aosm/cli_handlers/onboarding_vnf_handler.py b/src/aosm/azext_aosm/cli_handlers/onboarding_vnf_handler.py index 9dfd04aed68..5ed33c14336 100644 --- a/src/aosm/azext_aosm/cli_handlers/onboarding_vnf_handler.py +++ b/src/aosm/azext_aosm/cli_handlers/onboarding_vnf_handler.py @@ -2,52 +2,33 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import json +from __future__ import annotations + from pathlib import Path -from typing import Any, Dict, Optional +from abc import abstractmethod +from .onboarding_nfd_base_handler import OnboardingNFDBaseCLIHandler from knack.log import get_logger - -from azext_aosm.build_processors.arm_processor import ( - AzureCoreArmBuildProcessor, - BaseArmBuildProcessor, +# from azext_aosm.configuration_models.onboarding_vnf_input_config import ( +# OnboardingBaseVNFInputConfig, +# ) +from azext_aosm.common.utils import render_bicep_contents_from_j2, get_template_path +from azext_aosm.configuration_models.onboarding_vnf_input_config import (OnboardingCoreVNFInputConfig, OnboardingNexusVNFInputConfig) +from azext_aosm.definition_folder.builder.bicep_builder import ( + BicepDefinitionElementBuilder, ) -from azext_aosm.build_processors.vhd_processor import VHDProcessor +from azext_aosm.definition_folder.builder.artifact_builder import ArtifactDefinitionElementBuilder + from azext_aosm.common.constants import ( ARTIFACT_LIST_FILENAME, - BASE_FOLDER_NAME, - DEPLOYMENT_PARAMETERS_FILENAME, - MANIFEST_FOLDER_NAME, NF_DEFINITION_FOLDER_NAME, - TEMPLATE_PARAMETERS_FILENAME, - VHD_PARAMETERS_FILENAME, - VNF_BASE_TEMPLATE_FILENAME, VNF_DEFINITION_TEMPLATE_FILENAME, VNF_INPUT_FILENAME, - VNF_MANIFEST_TEMPLATE_FILENAME, VNF_OUTPUT_FOLDER_FILENAME, VNF_TEMPLATE_FOLDER_NAME, + MANIFEST_FOLDER_NAME, + VNF_MANIFEST_TEMPLATE_FILENAME ) -from azext_aosm.configuration_models.common_parameters_config import ( - VNFCommonParametersConfig, -) -from azext_aosm.configuration_models.onboarding_vnf_input_config import ( - OnboardingVNFInputConfig, -) -from azext_aosm.definition_folder.builder.artifact_builder import ( - ArtifactDefinitionElementBuilder, -) -from azext_aosm.definition_folder.builder.bicep_builder import ( - BicepDefinitionElementBuilder, -) -from azext_aosm.definition_folder.builder.json_builder import ( - JSONDefinitionElementBuilder, -) -from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder -from azext_aosm.inputs.arm_template_input import ArmTemplateInput -from azext_aosm.inputs.vhd_file_input import VHDFileInput - -from .onboarding_nfd_base_handler import OnboardingNFDBaseCLIHandler logger = get_logger(__name__) @@ -55,6 +36,8 @@ class OnboardingVNFCLIHandler(OnboardingNFDBaseCLIHandler): """CLI handler for publishing NFDs.""" + config: OnboardingCoreVNFInputConfig | OnboardingNexusVNFInputConfig + @property def default_config_file_name(self) -> str: """Get the default configuration file name.""" @@ -65,106 +48,15 @@ def output_folder_file_name(self) -> str: """Get the output folder file name.""" return VNF_OUTPUT_FOLDER_FILENAME - def _get_input_config( - self, input_config: Optional[Dict[str, Any]] = None - ) -> OnboardingVNFInputConfig: - """Get the configuration for the command.""" - if input_config is None: - input_config = {} - return OnboardingVNFInputConfig(**input_config) - - def _get_params_config(self, config_file: Path) -> VNFCommonParametersConfig: - """Get the configuration for the command.""" - with open(config_file, "r", encoding="utf-8") as _file: - params_dict = json.load(_file) - if params_dict is None: - params_dict = {} - return VNFCommonParametersConfig(**params_dict) - - def _get_processor_list(self): - processor_list = [] - # for each arm template, instantiate arm processor - for arm_template in self.config.arm_templates: - arm_input = ArmTemplateInput( - artifact_name=arm_template.artifact_name, - artifact_version=arm_template.version, - default_config=None, - template_path=Path(arm_template.file_path).absolute(), - ) - # TODO: generalise for nexus in nexus ready stories - processor_list.append( - AzureCoreArmBuildProcessor(arm_input.artifact_name, arm_input) - ) - - # Instantiate vhd processor - if not self.config.vhd.artifact_name: - self.config.vhd.artifact_name = self.config.nf_name + "-vhd" - vhd_processor = VHDProcessor( - name=self.config.vhd.artifact_name, - input_artifact=VHDFileInput( - artifact_name=self.config.vhd.artifact_name, - artifact_version=self.config.vhd.version, - default_config=self._get_default_config(self.config.vhd), - file_path=Path(self.config.vhd.file_path).absolute(), - blob_sas_uri=self.config.vhd.blob_sas_url, - ), - ) - processor_list.append(vhd_processor) - return processor_list - - def build_base_bicep(self): - """Build the base bicep file.""" - # Build manifest bicep contents, with j2 template - template_path = self._get_template_path( - VNF_TEMPLATE_FOLDER_NAME, VNF_BASE_TEMPLATE_FILENAME - ) - bicep_contents = self._render_base_bicep_contents(template_path) - # Create Bicep element with manifest contents - bicep_file = BicepDefinitionElementBuilder( - Path(VNF_OUTPUT_FOLDER_FILENAME, BASE_FOLDER_NAME), bicep_contents - ) - return bicep_file - - def build_manifest_bicep(self): - """Build the manifest bicep file.""" - acr_artifact_list = [] + def build_artifact_list(self) -> ArtifactDefinitionElementBuilder: + """Build the artifact list. - logger.info("Creating artifact manifest bicep") - - for processor in self.processors: - if isinstance(processor, BaseArmBuildProcessor): - acr_artifact_list.extend(processor.get_artifact_manifest_list()) - logger.debug( - "Created list of artifacts from %s arm template(s) provided: %s", - len(self.config.arm_templates), - acr_artifact_list, - ) - elif isinstance(processor, VHDProcessor): - sa_artifact_list = processor.get_artifact_manifest_list() - logger.debug( - "Created list of artifacts from vhd image provided: %s", - sa_artifact_list, - ) + Gets list of artifacts to be including in the artifacts.json. + This is used during the publish command, to upload the artifacts correctly. - # Build manifest bicep contents, with j2 template - template_path = self._get_template_path( - VNF_TEMPLATE_FOLDER_NAME, VNF_MANIFEST_TEMPLATE_FILENAME - ) - bicep_contents = self._render_manifest_bicep_contents( - template_path, acr_artifact_list, sa_artifact_list - ) - # Create Bicep element with manifest contents - bicep_file = BicepDefinitionElementBuilder( - Path(VNF_OUTPUT_FOLDER_FILENAME, MANIFEST_FOLDER_NAME), - bicep_contents, - ) - - logger.info("Created artifact manifest bicep element") - return bicep_file - - def build_artifact_list(self): - """Build the artifact list.""" + """ logger.info("Creating artifacts list for artifacts.json") + # assert isinstance(self.config, OnboardingBaseVNFInputConfig) artifact_list = [] # For each arm template, get list of artifacts and combine for processor in self.processors: @@ -172,7 +64,7 @@ def build_artifact_list(self): if artifacts not in artifact_list: artifact_list.extend(artifacts) logger.debug( - "Created list of artifact details from %s arm template(s) and the vhd image provided: %s", + "Created list of artifact details from %s arm template(s) and the image provided: %s", len(self.config.arm_templates), artifact_list, ) @@ -182,70 +74,46 @@ def build_artifact_list(self): Path(VNF_OUTPUT_FOLDER_FILENAME, ARTIFACT_LIST_FILENAME), artifact_list ) - def build_resource_bicep(self): - """Build the resource bicep file.""" + def build_resource_bicep(self) -> BicepDefinitionElementBuilder: + """Build the resource bicep file. + + Creates nfDefinition.bicep and its supporting files. + + For each processor: + - Generates NF application for each processor + - Generates deploymentParameters (flattened to be one schema overall) + - Generates supporting parameters files (to avoid stringified JSON in template) + + """ logger.info("Creating artifacts list for artifacts.json") - acr_nf_application_list = [] - sa_nf_application_list = [] + arm_nf_application_list = [] + image_nf_application_list = [] supporting_files = [] schema_properties = {} for processor in self.processors: - nf_application = processor.generate_nf_application() - logger.debug("Created nf application %s", nf_application.name) + # Generate NF Application + add to correct list for nfd template + (arm_nf_application, image_nf_application) = self._generate_type_specific_nf_application(processor) + arm_nf_application_list.extend(arm_nf_application) + image_nf_application_list.extend(image_nf_application) # Generate deploymentParameters schema properties params_schema = processor.generate_params_schema() schema_properties.update(params_schema) - # For each arm template, generate nf application - if isinstance(processor, BaseArmBuildProcessor): - acr_nf_application_list.append(nf_application) - - # Generate local file for template_parameters + add to supporting files list - params = ( - nf_application.deploy_parameters_mapping_rule_profile.template_mapping_rule_profile - ).template_parameters # Funky formatting is to stop black from reformatting to too long line - - template_name = TEMPLATE_PARAMETERS_FILENAME - logger.info( - "Created templatateParameters as supporting file for nfDefinition bicep" - ) - elif isinstance(processor, VHDProcessor): - # Generate NF Application - # nf_application = processor.generate_nf_application() - sa_nf_application_list.append(nf_application) - # Generate local file for vhd_parameters - params = ( - nf_application.deploy_parameters_mapping_rule_profile.vhd_image_mapping_rule_profile - ).user_configuration # Funky formatting is to stop black from reformatting to too long line - template_name = VHD_PARAMETERS_FILENAME - else: - raise TypeError(f"Type: {type(processor)} is not valid") - - parameters_file = LocalFileBuilder( - Path( - VNF_OUTPUT_FOLDER_FILENAME, - NF_DEFINITION_FOLDER_NAME, - template_name, - ), - json.dumps(json.loads(params), indent=4), - ) + # Generate local file for parameters, i.e imageParameters + parameters_file = processor.generate_parameters_file() supporting_files.append(parameters_file) # Create bicep contents using vnf defintion j2 template - template_path = self._get_template_path( + template_path = get_template_path( VNF_TEMPLATE_FOLDER_NAME, VNF_DEFINITION_TEMPLATE_FILENAME ) - params = { - "acr_nf_applications": acr_nf_application_list, - "sa_nf_application": sa_nf_application_list[0], - "deployment_parameters_file": DEPLOYMENT_PARAMETERS_FILENAME, - "vhd_parameters_file": VHD_PARAMETERS_FILENAME, - "template_parameters_file": TEMPLATE_PARAMETERS_FILENAME, - } - bicep_contents = self._render_definition_bicep_contents(template_path, params) + params = self._get_nfd_template_params(arm_nf_application_list, image_nf_application_list) + bicep_contents = render_bicep_contents_from_j2( + template_path, params + ) # Create a bicep element # + add its supporting files (deploymentParameters, vhdParameters and templateParameters) @@ -264,35 +132,47 @@ def build_resource_bicep(self): ) return bicep_file - def build_all_parameters_json(self): - params_content = { - "location": self.config.location, - "publisherName": self.config.publisher_name, - "publisherResourceGroupName": self.config.publisher_resource_group_name, - "acrArtifactStoreName": self.config.acr_artifact_store_name, - "saArtifactStoreName": self.config.blob_artifact_store_name, - "acrManifestName": self.config.acr_artifact_store_name + "-manifest", - "saManifestName": self.config.blob_artifact_store_name + "-manifest", - "nfDefinitionGroup": self.config.nf_name, - "nfDefinitionVersion": self.config.version, + def build_manifest_bicep(self) -> BicepDefinitionElementBuilder: + """Build the manifest bicep file.""" + acr_artifact_list = [] + sa_artifact_list = [] + + logger.info("Creating artifact manifest bicep") + + for processor in self.processors: + (arm_artifact, sa_artifact) = self._generate_type_specific_artifact_manifest(processor) + acr_artifact_list.extend(arm_artifact) + sa_artifact_list.extend(sa_artifact) + + # Build manifest bicep contents, with j2 template + template_path = get_template_path( + VNF_TEMPLATE_FOLDER_NAME, VNF_MANIFEST_TEMPLATE_FILENAME + ) + params = { + "acr_artifacts": acr_artifact_list, + "sa_artifacts": sa_artifact_list } - base_file = JSONDefinitionElementBuilder( - Path(VNF_OUTPUT_FOLDER_FILENAME), json.dumps(params_content, indent=4) + bicep_contents = render_bicep_contents_from_j2( + template_path, params ) - return base_file - - @staticmethod - def _get_default_config(vhd): - default_config = {} - if vhd.image_disk_size_GB: - default_config.update({"image_disk_size_GB": vhd.image_disk_size_GB}) - if vhd.image_hyper_v_generation: - default_config.update( - {"image_hyper_v_generation": vhd.image_hyper_v_generation} - ) - else: - # Default to V1 if not specified - default_config.update({"image_hyper_v_generation": "V1"}) - if vhd.image_api_version: - default_config.update({"image_api_version": vhd.image_api_version}) - return default_config + + # Create Bicep element with manifest contents + bicep_file = BicepDefinitionElementBuilder( + Path(VNF_OUTPUT_FOLDER_FILENAME, MANIFEST_FOLDER_NAME), + bicep_contents, + ) + + logger.info("Created artifact manifest bicep element") + return bicep_file + + @abstractmethod + def _get_nfd_template_params(self, arm_nf_application_list, image_nf_application_list): + return NotImplementedError + + @abstractmethod + def _generate_type_specific_nf_application(self, processor): + return NotImplementedError + + @abstractmethod + def _generate_type_specific_artifact_manifest(self, processor): + return NotImplementedError diff --git a/src/aosm/azext_aosm/common/artifact.py b/src/aosm/azext_aosm/common/artifact.py index 60d41529cd1..04782b696ac 100644 --- a/src/aosm/azext_aosm/common/artifact.py +++ b/src/aosm/azext_aosm/common/artifact.py @@ -9,24 +9,26 @@ from abc import ABC, abstractmethod from functools import lru_cache from pathlib import Path +import tempfile from time import sleep from typing import Any, MutableMapping, Optional -from knack.log import get_logger -from knack.util import CLIError -from oras.client import OrasClient - -from azext_aosm.common.command_context import CommandContext -from azext_aosm.common.utils import convert_bicep_to_arm from azext_aosm.configuration_models.common_parameters_config import ( BaseCommonParametersConfig, - VNFCommonParametersConfig, + NFDCommonParametersConfig, + CoreVNFCommonParametersConfig ) -from azext_aosm.vendored_sdks import HybridNetworkManagementClient from azext_aosm.vendored_sdks.azure_storagev2.blob.v2022_11_02 import ( BlobClient, BlobType, ) +from azext_aosm.vendored_sdks.models import ArtifactType +from azext_aosm.vendored_sdks import HybridNetworkManagementClient +from azext_aosm.common.command_context import CommandContext +from azext_aosm.common.utils import convert_bicep_to_arm +from knack.util import CLIError +from knack.log import get_logger +from oras.client import OrasClient logger = get_logger(__name__) @@ -187,32 +189,107 @@ def upload( target_acr = self._get_acr(oras_client) target = f"{target_acr}/{self.artifact_name}:{self.artifact_version}" logger.debug("Uploading %s to %s", self.file_path, target) - retries = 0 - while True: - try: - oras_client.push(files=[self.file_path], target=target) - break - except ValueError as error: - if retries < 20: - logger.info( - "Retrying pushing local artifact to ACR. Retries so far: %s", - retries, + + if self.artifact_type == ArtifactType.ARM_TEMPLATE.value: + retries = 0 + while True: + try: + oras_client.push(files=[self.file_path], target=target) + break + except ValueError as error: + if retries < 20: + logger.info( + "Retrying pushing local artifact to ACR. Retries so far: %s", + retries, + ) + retries += 1 + sleep(3) + continue + + logger.error( + "Failed to upload %s to %s. Check if this image exists in the" + " source registry %s.", + self.file_path, + target, + target_acr, ) - retries += 1 - sleep(3) - continue + logger.debug(error, exc_info=True) + raise error - logger.error( - "Failed to upload %s to %s. Check if this image exists in the" - " source registry %s.", - self.file_path, - target, - target_acr, - ) - logger.debug(error, exc_info=True) - raise error + logger.info("LocalFileACRArtifact uploaded %s to %s using oras push", self.file_path, target) + + elif self.artifact_type == ArtifactType.OCI_ARTIFACT.value: - logger.info("LocalFileACRArtifact uploaded %s to %s", self.file_path, target) + target_acr_name = target_acr.replace(".azurecr.io", "") + target_acr_with_protocol = f"oci://{target_acr}" + username = manifest_credentials["username"] + password = manifest_credentials["acr_token"] + + self._check_tool_installed("docker") + self._check_tool_installed("helm") + + # tmpdir is only used if file_path is dir, but `with` context manager is cleaner to use, so we always + # set up the tmpdir, even if it doesn't end up being used. + with tempfile.TemporaryDirectory() as tmpdir: + if self.file_path.is_dir(): + helm_package_cmd = [ + str(shutil.which("helm")), + "package", + self.file_path, + "--destination", + tmpdir, + ] + self._call_subprocess_raise_output(helm_package_cmd) + self.file_path = Path(tmpdir, f"{self.artifact_name}-{self.artifact_version}.tgz") + + # This seems to prevent occasional helm login failures + acr_login_cmd = [ + str(shutil.which("az")), + "acr", + "login", + "--name", + target_acr_name, + "--username", + username, + "--password", + password, + ] + self._call_subprocess_raise_output(acr_login_cmd) + + try: + helm_login_cmd = [ + str(shutil.which("helm")), + "registry", + "login", + target_acr, + "--username", + username, + "--password", + password, + ] + self._call_subprocess_raise_output(helm_login_cmd) + + push_command = [ + str(shutil.which("helm")), + "push", + self.file_path, + target_acr_with_protocol, + ] + self._call_subprocess_raise_output(push_command) + finally: + helm_logout_cmd = [ + str(shutil.which("helm")), + "registry", + "logout", + target_acr, + ] + self._call_subprocess_raise_output(helm_logout_cmd) + + logger.info("LocalFileACRArtifact uploaded %s to %s using helm push", self.file_path, target) + + else: # TODO: Make this one of the allowed Azure CLI exceptions + raise ValueError(f"Unexpected artifact type. Got {self.artifact_type}. " + "Expected {ArtifactType.ARM_TEMPLATE.value} or {ArtifactType.OCI_ARTIFACT.value}") class RemoteACRArtifact(BaseACRArtifact): @@ -553,7 +630,7 @@ def upload( """Upload the artifact.""" def _get_blob_client( - self, config: VNFCommonParametersConfig, command_context: CommandContext + self, config: BaseCommonParametersConfig, command_context: CommandContext ) -> BlobClient: container_basename = self.artifact_name.replace("-", "") container_name = f"{container_basename}-{self.artifact_version}" @@ -564,7 +641,9 @@ def _get_blob_client( blob_name = container_name logger.debug("container name: %s, blob name: %s", container_name, blob_name) - + # Liskov substitution dictates we must accept BaseCommonParametersConfig, but we should + # never be calling upload on this class unless we've got CoreVNFCommonParametersConfig + assert isinstance(config, CoreVNFCommonParametersConfig) manifest_credentials = ( command_context.aosm_client.artifact_manifests.list_credential( resource_group_name=config.publisherResourceGroupName, @@ -597,8 +676,8 @@ def upload( ): """Upload the artifact.""" # Liskov substitution dictates we must accept BaseCommonParametersConfig, but we should - # never be calling upload on this class unless we've got VNFCommonParametersConfig - assert isinstance(config, VNFCommonParametersConfig) + # never be calling upload on this class unless we've got NFDCommonParametersConfig + assert isinstance(config, NFDCommonParametersConfig) logger.debug("LocalFileStorageAccountArtifact config: %s", config) blob_client = self._get_blob_client( config=config, command_context=command_context @@ -653,8 +732,8 @@ def upload( ): """Upload the artifact.""" # Liskov substitution dictates we must accept BaseCommonParametersConfig, but we should - # never be calling upload on this class unless we've got VNFCommonParametersConfig - assert isinstance(config, VNFCommonParametersConfig) + # never be calling upload on this class unless we've got NFDCommonParametersConfig + assert isinstance(config, NFDCommonParametersConfig) logger.info("Copy from SAS URL to blob store") source_blob = BlobClient.from_blob_url(self.blob_sas_uri) diff --git a/src/aosm/azext_aosm/common/constants.py b/src/aosm/azext_aosm/common/constants.py index f9e998527dc..570c4ef0d28 100644 --- a/src/aosm/azext_aosm/common/constants.py +++ b/src/aosm/azext_aosm/common/constants.py @@ -11,6 +11,7 @@ VNF = "vnf" CNF = "cnf" NSD = "nsd" +VNF_NEXUS = "vnf-nexus" class DeployableResourceTypes(str, Enum): @@ -51,6 +52,7 @@ class ManifestsExist(str, Enum): DEPLOYMENT_PARAMETERS_FILENAME = "deploymentParameters.json" TEMPLATE_PARAMETERS_FILENAME = "templateParameters.json" VHD_PARAMETERS_FILENAME = "vhdParameters.json" +NEXUS_IMAGE_PARAMETERS_FILENAME = "imageParameters.json" NSD_OUTPUT_FOLDER_FILENAME = "nsd-cli-output" NSD_INPUT_FILENAME = "nsd-input.jsonc" @@ -59,12 +61,14 @@ class ManifestsExist(str, Enum): NSD_BASE_TEMPLATE_FILENAME = "nsdbase.bicep" NSD_TEMPLATE_FOLDER_NAME = "nsd" NSD_DEFINITION_FOLDER_NAME = "nsdDefinition" +NSD_NF_TEMPLATE_FILENAME = "nf_template.bicep.j2" VNF_OUTPUT_FOLDER_FILENAME = "vnf-cli-output" VNF_INPUT_FILENAME = "vnf-input.jsonc" VNF_DEFINITION_TEMPLATE_FILENAME = "vnfdefinition.bicep.j2" VNF_MANIFEST_TEMPLATE_FILENAME = "vnfartifactmanifest.bicep.j2" -VNF_BASE_TEMPLATE_FILENAME = "vnfbase.bicep" +VNF_CORE_BASE_TEMPLATE_FILENAME = "vnfbase.bicep" +VNF_NEXUS_BASE_TEMPLATE_FILENAME = "vnfnexusbase.bicep" VNF_TEMPLATE_FOLDER_NAME = "vnf" CNF_OUTPUT_FOLDER_FILENAME = "cnf-cli-output" @@ -76,6 +80,8 @@ class ManifestsExist(str, Enum): CNF_VALUES_SCHEMA_FILENAME = "values.schema.json" CNF_TEMPLATE_FOLDER_NAME = "cnf" +NEXUS_IMAGE_REGEX = r"^[\~]?(\d+)\.(\d+)\.(\d+)$" + ################# # OLD CONSTANTS # ################# diff --git a/src/aosm/azext_aosm/common/templates/nf_template.bicep b/src/aosm/azext_aosm/common/templates/nsd/nf_template.bicep.j2 similarity index 93% rename from src/aosm/azext_aosm/common/templates/nf_template.bicep rename to src/aosm/azext_aosm/common/templates/nsd/nf_template.bicep.j2 index 781aa874103..b6bbfbcfe27 100644 --- a/src/aosm/azext_aosm/common/templates/nf_template.bicep +++ b/src/aosm/azext_aosm/common/templates/nsd/nf_template.bicep.j2 @@ -33,7 +33,7 @@ resource nfResource 'Microsoft.HybridNetwork/networkFunctions@2023-09-01' = [for id: nfdv.id idType: 'Open' } - nfviType: (configObject.customLocationId == '') ? 'AzureCore' : 'AzureArcKubernetes' + nfviType: '{{nfvi_type}}' nfviId: (configObject.customLocationId == '') ? resourceGroupId : configObject.customLocationId allowSoftwareUpdate: true configurationType: 'Secret' diff --git a/src/aosm/azext_aosm/common/templates/nsd/nsddefinition.bicep.j2 b/src/aosm/azext_aosm/common/templates/nsd/nsddefinition.bicep.j2 index 8460f4c3b55..96d944a5a1e 100644 --- a/src/aosm/azext_aosm/common/templates/nsd/nsddefinition.bicep.j2 +++ b/src/aosm/azext_aosm/common/templates/nsd/nsddefinition.bicep.j2 @@ -71,7 +71,7 @@ resource nsdVersion 'Microsoft.Hybridnetwork/publishers/networkservicedesigngrou nfvisFromSite: { nfvi1: { name: nfviSiteName - type: 'AzureCore' + type: '{{ nfvi_type}}' } } // This field lists the templates that will be deployed by AOSM and the config mappings diff --git a/src/aosm/azext_aosm/common/templates/vnf/vnfartifactmanifest.bicep.j2 b/src/aosm/azext_aosm/common/templates/vnf/vnfartifactmanifest.bicep.j2 index 2a1d3de8b86..daf9072180e 100644 --- a/src/aosm/azext_aosm/common/templates/vnf/vnfartifactmanifest.bicep.j2 +++ b/src/aosm/azext_aosm/common/templates/vnf/vnfartifactmanifest.bicep.j2 @@ -6,12 +6,16 @@ param location string param publisherName string @description('Name of an existing ACR-backed Artifact Store, deployed under the publisher.') param acrArtifactStoreName string +{%- if sa_artifacts %} @description('Name of an existing Storage Account-backed Artifact Store, deployed under the publisher.') param saArtifactStoreName string +{%- endif %} @description('Name of the manifest to deploy for the ACR-backed Artifact Store') param acrManifestName string +{%- if sa_artifacts %} @description('Name of the manifest to deploy for the Storage Account-backed Artifact Store') param saManifestName string +{%- endif %} // The publisher resource is the top level AOSM resource under which all other designer resources // are created. @@ -27,12 +31,14 @@ resource acrArtifactStore 'Microsoft.HybridNetwork/publishers/artifactStores@202 name: acrArtifactStoreName } +{%- if sa_artifacts %} // The storage account is the resource in which the VHD images are stored. // If using publish command, this is created from deploying the vnfbase.bicep resource saArtifactStore 'Microsoft.HybridNetwork/publishers/artifactStores@2023-09-01' existing = { parent: publisher name: saArtifactStoreName } +{%- endif %} resource acrArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/artifactManifests@2023-09-01' = { parent: acrArtifactStore @@ -51,6 +57,7 @@ resource acrArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/ } } +{%- if sa_artifacts %} resource saArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/artifactManifests@2023-09-01' = { parent: saArtifactStore name: saManifestName @@ -67,3 +74,5 @@ resource saArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/a ] } } +{%- endif %} + diff --git a/src/aosm/azext_aosm/common/templates/vnf/vnfdefinition.bicep.j2 b/src/aosm/azext_aosm/common/templates/vnf/vnfdefinition.bicep.j2 index dc3f640a6dd..ab7f10875f1 100644 --- a/src/aosm/azext_aosm/common/templates/vnf/vnfdefinition.bicep.j2 +++ b/src/aosm/azext_aosm/common/templates/vnf/vnfdefinition.bicep.j2 @@ -6,8 +6,10 @@ param location string param publisherName string @description('Name of an existing ACR-backed Artifact Store, deployed under the publisher.') param acrArtifactStoreName string +{%- if sa_nf_applications %} @description('Name of an existing Storage Account-backed Artifact Store, deployed under the publisher.') param saArtifactStoreName string +{%- endif %} @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') @@ -27,12 +29,14 @@ resource acrArtifactStore 'Microsoft.HybridNetwork/publishers/artifactStores@202 name: acrArtifactStoreName } +{%- if sa_nf_applications %} // The storage account is the resource in which the VHD images are stored. // If using publish command, this is created from deploying the vnfbase.bicep resource saArtifactStore 'Microsoft.HybridNetwork/publishers/artifactStores@2023-09-01' existing = { parent: publisher name: saArtifactStoreName } +{%- endif %} // The NFD Group is the parent resource under which all NFD versions will be created. // If using publish command, this is created from deploying the vnfbase.bicep @@ -53,16 +57,17 @@ resource nfdv 'Microsoft.Hybridnetwork/publishers/networkfunctiondefinitiongroup deployParameters: string(loadJsonContent('{{deployment_parameters_file}}')) networkFunctionType: 'VirtualNetworkFunction' networkFunctionTemplate: { - nfviType: 'AzureCore' + nfviType: '{{ nfvi_type }}' networkFunctionApplications: [ + {%- for configuration in sa_nf_applications %} { artifactType: 'VhdImageFile' - name: '{{ sa_nf_application.artifact_profile.vhd_artifact_profile.vhd_name }}' + name: '{{ configuration.artifact_profile.vhd_artifact_profile.vhd_name }}' dependsOnProfile: null artifactProfile: { vhdArtifactProfile: { - vhdName: '{{ sa_nf_application.artifact_profile.vhd_artifact_profile.vhd_name }}' - vhdVersion: '{{ sa_nf_application.artifact_profile.vhd_artifact_profile.vhd_version }}' + vhdName: '{{ configuration.artifact_profile.vhd_artifact_profile.vhd_name }}' + vhdVersion: '{{ configuration.artifact_profile.vhd_artifact_profile.vhd_version }}' } artifactStore: { id: saArtifactStore.id @@ -73,10 +78,10 @@ resource nfdv 'Microsoft.Hybridnetwork/publishers/networkfunctiondefinitiongroup vhdImageMappingRuleProfile: { userConfiguration: string(loadJsonContent('{{vhd_parameters_file}}')) } - // ?? - applicationEnablement: 'Unknown' + applicationEnablement: '{{ configuration.deploy_parameters_mapping_rule_profile.application_enablement.value }}' } } + {%- endfor %} {%- for configuration in acr_nf_applications %} { artifactType: 'ArmTemplate' @@ -93,9 +98,31 @@ resource nfdv 'Microsoft.Hybridnetwork/publishers/networkfunctiondefinitiongroup } deployParametersMappingRuleProfile: { templateMappingRuleProfile: { - templateParameters: string(loadJsonContent('{{template_parameters_file}}')) + templateParameters: string(loadJsonContent('{{ configuration.name }}-{{template_parameters_file}}')) + } + applicationEnablement: '{{ configuration.deploy_parameters_mapping_rule_profile.application_enablement.value }}' + } + } + {%- endfor %} + {%- for configuration in nexus_image_nf_applications %} + { + artifactType: 'ImageFile' + name: '{{ configuration.name }}' + dependsOnProfile: null + artifactProfile: { + imageArtifactProfile: { + imageName: '{{ configuration.artifact_profile.image_artifact_profile.image_name }}' + imageVersion: '{{ configuration.artifact_profile.image_artifact_profile.image_version }}' + } + artifactStore: { + id: acrArtifactStore.id + } + } + deployParametersMappingRuleProfile: { + imageMappingRuleProfile: { + userConfiguration: string(loadJsonContent('{{ configuration.name }}-{{image_parameters_file}}')) } - applicationEnablement: 'Unknown' + applicationEnablement: '{{ configuration.deploy_parameters_mapping_rule_profile.application_enablement.value }}' } } {%- endfor %} diff --git a/src/aosm/azext_aosm/common/templates/vnf/vnfnexusbase.bicep b/src/aosm/azext_aosm/common/templates/vnf/vnfnexusbase.bicep new file mode 100644 index 00000000000..be16ebaf457 --- /dev/null +++ b/src/aosm/azext_aosm/common/templates/vnf/vnfnexusbase.bicep @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. + +// This file creates the base AOSM resources for a Nexus VNF +param location string +@description('Name of a publisher, expected to be in the resource group where you deploy the template') +param publisherName string +@description('Name of an ACR-backed Artifact Store, deployed under the publisher.') +param acrArtifactStoreName string +@description('Name of a Network Function Definition Group') +param nfDefinitionGroup string + +// The publisher resource is the top level AOSM resource under which all other designer resources +// are created. +resource publisher 'Microsoft.HybridNetwork/publishers@2023-09-01' = { + name: publisherName + location: location + properties: { scope: 'Private'} +} + +// The artifact store is the resource in which all the artifacts required to deploy the NF are stored. +resource acrArtifactStore 'Microsoft.HybridNetwork/publishers/artifactStores@2023-09-01' = { + parent: publisher + name: acrArtifactStoreName + location: location + properties: { + storeType: 'AzureContainerRegistry' + } +} + +// The NFD Group is the parent resource under which all NFD versions will be created. +resource nfdg 'Microsoft.Hybridnetwork/publishers/networkfunctiondefinitiongroups@2023-09-01' = { + parent: publisher + name: nfDefinitionGroup + location: location +} diff --git a/src/aosm/azext_aosm/common/utils.py b/src/aosm/azext_aosm/common/utils.py index 6fef0db0d60..025fa8c8030 100644 --- a/src/aosm/azext_aosm/common/utils.py +++ b/src/aosm/azext_aosm/common/utils.py @@ -2,17 +2,18 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- - -import json +import re import os +import tarfile +from pathlib import Path +from jinja2 import StrictUndefined, Template +import json import shutil import subprocess -import tarfile import tempfile -from pathlib import Path from knack.log import get_logger - +from azext_aosm.common.constants import NEXUS_IMAGE_REGEX from azext_aosm.common.exceptions import InvalidFileTypeError, MissingDependency logger = get_logger(__name__) @@ -60,9 +61,27 @@ def convert_bicep_to_arm(bicep_template_path: Path) -> dict: return arm_json -def create_bicep_from_template(): - # Take j2 template, take params, return bicep file - return NotImplementedError +def render_bicep_contents_from_j2(template_path: Path, params): + """Write the definition bicep file from given template.""" + with open(template_path, "r", encoding="UTF-8") as f: + template: Template = Template( + f.read(), + undefined=StrictUndefined, + ) + + bicep_contents: str = template.render(params) + return bicep_contents + + +def get_template_path(definition_type: str, template_name: str) -> Path: + """Get the path to a template.""" + return ( + Path(__file__).parent.parent + / "common" + / "templates" + / definition_type + / template_name + ) def extract_tarfile(file_path: Path, target_dir: Path) -> Path: @@ -104,3 +123,20 @@ def check_tool_installed(tool_name: str) -> None: """ if shutil.which(tool_name) is None: raise MissingDependency(f"You must install {tool_name} to use this command.") + + +def split_image_path(image) -> "tuple[str, str, str]": + """Split the image path into source acr registry, name and version.""" + (source_acr_registry, name_and_version) = image.split("/", 2) + (name, version) = name_and_version.split(":", 2) + return (source_acr_registry, name, version) + + +def is_valid_nexus_image_version(string): + """Check if image version is valid. + + This is based on validation in pez repo. + It requires the image version to be major.minor.patch, + but does not enforce full semver validation. + """ + return re.match(NEXUS_IMAGE_REGEX, string) is not None diff --git a/src/aosm/azext_aosm/configuration_models/common_parameters_config.py b/src/aosm/azext_aosm/configuration_models/common_parameters_config.py index 226a144f064..5c65e291345 100644 --- a/src/aosm/azext_aosm/configuration_models/common_parameters_config.py +++ b/src/aosm/azext_aosm/configuration_models/common_parameters_config.py @@ -32,13 +32,18 @@ class NFDCommonParametersConfig(BaseCommonParametersConfig): @dataclass(frozen=True) -class VNFCommonParametersConfig(NFDCommonParametersConfig): +class CoreVNFCommonParametersConfig(NFDCommonParametersConfig): """Common parameters configuration for VNFs.""" saArtifactStoreName: str saManifestName: str +@dataclass(frozen=True) +class NexusVNFCommonParametersConfig(NFDCommonParametersConfig): + """Common parameters configuration for VNFs.""" + + @dataclass(frozen=True) class CNFCommonParametersConfig(NFDCommonParametersConfig): """Common parameters configuration for VNFs.""" diff --git a/src/aosm/azext_aosm/configuration_models/onboarding_nfd_base_input_config.py b/src/aosm/azext_aosm/configuration_models/onboarding_nfd_base_input_config.py index 9129946aa0c..ce8a2c7a16a 100644 --- a/src/aosm/azext_aosm/configuration_models/onboarding_nfd_base_input_config.py +++ b/src/aosm/azext_aosm/configuration_models/onboarding_nfd_base_input_config.py @@ -26,6 +26,12 @@ class OnboardingNFDBaseInputConfig(OnboardingBaseInputConfig): }, ) + @property + def acr_manifest_name(self) -> str: + """Return the ACR manifest name from the NFD name and version.""" + sanitized_nf_name = self.nf_name.lower().replace("_", "-") + return f"{sanitized_nf_name}-acr-manifest-{self.version.replace('.', '-')}" + def validate(self): """Validate the configuration.""" super().validate() diff --git a/src/aosm/azext_aosm/configuration_models/onboarding_nsd_input_config.py b/src/aosm/azext_aosm/configuration_models/onboarding_nsd_input_config.py index e51334ebbd3..9ad8c209083 100644 --- a/src/aosm/azext_aosm/configuration_models/onboarding_nsd_input_config.py +++ b/src/aosm/azext_aosm/configuration_models/onboarding_nsd_input_config.py @@ -175,6 +175,15 @@ class OnboardingNSDInputConfig(OnboardingBaseInputConfig): "comment": "Optional. Description of the Network Service Design Version (NSDV)." }, ) + nfvi_type: str = field( + default="AzureCore", + metadata={ + "comment": ( + "Type of NFVI (for nfvisFromSite). Defaults to 'AzureCore'.\n" + "Valid values are 'AzureCore', 'AzureOperatorNexus' or 'AzureArcKubernetes." + ) + }, + ) # # TODO: Add detailed comment for this resource_element_templates: "list[NetworkFunctionConfig | ArmTemplateConfig]" = ( @@ -184,6 +193,12 @@ class OnboardingNSDInputConfig(OnboardingBaseInputConfig): ) ) + @property + def acr_manifest_name(self) -> str: + """Return the ACR manifest name from the NSD name and version.""" + sanitized_nsd_name = self.nsd_name.lower().replace("_", "-") + return f"{sanitized_nsd_name}-nsd-manifest-{self.nsd_version.replace('.', '-')}" + def validate(self): """Validate the configuration.""" super().validate() @@ -196,7 +211,10 @@ def validate(self): raise ValidationError("nsd_name must be set") if not self.nsd_version: raise ValidationError("nsd_version must be set") - + if self.nfvi_type not in ["AzureCore", "AzureOperatorNexus", "AzureArcKubernetes"]: + raise ValidationError( + "nfvi_type must be either 'AzureCore', 'AzureOperatorNexus' or 'AzureArcKubernetes'" + ) # Validate each RET for configuration in self.resource_element_templates: configuration.validate() diff --git a/src/aosm/azext_aosm/configuration_models/onboarding_vnf_input_config.py b/src/aosm/azext_aosm/configuration_models/onboarding_vnf_input_config.py index 6490a30614b..97f52dea77f 100644 --- a/src/aosm/azext_aosm/configuration_models/onboarding_vnf_input_config.py +++ b/src/aosm/azext_aosm/configuration_models/onboarding_vnf_input_config.py @@ -3,7 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- from __future__ import annotations - from dataclasses import dataclass, field from typing import List @@ -13,6 +12,7 @@ from azext_aosm.configuration_models.onboarding_nfd_base_input_config import ( OnboardingNFDBaseInputConfig, ) +from azext_aosm.common.utils import split_image_path, is_valid_nexus_image_version @dataclass @@ -100,10 +100,10 @@ def validate(self): @dataclass -class OnboardingVNFInputConfig(OnboardingNFDBaseInputConfig): +class OnboardingCoreVNFInputConfig(OnboardingNFDBaseInputConfig): """Input configuration for onboarding VNFs.""" - blob_artifact_store_name: str | None = field( + blob_artifact_store_name: str = field( default="", metadata={ "comment": ( @@ -122,7 +122,6 @@ class OnboardingVNFInputConfig(OnboardingNFDBaseInputConfig): }, ) - # TODO: Add better comments arm_templates: List[ArmTemplatePropertiesConfig] = field( default_factory=lambda: [ArmTemplatePropertiesConfig()], metadata={ @@ -148,16 +147,19 @@ def __post_init__(self): if self.vhd and isinstance(self.vhd, dict): self.vhd = VhdImageConfig(**self.vhd) + sanitized_nf_name = self.nf_name.lower().replace("_", "-") + if not self.blob_artifact_store_name: + self.blob_artifact_store_name = sanitized_nf_name + "-sa" + @property def sa_manifest_name(self) -> str: - """Return the Storage account manifest name from the NFD name.""" + """Return the Storage account manifest name from the NFD name and version.""" sanitized_nf_name = self.nf_name.lower().replace("_", "-") return f"{sanitized_nf_name}-sa-manifest-{self.version.replace('.', '-')}" def validate(self): """Validate the configuration.""" super().validate() - if not self.image_name_parameter: raise ValidationError("image_name_parameter must be set") if not self.arm_templates: @@ -169,3 +171,73 @@ def validate(self): for arm_template in self.arm_templates: arm_template.validate() self.vhd.validate() + + +@dataclass +class OnboardingNexusVNFInputConfig(OnboardingNFDBaseInputConfig): + """Input configuration for onboarding VNFs.""" + + image_name_parameter: str = field( + default="", + metadata={ + "comment": ( + "The parameter name in the VM ARM template which " + "specifies the name of the image to use for the VM." + ) + }, + ) + + arm_templates: List[ArmTemplatePropertiesConfig] = field( + default_factory=lambda: [ArmTemplatePropertiesConfig()], + metadata={ + "comment": ( + "ARM template configuration. The ARM templates given here would deploy a VM if run." + "They will be used to generate the VNF." + ) + }, + ) + + images: List[str] = field( + default_factory=lambda: [], + metadata={ + "comment": ( + "List of images to be pulled from the acr registry.\n" + "You must provide the source acr registry, the image name and the version.\n" + "For example: 'sourceacr.azurecr.io/imagename:imageversion'." + ) + }, + ) + + def __post_init__(self): + arm_list = [] + for arm_template in self.arm_templates: + if arm_template and isinstance(arm_template, dict): + arm_list.append(ArmTemplatePropertiesConfig(**arm_template)) + else: + arm_list.append(arm_template) + self.arm_templates = arm_list + + @property + def sa_manifest_name(self) -> str: + """Return the Storage account manifest name from the NFD name and version.""" + sanitized_nf_name = self.nf_name.lower().replace("_", "-") + return f"{sanitized_nf_name}-sa-manifest-{self.version.replace('.', '-')}" + + def validate(self): + """Validate the configuration.""" + super().validate() + if not self.image_name_parameter: + raise ValidationError("image_name_parameter must be set") + if not self.arm_templates: + raise ValidationError("arm_template must be set") + if not self.images: + raise ValidationError("You must include at least one image") + for image in self.images: + (_, _, version) = split_image_path(image) + if not is_valid_nexus_image_version(version): + raise ValidationError(f"{image} has invalid version '{version}'.\n" + "Allowed format is major.minor.patch") + if not self.arm_templates: + raise ValidationError("You must include at least one arm template") + for arm_template in self.arm_templates: + arm_template.validate() diff --git a/src/aosm/azext_aosm/custom.py b/src/aosm/azext_aosm/custom.py index 3865e773ada..17d89aef878 100644 --- a/src/aosm/azext_aosm/custom.py +++ b/src/aosm/azext_aosm/custom.py @@ -11,24 +11,28 @@ from azure.cli.core.commands import AzCliCommand from azext_aosm.cli_handlers.onboarding_cnf_handler import OnboardingCNFCLIHandler -from azext_aosm.cli_handlers.onboarding_nsd_handler import OnboardingNSDCLIHandler from azext_aosm.cli_handlers.onboarding_vnf_handler import OnboardingVNFCLIHandler +from azext_aosm.cli_handlers.onboarding_core_vnf_handler import OnboardingCoreVNFCLIHandler +from azext_aosm.cli_handlers.onboarding_nexus_vnf_handler import OnboardingNexusVNFCLIHandler +from azext_aosm.cli_handlers.onboarding_nsd_handler import OnboardingNSDCLIHandler from azext_aosm.common.command_context import CommandContext -from azext_aosm.common.constants import ALL_PARAMETERS_FILE_NAME, CNF, VNF +from azext_aosm.common.constants import ALL_PARAMETERS_FILE_NAME, CNF, VNF, VNF_NEXUS def onboard_nfd_generate_config(definition_type: str, output_file: str | None): """Generate config file for onboarding NFs.""" # Declare types explicitly - handler: OnboardingCNFCLIHandler | OnboardingVNFCLIHandler + handler: OnboardingCNFCLIHandler | OnboardingVNFCLIHandler | OnboardingNexusVNFCLIHandler if definition_type == CNF: handler = OnboardingCNFCLIHandler() - handler.generate_config(output_file) elif definition_type == VNF: - handler = OnboardingVNFCLIHandler() - handler.generate_config(output_file) + handler = OnboardingCoreVNFCLIHandler() + elif definition_type == VNF_NEXUS: + handler = OnboardingNexusVNFCLIHandler() else: - raise UnrecognizedArgumentError("Invalid definition type") + raise UnrecognizedArgumentError( + "Invalid definition type, valid values are 'cnf', 'vnf' or 'vnfnexus'") + handler.generate_config(output_file) def onboard_nfd_build( @@ -36,15 +40,17 @@ def onboard_nfd_build( ): """Build the NF definition.""" # Declare types explicitly - handler: OnboardingCNFCLIHandler | OnboardingVNFCLIHandler + handler: OnboardingCNFCLIHandler | OnboardingVNFCLIHandler | OnboardingNexusVNFCLIHandler if definition_type == CNF: - handler = OnboardingCNFCLIHandler(Path(config_file), skip=skip) - handler.build() + handler = OnboardingCNFCLIHandler(config_file_path=Path(config_file), skip=skip) elif definition_type == VNF: - handler = OnboardingVNFCLIHandler(Path(config_file)) - handler.build() + handler = OnboardingCoreVNFCLIHandler(config_file_path=Path(config_file)) + elif definition_type == VNF_NEXUS: + handler = OnboardingNexusVNFCLIHandler(config_file_path=Path(config_file)) else: - raise UnrecognizedArgumentError("Invalid definition type") + raise UnrecognizedArgumentError( + "Invalid definition type, valid values are 'cnf', 'vnf' or 'vnfnexus'") + handler.build() def onboard_nfd_publish( @@ -65,16 +71,17 @@ def onboard_nfd_publish( handler: OnboardingCNFCLIHandler | OnboardingVNFCLIHandler if definition_type == CNF: handler = OnboardingCNFCLIHandler( - Path(build_output_folder, ALL_PARAMETERS_FILE_NAME) - ) - handler.publish(command_context=command_context) + all_deploy_params_file_path=Path(build_output_folder, ALL_PARAMETERS_FILE_NAME)) elif definition_type == VNF: - handler = OnboardingVNFCLIHandler( - Path(build_output_folder, ALL_PARAMETERS_FILE_NAME) - ) - handler.publish(command_context=command_context) + handler = OnboardingCoreVNFCLIHandler( + all_deploy_params_file_path=Path(build_output_folder, ALL_PARAMETERS_FILE_NAME)) + elif definition_type == VNF_NEXUS: + handler = OnboardingNexusVNFCLIHandler( + all_deploy_params_file_path=Path(build_output_folder, ALL_PARAMETERS_FILE_NAME)) else: - raise UnrecognizedArgumentError("Invalid definition type") + raise UnrecognizedArgumentError( + "Invalid definition type, valid values are 'cnf', 'vnf' or 'vnfnexus'") + handler.publish(command_context=command_context) # def onboard_nfd_delete(cmd: AzCliCommand, definition_type: str, config_file: str): @@ -99,7 +106,8 @@ def onboard_nsd_generate_config(output_file: str | None): def onboard_nsd_build(config_file: Path, cmd: AzCliCommand): """Build the NSD definition.""" command_context = CommandContext(cli_ctx=cmd.cli_ctx) - handler = OnboardingNSDCLIHandler(Path(config_file), command_context.aosm_client) + handler = OnboardingNSDCLIHandler(config_file_path=Path(config_file), + aosm_client=command_context.aosm_client) handler.build() @@ -117,7 +125,7 @@ def onboard_nsd_publish( }, ) handler = OnboardingNSDCLIHandler( - Path(build_output_folder, ALL_PARAMETERS_FILE_NAME) + all_deploy_params_file_path=Path(build_output_folder, ALL_PARAMETERS_FILE_NAME) ) handler.publish(command_context=command_context) diff --git a/src/aosm/azext_aosm/definition_folder/builder/artifact_builder.py b/src/aosm/azext_aosm/definition_folder/builder/artifact_builder.py index 0495e2a49ec..575d71cc982 100644 --- a/src/aosm/azext_aosm/definition_folder/builder/artifact_builder.py +++ b/src/aosm/azext_aosm/definition_folder/builder/artifact_builder.py @@ -37,11 +37,12 @@ def write(self): # TODO: Handle converting path to string that doesn't couple this code to the artifact. # Probably should be in to_dict method. for artifact in self.artifacts: - logger.debug( - "Writing artifact %s as: %s", artifact.artifact_name, artifact.to_dict() - ) if hasattr(artifact, "file_path") and artifact.file_path is not None: artifact.file_path = str(artifact.file_path) - artifacts_list.append(artifact.to_dict()) + artifact_dict = artifact.to_dict() + artifacts_list.append(artifact_dict) + logger.debug( + "Writing artifact %s as: %s", artifact.artifact_name, artifact_dict + ) (self.path / "artifacts.json").write_text(json.dumps(artifacts_list, indent=4)) self._write_supporting_files() diff --git a/src/aosm/azext_aosm/definition_folder/reader/bicep_definition.py b/src/aosm/azext_aosm/definition_folder/reader/bicep_definition.py index 0efe76f4d22..f3078d2595f 100644 --- a/src/aosm/azext_aosm/definition_folder/reader/bicep_definition.py +++ b/src/aosm/azext_aosm/definition_folder/reader/bicep_definition.py @@ -13,15 +13,13 @@ from azure.mgmt.resource import ResourceManagementClient from azure.mgmt.resource.resources.models import DeploymentExtended from knack.log import get_logger - from azext_aosm.common.command_context import CommandContext -from azext_aosm.common.constants import ManifestsExist from azext_aosm.common.utils import convert_bicep_to_arm -from azext_aosm.configuration_models.common_parameters_config import ( - BaseCommonParametersConfig, - VNFCommonParametersConfig, -) -from azext_aosm.definition_folder.reader.base_definition import BaseDefinitionElement +from azext_aosm.configuration_models.common_parameters_config import \ + BaseCommonParametersConfig, CoreVNFCommonParametersConfig +from azext_aosm.definition_folder.reader.base_definition import \ + BaseDefinitionElement +from azext_aosm.common.constants import ManifestsExist logger = get_logger(__name__) @@ -117,6 +115,7 @@ def _artifact_manifests_exist( config: BaseCommonParametersConfig, command_context: CommandContext ) -> ManifestsExist: """ + Returns True if all required manifests exist, False if none do, and raises an AzCLIError if some but not all exist. @@ -133,8 +132,8 @@ def _artifact_manifests_exist( acr_manifest_exists = True except azure_exceptions.ResourceNotFoundError: acr_manifest_exists = False - - if isinstance(config, VNFCommonParametersConfig): + # TODO: test config type change works + if isinstance(config, CoreVNFCommonParametersConfig): try: command_context.aosm_client.artifact_manifests.get( resource_group_name=config.publisherResourceGroupName, diff --git a/src/aosm/azext_aosm/definition_folder/reader/definition_folder.py b/src/aosm/azext_aosm/definition_folder/reader/definition_folder.py index 7dfc61c70dc..95cf98813e7 100644 --- a/src/aosm/azext_aosm/definition_folder/reader/definition_folder.py +++ b/src/aosm/azext_aosm/definition_folder/reader/definition_folder.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List from knack.log import get_logger - +from azure.mgmt.resource.resources.models import ResourceGroup from azext_aosm.common.command_context import CommandContext from azext_aosm.configuration_models.common_parameters_config import ( BaseCommonParametersConfig, @@ -71,10 +71,26 @@ def _parse_index_file(self, file_content: str) -> List[Dict[str, Any]]: ) return parsed_elements + def _create_or_confirm_existence_of_resource_group(self, config, command_context): + """Ensure resource group exists before deploying of elements begins. + + Using ResourceManagementClient: + - Check for existence of resource group specified in allDeployParameters.json. + - Create resource group if doesn't exist. + """ + resources_client = command_context.resources_client + if not resources_client.resource_groups.check_existence(config.publisherResourceGroupName): + rg_params = ResourceGroup(location=config.location) + resources_client.resource_groups.create_or_update( + resource_group_name=config.publisherResourceGroupName, + parameters=rg_params + ) + def deploy( self, config: BaseCommonParametersConfig, command_context: CommandContext ): """Deploy the resources defined in the folder.""" + self._create_or_confirm_existence_of_resource_group(config, command_context) for element in self.elements: logger.debug( "Deploying definition element %s of type %s", diff --git a/src/aosm/azext_aosm/inputs/nexus_image_input.py b/src/aosm/azext_aosm/inputs/nexus_image_input.py new file mode 100644 index 00000000000..58dfb77e052 --- /dev/null +++ b/src/aosm/azext_aosm/inputs/nexus_image_input.py @@ -0,0 +1,57 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from typing import Any, Dict, Optional +from knack.log import get_logger +from azext_aosm.inputs.base_input import BaseInput + +logger = get_logger(__name__) + + +class NexusImageFileInput(BaseInput): + """ + A utility class for working with VHD file inputs. + + :param artifact_name: The name of the artifact. + :type artifact_name: str + :param artifact_version: The version of the artifact. + :type artifact_version: str + :param file_path: The path to the VHD file. + :type file_path: Path + :param default_config: The default configuration. + :type default_config: Optional[Dict[str, Any]] + :param blob_sas_uri: The blob SAS URI. + :type blob_sas_uri: Optional[str] + """ + + def __init__( + self, + artifact_name: str, + artifact_version: str, + source_acr_registry: str, + default_config: Optional[Dict[str, Any]] = None, + ): + super().__init__(artifact_name, artifact_version, default_config) + self.source_acr_registry = source_acr_registry + + def get_defaults(self) -> Dict[str, Any]: + """ + Gets the default values for configuring the input. + + For Nexus images, there are no defaults. + :return: An empty dictionary. + :rtype: Dict[str, Any] + """ + return {} + + def get_schema(self) -> Dict[str, Any]: + """ + Gets the schema for the file input. + + For Nexus images, there is no schema. + :return: An empty dictionary. + :rtype: Dict[str, Any] + """ + return {} diff --git a/src/aosm/azext_aosm/inputs/nfd_input.py b/src/aosm/azext_aosm/inputs/nfd_input.py index 1d9fbd0ddeb..de8504c3a6f 100644 --- a/src/aosm/azext_aosm/inputs/nfd_input.py +++ b/src/aosm/azext_aosm/inputs/nfd_input.py @@ -11,7 +11,11 @@ from azext_aosm.common.constants import BASE_SCHEMA from azext_aosm.inputs.base_input import BaseInput -from azext_aosm.vendored_sdks.models import NetworkFunctionDefinitionVersion +from azext_aosm.vendored_sdks.models import ( + NetworkFunctionDefinitionVersion, + ContainerizedNetworkFunctionDefinitionVersion, + VirtualNetworkFunctionDefinitionVersion, +) logger = get_logger(__name__) @@ -72,9 +76,22 @@ def get_defaults(self) -> Dict[str, Any]: } } - if self.network_function_definition.properties and ( - self.network_function_definition.properties.network_function_type - == "VirtualNetworkFunction" + # This horrendous if statement is required because: + # - the 'properties' and 'network_function_template' attributes are optional + # - the isinstance check is because the base NetworkFunctionDefinitionVersionPropertiesFormat class + # doesn't define the network_function_template attribute, even though both subclasses do. + # Not switching to EAFP style because mypy doesn't account for `except AttributeError` (for good reason). + # Similar test required in the NFD processor, but we can't deduplicate the code because mypy doesn't + # propagate type narrowing from isinstance(). + if ( + self.network_function_definition.properties + and isinstance( + self.network_function_definition.properties, + (ContainerizedNetworkFunctionDefinitionVersion, VirtualNetworkFunctionDefinitionVersion), + ) + and self.network_function_definition.properties.network_function_template + and self.network_function_definition.properties.network_function_template.nfvi_type + not in ("AzureArcKubernetes", "AzureOperatorNexus") ): base_defaults["configObject"]["customLocationId"] = "" diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_filepath.json b/src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_filepath copy.json similarity index 100% rename from src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_filepath.json rename to src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_filepath copy.json diff --git a/src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_filepath.jsonc b/src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_filepath.jsonc new file mode 100644 index 00000000000..d9eb7e128e4 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_filepath.jsonc @@ -0,0 +1,56 @@ +{ + // Azure location to use when creating resources. + "location": "eastus", + // Name of the Publisher resource you want your definition published to. + // Will be created if it does not exist. + "publisher_name": "jamie-mobile-publisher", + // Optional. Resource group for the Publisher resource. + // Will be created if it does not exist (with a default name if none is supplied). + "publisher_resource_group_name": "Jamie-publisher", + // Optional. Name of the ACR Artifact Store resource. + // Will be created if it does not exist (with a default name if none is supplied). + "acr_artifact_store_name": "ubuntu-acr", + // Name of NF definition. + "nf_name": "ubuntu-vm", + // Version of the NF definition in A.B.C format. + "version": "1.0.0", + // Optional. Name of the storage account Artifact Store resource. + // Will be created if it does not exist (with a default name if none is supplied). + "blob_artifact_store_name": "ubuntu-blob-store", + // The parameter name in the VM ARM template which specifies the name of the image to use for the VM. + "image_name_parameter": "imageName", + // ARM template configuration. + "arm_templates": [ + { + // Name of the artifact. + "artifact_name": "test-art", + // Version of the artifact in A.B.C format. + "version": "1.0.0", + // File path of the artifact you wish to upload from your local disk. + // Relative paths are relative to the configuration file. On Windows escape any backslash with another backslash. + "file_path": "ubuntu-template.json" + } + ], + // VHD image configuration. + "vhd": { + // Optional. Name of the artifact. + "artifact_name": "", + // Version of the artifact in A-B-C format. + "version": "1-0-0", + // 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. + "file_path": "livecd.ubuntu-cpc.azure.vhd", + // Optional. SAS URL of the blob artifact you wish to copy to your Artifact Store. + // Delete if not required. On Windows escape any backslash with another backslash. + "blob_sas_url": "", + // Optional. Specifies the size of empty data disks in gigabytes. + // This value cannot be larger than 1023 GB. Delete if not required. + "image_disk_size_GB": "", + // Optional. Specifies the HyperVGenerationType of the VirtualMachine created from the image. + // Valid values are V1 and V2. V1 is the default if not specified. Delete if not required. + "image_hyper_v_generation": "", + // Optional. The ARM API version used to create the Microsoft.Compute/images resource. + // Delete if not required. + "image_api_version": "" + } +} \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_fp.json b/src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_fp.json similarity index 100% rename from src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_fp.json rename to src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_fp.json diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_sas.json b/src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_sas.json similarity index 100% rename from src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_sas.json rename to src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_sas.json diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_sas_token.json b/src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_sas_token.json similarity index 100% rename from src/aosm/azext_aosm/tests/latest/mock_vnf/input_with_sas_token.json rename to src/aosm/azext_aosm/tests/latest/mock_core_vnf/input_with_sas_token.json diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf/ubuntu-template.json b/src/aosm/azext_aosm/tests/latest/mock_core_vnf/ubuntu-template.json similarity index 100% rename from src/aosm/azext_aosm/tests/latest/mock_vnf/ubuntu-template.json rename to src/aosm/azext_aosm/tests/latest/mock_core_vnf/ubuntu-template.json diff --git a/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_filepath.json b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_filepath.json new file mode 100644 index 00000000000..b3a3b991a1d --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_filepath.json @@ -0,0 +1,18 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "file_path": "livecd.ubuntu-cpc.azure.vhd", + "version": "1-0-0" + } +} diff --git a/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_fp.json b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_fp.json new file mode 100644 index 00000000000..b3a3b991a1d --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_fp.json @@ -0,0 +1,18 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "file_path": "livecd.ubuntu-cpc.azure.vhd", + "version": "1-0-0" + } +} diff --git a/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_sas.json b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_sas.json new file mode 100644 index 00000000000..5222d940186 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_sas.json @@ -0,0 +1,21 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "blob_sas_url": "https://a/dummy/sas-url", + "version": "1-0-0", + "image_disk_size_GB": 30, + "image_hyper_v_generation": "V1", + "image_api_version": "2023-03-01" + } +} \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_sas_token.json b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_sas_token.json new file mode 100644 index 00000000000..5222d940186 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/input_with_sas_token.json @@ -0,0 +1,21 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "blob_sas_url": "https://a/dummy/sas-url", + "version": "1-0-0", + "image_disk_size_GB": 30, + "image_hyper_v_generation": "V1", + "image_api_version": "2023-03-01" + } +} \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/ubuntu-template.json b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/ubuntu-template.json new file mode 100644 index 00000000000..378927a3fe5 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_nexus_vnf/ubuntu-template.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.15.31.15270", + "templateHash": "1656082395923655778" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "subnetName": { + "type": "string" + }, + "ubuntuVmName": { + "type": "string", + "defaultValue": "ubuntu-vm" + }, + "virtualNetworkId": { + "type": "string" + }, + "sshPublicKeyAdmin": { + "type": "string" + }, + "imageName": { + "type": "string" + } + }, + "variables": { + "imageResourceGroup": "[resourceGroup().name]", + "subscriptionId": "[subscription().subscriptionId]", + "vmSizeSku": "Standard_D2s_v3" + }, + "resources": [ + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2021-05-01", + "name": "[format('{0}_nic', parameters('ubuntuVmName'))]", + "location": "[parameters('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[format('{0}/subnets/{1}', parameters('virtualNetworkId'), parameters('subnetName'))]" + }, + "primary": true, + "privateIPAddressVersion": "IPv4" + } + } + ] + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-07-01", + "name": "[parameters('ubuntuVmName')]", + "location": "[parameters('location')]", + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSizeSku')]" + }, + "storageProfile": { + "imageReference": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('subscriptionId'), variables('imageResourceGroup')), 'Microsoft.Compute/images', parameters('imageName'))]" + }, + "osDisk": { + "osType": "Linux", + "name": "[format('{0}_disk', parameters('ubuntuVmName'))]", + "createOption": "FromImage", + "caching": "ReadWrite", + "writeAcceleratorEnabled": false, + "managedDisk": "[json('{\"storageAccountType\": \"Premium_LRS\"}')]", + "deleteOption": "Delete", + "diskSizeGB": 30 + } + }, + "osProfile": { + "computerName": "[parameters('ubuntuVmName')]", + "adminUsername": "azureuser", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "/home/azureuser/.ssh/authorized_keys", + "keyData": "[parameters('sshPublicKeyAdmin')]" + } + ] + }, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}_nic', parameters('ubuntuVmName')))]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}_nic', parameters('ubuntuVmName')))]" + ] + } + ] +} \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_filepath.json b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_filepath.json new file mode 100644 index 00000000000..b3a3b991a1d --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_filepath.json @@ -0,0 +1,18 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "file_path": "livecd.ubuntu-cpc.azure.vhd", + "version": "1-0-0" + } +} diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_fp.json b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_fp.json new file mode 100644 index 00000000000..b3a3b991a1d --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_fp.json @@ -0,0 +1,18 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "file_path": "livecd.ubuntu-cpc.azure.vhd", + "version": "1-0-0" + } +} diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_sas.json b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_sas.json new file mode 100644 index 00000000000..5222d940186 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_sas.json @@ -0,0 +1,21 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "blob_sas_url": "https://a/dummy/sas-url", + "version": "1-0-0", + "image_disk_size_GB": 30, + "image_hyper_v_generation": "V1", + "image_api_version": "2023-03-01" + } +} \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_sas_token.json b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_sas_token.json new file mode 100644 index 00000000000..5222d940186 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/input_with_sas_token.json @@ -0,0 +1,21 @@ +{ + "publisher_name": "jamie-mobile-publisher", + "publisher_resource_group_name": "Jamie-publisher", + "nf_name": "ubuntu-vm", + "version": "1.0.0", + "acr_artifact_store_name": "ubuntu-acr", + "location": "eastus", + "blob_artifact_store_name": "ubuntu-blob-store", + "image_name_parameter": "imageName", + "arm_template": { + "file_path": "ubuntu-template.json", + "version": "1.0.0" + }, + "vhd": { + "blob_sas_url": "https://a/dummy/sas-url", + "version": "1-0-0", + "image_disk_size_GB": 30, + "image_hyper_v_generation": "V1", + "image_api_version": "2023-03-01" + } +} \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/ubuntu-template.json b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/ubuntu-template.json new file mode 100644 index 00000000000..378927a3fe5 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/mock_vnf_OUTDATED/ubuntu-template.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.15.31.15270", + "templateHash": "1656082395923655778" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "subnetName": { + "type": "string" + }, + "ubuntuVmName": { + "type": "string", + "defaultValue": "ubuntu-vm" + }, + "virtualNetworkId": { + "type": "string" + }, + "sshPublicKeyAdmin": { + "type": "string" + }, + "imageName": { + "type": "string" + } + }, + "variables": { + "imageResourceGroup": "[resourceGroup().name]", + "subscriptionId": "[subscription().subscriptionId]", + "vmSizeSku": "Standard_D2s_v3" + }, + "resources": [ + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2021-05-01", + "name": "[format('{0}_nic', parameters('ubuntuVmName'))]", + "location": "[parameters('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[format('{0}/subnets/{1}', parameters('virtualNetworkId'), parameters('subnetName'))]" + }, + "primary": true, + "privateIPAddressVersion": "IPv4" + } + } + ] + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-07-01", + "name": "[parameters('ubuntuVmName')]", + "location": "[parameters('location')]", + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSizeSku')]" + }, + "storageProfile": { + "imageReference": { + "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('subscriptionId'), variables('imageResourceGroup')), 'Microsoft.Compute/images', parameters('imageName'))]" + }, + "osDisk": { + "osType": "Linux", + "name": "[format('{0}_disk', parameters('ubuntuVmName'))]", + "createOption": "FromImage", + "caching": "ReadWrite", + "writeAcceleratorEnabled": false, + "managedDisk": "[json('{\"storageAccountType\": \"Premium_LRS\"}')]", + "deleteOption": "Delete", + "diskSizeGB": 30 + } + }, + "osProfile": { + "computerName": "[parameters('ubuntuVmName')]", + "adminUsername": "azureuser", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "/home/azureuser/.ssh/authorized_keys", + "keyData": "[parameters('sshPublicKeyAdmin')]" + } + ] + }, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}_nic', parameters('ubuntuVmName')))]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}_nic', parameters('ubuntuVmName')))]" + ] + } + ] +} \ No newline at end of file diff --git a/src/aosm/azext_aosm/tests/latest/unit_test/test_artifact_builder.py b/src/aosm/azext_aosm/tests/latest/unit_test/test_artifact_builder.py index 9b0ec161e02..7be432b63b6 100644 --- a/src/aosm/azext_aosm/tests/latest/unit_test/test_artifact_builder.py +++ b/src/aosm/azext_aosm/tests/latest/unit_test/test_artifact_builder.py @@ -1,40 +1,41 @@ -# # -------------------------------------------------------------------------------------------- -# # Copyright (c) Microsoft Corporation. All rights reserved. -# # Licensed under the MIT License. See License.txt in the project root for license information. -# # -------------------------------------------------------------------------------------------- - -# from pathlib import Path -# from unittest import TestCase -# from unittest.mock import MagicMock, patch - -# from azext_aosm.definition_folder.builder.artifact_builder import ArtifactDefinitionElementBuilder - - -# class TestArtifactDefinitionElementBuilder(TestCase): -# """Test the artifact definition element builder.""" - -# @patch("pathlib.Path.write_text") -# @patch("pathlib.Path.mkdir") -# def test_write(self, mock_mkdir, mock_write_text): -# """Test writing the definition element to disk.""" - -# # Create some mocks to act as artifacts. -# artifact_1 = MagicMock() -# artifact_1.to_dict.return_value = {"abc": "def"} -# artifact_2 = MagicMock() -# artifact_2.to_dict.return_value = {"ghi": "jkl"} - -# # Create a Artifact definition element builder. -# artifact_definition_element_builder = ArtifactDefinitionElementBuilder( -# Path("/some/folder"), -# [artifact_1, artifact_2] -# ) - -# # Write the definition element to disk. -# artifact_definition_element_builder.write() - -# # Check that the definition element was written to disk. -# mock_mkdir.assert_called_once() -# artifact_1.to_dict.assert_called_once() -# artifact_2.to_dict.assert_called_once() -# mock_write_text.assert_called_once_with('[{"abc": "def"}, {"ghi": "jkl"}]') +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from pathlib import Path +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from azext_aosm.definition_folder.builder.artifact_builder import ArtifactDefinitionElementBuilder + + +class TestArtifactDefinitionElementBuilder(TestCase): + """Test the artifact definition element builder.""" + + @patch("pathlib.Path.write_text") + @patch("pathlib.Path.mkdir") + def test_write(self, mock_mkdir, mock_write_text): + """Test writing the definition element to disk.""" + + # Create some mocks to act as artifacts. + artifact_1 = MagicMock() + artifact_1.to_dict.return_value = {"abc": "def"} + artifact_2 = MagicMock() + artifact_2.to_dict.return_value = {"ghi": "jkl"} + + # Create a Artifact definition element builder. + artifact_definition_element_builder = ArtifactDefinitionElementBuilder( + Path("/some/folder"), + [artifact_1, artifact_2] + ) + + # Write the definition element to disk. + artifact_definition_element_builder.write() + + # Check that the definition element was written to disk. + mock_mkdir.assert_called_once() + artifact_1.to_dict.assert_called_once() + artifact_2.to_dict.assert_called_once() + mock_write_text.assert_called_once_with( + '[\n {\n "abc": "def"\n },\n {\n "ghi": "jkl"\n }\n]') diff --git a/src/aosm/azext_aosm/tests/latest/unit_test/test_core_vnf_handler.py b/src/aosm/azext_aosm/tests/latest/unit_test/test_core_vnf_handler.py new file mode 100644 index 00000000000..06ce64c0991 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/unit_test/test_core_vnf_handler.py @@ -0,0 +1,105 @@ +# from unittest import TestCase +# from unittest.mock import patch, MagicMock, Mock +# from pathlib import Path +# from typing import List +# from azext_aosm.cli_handlers.onboarding_core_vnf_handler import OnboardingCoreVNFCLIHandler +# from azext_aosm.definition_folder.builder.artifact_builder import ArtifactDefinitionElementBuilder +# from azext_aosm.definition_folder.builder.bicep_builder import BicepDefinitionElementBuilder +# from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder +# from azext_aosm.common.constants import VNF_OUTPUT_FOLDER_FILENAME, ARTIFACT_LIST_FILENAME +# from azext_aosm.build_processors.arm_processor import AzureCoreArmBuildProcessor +# from azext_aosm.build_processors.vhd_processor import VHDProcessor +# from azext_aosm.vendored_sdks.models import ( +# AzureCoreVhdImageDeployMappingRuleProfile, AzureCoreNetworkFunctionVhdApplication, +# AzureCoreVhdImageArtifactProfile, VhdImageArtifactProfile, VhdImageMappingRuleProfile, ApplicationEnablement +# ) + +# class VNFCoreBuildTest(TestCase): + +# def setUp(self): +# self.vnf_handler = OnboardingCoreVNFCLIHandler() + +# # def test_valid_nexus_config_provided(): +# # # give it nexus specific config +# # pass + +# # def test_invalid_nexus_config_provided(): +# # # give it nexus specific config with an error +# # pass + +# # def test_core_config_provided(): +# # # give it core specific config +# # pass + +# # # def test_build_base_bicep(self): +# # # with patch("pathlib.Path.write_text") as mock_write_text: +# # # self.nexus_vnf_cli_handler.build_base_bicep() +# # # mock_write_text.assert_called() + +# def test_build_artifact_list_type(self): +# """ Testing build artifact list for Nexus VNFs + +# Test if path is as expected, and if list returned is correct type +# """ +# self.vnf_handler.processors = MagicMock() +# artifact_list = self.vnf_handler.build_artifact_list() +# self.assertEqual(artifact_list.path, Path(VNF_OUTPUT_FOLDER_FILENAME, ARTIFACT_LIST_FILENAME)) +# self.assertIsInstance(artifact_list, ArtifactDefinitionElementBuilder) + +# def test_build_resource_bicep_type(self): +# """Testing build resource bicep for Nexus VNFs + +# We only need to test the type of the bicep and the supporting files, +# and that they have the correct names. +# This is because the complicated logic is tested in the processors tests. + +# """ +# # we are testing the rest of the logic in the processors? (are we? we should) +# # TODO: fix this mocking, it works for deploymentParameters but not for actual processors (duh?) +# # mocked_input = MagicMock() +# # arm_processor = AzureCoreArmProcessor(mocked_input) +# arm_input = MagicMock() +# vhd_input = MagicMock() +# arm_processor = AzureCoreArmBuildProcessor("arm_test", arm_input) +# vhd_processor = VHDProcessor("test_vhd", vhd_input) +# arm_processor = MagicMock(spec=AzureCoreArmBuildProcessor) +# vhd_processor = MagicMock(spec=VHDProcessor) +# # arm_processor = Mock(spec=AzureCoreArmBuildProcessor) +# # vhd_processor = Mock(spec=VHDProcessor) +# # assert isinstance(arm_processor, AzureCoreArmBuildProcessor) +# # # TODO: one of these returns local file build +# # vhd_processor.generate_nf_application.return_value = AzureCoreNetworkFunctionVhdApplication(name="test",depends_on_profile=None, +# # artifact_profile=AzureCoreVhdImageArtifactProfile( +# # artifact_store=None, +# # vhd_artifact_profile=VhdImageArtifactProfile( +# # vhd_name="vhd_name", +# # vhd_version="1-0-0", +# # ),), deploy_parameters_mapping_rule_profile=AzureCoreVhdImageDeployMappingRuleProfile( +# # application_enablement=ApplicationEnablement.ENABLED, +# # vhd_image_mapping_rule_profile=VhdImageMappingRuleProfile(user_configuration=None) +# # ) +# # ) +# arm_processor.generate_params_schema.return_value = {} +# # arm_processor.generate_parameters_file.return_value = LocalFileBuilder("", {}) +# vhd_processor.generate_params_schema.return_value = {} +# # We want to test a specific private method so disable the pylint warning +# # pylint: disable=protected-access +# vhd_processor._generate_mapping_rule_profile.return_value = AzureCoreVhdImageDeployMappingRuleProfile(application_enablement=None, vhd_image_mapping_rule_profile=None) +# self.vnf_handler.processors = [arm_processor, vhd_processor] +# resource_bicep = self.vnf_handler.build_resource_bicep() +# print(resource_bicep.supporting_files[1].path) +# # TODO: check that the nexus one contains deploymentParameters, imageParameters and at least one templateParams? +# # special assert? +# self.assertIsInstance(resource_bicep.supporting_files, List[LocalFileBuilder]) +# self.assertIsInstance(resource_bicep, BicepDefinitionElementBuilder) + +# # def test_build_all_parameters_json(): +# # def test_build_artifact_manifest(self): +# # # self.vnf_handler._generate_type_specific_artifact_manifest +# # self.vnf_handler.processors = MagicMock() +# # manifest_bicep = self.vnf_handler.build_manifest_bicep() +# # # We want to test a specific private method so disable the pylint warning +# # # pylint: disable=protected-access +# # (arm_list, sa_list) = self.vnf_handler._generate_type_specific_artifact_manifest(self.vnf_handler.processors[0]) +# # self.assertEqual() + diff --git a/src/aosm/azext_aosm/tests/latest/unit_test/test_nexus_vnf_handler.py b/src/aosm/azext_aosm/tests/latest/unit_test/test_nexus_vnf_handler.py new file mode 100644 index 00000000000..a4d74cf196d --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/unit_test/test_nexus_vnf_handler.py @@ -0,0 +1,61 @@ +# from unittest import TestCase +# from unittest.mock import patch, MagicMock +# from pathlib import Path +# from typing import List +# from azext_aosm.cli_handlers.onboarding_nexus_vnf_handler import OnboardingNexusVNFCLIHandler +# from azext_aosm.definition_folder.builder.artifact_builder import ArtifactDefinitionElementBuilder +# from azext_aosm.definition_folder.builder.bicep_builder import BicepDefinitionElementBuilder +# from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder +# from azext_aosm.common.constants import VNF_OUTPUT_FOLDER_FILENAME, ARTIFACT_LIST_FILENAME + + +# class VNFNexusBuildTest(TestCase): + +# def setUp(self): +# self.nexus_handler = OnboardingNexusVNFCLIHandler() + +# # def test_valid_nexus_config_provided(): +# # # give it nexus specific config +# # pass + +# # def test_invalid_nexus_config_provided(): +# # # give it nexus specific config with an error +# # pass + +# # def test_core_config_provided(): +# # # give it core specific config +# # pass + +# # # def test_build_base_bicep(self): +# # # with patch("pathlib.Path.write_text") as mock_write_text: +# # # self.nexus_vnf_cli_handler.build_base_bicep() +# # # mock_write_text.assert_called() + +# def test_build_artifact_list_type(self): +# """ Testing build artifact list for Nexus VNFs + +# Test if path is as expected, and if list returned is correct type +# """ +# self.nexus_handler.processors = MagicMock() +# artifact_list = self.nexus_handler.build_artifact_list() +# self.assertEqual(artifact_list.path, Path(VNF_OUTPUT_FOLDER_FILENAME, ARTIFACT_LIST_FILENAME)) +# self.assertIsInstance(artifact_list, ArtifactDefinitionElementBuilder) + +# def test_build_resource_bicep_type(self): +# """Testing build resource bicep for Nexus VNFs + +# We only need to test the type of the bicep and the supporting files, +# and that they have the correct names. +# This is because the complicated logic is tested in the processors tests. + +# """ +# # we are testing the rest of the logic in the processors? (are we? we should) +# # TODO: fix this mocking, it works for deploymentParameters but not for actual processors (duh?) +# self.nexus_handler.processors = MagicMock() +# resource_bicep = self.nexus_handler.build_resource_bicep() +# print(resource_bicep.supporting_files[0].path) +# # TODO: check that the nexus one contains deploymentParameters, imageParameters and at least one templateParams? +# self.assertIsInstance(resource_bicep.supporting_files, List[LocalFileBuilder]) +# self.assertIsInstance(resource_bicep, BicepDefinitionElementBuilder) + +# # def test_build_all_parameters_json(): diff --git a/src/aosm/azext_aosm/tests/latest/unit_test/test_nsd_cli_handler.py b/src/aosm/azext_aosm/tests/latest/unit_test/test_nsd_cli_handler.py index 3a905fd18ba..87cc3f9c8e2 100644 --- a/src/aosm/azext_aosm/tests/latest/unit_test/test_nsd_cli_handler.py +++ b/src/aosm/azext_aosm/tests/latest/unit_test/test_nsd_cli_handler.py @@ -5,7 +5,7 @@ # from azure.cli.core.azclierror import ( # UnclassifiedUserFault, # ) -# TODO: Fix tests with correct mocking for input() +# # TODO: Fix tests with correct mocking for input() # @patch("pathlib.Path.exists") # class TestNsdCliHandler(TestCase): diff --git a/src/aosm/azext_aosm/tests/latest/unit_test/test_processors/test_nexus_arm_processor.py b/src/aosm/azext_aosm/tests/latest/unit_test/test_processors/test_nexus_arm_processor.py new file mode 100644 index 00000000000..78b08111e63 --- /dev/null +++ b/src/aosm/azext_aosm/tests/latest/unit_test/test_processors/test_nexus_arm_processor.py @@ -0,0 +1,24 @@ +from unittest import TestCase +from unittest.mock import patch, MagicMock +from pathlib import Path +from typing import List +from azext_aosm.build_processors.arm_processor import NexusArmBuildProcessor +from azext_aosm.inputs.arm_template_input import ArmTemplateInput + +from azext_aosm.cli_handlers.onboarding_nexus_vnf_handler import OnboardingNexusVNFCLIHandler +from azext_aosm.definition_folder.builder.artifact_builder import ArtifactDefinitionElementBuilder +from azext_aosm.definition_folder.builder.bicep_builder import BicepDefinitionElementBuilder +from azext_aosm.definition_folder.builder.local_file_builder import LocalFileBuilder +from azext_aosm.common.constants import VNF_OUTPUT_FOLDER_FILENAME, ARTIFACT_LIST_FILENAME + + +class NexusArmProcessorTest(TestCase): + + def setUp(self): + nexus_arm_input = ArmTemplateInput( + artifact_name="test-artifact-name", + artifact_version="1.1.1", + template_path="", + default_config=None + ) + self.processor = NexusArmBuildProcessor() diff --git a/src/aosm/setup.py b/src/aosm/setup.py index 36a210d0207..09285dd7913 100644 --- a/src/aosm/setup.py +++ b/src/aosm/setup.py @@ -17,7 +17,7 @@ # Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = "1.0.0b5" +VERSION = "1.0.0b8" # The full list of classifiers is available at