# Day 1: VNF Services Initialization ## Description of this phase The objective of this section is to provide the guidelines to include all necessary elements in the VNF Package. This allows the exposed services inside the VNF to be automatically initialized right after the VNF instantiation. The main mechanism to achieve this in OSM is to build a Charm and include it in the descriptor. In the VNFD you will find metadata, which is declarative data specified in the YAML file, and code that takes care of the operations related to a VNF. The operations code is call "Charm", and it can handle the lifecycle, configuration, integration, and actions/primitives in your workloads. There are two kinds of Charms, and at this point you have to decide which one you need, and that depends on the nature of your workload. These are the two types of Charms: - Proxy Charms - Native Charms If you are using a fixed image for your workload, which CANNOT be modified, then the Charm has to be allocated not in the workload **(Proxy Charm)**. For those cases, the VCA (VNF Configuration and Abstraction) has an LXD and Kubernetes clouds available in which the Charm will live. However, if the the workload CAN be modified, then the code can live in the same workload (Native Charm). Besides charms, there is an experimental way of configuring network functions, powered by helm-based execution environments launched as PODs. ## Using Juju-based or Helm-based execution environments ### Adding Day-1 primitives to the descriptor This type of initial actions will run automatically after instantiation and should be specified in the VNF descriptor. These can be defined at two different levels: - VDU-level: for a specific VDU, used when a VDU needs configuration, which is different than the VDU used for managing the VNF. - VNF-level: for the "management VDU", used when the configuration applies to the VDU exposing a interface for managing the whole VNF. **Initial primitives** must include a primitive named `config` that passes information for OSM VCA to be able to authenticate and run further primitives into the VNF. The _config primitive_ should provide, at least, the following parameters: - `ssh-hostname`: Typically used with the , which is automatically replaced by the VNF or VDU management IP address specified in the correspondent section. - `ssh-username`: The username used for authentication with the VDU. Additionally, OSM VCA needs the credentials to succeed the authentication. For that, there are two options: - Add `ssh-password` in the config initial-config-primitive: A static password - Add `config-access in the vnf/vdu-configuration: With this method, OSM will inject the public keys generated by the Proxy Charm to the workload. ``` vnf-configuration: config-access: ssh-access: default-user: ubuntu required: true ``` > NOTE: Any primitive can require a set of configuration parameters in a config.yaml file. The value for those parameters should be specified in the `config` initial primitive. Additional to the _config primitive_, more initial primitives can be run in the desired order so that the VNF initializes its services. Note that each of these additional actions will be later detailed in the proxy charm that implements them. The following example shows VNF-level initial primitives: both the expected _config_ primitive in the beginning, but also the _configure-remote_ and _start-service_ to be run in addition right after initialization. ```yaml vnfd: ... df: - ... # VNF/VDU Configuration must use the ID of the VNF/VDU to be configured lcm-operations-configuration: operate-vnf-op-config: day1-2: - id: vnf_id execution-environment-list: - id: configure-vnf connection-point-ref: vnf-mgmt juju: charm: samplecharm initial-config-primitive: - execution-environment-ref: configure-vnf name: config parameter: - name: ssh-hostname value: - name: ssh-username value: admin - name: ssh-password value: secretpassword seq: '1' - name: configure-remote parameter: - name: dest-ip value: 10.1.1.1 seq: '2' - name: start-service seq: '3' ``` **Instantiation parameters** can be used to define the values of these parameters in a later time, during the NS instantiation. Notice that the `connection-point-ref` can be used to map the primitive to any given VDU CP, enabling the posibility of having multiple primitives mapped to different management interfaces of different VDUs. The values for the variables used at the primitive level are defined at instantiation time, just like in the `cloud-init` case: ``` osm ns-create ... --config "{additionalParamsForVnf: [{member-vnf-index: '1', additionalParams:{password: 'secretpassword', destination_ip: '10.1.1.1'}}]}" ``` Remember that when dealing with multiple variables, it might be useful to pass a YAML file instead. ``` osm ns-create ... --config-file vars.yaml ``` ### Creating Juju-based execution environments with Proxy Charms #### New operator framework (Recommended) This section will focus the attention on creating a Proxy charm to configure a workload. Create a folder for the `samplecharm` in the VNFD directory, and create necessary files: > Go to [charms.osm](https://github.com/charmed-osm/charms.osm) if you want to learn more about how to use the charms.osm library. ```bash mkdir -p charms/samplecharm/ cd charms/samplecharm/ mkdir hooks lib mod src touch src/charm.py touch actions.yaml metadata.yaml config.yaml chmod +x src/charm.py ln -s ../src/charm.py hooks/upgrade-charm ln -s ../src/charm.py hooks/install ln -s ../src/charm.py hooks/start git clone https://github.com/canonical/operator mod/operator git clone https://github.com/charmed-osm/charms.osm mod/charms.osm ln -s ../mod/operator/ops lib/ops ln -s ../mod/charms.osm/charms lib/charms ``` Include the following high level metadata in `metadata.yaml`: ```yaml name: samplecharm summary: this is an example maintainer: David Garcia description: | This is an example of a proxy charm deployed by Open Source Mano. tags: - nfv subordinate: false series: - bionic - xenial peers: # This will give HA capabilities to your Proxy Charm proxypeer: interface: proxypeer ``` Add the following configuration parameters in `config.yaml`: ```yaml options: ssh-hostname: type: string default: "" description: "The hostname or IP address of the machine to" ssh-username: type: string default: "" description: "The username to login as." ssh-password: type: string default: "" description: "The password used to authenticate." ssh-public-key: type: string default: "" description: "The public key of this unit." ssh-key-type: type: string default: "rsa" description: "The type of encryption to use for the SSH key." ssh-key-bits: type: int default: 4096 description: "The number of bits to use for the SSH key." ``` Add the following actions in `actions.yaml` (the implementation will be done in src/charm.py): ```yaml # Actions to be implemented in src/charm.py configure-remote: description: "Configures the remote server" params: destination_ip: description: "IP of the remote server" type: string default: "" required: - destination_ip start-service: description: "Starts the service of the VNF" # Required by charms.osm.sshproxy run: description: "Run an arbitrary command" params: command: description: "The command to execute." type: string default: "" required: - command generate-ssh-key: description: "Generate a new SSH keypair for this unit. This will replace any existing previously generated keypair." verify-ssh-credentials: description: "Verify that this unit can authenticate with server specified by ssh-hostname and ssh-username." get-ssh-public-key: description: "Get the public SSH key for this unit." ``` Add the following code to `src/charm.py`, which will implement the Day-1 primitives: > Note: Actions in the Charm can be used in the VNFD for either Day-1 and Day-2 primitives. There's no difference in the Charm. ```python #!/usr/bin/env python3 import sys sys.path.append("lib") from charms.osm.sshproxy import SSHProxyCharm from ops.main import main class SampleProxyCharm(SSHProxyCharm): def __init__(self, framework, key): super().__init__(framework, key) # Listen to charm events self.framework.observe(self.on.config_changed, self.on_config_changed) self.framework.observe(self.on.install, self.on_install) self.framework.observe(self.on.start, self.on_start) # self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm) # Listen to the touch action event self.framework.observe(self.on.configure_remote_action, self.configure_remote) self.framework.observe(self.on.start_service_action, self.start_service) def on_config_changed(self, event): """Handle changes in configuration""" super().on_config_changed(event) def on_install(self, event): """Called when the charm is being installed""" super().on_install(event) def on_start(self, event): """Called when the charm is being started""" super().on_start(event) def configure_remote(self, event): """Configure remote action.""" if self.model.unit.is_leader(): stderr = None try: mgmt_ip = self.model.config["ssh-hostname"] destination_ip = event.params["destination_ip"] cmd = "vnfcli set license {} server {}".format( mgmt_ip, destination_ip ) proxy = self.get_ssh_proxy() stdout, stderr = proxy.run(cmd) event.set_results({"output": stdout}) except Exception as e: event.fail("Action failed {}. Stderr: {}".format(e, stderr)) else: event.fail("Unit is not leader") def start_service(self, event): """Start service action.""" if self.model.unit.is_leader(): stderr = None try: cmd = "sudo service vnfoper start" proxy = self.get_ssh_proxy() stdout, stderr = proxy.run(cmd) event.set_results({"output": stdout}) except Exception as e: event.fail("Action failed {}. Stderr: {}".format(e, stderr)) else: event.fail("Unit is not leader") if __name__ == "__main__": main(SampleProxyCharm) ``` As you can see, the Charm is pure Python code, which makes it very easy to develop. There are a few things that need explanation from the code above: ```python from charms.osm.sshproxy import SSHProxyCharm class SampleProxyCharm(SSHProxyCharm): ``` In the charms.osm library, you can find an SSHProxyCharm library that handles scalability of proxy charms, and many other actions needed particularly in Proxy Charms. It is recommendable to use that class as the base class for any Proxy Charm. ```python def __init__(self, framework, key): super().__init__(framework, key) # Listen to charm events self.framework.observe(self.on.config_changed, self.on_config_changed) self.framework.observe(self.on.install, self.on_install) self.framework.observe(self.on.start, self.on_start) # self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm) # Listen to the touch action event self.framework.observe(self.on.configure_remote_action, self.configure_remote) self.framework.observe(self.on.start_service_action, self.start_service) ``` In the initialization of the Charm, we need to observe to start (self.on.start), install(self.on.install), and config_changed (self.on.config_changed) events. Additionally, we need to observe the events for the implemented actions, which have the following format: self.on.\_action. ```python def on_config_changed(self, event): """Handle changes in configuration""" super().on_config_changed(event) def on_install(self, event): """Called when the charm is being installed""" super().on_install(event) def on_start(self, event): """Called when the charm is being started""" super().on_start(event) ``` This functions will be called in config_change, install, and start events respectively. The methods implemented by the SSHProxyCharm class are called because it handles some important things related to ProxyCharms. But after calling the super() methods you can write code in each one of those events. ```python def configure_remote(self, event): ... def start_service(self, event): ... ``` Finally, we defined the functions for the actions. #### DEPRECATED: Reactive (Method 1): Building a Proxy Charm the traditional way a) Install the charm tools and setup your environment. You might want to copy the `export` lines to your "~/.bashrc" profile file to automatically load them in the next session. ``` snap install charm --classic mkdir -p ~/charms/layers export JUJU_REPOSITORY=~/charms export LAYER_PATH=$JUJU_REPOSITORY/layers cd $LAYER_PATH ``` b) A proxy charm includes, by default, the "VNF" and "basic" layers, which take care of the initial SSH connection to the VNF. Create the new personalized _layer_ for your proxy charm: ``` charm create samplecharm cd samplecharm ``` Note: Charm names do not support underscores. c) Modify the basic files like this: ``` # layer.yaml file includes: - layer:basic - layer:vnfproxy # metadata.yaml file name: samplecharm summary: this is an example maintainer: Gianpietro Lavado description: | This is an example of a proxy charm deployed by Open Source Mano. tags: - nfv subordinate: false series: - trusty - xenial ``` d) Create and modify the "actions.yaml" file, adding all actions/primitives and their parameters. Note that the value of these parameters are defined at the VNFD, either statically or by using variables, as explained earlier. ``` # actions.yaml file configure-remote: description: "Configures the remote server" params: destination_ip: description: "IP of the remote server" type: string default: "" start-service: description: "Starts the service of the VNF" ``` e) Create an "actions" folder and populate it with files representing each action. Filenames should match the name of the primitive, should be made executable with `chmod +x` and all must contain the following exact content. ``` # actions/configure-remote and actions/start-service files cat <<'EOF' >> actions/set-server #!/usr/bin/env python3 import sys sys.path.append('lib') from charms.reactive import main from charms.reactive import set_state from charmhelpers.core.hookenv import action_fail, action_name """ `set_state` only works here because it's flushed to disk inside the `main()` loop. remove_state will need to be called inside the action method. """ set_state('actions.{}'.format(action_name())) try: main() except Exception as e: action_fail(repr(e)) EOF ``` f) Open the respective file at the 'reactive/' folder. This will be used to code, in Python, the actual actions that will run through SSH when each primitive is triggered. Note that any variable can be recovered in two ways: - Using the `config()` function if the variable belongs to that specific primitive. - Using the `action_get('name-of-parameter')` function to get any other parameter. The following example provides an idea of the contents of a reactive file. ``` # reactive/samplecharm.py file # dependencies that might be needed from charmhelpers.core.hookenv import ( action_get, action_fail, action_set, config, status_set, ) from charms.reactive import ( remove_state as remove_flag, set_state as set_flag, when, when_not ) import charms.sshproxy # Your actions here @when('actions.configure-remote') def configure_remote(): err = '' # Variables should be retrieved, if needed cfg = config() mgmt_ip = cfg['ssh-hostname'] destination_ip = action_get('dest-ip') try: # Commands to be run through SSH should go here cmd = "vnfcli set license " + mgmt_ip + " server " + destination_ip result, err = charms.sshproxy._run(cmd) except: action_fail('command failed:' + err) else: action_set({'output': result}) finally: remove_flag('actions.configure-remote') @when('actions.start-service') def start_service(): err = '' # Variables should be retrieved, if needed try: # Commands to be run through SSH should go here cmd = "sudo service vnfoper start" result, err = charms.sshproxy._run(cmd) except: action_fail('command failed:' + err) else: action_set({'output': result}) finally: remove_flag('actions.start-service') ``` g) If your proxy charm layer needs some extra dependencies, the debian or pip package should be added to the layer.yaml file. This is done through the 'packages' and 'python_packages' options inside the layer, for example: ``` includes: - "layer:basic" - "layer:ansible-base" - "layer:vnfproxy" options: basic: use_venv: false packages: ["build-essential","libssl-dev"] python_packages: ["pyyaml"] ``` h) Finally, build the charm with `charm build` and copy the resulting folder (in this case the `~/charms/builds/simplecharm` directory) inside the `charms` folder of your VNF Package. Futher information about building charms can be found [here](). #### DEPRECATED: Reactive (Method 2): Using Proxy Charm Generators To date, the only supported generator is Ansible, which means that a Proxy Charm can be automatically populated based on an Ansible Playbook. A sample Ansible playbook would look like this: ``` # This sample playbook applies to a VyOS router # The hosts where the playbook will be executed will automatically contain the IP address of the VDU, in a /etc/ansible/hosts file at the charm container - hosts: vyos-routers # note that the following setting is needed on most cases connection: local tasks: - name: configure the remote device vyos_config: lines: - set nat destination rule 1 inbound-interface eth0 - set nat destination rule 1 destination port 80 - set nat destination rule 1 protocol tcp - set nat destination rule 1 translation address {{dest_ip}} ``` Once the Ansible playbook has been tested against your VNF, the procedure to incorporate it in a charm is as follows: a) Create your environment and charm in the traditional way (that is, steps (a) and (b) from the previous method) b) Clone the devops repository elsewhere and copy the generator files to your charm root folder. For example: `cp -r ~/devops/descriptor-packages/tools/charm-generator/* ./` `[TODO: migrate to binary]` c) Install the dependencies of the generator with `sudo pip3 install -r requirements.txt` d) Run the generator, which will populate all the required files automatically. It requires an Ansible playbook to be copied inside a new "playbooks" folder under the charm root directory, and the primitive to be named "playbook" (by default). The following example runs the generator with the minimal options. ``` python3 generator-runner.py --ansible --summary "Configures VNF using Ansible" \ --maintainer "Gianpietro Lavado " \ --description "Configures VNF using Ansible" ``` e) Adjust the "reactive" file as desired, for example, if you wish to pass some parameters to your playbook (which supports Jinja) ``` @when('actions.playbook') def playbook(): try: # getting a variables from the config primitive cfg = config() mgmt_ip = cfg['ssh-hostname'] # a sample on passing a specific file to the playbook, considering that the charm will be located at the /var/lib/juju/agents folder config_file = charms.libansible.find('config.conf', '/var/lib/juju/agents/') # populating an object with the variables dict_vars = {'dest_ip': mgmt_ip,'config_file': config_file} # running the playbook along with the given variables result = charms.libansible.execute_playbook('playbook.yaml', dict_vars) except: exc_type, exc_value, exc_traceback = sys.exc_info() err = traceback.format_exception(exc_type, exc_value, exc_traceback) action_fail('playbook failed: ' + str(err)) else: action_set({'output': result}) finally: remove_flag('actions.playbook') ``` f) Finally, build the charm with `charm build` and copy the resulting folder (in this case the "~/charms/builds/simplecharm" directory) inside the "charms" folder of your VNF Package. Once the VNF is launched, the results from running the generator will be found inside the proxy charm _lxc_ container, at the "/var/log/ansible.log" file. If not successful, it could indicate the need for other possible modifications which are applicable for certain VNFs. **Note**: some VNFs will not pass some SSH pre-checks that Ansible performs in some operations (SFTP, SCP, etc.) In those cases, it has been noted that `ansible_connection=ssh`, which is a default set of the generator, needs to be disabled. This preset would need to be deleted from the `lib/charms/libansible.py` file, `create_hosts` function. `[TODO: explore an enhancement to the Ansible Generator, to be as generic as possible]` #### Scaling proxy charms In the additional configuration parameters, there is a `config-units` option that will scale the Proxy Charms you want. ```yaml additionalParamsForVnf: - member-vnf-index: "1" config-units: 2 ``` ### Creating Helm-based execution environments As of OSM version 8, NF configurations can also be done with Helm-based execution environments, which deploy a pair of extra pods in the OSM K8s namespace when included. These PODs follow the VNF lifecycle (as charms do) and can be also used to collect indicators as explained in the Day 2 section. At the VNF package level: - The only file that needs to be modified before building it is the `vnf_ee.py` file at the `helm-charts/chart_name/source/` folder, just as in [this sample VNF Package](https://osm.etsi.org/gitlab/vnf-onboarding/osm-packages/-/tree/master/simple_ee_vnf), where the chart is called `eechart`. - The file contains the primitives, in this case, the `touch` primitive, that creates a file in the NF as a quick example. - The rest of the structure inside the `helm-chart` folder shown in the example above needs to be included. Once the primitives have been defined and included in the `vnf_ee.py` file, the descriptor needs to specify the helm-based day-1 primitives that will be launched. For example: ```yaml vnfd: ... df: - ... lcm-operations-configuration: operate-vnf-op-config: day1-2: - id: simple_ee-vnf config-access: ssh-access: default-user: ubuntu required: true execution-environment-list: - external-connection-point-ref: vnf-mgmt-ext helm-chart: eechart id: monitor initial-config-primitive: - execution-environment-ref: monitor name: config parameter: - name: ssh-hostname value: - name: ssh-username value: ubuntu - name: ssh-password value: osm2020 seq: '1' - execution-environment-ref: monitor name: touch parameter: - name: file-path value: /home/ubuntu/first-touch seq: '2' ``` ### Testing Instantiation of the VNF Package Remember the objective of this phase: **to configure the VNF automatically so it starts providing the expected service**. To test this out, the NS can be launched using the OSM client, like this: ``` osm ns-create --ns_name [ns name] --nsd_name [nsd name] --vim_account [vim name] --ssh_keys [comma separated list of public key files to inject to vnfs] ``` Furthermore, and as mentioned earlier, extra **_instantiation parameters_** can be passed so that the VNF can be adapted to the particular instantiation environment or to achieve a proper inter-operation with other VNFs into the specific NS. For example, if using IP Profiles to predefine subnet values, a specific IP address could be passed to an interface like this: ``` osm ns-create ... --config '{vnf: [ {member-vnf-index: "1", internal-vld: [ {name: internal, ip-profile: {...}, internal-connection-point: [{id-ref: id1, ip-address: "a.b.c.d"}] ] } ], additionalParamsForVnf...}' ``` When dealing with multiple fixed IP addresses, variables or other additions to the original descriptor, it might be useful to pass a YAML file instead. ``` osm ns-create ... --config-file ip-vars.yaml ``` As you can see, the parameters being defined at instantiation time follow the information model structure. Further information and examples about these parameters can be reviewed [here](https://osm.etsi.org/wikipub/index.php/OSM_instantiation_parameters). After deployment is done, proxy charms can be monitored and debugged by using the `juju status` and `juju debug-log` commands, respectively. Since Release 6, every NS has its own juju instance (model) for its charms, so before running juju commands, you need to switch to the right model using `juju switch [NS ID]` If proxy charms need to be started at any particular order, please note that the order of proxy charm initialization follows the order in which 'constituent VNFs' are listed at the NSD, but the actual operations could be executed in a different order, depending on the time it takes for each proxy charm container to be ready.