# 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 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: <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](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 <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`:

```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_name>\_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 <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](<https://osm.etsi.org/wikipub/index.php/Creating_your_own_VNF_charm_(Release_THREE)>).

#### 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.

```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: <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](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.
