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 <rw_mgmt_ip>, 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 passwordAdd `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.
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: <rw_mgmt_ip>
- 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 if you want to learn more about how to use the charms.osm library.
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
:
name: samplecharm
summary: this is an example
maintainer: David Garcia <david.garcia@canonical.com>
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
:
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):
# 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.
#!/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:
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.
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_name>_action.
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.
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 <gianpietro1@gmail.com>
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 <glavado@whitestack.com>" \
--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.
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 thehelm-charts/chart_name/source/
folder, just as in this sample VNF Package, where the chart is calledeechart
.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:
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: <rw_mgmt_ip>
- 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.
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.