Skip to content

Commit

Permalink
Merge pull request #45 from jddarby/JDP/multiple_NFs
Browse files Browse the repository at this point in the history
Multiple NFs of the same type
  • Loading branch information
jamiedparsons authored Jul 17, 2023
2 parents 44344ff + 60140e2 commit ab98755
Show file tree
Hide file tree
Showing 11 changed files with 820 additions and 109 deletions.
1 change: 1 addition & 0 deletions src/aosm/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ unreleased
* Workaround Oras client bug (#90) on Windows for Artifact upload to ACR
* Take Oras 0.1.18 so above Workaround could be removed
* Take Oras 0.1.19 to fix NSD Artifact upload on Windows
* Support deploying multiple instances of the same NF in an SNS

0.2.0
++++++
Expand Down
17 changes: 16 additions & 1 deletion src/aosm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ https://github.com/jddarby/azure-cli-extensions/releases/download/aosm-extension
To install, download this wheel and run:
`az extension add --source path/to/aosm-0.2.0-py2.py3-none-any.whl`

You must also have helm installed, instructions can be found here: https://helm.sh/docs/intro/install/#through-package-managers

## Updating

We are currently not bumping versions, so if you would like the most up to date version of the CLI. You should run:
Expand Down Expand Up @@ -195,5 +197,18 @@ az config set logging.enable_log_file=false
```

## Development

Information about setting up and maintaining a development environment for this extension can be found [here](./development.md).

## Linting
Please run mypy on your changes and fix up any issues before merging.
```bash
cd src/aosm
mypy . --ignore-missing-imports --no-namespace-packages --exclude "azext_aosm/vendored_sdks/*"
```

## Pipelines
The pipelines for the Azure CLI run in ADO, not in github.
To trigger a pipeline you need to create a PR against main.
Until we do the initial merge to main we don't want to have a PR to main for every code review.
Instead we have a single PR for the `add-aosm-extension` branch: https://github.com/Azure/azure-cli-extensions/pull/6426
Once you have merged your changes to `add-aosm-extension` then look at the Azure Pipelines under https://github.com/Azure/azure-cli-extensions/pull/6426/checks, click on the link that says `<X> errors / <Y> warnings`.
20 changes: 19 additions & 1 deletion src/aosm/azext_aosm/_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union

from azure.cli.core.azclierror import InvalidArgumentValueError, ValidationError
from azext_aosm.util.constants import (
Expand Down Expand Up @@ -84,6 +84,11 @@
"network_function_type": (
"Type of nf in the definition. Valid values are 'cnf' or 'vnf'"
),
"multiple_instances": (
"Set to true or false. Whether the NSD should allow arbitrary numbers of this "
"type of NF. If set to false only a single instance will be allowed. Only "
"supported on VNFs, must be set to false on CNFs."
),
"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 @@ -221,6 +226,7 @@ class NSConfiguration(Configuration):
nsdg_name: str = DESCRIPTION_MAP["nsdg_name"]
nsd_version: str = DESCRIPTION_MAP["nsd_version"]
nsdv_description: str = DESCRIPTION_MAP["nsdv_description"]
multiple_instances: Union[str, bool] = DESCRIPTION_MAP["multiple_instances"]

def validate(self):
"""Validate that all of the configuration parameters are set."""
Expand Down Expand Up @@ -263,13 +269,25 @@ 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 "":
raise ValueError("NSD Version must be set")

if not isinstance(self.multiple_instances, bool):
raise ValueError("multiple_instances must be a boolean")

# There is currently a NFM bug that means that multiple copies of the same NF
# cannot be deployed to the same custom location:
# https://portal.microsofticm.com/imp/v3/incidents/details/405078667/home
if self.network_function_type == CNF and self.multiple_instances:
raise ValueError("Multiple instances is not supported on CNFs.")

@property
def output_directory_for_build(self) -> Path:
"""Return the local folder for generating the bicep template to."""
Expand Down
2 changes: 1 addition & 1 deletion src/aosm/azext_aosm/generate_nfd/cnf_nfd_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ def traverse_dict(
:param d: The dictionary to traverse.
:param target: The regex to search for.
"""

# pylint: disable=too-many-nested-blocks
@dataclass
class DictNode:
Expand All @@ -575,7 +576,6 @@ class DictNode:

# For each key-value pair in the popped item
for key, value in node.sub_dict.items():

# If the value is a dictionary
if isinstance(value, dict):
# Add the dictionary to the stack with the path
Expand Down
127 changes: 70 additions & 57 deletions src/aosm/azext_aosm/generate_nsd/nsd_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# --------------------------------------------------------------------------------------
"""Contains a class for generating NSDs and associated resources."""
import json
import copy
import os
import shutil
import tempfile
Expand Down Expand Up @@ -62,10 +61,6 @@ def __init__(self, api_clients: ApiClients, config: NSConfiguration):
self.nsd_bicep_template_name = NSD_DEFINITION_JINJA2_SOURCE_TEMPLATE
self.nf_bicep_template_name = NF_TEMPLATE_JINJA2_SOURCE_TEMPLATE
self.nsd_bicep_output_name = NSD_BICEP_FILENAME
self.nfdv_parameter_name = (
f"{self.config.network_function_definition_group_name.replace('-', '_')}"
"_nfd_version"
)
nfdv = self._get_nfdv(config, api_clients)
print("Finding the deploy parameters of the NFDV resource")
if not nfdv.deploy_parameters:
Expand All @@ -75,6 +70,10 @@ def __init__(self, api_clients: ApiClients, config: NSConfiguration):
self.deploy_parameters: Optional[Dict[str, Any]] = json.loads(
nfdv.deploy_parameters
)
self.nf_type = self.config.network_function_definition_group_name.replace(
"-", "_"
)
self.nfdv_parameter_name = f"{self.nf_type}_nfd_version"

# pylint: disable=no-self-use
def _get_nfdv(
Expand All @@ -100,7 +99,9 @@ def generate_nsd(self) -> None:

# Create temporary folder.
with tempfile.TemporaryDirectory() as tmpdirname:
self.tmp_folder_name = tmpdirname # pylint: disable=attribute-defined-outside-init
self.tmp_folder_name = (
tmpdirname # pylint: disable=attribute-defined-outside-init
)

self.create_config_group_schema_files()
self.write_nsd_manifest()
Expand All @@ -126,24 +127,12 @@ def config_group_schema_dict(self) -> Dict[str, Any]:
"""
assert self.deploy_parameters

# Take a copy of the deploy parameters.
cgs_dict = copy.deepcopy(self.deploy_parameters)

# Re-title it.
cgs_dict["title"] = self.config.cg_schema_name

# Add in the NFDV version as a parameter.
description_string = (
nfdv_version_description_string = (
f"The version of the {self.config.network_function_definition_group_name} "
"NFD to use. This version must be compatible with (have the same "
"parameters exposed as) "
f"{self.config.network_function_definition_version_name}."
)
cgs_dict["properties"][self.nfdv_parameter_name] = {
"type": "string",
"description": description_string,
}
cgs_dict.setdefault("required", []).append(self.nfdv_parameter_name)

managed_identity_description_string = (
"The managed identity to use to deploy NFs within this SNS. This should "
Expand All @@ -152,25 +141,65 @@ def config_group_schema_dict(self) -> Dict[str, Any]:
"userAssignedIdentities/{identityName}. "
"If you wish to use a system assigned identity, set this to a blank string."
)
cgs_dict["properties"]["managedIdentity"] = {
"type": "string",
"description": managed_identity_description_string,

if self.config.multiple_instances:
deploy_parameters = {
"type": "array",
"items": {
"type": "object",
"properties": self.deploy_parameters["properties"],
},
}
else:
deploy_parameters = {
"type": "object",
"properties": self.deploy_parameters["properties"],
}

cgs_dict: Dict[str, Any] = {
"$schema": "https://json-schema.org/draft-07/schema#",
"title": self.config.cg_schema_name,
"type": "object",
"properties": {
self.config.network_function_definition_group_name: {
"type": "object",
"properties": {
"deploymentParameters": deploy_parameters,
self.nfdv_parameter_name: {
"type": "string",
"description": nfdv_version_description_string,
},
},
"required": ["deploymentParameters", self.nfdv_parameter_name],
},
"managedIdentity": {
"type": "string",
"description": managed_identity_description_string,
},
},
"required": [
self.config.network_function_definition_group_name,
"managedIdentity",
],
}
cgs_dict["required"].append("managedIdentity")

if self.config.network_function_type == CNF:
nf_schema = cgs_dict["properties"][
self.config.network_function_definition_group_name
]
custom_location_description_string = (
"The custom location ID of the ARC-Enabled AKS Cluster to deploy the CNF "
"to. Should be of the form "
"'/subscriptions/{subscriptionId}/resourcegroups"
"/{resourceGroupName}/providers/microsoft.extendedlocation/"
"customlocations/{customLocationName}'"
)
cgs_dict["properties"]["customLocationId"] = {

nf_schema["properties"]["customLocationId"] = {
"type": "string",
"description": custom_location_description_string,
}
cgs_dict["required"].append("customLocationId")
nf_schema["required"].append("customLocationId")

return cgs_dict

Expand Down Expand Up @@ -207,14 +236,26 @@ def write_config_mappings(self, folder_path: str) -> None:
:param folder_path: The folder to put this file in.
"""
deploy_properties = self.config_group_schema_dict["properties"]
nf = self.config.network_function_definition_group_name

logger.debug("Create %s", NSD_CONFIG_MAPPING_FILENAME)

deployment_parameters = f"{{configurationparameters('{self.config.cg_schema_name}').{nf}.deploymentParameters}}"

if not self.config.multiple_instances:
deployment_parameters = f"[{deployment_parameters}]"

logger.debug("Create configMappings.json")
config_mappings = {
key: f"{{configurationparameters('{self.config.cg_schema_name}').{key}}}"
for key in deploy_properties
"deploymentParameters": deployment_parameters,
self.nfdv_parameter_name: f"{{configurationparameters('{self.config.cg_schema_name}').{nf}.{self.nfdv_parameter_name}}}",
"managedIdentity": f"{{configurationparameters('{self.config.cg_schema_name}').managedIdentity}}",
}

if self.config.network_function_type == CNF:
config_mappings[
"customLocationId"
] = f"{{configurationparameters('{self.config.cg_schema_name}').{nf}.customLocationId}}"

config_mappings_path = os.path.join(folder_path, NSD_CONFIG_MAPPING_FILENAME)

with open(config_mappings_path, "w", encoding="utf-8") as _file:
Expand All @@ -224,38 +265,10 @@ def write_config_mappings(self, folder_path: str) -> None:

def write_nf_bicep(self) -> None:
"""Write out the Network Function bicep file."""
bicep_params = ""

bicep_deploymentValues = ""

if not self.deploy_parameters or not self.deploy_parameters.get("properties"):
raise ValueError(
f"NFDV in {self.config.network_function_definition_group_name} has "
"no properties within deployParameters"
)
deploy_properties = self.deploy_parameters["properties"]
logger.debug("Deploy properties: %s", deploy_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
logger.debug(
"Adding deploy parameter key: %s, value: %s to nf template", key, value
)
if key != "location":
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 "

# pylint: disable=no-member
self.generate_bicep(
self.nf_bicep_template_name,
NF_DEFINITION_BICEP_FILENAME,
{
"bicep_params": bicep_params,
"deploymentValues": bicep_deploymentValues,
"network_function_name": self.config.network_function_name,
"publisher_name": self.config.publisher_name,
"network_function_definition_group_name": (
Expand Down
14 changes: 5 additions & 9 deletions src/aosm/azext_aosm/generate_nsd/templates/nf_template.bicep.j2
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ param nfviType string = '{{nfvi_type}}'

param resourceGroupId string = resourceGroup().id

{{bicep_params}}

var deploymentValues = {
{{deploymentValues}}
}
param deploymentParameters array

var identityObject = (managedIdentity == '') ? {
type: 'SystemAssigned'
Expand All @@ -44,8 +40,8 @@ var identityObject = (managedIdentity == '') ? {
}
}

resource nf_resource 'Microsoft.HybridNetwork/networkFunctions@2023-04-01-preview' = {
name: '{{network_function_name}}'
resource nf_resource 'Microsoft.HybridNetwork/networkFunctions@2023-04-01-preview' = [for (values, i) in deploymentParameters: {
name: '{{network_function_name}}${i}'
location: location
identity: identityObject
properties: {
Expand All @@ -61,6 +57,6 @@ resource nf_resource 'Microsoft.HybridNetwork/networkFunctions@2023-04-01-previe
nfviId: resourceGroupId
{%- endif %}
allowSoftwareUpdate: true
deploymentValues: string(deploymentValues)
deploymentValues: string(values)
}
}
}]
Loading

0 comments on commit ab98755

Please sign in to comment.