Skip to content

Commit

Permalink
Nsd for cnfs (Azure#33)
Browse files Browse the repository at this point in the history
* NSD building for CNFs

* linting
  • Loading branch information
sunnycarter authored Jun 22, 2023
1 parent 9419ef9 commit 4013895
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 67 deletions.
4 changes: 4 additions & 0 deletions src/aosm/azext_aosm/_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"Exising Network Function Definition Version Name. "
"This can be created using the 'az aosm nfd' commands.",
"network_function_definition_offering_location": "Offering location of the Network Function Definition",
"network_function_type": "Type of nf in the definition. Valid values are 'cnf' or 'vnf'",
"helm_package_name": "Name of the Helm package",
"path_to_chart":
"File path of Helm Chart on local disk. Accepts .tgz, .tar or .tar.gz",
Expand Down Expand Up @@ -123,6 +124,7 @@ class NSConfiguration:
network_function_definition_offering_location: str = DESCRIPTION_MAP[
"network_function_definition_offering_location"
]
network_function_type: str = DESCRIPTION_MAP["network_function_type"]
nsdg_name: str = DESCRIPTION_MAP["nsdg_name"]
nsd_version: str = DESCRIPTION_MAP["nsd_version"]
nsdv_description: str = DESCRIPTION_MAP["nsdv_description"]
Expand Down Expand Up @@ -165,6 +167,8 @@ def validate(self):
raise ValueError(
"Network Function Definition Offering Location must be set"
)
if self.network_function_type not in [CNF, VNF]:
raise ValueError("Network Function Type must be cnf or vnf")
if self.nsdg_name == DESCRIPTION_MAP["nsdg_name"] or "":
raise ValueError("NSDG name must be set")
if self.nsd_version == DESCRIPTION_MAP["nsd_version"] or "":
Expand Down
30 changes: 6 additions & 24 deletions src/aosm/azext_aosm/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def _generate_config(configuration_type: str, output_file: str = "input.json"):

with open(output_file, "w", encoding="utf-8") as f:
f.write(config_as_dict)
if configuration_type in (CNF,VNF):
if configuration_type in (CNF, VNF):
prtName = "definition"
else:
prtName = "design"
Expand All @@ -264,7 +264,7 @@ def build_design(cmd, client: HybridNetworkManagementClient, config_file: str):

# Read the config from the given file
config = _get_config_from_file(config_file=config_file, configuration_type=NSD)

assert isinstance(config, NSConfiguration)
config.validate()

# Generate the NSD and the artifact manifest.
Expand Down Expand Up @@ -344,14 +344,8 @@ def publish_design(
)


def _generate_nsd(config: NSDGenerator, api_clients):
"""Generate a Network Service Design for the given type and config."""
if config:
nsd_generator = NSDGenerator(config)
else:
raise CLIInternalError("Generate NSD called without a config file")
deploy_parameters = _get_nfdv_deployment_parameters(config, api_clients)

def _generate_nsd(config: NSConfiguration, api_clients: ApiClients):
"""Generate a Network Service Design for the given config."""
if os.path.exists(config.build_output_folder_name):
carry_on = input(
f"The folder {config.build_output_folder_name} already exists - delete it and continue? (y/n)"
Expand All @@ -360,17 +354,5 @@ def _generate_nsd(config: NSDGenerator, api_clients):
raise UnclassifiedUserFault("User aborted! ")

shutil.rmtree(config.build_output_folder_name)

nsd_generator.generate_nsd(deploy_parameters)


def _get_nfdv_deployment_parameters(config: NSConfiguration, api_clients):
"""Get the properties of the existing NFDV."""
NFDV_object = api_clients.aosm_client.network_function_definition_versions.get(
resource_group_name=config.publisher_resource_group_name,
publisher_name=config.publisher_name,
network_function_definition_group_name=config.network_function_definition_group_name,
network_function_definition_version_name=config.network_function_definition_version_name,
)

return NFDV_object.deploy_parameters
nsd_generator = NSDGenerator(api_clients, config)
nsd_generator.generate_nsd()
89 changes: 59 additions & 30 deletions src/aosm/azext_aosm/generate_nfd/cnf_nfd_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
SCHEMA_PREFIX,
SCHEMAS,
IMAGE_PULL_SECRETS_START_STRING,
IMAGE_START_STRING
IMAGE_START_STRING,
)
from azext_aosm.util.utils import input_ack

Expand Down Expand Up @@ -222,7 +222,8 @@ def _generate_chart_value_mappings(self, helm_package: HelmPackageConfig) -> Non
def _read_top_level_values_yaml(
self, helm_package: HelmPackageConfig
) -> Dict[str, Any]:
"""Return a dictionary of the values.yaml|yml read from the root of the helm package.
"""
Return a dictionary of the values.yaml|yml read from the root of the helm package.
:param helm_package: The helm package to look in
:type helm_package: HelmPackageConfig
Expand Down Expand Up @@ -387,16 +388,15 @@ def find_pattern_matches_in_chart(
"""
Find pattern matches in Helm chart, using provided REGEX pattern.
param helm_package: The helm package config.
param start_string: The string to search for, either imagePullSecrets: or image:
:param helm_package: The helm package config.
:param start_string: The string to search for, either imagePullSecrets: or image:
If searching for imagePullSecrets,
returns list of lists containing image pull secrets paths,
e.g. Values.foo.bar.imagePullSecret
If searching for imagePullSecrets, returns list of lists containing image pull
secrets paths, e.g. Values.foo.bar.imagePullSecret
If searching for image,
returns list of tuples containing the list of image paths and the name and version of the image.
e.g. (Values.foo.bar.repoPath, foo, 1.2.3)
If searching for image, returns list of tuples containing the list of image
paths and the name and version of the image. e.g. (Values.foo.bar.repoPath, foo,
1.2.3)
"""
chart_dir = os.path.join(self._tmp_folder_name, helm_package.name)
matches = []
Expand All @@ -409,8 +409,16 @@ def find_pattern_matches_in_chart(
path = re.findall(IMAGE_PATH_REGEX, line)
# If "image:", search for chart name and version
if start_string == IMAGE_START_STRING:
name_and_version = re.search(IMAGE_NAME_AND_VERSION_REGEX, line)
matches.append((path, name_and_version.group(1), name_and_version.group(2)))
name_and_version = re.search(
IMAGE_NAME_AND_VERSION_REGEX, line
)
matches.append(
(
path,
name_and_version.group(1),
name_and_version.group(2),
)
)
else:
matches += path
return matches
Expand All @@ -423,8 +431,8 @@ def get_artifact_list(
"""
Get the list of artifacts for the chart.
param helm_package: The helm package config. param image_line_matches: The list
of image line matches.
:param helm_package: The helm package config.
:param image_line_matches: The list of image line matches.
"""
artifact_list = []
(chart_name, chart_version) = self.get_chart_name_and_version(helm_package)
Expand Down Expand Up @@ -478,7 +486,9 @@ def get_chart_mapping_schema(
schema_data = json.load(f)

try:
deploy_params_dict = self.traverse_dict(values_data, DEPLOYMENT_PARAMETER_MAPPING_REGEX)
deploy_params_dict = self.traverse_dict(
values_data, DEPLOYMENT_PARAMETER_MAPPING_REGEX
)
new_schema = self.search_schema(deploy_params_dict, schema_data)
except KeyError as e:
raise InvalidTemplateError(
Expand All @@ -492,24 +502,39 @@ def get_chart_mapping_schema(
def traverse_dict(self, d, target):
"""
Traverse the dictionary that is loaded from the file provided by path_to_mappings in the input.json.
Returns a dictionary of all the values that match the target regex,
with the key being the deploy parameter and the value being the path to the value.
e.g. {"foo": ["global", "foo", "bar"]}
param d: The dictionary to traverse.
param target: The regex to search for.
:param d: The dictionary to traverse.
:param target: The regex to search for.
"""
stack = [(d, [])] # Initialize the stack with the dictionary and an empty path
result = {} # Initialize empty dictionary to store the results
while stack: # While there are still items in the stack
# Pop the last item from the stack and unpack it into node (the dictionary) and path
(node, path) = stack.pop()
for k, v in node.items(): # For each key-value pair in the popped item
if isinstance(v, dict): # If the value is a dictionary
stack.append((v, path + [k])) # Add the dictionary to the stack with the path
elif isinstance(v, str) and re.search(target, v): # If the value is a string + matches target regex
match = re.search(target, v) # Take the match i.e, foo from {deployParameter.foo}
result[match.group(1)] = path + [k] # Add it to the result dictionary with its path as the value
# For each key-value pair in the popped item
for k, v in node.items():
# If the value is a dictionary
if isinstance(v, dict):
# Add the dictionary to the stack with the path
stack.append(
(v, path + [k])
)
# If the value is a string + matches target regex
elif isinstance(v, str) and re.search(
target, v
):
# Take the match i.e, foo from {deployParameter.foo}
match = re.search(
target, v
)
# Add it to the result dictionary with its path as the value
result[match.group(1)] = path + [
k
]
elif isinstance(v, list):
for i in v:
if isinstance(i, str) and re.search(target, i):
Expand Down Expand Up @@ -540,7 +565,7 @@ def search_schema(self, result, full_schema):
no_schema_list.append(deploy_param)
new_schema.update({deploy_param: {"type": "string"}})
if deploy_param not in new_schema:
new_schema.update({deploy_param: {"type": node.get('type', None)}})
new_schema.update({deploy_param: {"type": node.get("type", None)}})
if no_schema_list:
print("No schema found for deployment parameter(s):", no_schema_list)
print("We default these parameters to type string")
Expand Down Expand Up @@ -582,24 +607,28 @@ def _replace_values_with_deploy_params(
elif isinstance(v, list):
final_values_mapping_dict[k] = []
for index, item in enumerate(v):
param_name = f"{param_prefix}_{k}_{index}" if param_prefix else f"{k})_{index}"
param_name = (
f"{param_prefix}_{k}_{index}"
if param_prefix
else f"{k})_{index}"
)
if isinstance(item, dict):
final_values_mapping_dict[k].append(
self._replace_values_with_deploy_params(
item, param_name
)
self._replace_values_with_deploy_params(item, param_name)
)
elif isinstance(v, (str, int, bool)):
replacement_value = f"{{deployParameters.{param_name}}}"
final_values_mapping_dict[k].append(replacement_value)
else:
raise ValueError(
f"Found an unexpected type {type(v)} of key {k} in "
"values.yaml, cannot generate values mapping file.")
"values.yaml, cannot generate values mapping file."
)
else:
raise ValueError(
f"Found an unexpected type {type(v)} of key {k} in values.yaml, "
"cannot generate values mapping file.")
"cannot generate values mapping file."
)

return final_values_mapping_dict

Expand Down
68 changes: 57 additions & 11 deletions src/aosm/azext_aosm/generate_nsd/nsd_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@
# Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT
# License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------
"""Contains a class for generating VNF NFDs and associated resources."""
"""Contains a class for generating NSDs and associated resources."""
import json
import logging
import os
import shutil
import tempfile
from functools import cached_property
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Dict

from jinja2 import Template
from knack.log import get_logger
from azext_aosm.vendored_sdks.models import NFVIType

from azext_aosm._configuration import NSConfiguration
from azext_aosm.generate_nfd.nfd_generator_base import NFDGenerator
from azext_aosm.util.constants import (
CONFIG_MAPPINGS,
NF_DEFINITION_BICEP_FILE,
Expand All @@ -28,10 +25,22 @@
NSD_DEFINITION_BICEP_SOURCE_TEMPLATE,
SCHEMAS,
TEMPLATES,
VNF,
)
from azext_aosm.util.management_clients import ApiClients
from azext_aosm.vendored_sdks.models import NetworkFunctionDefinitionVersion

logger = get_logger(__name__)

# Different types are used in Bicep templates and NFDs. The list accepted by NFDs is
# documented in the AOSM meta-schema. This will be published in the future but for now
# can be found in
# https://microsoft.sharepoint.com/:w:/t/NSODevTeam/Ec7ovdKroSRIv5tumQnWIE0BE-B2LykRcll2Qb9JwfVFMQ
NFV_TO_BICEP_PARAM_TYPES: Dict[str, str] = {
"integer": "int",
"boolean": "bool",
}


class NSDGenerator:
"""
Expand All @@ -46,20 +55,42 @@ class NSDGenerator:
be deployed by the NSDV
"""

def __init__(self, config: NSConfiguration):
def __init__(self, api_clients: ApiClients, config: NSConfiguration):
self.config = config
self.nsd_bicep_template_name = NSD_DEFINITION_BICEP_SOURCE_TEMPLATE
self.nf_bicep_template_name = NF_TEMPLATE_BICEP_FILE
self.nsd_bicep_output_name = NSD_DEFINITION_BICEP_FILE

self.build_folder_name = self.config.build_output_folder_name
nfdv = self._get_nfdv(config, api_clients)
print("Finding the deploy parameters of the NFDV resource")
if not nfdv.deploy_parameters:
raise NotImplementedError(
"NFDV has no deploy parameters, cannot generate NSD."
)
self.deploy_parameters: str = nfdv.deploy_parameters

def _get_nfdv(
self, config: NSConfiguration, api_clients
) -> NetworkFunctionDefinitionVersion:
"""Get the existing NFDV resource object."""
print(
"Reading existing NFDV resource object "
f"{config.network_function_definition_version_name} from group "
f"{config.network_function_definition_group_name}"
)
nfdv_object = api_clients.aosm_client.network_function_definition_versions.get(
resource_group_name=config.publisher_resource_group_name,
publisher_name=config.publisher_name,
network_function_definition_group_name=config.network_function_definition_group_name,
network_function_definition_version_name=config.network_function_definition_version_name,
)
return nfdv_object

def generate_nsd(self, deploy_parameters) -> None:
def generate_nsd(self) -> None:
"""Generate a NSD templates which includes an Artifact Manifest, NFDV and NF templates."""
logger.info(f"Generate NSD bicep templates")

self.deploy_parameters = deploy_parameters

# Create temporary folder.
with tempfile.TemporaryDirectory() as tmpdirname:
self.tmp_folder_name = tmpdirname
Expand Down Expand Up @@ -131,13 +162,22 @@ def write_nf_bicep(self) -> None:
bicep_deploymentValues = ""

deploy_parameters_dict = json.loads(self.deploy_parameters)
if "properties" not in deploy_parameters_dict:
raise ValueError(
f"NFDV in {self.config.network_function_definition_group_name} has "
"no properties within deployParameters"
)

deploy_properties = deploy_parameters_dict["properties"]

for key, value in deploy_properties.items():
# location is sometimes part of deploy_properties.
# We want to avoid having duplicate params in the bicep template
if key != "location":
bicep_params += f"param {key} {value['type']}\n"
bicep_type = (
NFV_TO_BICEP_PARAM_TYPES.get(value["type"]) or value["type"]
)
bicep_params += f"param {key} {bicep_type}\n"
bicep_deploymentValues += f"{key}: {key}\n "

self.generate_bicep(
Expand All @@ -152,6 +192,12 @@ def write_nf_bicep(self) -> None:
"network_function_definition_version_name": self.config.network_function_definition_version_name,
"network_function_definition_offering_location": self.config.network_function_definition_offering_location,
"location": self.config.location,
# Ideally we would use the network_function_type from reading the actual
# NF, as we do for deployParameters, but the SDK currently doesn't
# support this and needs to be rebuilt to do so.
"nfvi_type": NFVIType.AZURE_CORE
if self.config.network_function_type == VNF
else NFVIType.AZURE_ARC_KUBERNETES.value,
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ resource acrArtifactManifest 'Microsoft.Hybridnetwork/publishers/artifactStores/
artifacts: [
{
artifactName: armTemplateName
artifactType: 'OCIArtifact'
artifactType: 'ArmTemplate'
artifactVersion: armTemplateVersion
}
]
Expand Down
Loading

0 comments on commit 4013895

Please sign in to comment.