aboutsummaryrefslogtreecommitdiffstats
path: root/vicn
diff options
context:
space:
mode:
authorJordan Augé <jordan.auge+fdio@email.com>2017-02-24 14:58:01 +0100
committerJordan Augé <jordan.auge+fdio@cisco.com>2017-02-24 18:36:29 +0000
commit85a341d645b57b7cd88a26ed2ea0a314704240ea (patch)
treebdda2b35003aae20103a796f86daced160b8a730 /vicn
parent9b30fc10fb1cbebe651e5a107e8ca5b24de54675 (diff)
Initial commit: vICN
Change-Id: I7ce66c4e84a6a1921c63442f858b49e083adc7a7 Signed-off-by: Jordan Augé <jordan.auge+fdio@cisco.com>
Diffstat (limited to 'vicn')
-rw-r--r--vicn/__init__.py0
-rw-r--r--vicn/__main__.py21
-rw-r--r--vicn/bin/__init__.py0
-rwxr-xr-xvicn/bin/vicn.py104
-rw-r--r--vicn/clients/__init__.py0
-rwxr-xr-xvicn/clients/command_line.py322
-rw-r--r--vicn/core/__init__.py0
-rw-r--r--vicn/core/address_mgr.py181
-rw-r--r--vicn/core/api.py118
-rw-r--r--vicn/core/attribute.py270
-rw-r--r--vicn/core/command_helpers.py48
-rw-r--r--vicn/core/commands.py376
-rw-r--r--vicn/core/event.py25
-rw-r--r--vicn/core/exception.py39
-rw-r--r--vicn/core/requirement.py229
-rw-r--r--vicn/core/resource.py898
-rw-r--r--vicn/core/resource_factory.py84
-rw-r--r--vicn/core/resource_mgr.py1436
-rw-r--r--vicn/core/sa_collections.py249
-rw-r--r--vicn/core/sa_compat.py270
-rw-r--r--vicn/core/scheduling_algebra.py97
-rw-r--r--vicn/core/state.py177
-rw-r--r--vicn/core/task.py352
-rw-r--r--vicn/helpers/resource_definition.py22
-rw-r--r--vicn/helpers/tasks.py21
-rw-r--r--vicn/resource/__init__.py0
-rw-r--r--vicn/resource/application.py29
-rw-r--r--vicn/resource/central.py789
-rw-r--r--vicn/resource/channel.py44
-rw-r--r--vicn/resource/dns_server.py33
-rw-r--r--vicn/resource/gui.py29
-rw-r--r--vicn/resource/icn/__init__.py0
-rw-r--r--vicn/resource/icn/ccnx_consumer_producer_test.py109
-rw-r--r--vicn/resource/icn/ccnx_keystore.py87
-rw-r--r--vicn/resource/icn/ccnx_metis.py368
-rw-r--r--vicn/resource/icn/ccnx_simpleTrafficGenerator.py106
-rw-r--r--vicn/resource/icn/consumer.py25
-rw-r--r--vicn/resource/icn/face.py140
-rw-r--r--vicn/resource/icn/forwarder.py64
-rw-r--r--vicn/resource/icn/icn_application.py37
-rw-r--r--vicn/resource/icn/icn_tools.py26
-rw-r--r--vicn/resource/icn/iping.py125
-rw-r--r--vicn/resource/icn/ndnpingserver.py76
-rw-r--r--vicn/resource/icn/nfd.py136
-rw-r--r--vicn/resource/icn/producer.py29
-rw-r--r--vicn/resource/icn/repo-ng.py25
-rw-r--r--vicn/resource/icn/route.py36
-rw-r--r--vicn/resource/icn/virtual-repo.py37
-rw-r--r--vicn/resource/icn/webserver.py29
-rw-r--r--vicn/resource/interface.py47
-rw-r--r--vicn/resource/ip/__init__.py0
-rw-r--r--vicn/resource/ip/route.py32
-rw-r--r--vicn/resource/ip/routing_table.py175
-rw-r--r--vicn/resource/linux/__init__.py0
-rw-r--r--vicn/resource/linux/application.py56
-rw-r--r--vicn/resource/linux/bridge.py104
-rw-r--r--vicn/resource/linux/bridge_mgr.py34
-rw-r--r--vicn/resource/linux/certificate.py74
-rw-r--r--vicn/resource/linux/dnsmasq.py118
-rw-r--r--vicn/resource/linux/file.py110
-rw-r--r--vicn/resource/linux/iperf.py27
-rw-r--r--vicn/resource/linux/link.py201
-rw-r--r--vicn/resource/linux/macvlan.py52
-rw-r--r--vicn/resource/linux/macvtap.py52
-rw-r--r--vicn/resource/linux/net_device.py519
-rw-r--r--vicn/resource/linux/netmon.py29
-rw-r--r--vicn/resource/linux/numa_mgr.py113
-rw-r--r--vicn/resource/linux/ovs.py68
-rw-r--r--vicn/resource/linux/package_manager.py232
-rw-r--r--vicn/resource/linux/phy_interface.py49
-rw-r--r--vicn/resource/linux/phy_link.py39
-rw-r--r--vicn/resource/linux/physical.py144
-rw-r--r--vicn/resource/linux/repository.py41
-rw-r--r--vicn/resource/linux/service.py83
-rw-r--r--vicn/resource/linux/sym_veth_pair.py151
-rw-r--r--vicn/resource/linux/tap_device.py48
-rw-r--r--vicn/resource/linux/traceroute.py23
-rw-r--r--vicn/resource/linux/veth_pair.py62
-rw-r--r--vicn/resource/lxd/__init__.py0
-rw-r--r--vicn/resource/lxd/lxc_container.py317
-rw-r--r--vicn/resource/lxd/lxc_image.py116
-rw-r--r--vicn/resource/lxd/lxd_hypervisor.py223
-rw-r--r--vicn/resource/node.py93
-rw-r--r--vicn/resource/ns3/__init__.py0
-rw-r--r--vicn/resource/ns3/emulated_channel.py209
-rw-r--r--vicn/resource/ns3/emulated_lte_channel.py188
-rw-r--r--vicn/resource/ns3/emulated_wifi_channel.py148
-rw-r--r--vicn/resource/script.py31
-rw-r--r--vicn/resource/vpp/__init__.py0
-rw-r--r--vicn/resource/vpp/cicn.py138
-rw-r--r--vicn/resource/vpp/dpdk_device.py35
-rw-r--r--vicn/resource/vpp/interface.py125
-rw-r--r--vicn/resource/vpp/scripts.py287
-rw-r--r--vicn/resource/vpp/vpp.py187
-rw-r--r--vicn/resource/vpp/vpp_bridge.py130
-rw-r--r--vicn/resource/vpp/vpp_commands.py41
-rw-r--r--vicn/resource/vpp/vpp_host.py144
97 files changed, 12813 insertions, 0 deletions
diff --git a/vicn/__init__.py b/vicn/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/__init__.py
diff --git a/vicn/__main__.py b/vicn/__main__.py
new file mode 100644
index 00000000..802d23e4
--- /dev/null
+++ b/vicn/__main__.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+if __name__ == '__main__':
+ from runpy import _run_module_as_main
+ _run_module_as_main('vicn.bin.vicn')
diff --git a/vicn/bin/__init__.py b/vicn/bin/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/bin/__init__.py
diff --git a/vicn/bin/vicn.py b/vicn/bin/vicn.py
new file mode 100755
index 00000000..9a43cf6d
--- /dev/null
+++ b/vicn/bin/vicn.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os, sys, time, asyncio, argparse, shutil, threading, asyncio, logging
+import traceback
+
+log = logging.getLogger(__name__)
+
+PATH=os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir)
+sys.path.insert(0, os.path.abspath(PATH))
+
+from netmodel.model.query import Query
+from netmodel.util.daemon import Daemon
+from netmodel.util.log import textcolor, initialize_logging
+from vicn.clients.command_line import VICNCmd
+from vicn.core.api import API
+from vicn.core.resource_mgr import ResourceManager
+from vicn.resource.node import Node
+
+class ArgumentParser(argparse.ArgumentParser):
+ def error(self, message):
+ self._print_message(textcolor('red', '{prog}: error: {message}\n'.format(prog=self.prog, message=message)),
+ sys.stderr)
+
+ self.print_usage(sys.stdout)
+ sys.exit(-1)
+
+class VICNDaemon(Daemon):
+ def initialize(self):
+ # FIXME UGLY
+ n_times = 1
+ background = False
+ setup = False
+ scenario = None
+ node_list, net, ndn, mob, cluster = None, None, None, None, None
+
+ parser = ArgumentParser(description=textcolor('green', "Batch usage of VICN."))
+ parser.add_argument('-s', metavar='configuration_file_path',
+ help="JSON file containing the topology")
+ parser.add_argument('-n', metavar='n_times', type=int, help='Execute the test multiple times')
+ parser.add_argument('-x', action='store_false', help='No automatic execution')
+
+ arguments = parser.parse_args()
+ args = vars(arguments)
+
+
+ for option in args.keys():
+ if args[option] is not None:
+ if option == "s":
+ print(" * Loading the configuration file at {0}".format(args[option]))
+ scenario = args[option]
+ elif option == "t" and args[option] is True:
+ background = True
+ elif option == "x" and args[option] is True:
+ setup = True
+ elif option == "n":
+ n_times = args[option]
+
+ self._api = API()
+ self._api.configure(scenario, setup)
+
+ if node_list is not None:
+ ResourceManager().set(node_list)
+
+ def main(self):
+ """
+ Main asyncio loop.
+
+ Ctrl+C properly terminates the loop by terminating all running
+ instances.
+ """
+ loop = asyncio.get_event_loop()
+ try:
+ loop.run_forever()
+ except KeyboardInterrupt:
+ pass
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ finally:
+ loop.stop()
+ self._api.terminate()
+
+def main():
+ initialize_logging()
+ VICNDaemon('vicn', no_daemon = True).start()
+
+if __name__ == "__main__":
+ main()
diff --git a/vicn/clients/__init__.py b/vicn/clients/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/clients/__init__.py
diff --git a/vicn/clients/command_line.py b/vicn/clients/command_line.py
new file mode 100755
index 00000000..c1776bf5
--- /dev/null
+++ b/vicn/clients/command_line.py
@@ -0,0 +1,322 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import argparse
+import cmd
+import logging
+import os
+import shlex
+import sys
+import traceback
+import time
+
+from vicn.core.api import API
+from netmodel.util.log import textcolor
+
+# FIXME
+log_file = "/tmp/vicn.log"
+
+log = logging.getLogger(__name__)
+
+# FIXME
+EXPERIMENT_ID = 'vicn'
+
+welcome = textcolor("cyan",
+ """
+ ================================================
+ Welcome to VICN
+ ================================================
+ * Type 'help' for a brief help
+ """)
+
+class ArgumentParser(argparse.ArgumentParser):
+ """
+ The class ArgumentParser extends the system class :class:`argparse.ArgumentParser`
+ for what concerns the error management.
+ """
+
+ def error(self, message):
+ """
+ Print the error and the list of available commands.
+
+ :param message: The error message
+ :return:
+ """
+ self.print_usage(sys.stderr)
+ self._print_message('{prog}: error: {message}\n'.format(prog=self.prog, message=message), sys.stderr)
+
+
+def raw_input(prompt):
+ """
+ Print a message on command line and get user input
+ :param prompt: message to be shown
+ :return: the input line (without the '\n' character)
+ """
+ print(prompt, end='', flush=True)
+ return sys.stdin.readline()[:-1]
+
+
+class MyCmd(cmd.Cmd, object):
+ """
+ The class MyCmd inherits from the system class :class:`cmd.Cmd` and adds the following extensions:
+ - The preloop prints the welcome message::
+
+ ================================================
+ Welcome to VICN
+ ================================================
+ * Type 'help' for a brief help
+ - The list of available commands does not show the EOF command (ctrl + D)
+ - The empty line command does not execute the last command
+ - It override the class variables prompt, doc_header and ruler
+
+ :cvar prompt: the prompt of the CLI. Value: "VICN >"
+ :cvar doc_header: the header for the list of available commands. Value: "Available commands (type help <command> for more info)"
+ :cvar ruler: The character used to draw separator lines under the help-message headers. Value: "-"
+
+ """
+ prompt = textcolor('blue', 'VICN > ')
+ doc_header = textcolor("green", 'Available commands (type help <command> for more info)')
+ ruler = '-'
+
+ def preloop(self):
+ """
+ Print the welcome message before starting the CLI loop.
+
+ :return:
+ """
+ print(welcome)
+
+ def get_names(self):
+ """
+ Return the list of available commands.
+
+ :return: The list of commands without the EOF (Ctrl + D) function.
+ """
+ names = super(MyCmd, self).get_names()
+ if "do_EOF" in names:
+ names.remove("do_EOF")
+ return names
+
+ def emptyline(self):
+ """
+ Override the base function in order to avoid re-executing the last
+ command.
+ """
+ pass
+
+
+class VICNCmd(MyCmd):
+
+ def __init__(self, api):
+ """
+ Instantiate a CLI for vicn. If all the inputs are valid (not None) it
+ configures the experiment with the received parameters, otherwise the
+ CLI will be started without configuration. The user later has to
+ specify the path of the experiment folder through the configure
+ command.
+
+ :return:
+ """
+ super(MyCmd, self).__init__()
+
+ log.debug("Creating CLI instance.")
+
+ self._api = api
+
+ def run(self):
+ """Runs the CLI main loop in a thread
+ """
+ while True:
+ try:
+ self.cmdloop()
+ break
+ except KeyboardInterrupt:
+ self.exit_gracefully()
+ except Exception as e:
+ traceback.print_exc()
+ self.exit()
+
+ def terminate(self):
+ """
+ """
+
+ def do_EOF(self, line):
+ """
+ Type Ctrl+D to exit from User CLI
+ """
+ log.debug("Exiting from vicn.")
+ self.exit_gracefully()
+ return True
+
+ def do_clear(self, line):
+ """
+ Clear the screen.
+ """
+ os.system('clear')
+
+ def do_quit(self, line):
+ """
+ Quit from VICN and clean the testbed.
+ """
+
+ log.info("Exit from VICN")
+ self.exit()
+
+ def do_configure(self, line):
+ """
+ Configure the experiment by reading the configuration file specified.
+ Available command line arguments:
+ -c <conf_directory_path>: Path to the directory containing the
+ configuration files
+ -s Show the current VICN configuration
+ """
+
+ log.debug("Parsing configuration files")
+ log.debug("Parsing configuration files")
+
+ try:
+ args = self.configure_parser.parse_args(shlex.split(line))
+ except TypeError:
+ return
+ except SystemExit:
+ return
+
+ if args is None:
+ log.warning("You must specify the directory with the input files,"\
+ "or write -s to show the configuration file!")
+ return
+
+ if args.show:
+
+ log.debug("Showing the list of nodes")
+
+ if self.node_list is not None:
+ for node in self.node_list.values():
+ print(node)
+ else:
+ log.warning("No configuration!")
+ elif args.conf_dir is not None:
+ self._api.configure(args.conf_dir)
+
+ else:
+ log.warning("No options found.")
+
+ def help_configure(self):
+ """
+ Print the help for the configure command.
+ """
+ self.configure_parser.print_help()
+
+ def complete_configure(self, text, line, begidx, endidx):
+ """
+ Auto completion for the file name. This function is really useful when
+ the user has to specify the path to the experiment folder in the
+ configure command.
+
+ :param text: String prefix we are attempting to match
+ :param line: Current input line with leading whitespace removed
+ :param begidx: beginning index of the prefix text, which could be used
+ to provide different completion depending upon which position
+ the argument is in.
+ :param endidx: ending index of the prefix text, which could be used to
+ provide different completion depending upon which position the
+ argument is in.
+ :return: List of matches for the current file path
+ """
+
+ line = line.split()
+ if len(line) == 2 and begidx == endidx:
+ filename = ''
+ path = './'
+ elif len(line) == 3:
+ path = line[2]
+ if '/' in path:
+ i = path.rfind('/')
+ filename = path[i + 1:]
+ path = path[0:i + 1]
+ else:
+ filename = path
+ path = './'
+ else:
+ return
+
+ ls = os.listdir(path)
+ ls.sort()
+
+ ls = ls[:]
+ for i in range(len(ls)):
+ if os.path.isdir(os.path.join(path, ls[i])):
+ ls[i] += '/'
+ if filename == '':
+ return ls
+ else:
+ return [f for f in ls if f.startswith(filename)]
+
+ def do_print(self, line):
+ ll = {}
+ for node in self.node_list.keys():
+ print(node)
+
+ def evaluate(self, line):
+ dic = SQLParser().parse(command)
+ if not dic:
+ raise RuntimeError("Can't parse input command: %s" % command)
+
+ query = Query.from_dict(dic)
+
+ return self._api.execute(query, annotation)
+
+
+ def do_select(self, line):
+ """
+ Query interface: select
+ """
+ query = self._api.parse_query('select {}'.format(line))
+ results = self._api.execute(query)
+ for result in results:
+ print(result)
+
+ def do_setup(self, line):
+ assert not line
+ self._api.setup()
+
+ def do_teardown(self, line):
+ assert not line
+ self._api.teardown()
+
+ def exit(self):
+ """
+ Exit from the program by cleaning the environment.
+ """
+
+ print(textcolor("green", "Cleaning the cluster. Wait.."))
+ sys.exit()
+
+ def exit_gracefully(self):
+ """
+ Ask the user if he really wants to exit from the cmd loop
+ :return:
+ """
+
+ try:
+ inp = raw_input(textcolor("yellow", "\nReally quit? (y/n) "))
+ if inp.lower().startswith('y'):
+ self.exit()
+ except KeyboardInterrupt:
+ print(textcolor("red", "\nOk ok, quitting"))
+ self.exit()
diff --git a/vicn/core/__init__.py b/vicn/core/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/core/__init__.py
diff --git a/vicn/core/address_mgr.py b/vicn/core/address_mgr.py
new file mode 100644
index 00000000..7df5e4ac
--- /dev/null
+++ b/vicn/core/address_mgr.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import ipaddress
+import logging
+import random
+import struct
+import socket
+
+from netmodel.util.deprecated import deprecated
+from netmodel.util.singleton import Singleton
+from netmodel.util.toposort import toposort, toposort_flatten
+
+log = logging.getLogger(__name__)
+
+#------------------------------------------------------------------------------
+# SharedResource
+#------------------------------------------------------------------------------
+
+class SharedResource:
+ """
+ Base class for allocating shared resource
+ """
+ def __init__(self):
+ self._counter = 0
+ self._values = dict()
+
+ def __next__(self):
+ ret = self._counter
+ self._counter += 1
+ return ret
+
+ def get(self, requestor, tag = None):
+ if requestor not in self._values:
+ self._values[requestor] = dict()
+ if tag not in self._values[requestor]:
+ self._values[requestor][tag] = next(self)
+ return self._values[requestor][tag]
+
+#------------------------------------------------------------------------------
+
+class Vlan(SharedResource):
+ """
+ SharedResource: Vlan
+
+ Manages VLAN allocation
+ """
+
+ def get(self, requestor, tag = None):
+ if requestor not in self._values:
+ self._values[requestor] = dict()
+ if tag not in self._values[requestor]:
+ self._values[requestor][tag] = (next(self)+1)
+ return self._values[requestor][tag]
+
+#------------------------------------------------------------------------------
+
+MAX_DEVICE_NAME_SIZE = 15
+
+class DeviceName(SharedResource):
+
+ def get(self, *args, prefix = None, **kwargs):
+ count = super().get(*args, **kwargs)
+ device_name = '{}{}'.format(prefix, count)
+ if len(device_name) > MAX_DEVICE_NAME_SIZE:
+ overflow = len(device_name) - MAX_DEVICE_NAME_SIZE
+ max_prefix_len = len(prefix) - overflow
+ device_name = '{}{}'.format(prefix[:max_prefix_len], count)
+ return device_name
+
+#------------------------------------------------------------------------------
+
+class IpAddress(SharedResource):
+ pass
+
+class Ipv4Address(IpAddress):
+ pass
+
+class Ipv6Address(IpAddress):
+ pass
+
+#------------------------------------------------------------------------------
+# AddressManager
+#------------------------------------------------------------------------------
+
+class AddressManager(metaclass = Singleton):
+ """
+ The purpose of this class is to generate sequential deterministic MAC/IP
+ addresses in order to assign them to the node in the network.
+ """
+
+ MAP_TYPE = {
+ 'vlan': Vlan,
+ 'device_name': DeviceName,
+ }
+
+ def __init__(self):
+ self._ips = dict()
+ self._macs = dict()
+
+ self._pools = dict()
+
+ from vicn.core.resource_mgr import ResourceManager
+
+ network = ResourceManager().get('network')
+ network = ipaddress.ip_network(network, strict=False)
+ self._next_ip = network[1]
+
+ mac_address_base = ResourceManager().get('mac_address_base')
+ self._next_mac = int(mac_address_base, 0) + 1
+
+ def get(self, resource_type, requestor, *args, tag=None, scope=None,
+ **kwargs):
+ """
+ Params:
+ type : the type of shared resource to be requested
+ requestor: name of the resource that requests the shared resource, in
+ order to reattribute the same if requested multiple times.
+ tag: use when a single resource request multiple times the same
+ resource.
+ scope: None = global scope by default. Ensure uniqueness of resource
+ at global scope
+ """
+ if not scope in self._pools:
+ self._pools[scope] = dict()
+ if not resource_type in self._pools[scope]:
+ self._pools[scope][resource_type] = self.MAP_TYPE[resource_type]()
+ return self._pools[scope][resource_type].get(requestor, tag,
+ *args, **kwargs)
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def get_mac(self, resource_name):
+ """
+ Generate a new mac address to assign to the containers created.
+
+ :return: The MAC address
+ """
+
+ if resource_name in self._macs:
+ return self._macs[resource_name]
+
+ mac = ':'.join(map(''.join,
+ zip(*[iter(hex(self._next_mac)[2:].zfill(12))]*2)))
+ self._next_mac += 1
+
+ self._macs[resource_name] = mac
+ return mac
+
+ def get_ip(self, resource):
+ """
+ Generate a new ip address to assign to the containers created.
+
+ :return: The IP address
+ """
+
+ if resource in self._ips:
+ return self._ips[resource]
+
+ ip = str(self._next_ip)
+ self._next_ip += 1
+
+ self._ips[resource] = ip
+ return ip
diff --git a/vicn/core/api.py b/vicn/core/api.py
new file mode 100644
index 00000000..761177c9
--- /dev/null
+++ b/vicn/core/api.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+import json
+import logging
+import resource as ulimit
+import sys
+
+from netmodel.model.query import Query
+from netmodel.model.query import ACTION_SELECT, ACTION_INSERT
+from netmodel.model.query import ACTION_UPDATE, ACTION_SUBSCRIBE
+from netmodel.network.interface import InterfaceState
+from netmodel.util.singleton import Singleton
+from vicn.core.exception import NotConfigured
+from vicn.core.resource_mgr import ResourceManager
+from vicn.resource.node import Node
+
+DEFAULT_SETTINGS = {
+ 'network': '192.168.0.0/16',
+ 'mac_address_base': '0x00163e000000',
+ 'websocket_port': 9999
+}
+
+log = logging.getLogger(__name__)
+
+class Event_ts(asyncio.Event):
+ def set(self):
+ self._loop.call_soon_threadsafe(super().set)
+
+#------------------------------------------------------------------------------
+
+class API(metaclass = Singleton):
+
+ def terminate(self):
+ ResourceManager().terminate()
+
+ def parse_topology_file(self, topology_fn):
+ log.debug("Parsing topology file %(topology_fn)s" % locals())
+ try:
+ topology_fd = open(topology_fn, 'r')
+ except IOError:
+ self.error("Topology file '%(topology_fn)s not found" % locals())
+ return None
+
+ try:
+ topology = json.loads(topology_fd.read())
+
+ # SETTING
+ settings = DEFAULT_SETTINGS
+ settings.update(topology.get('settings'))
+
+ # VICN process-related initializations
+ nofile = settings.get('ulimit-n', None)
+ if nofile is not None and nofile > 0:
+ if nofile < 1024:
+ log.error('Too few allowed open files for the process')
+ import os; os._exit(1)
+
+ log.info('Setting open file descriptor limit to {}'.format(
+ nofile))
+ ulimit.setrlimit(
+ ulimit.RLIMIT_NOFILE,
+ (nofile, nofile))
+
+ ResourceManager(base=topology_fn, settings=settings)
+
+ # NODES
+ resources = topology.get('resources', list())
+ for resource in resources:
+ try:
+ ResourceManager().create_from_dict(**resource)
+ except Exception as e:
+ log.warning("Could not create resource '%r': %r" % \
+ (resource, e,))
+ import traceback; traceback.print_exc()
+ continue
+
+ except SyntaxError:
+ log.error("Error reading topology file '%s'" % (topology_fn,))
+ sys.exit(1)
+
+ log.debug("Done parsing topology file %(topology_fn)s" % locals())
+
+ def configure(self, name, setup=False):
+ log.info("Parsing configuration file", extra={'category': 'blue'})
+ self.parse_topology_file(name)
+ self._configured = True
+ ResourceManager().setup(commit=setup)
+
+ def setup(self):
+ if not self._configured:
+ raise NotConfigured
+ ResourceManager().setup()
+
+ def teardown(self):
+ ResourceManager().teardown()
+
+ def open_terminal(self, node_name):
+ node = ResourceManager().by_name(node_name)
+ assert isinstance(node, Node)
+
+ node.open_terminal()
diff --git a/vicn/core/attribute.py b/vicn/core/attribute.py
new file mode 100644
index 00000000..f6ec7c70
--- /dev/null
+++ b/vicn/core/attribute.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import abc
+import copy
+import logging
+import operator
+import types
+
+from netmodel.model.mapper import ObjectSpecification
+from netmodel.model.type import Type, Self
+from netmodel.util.meta import inheritors
+from netmodel.util.misc import is_iterable
+from vicn.core.exception import VICNListException
+from vicn.core.requirement import Requirement, RequirementList
+from vicn.core.sa_collections import InstrumentedList
+from vicn.core.state import UUID, NEVER_SET, Operations
+
+log = logging.getLogger(__name__)
+
+#------------------------------------------------------------------------------
+# Attribute Multiplicity
+#------------------------------------------------------------------------------
+
+class Multiplicity:
+ OneToOne = '1_1'
+ OneToMany = '1_N'
+ ManyToOne = 'N_1'
+ ManyToMany = 'N_N'
+
+
+ @staticmethod
+ def reverse(value):
+ reverse_map = {
+ Multiplicity.OneToOne: Multiplicity.OneToOne,
+ Multiplicity.OneToMany: Multiplicity.ManyToOne,
+ Multiplicity.ManyToOne: Multiplicity.OneToMany,
+ Multiplicity.ManyToMany: Multiplicity.ManyToMany,
+ }
+ return reverse_map[value]
+
+
+# Default attribute properties values (default to None)
+DEFAULT = {
+ 'multiplicity' : Multiplicity.OneToOne,
+ 'mandatory' : False,
+}
+
+#------------------------------------------------------------------------------
+# Attribute
+#------------------------------------------------------------------------------
+
+class Attribute(abc.ABC, ObjectSpecification):
+ properties = [
+ 'name',
+ 'type',
+ 'key',
+ 'description',
+ 'default',
+ 'choices',
+ 'mandatory',
+ 'multiplicity',
+ 'ro',
+ 'auto',
+ 'func',
+ 'requirements',
+ 'reverse_name',
+ 'reverse_description',
+ 'reverse_auto'
+ ]
+
+ def __init__(self, *args, **kwargs):
+ for key in Attribute.properties:
+ value = kwargs.pop(key, NEVER_SET)
+ setattr(self, key, value)
+
+ if len(args) == 1:
+ self.type, = args
+ elif len(args) == 2:
+ self.name, self.type = args
+
+ # self.type is optional since the type can be inherited. Although we
+ # will have to verify the attribute is complete at some point
+ if self.type:
+ if isinstance(self.type, str):
+ self.type = Type.from_string(self.type)
+ assert self.type is Self or Type.exists(self.type)
+
+ # Post processing attribute properties
+ if self.requirements is not NEVER_SET:
+ self.requirements = RequirementList(self.requirements)
+
+ self.is_aggregate = False
+
+ self._reverse_attributes = list()
+
+ #--------------------------------------------------------------------------
+ # Display
+ #--------------------------------------------------------------------------
+
+ def __repr__(self):
+ return '<Attribute {}>'.format(self.name)
+
+ __str__ = __repr__
+
+ # The following functions are required to allow comparing attributes, and
+ # using them as dict keys
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __hash__(self):
+ return hash(self.name)
+
+ #--------------------------------------------------------------------------
+ # Descriptor protocol
+ #
+ # see. https://docs.python.org/3/howto/descriptor.html
+ #--------------------------------------------------------------------------
+
+ def __get__(self, instance, owner=None):
+ if not instance:
+ return self
+
+ return instance.get(self.name, blocking=False)
+
+ def __set__(self, instance, value):
+ if not instance:
+ raise NotImplementedError('Setting default value not implemented')
+
+ instance.set(self.name, value, blocking=False)
+
+ def __delete__(self, instance):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+
+ def do_list_add(self, instance, value):
+ if instance.is_local_attribute(self.name):
+ from vicn.core.resource import Resource
+ if isinstance(value, Resource):
+ value = value.get_uuid()
+ return value
+ else:
+ try:
+ cur_value = vars(instance)[self.name]
+ if self.is_collection:
+ # copy the list
+ cur_value = list(cur_value)
+ except KeyError as e:
+ cur_value = None
+ if self.is_collection:
+ cur_value = list()
+
+ instance._state.dirty[self.name].trigger(Operations.LIST_ADD,
+ value, cur_value)
+
+ # prevent instrumented list to perform operation
+ raise VICNListException
+
+ def do_list_remove(self, instance, value):
+ if instance.is_local_attribute(self.name):
+ from vicn.core.resource import Resource
+ if isinstance(value, Resource):
+ value = value.get_uuid()
+ return value
+ else:
+ cur_value = vars(instance)[self.name]
+ if self.is_collection:
+ # copy the list
+ cur_value = list(cur_value)
+ instance._state.dirty[self.name].trigger(Operations.LIST_REMOVE,
+ value, cur_value)
+
+ # prevent instrumented list to perform operation
+ raise VICNListException
+
+ def do_list_clear(self, instance):
+ if instance.is_local_attribute(self.name):
+ return
+ else:
+ cur_value = vars(instance)[self.name]
+ if self.is_collection:
+ # copy the list
+ cur_value = list(cur_value)
+ instance._state.dirty[self.name].trigger(Operations.LIST_CLEAR,
+ value, cur_value)
+
+ # prevent instrumented list to perform operation
+ raise VICNListException
+
+ def handle_getitem(self, instance, item):
+ if isinstance(item, UUID):
+ from vicn.core.resource_mgr import ResourceManager
+ return ResourceManager().by_uuid(item)
+ return item
+
+ #--------------------------------------------------------------------------
+ # Accessors
+ #--------------------------------------------------------------------------
+
+ def __getattribute__(self, name):
+ value = super().__getattribute__(name)
+ if value is NEVER_SET:
+ if name == 'default':
+ return list() if self.is_collection else None
+ return DEFAULT.get(name, None)
+ return value
+
+ def has_reverse_attribute(self):
+ return self.reverse_name and self.multiplicity
+
+ @property
+ def is_collection(self):
+ return self.multiplicity in (Multiplicity.OneToMany,
+ Multiplicity.ManyToMany)
+
+ def is_set(self, instance):
+ return instance.is_set(self.name)
+
+ #--------------------------------------------------------------------------
+ # Operations
+ #--------------------------------------------------------------------------
+
+ def merge(self, parent):
+ for prop in Attribute.properties:
+ # NOTE: we cannot use getattr otherwise we get the default value,
+ # and we never override
+ value = vars(self).get(prop, NEVER_SET)
+ if value is not NEVER_SET and not is_iterable(value):
+ continue
+
+ parent_value = vars(parent).get(prop, NEVER_SET)
+ if parent_value is NEVER_SET:
+ continue
+
+ if parent_value:
+ if is_iterable(value):
+ value.extend(parent_value)
+ else:
+ setattr(self, prop, parent_value)
+
+#------------------------------------------------------------------------------
+
+class Reference:
+ """
+ Value reference.
+
+ Attribute value refers to attribute value on a different resource.
+ Use resource = Self to point to another attribute of the same resource.
+ """
+
+ def __init__(self, resource, attribute=None):
+ self._resource = resource
+ self._attribute = attribute
diff --git a/vicn/core/command_helpers.py b/vicn/core/command_helpers.py
new file mode 100644
index 00000000..4732e7b5
--- /dev/null
+++ b/vicn/core/command_helpers.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.commands import Command, Commands
+
+CMD_PRINT_TO_FILE = 'echo -n "{content}" > {filename}'
+
+class CommandHelper:
+
+ @staticmethod
+ def if_cmd(cmd_condition, if_true = None, if_false = None):
+ cmd = cmd_condition
+ if if_true:
+ cmd = cmd & if_true
+ if if_false:
+ cmd = cmd | if_false
+ return cmd
+
+ @staticmethod
+ def file_exists(filename, if_true = None, if_false = None):
+ cmd = Command('test -f {}'.format(filename))
+ return CommandHelper.if_cmd(cmd, if_true, if_false)
+
+ @staticmethod
+ def print_to_file(node, filename, content):
+ escaped_content = content.replace('{', '{{').replace('}', '}}')
+ return BashTask(self.node, CMD_PRINT_TO_FILE,
+ {'content': escaped_content, 'filename': filename})
+
+ @staticmethod
+ def print_to_file_no_escape(filename, content):
+ return BashTask(self.node, CMD_PRINT_TO_FILE,
+ {'content': content, 'filename': filename})
diff --git a/vicn/core/commands.py b/vicn/core/commands.py
new file mode 100644
index 00000000..41c06bf5
--- /dev/null
+++ b/vicn/core/commands.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import inspect
+import logging
+import shlex
+
+from vicn.core.exception import CommandException
+
+log = logging.getLogger(__name__)
+
+#------------------------------------------------------------------------------
+# Helper functions
+#------------------------------------------------------------------------------
+
+def bashize(command):
+ ret = "bash -c " + shlex.quote(command)
+ return ret
+
+def parenthesize(command):
+ return '({})'.format(command)
+
+def do_parenthesize(command):
+ if '&&' in command or '||' in command:
+ if command[0] == '(':
+ return command
+ else:
+ return parenthesize(command)
+ else:
+ return command
+
+#------------------------------------------------------------------------------
+
+class ReturnValue:
+ def __init__(self, return_value = None, stdout = None, stderr = None):
+ self._return_value = return_value
+
+ # We use accessors since it seems impossible to trigger properties from
+ # __init__
+ self._set_stdout(stdout)
+ self._set_stderr(stderr)
+
+ def __repr__(self):
+ return '<ReturnValue ({}) - OUT [{}] - ERR [{}]>'.format(
+ self._return_value, self._stdout, self._stderr)
+
+ def __str__(self):
+ return self.__repr__()
+
+ def _clean(self, value):
+ if value is None or isinstance(value, str):
+ return value
+ return value.decode('utf-8')
+
+ def _set_stdout(self, value):
+ self._stdout = self._clean(value)
+
+ def _set_stderr(self, value):
+ self._stderr = self._clean(value)
+
+ @property
+ def stdout(self):
+ return self._stdout
+
+ @stdout.setter
+ def stdout(self, value):
+ return self._set_stdout(value)
+
+ @property
+ def stderr(self):
+ return self._stderr
+
+ @stderr.setter
+ def stderr(self, value):
+ return self._set_stderr(value)
+
+ @property
+ def return_value(self):
+ return self._return_value
+
+ @return_value.setter
+ def return_value(self, value):
+ self._return_value = value
+
+ def __bool__(self):
+ return self._return_value == 0
+
+#------------------------------------------------------------------------------
+
+class Command:
+ """
+ Bash command
+
+ Todo:
+ - Commands with ; should be "bashized"
+ """
+ def __init__(self, commandline, node=None, parameters = None,
+ description = None, callback = None, blocking=True, lock=None):
+ self._commandline = commandline
+ self._node = node
+ self._parameters = parameters if parameters else dict()
+ self._description = description
+ self._callback = callback
+ self._blocking = blocking
+ self._lock = lock
+
+ def __str__(self):
+ try:
+ return self.full_commandline
+ except:
+ return self.commandline + ' -- ' + str(self.parameters)
+
+ def __repr__(self):
+ return '<Command {} -- {}'.format(self.commandline, self.parameters)
+
+ @property
+ def commandline(self):
+ return self._commandline
+
+ @property
+ def full_commandline(self):
+ cmd = self._commandline.format(**self._parameters)
+ if ('||' in cmd or '&&' in cmd or '|' in cmd or '<' in cmd or
+ '>' in cmd):
+ return bashize(cmd)
+ return cmd
+
+ @property
+ def full_commandline_nobashize(self):
+ """
+ TMP to fix issue with bashize heuristic above...
+ """
+ cmd = self._commandline.format(**self.parameters)
+ return cmd
+
+ @property
+ def command(self):
+ return self
+
+ @property
+ def node(self):
+ return self._node
+
+ @node.setter
+ def node(self, node):
+ self._node = node
+
+ @property
+ def parameters(self):
+ return self._parameters
+
+ @parameters.setter
+ def parameters(self, parameters):
+ self._parameters = parameters
+
+ @property
+ def description(self):
+ if not self._description:
+ return self._description
+ return self._description.format(**self.parameters)
+
+ @property
+ def success_callback(self):
+ return self._on_success
+
+ @property
+ def failure_callback(self):
+ return self._on_failure
+
+ @property
+ def blocking(self):
+ return self._blocking
+
+ @property
+ def lock(self):
+ return self._lock
+
+ def apply(self, params):
+ self._parameters.update(params)
+ return self
+
+ def __and__(self, other):
+ commandline = self.commandline + ' && ' + other.commandline
+ all_params = dict(i for c in (self, other)
+ for i in c.parameters.items())
+ return Command(commandline, parameters = all_params)
+
+ def __or__(self, other):
+ commandline = self.commandline + ' || ' + other.commandline
+ all_params = dict(i for c in (self, other)
+ for i in c.parameters.items())
+ return Command(commandline, parameters = all_params)
+
+ def __bool__(self):
+ return bool(self._commandline)
+
+ def submit(self):
+ CMD_MGR.execute([self])
+
+ def execute(self):
+ cmd = self.full_commandline
+ cmd_str = cmd[:77]+'...' if len(cmd) > 80 else cmd
+ log.debug('Node {}: {} ({})'.format(self.node.name, cmd_str,
+ self._description))
+
+ rv = self.node.execute(cmd)
+
+ if not rv:
+ raise CommandException
+ if self._callback:
+ if self._lock:
+ self._lock.acquire()
+ self._callback(rv)
+ if self._lock:
+ self._lock.release()
+ return rv
+
+#------------------------------------------------------------------------------
+
+class BackgroundCommand(Command):
+ pass
+
+#------------------------------------------------------------------------------
+
+class Commands(Command):
+
+ def __init__(self):
+ self._commands = list()
+ self._node = None
+
+ def __repr__(self):
+ return '<Commands {}>'.format(str(self))
+
+ def __str__(self):
+ return self.commandline
+
+ def add(self, command):
+ if not command:
+ return
+ if not isinstance(command, Command):
+ command = Command(command)
+ self._commands.append(command)
+
+ def _do_command(self, sep):
+ if len(self.commands) == 1:
+ full_cmd = sep.join(c.commandline for c in self.commands)
+ else:
+ full_cmd = sep.join(do_parenthesize(c.commandline)
+ for c in self.commands)
+ all_params = dict(i for c in self.commands
+ for i in c.parameters.items())
+
+ return Command(full_cmd, parameters = all_params)
+
+ @property
+ def command(self):
+ raise NotImplementedError('Not implemented')
+
+ @property
+ def commandline(self):
+ return self.command.commandline
+
+ @property
+ def parameters(self):
+ parameters = dict()
+ for command in self.commands:
+ parameters.update(command.parameters)
+ return parameters
+
+ @parameters.setter
+ def parameters(self, parameters):
+ for command in self.commands:
+ command.parameters = parameters
+
+ @property
+ def commands(self):
+ return self._commands
+
+ def apply(self, params):
+ self._commands = [c.apply(params) for c in self._commands]
+ return self._commands
+
+ __lshift__ = add
+
+ def __bool__(self):
+ return any(bool(c) for c in self._commands)
+
+#------------------------------------------------------------------------------
+
+class ParallelCommands(Commands):
+ @property
+ def command(self):
+ log.warning('Commands executed sequentially')
+ return self._do_command(';')
+
+#------------------------------------------------------------------------------
+
+class SequentialCommands(Commands):
+ @property
+ def command(self, fatal = True):
+ SEP = ' && ' if fatal else '; '
+ return self._do_command(SEP)
+
+#------------------------------------------------------------------------------
+
+def sequential_bash_from_docstring(fn):
+ def decorator(*args, **kwargs):
+ c = SequentialCommands()
+
+ desc = None
+ for line in fn.__doc__.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith('#'):
+ desc = line[1:].strip()
+ continue
+ c << Command(line, description = desc)
+ desc = None
+
+ # XXX we don't support keyword args
+ arg_info = inspect.getargspec(fn)
+ assert not arg_info.varargs
+ c.parameters = dict(zip(arg_info.args, args))
+ log.debug('sequential_bash_from_docstring: {}'.format(c))
+
+ # Execute the code in the function
+ fn(*args, **kwargs)
+
+ return c
+
+ return decorator
+
+bash_from_docstring = sequential_bash_from_docstring
+
+def execute_on_node(fn):
+ """
+ Decorator: execute the command returned by the function on the node found
+ in attributes. Note that such an attribute should be available.
+ This assumes the function returns a command
+
+ We need output in case apply_rx is used. This should be made an option
+ """
+
+ def wrapper(self, *args, **kwargs):
+ return self.node.execute(fn(self, *args, **kwargs), output = True)
+ return wrapper
+
+def apply_rx(rx):
+ """
+ Apply a compiled regular expression to the result of the decorated function.
+ Returns a dict (whose keys should be attributes of the resource that need
+ to be updated).
+ """
+
+ def decorator(fn):
+ def wrapper(*args, **kwargs):
+ ret = fn(*args, **kwargs)
+ return [m.groupdict() for m in rx.finditer(ret.stdout)]
+ return wrapper
+ return decorator
diff --git a/vicn/core/event.py b/vicn/core/event.py
new file mode 100644
index 00000000..ee418257
--- /dev/null
+++ b/vicn/core/event.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Event:
+ pass
+
+class AttributeChangedEvent(Event):
+ def __init__(self, condition=None):
+ self._condition = condition
+
diff --git a/vicn/core/exception.py b/vicn/core/exception.py
new file mode 100644
index 00000000..d7422723
--- /dev/null
+++ b/vicn/core/exception.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class VICNException(Exception): pass
+
+class CommandException(VICNException): pass
+
+class ProcessException(VICNException): pass
+class NotConfigured(ProcessException): pass
+
+class ParameterException(VICNException): pass
+class InvalidResource(ParameterException): pass
+
+class ABCException(VICNException): pass
+class NotImplemented(VICNException): pass
+
+class DependencyException(VICNException): pass
+class InitializeException(VICNException): pass
+class CheckException(VICNException): pass
+class SetupException(VICNException): pass
+
+class VICNListException(VICNException): pass
+
+class ResourceNotFound(VICNException): pass
diff --git a/vicn/core/requirement.py b/vicn/core/requirement.py
new file mode 100644
index 00000000..c42cecd9
--- /dev/null
+++ b/vicn/core/requirement.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import enum
+
+from netmodel.model.mapper import ObjectSpecification
+from vicn.core.exception import VICNException
+
+#------------------------------------------------------------------------------
+# Enums
+#------------------------------------------------------------------------------
+
+class RequirementScope(enum.Enum):
+ INSTANCE = 'Instance'
+ CLASS = 'Class'
+
+#------------------------------------------------------------------------------
+# Exceptions
+#------------------------------------------------------------------------------
+
+class RequirementError(VICNException):
+
+ def __init__(self, instance, attr):
+ super().__init__()
+ self._instance = instance
+ self._attr = attr
+
+ def __str__(self):
+ return "Requirement on {}.{} could not be satisfied:".format(
+ self._instance, self._attr)
+
+class RequiredAttributeError(RequirementError):
+
+ def __str__(self):
+ return super().__str__() + "could not find attribute {}".format(
+ self._attr)
+
+class RequiredPropertyError(RequirementError):
+ def __init__(self, instance, attr, prop):
+ super().__init__(instance, attr)
+ self._prop = prop
+
+ def __str__(self):
+ return super().__str__()+ "property {} is not verified".format(
+ self._prop)
+
+#------------------------------------------------------------------------------
+# Class: Property
+#------------------------------------------------------------------------------
+
+class Property:
+
+ TYPE_ANY_OF = 0
+ #XXX cant think of a good use case for that
+ #TYPE_ALL_OF = 1
+
+ def __init__(self, value, property_type=TYPE_ANY_OF):
+ self._type = property_type
+ try:
+ self._value = set(value)
+ except TypeError: #value is not iterable, it is a single value
+ self._value = set()
+ self._value.add(value)
+
+
+ @property
+ def property_type(self):
+ return self._type
+
+ @property
+ def value(self):
+ return self._value
+
+ def check(self, value):
+ return value in self._value
+
+ def __str__(self):
+ return str(self._value)
+
+ def merge(self, other):
+ assert self._type is other.property_type, \
+ "Properties must be of same type to be merged"
+
+ #if self._type is TYPE_ANY_OF:
+ self._value.intersection_update(other.value)
+ #elif self._type is TYPE_ALL_OF:
+ # self._value.union_update(other.value)
+
+#------------------------------------------------------------------------------
+# Class: Requirement
+#------------------------------------------------------------------------------
+
+class Requirement(ObjectSpecification):
+ """Resource requirement
+
+ This class allows to specify a requirement on a given resource, or on a
+ class of resources (all instances of a given class).
+ """
+
+ #--------------------------------------------------------------------------
+ # Constructor
+ #--------------------------------------------------------------------------
+
+ def __init__(self, requirement_type, properties = None,
+ capabilities = None, scope = RequirementScope.INSTANCE,
+ fatal = True, must_be_setup = False):
+ """
+ Args:
+ requirement_type (): the attribute on which the requirement is made
+ properties (Optional[XXX]): XXX (defaults to None)
+ scope (Optional[enum RequirementScope]): Is the requirement dealing
+ with an instance, or a class (all instance of the class)
+ (defaults to RequirementScope.INSTANCE)
+ fatal (Optional[bool]): is the failure of the requirement fatal
+ (raises an error), or not (raises a warning) (defaults to True)
+ must_be_setup (Optional[bool]): defaults to False
+ """
+ self._type = requirement_type
+ self._properties = {}
+ if properties:
+ for prop in properties:
+ self._properties[prop] = Property(properties[prop])
+ self._capabilities = capabilities if capabilities else set()
+ self._scope = scope
+ self._fatal = fatal
+ self._must_be_up = must_be_setup
+
+ #--------------------------------------------------------------------------
+ # Accessors and properties
+ #--------------------------------------------------------------------------
+
+ @property
+ def properties(self):
+ return self._properties
+
+ @property
+ def requirement_type(self):
+ return self._type
+
+ @property
+ def must_be_up(self):
+ return self._must_be_up
+
+ #--------------------------------------------------------------------------
+ # Display
+ #--------------------------------------------------------------------------
+
+ def __str__(self):
+ prop_str = "{" + ",".join(map(lambda x: "'{}': {}".format(x,
+ self._properties[x]), self._properties.keys())) +"}"
+ return "<type={}, properties={}, must_be_up={}>".format(self._type,
+ prop_str, self._must_be_up)
+
+ #--------------------------------------------------------------------------
+ # Requirement operators
+ #--------------------------------------------------------------------------
+
+ def check(self, instance):
+ if not hasattr(instance, self._type):
+ raise RequiredAttributeError(instance, self._type)
+
+ instance_attr = getattr(instance, self._type)
+ if not instance_attr:
+ raise TypeError("instance_attr is none")
+
+ for prop in self.properties:
+ if not hasattr(instance_attr, prop):
+ raise RequiredAttributeError(instance, self._type)
+ if not self._properties[prop].check(getattr(instance_attr, prop)):
+ raise RequiredPropertyError(instance, self._type, prop)
+
+ return True
+
+ #--------------------------------------------------------------------------
+ # Requirement logic
+ #--------------------------------------------------------------------------
+
+ def merge(self, other):
+ assert other.requirement_type == self._type, \
+ "Cannot merge Requirements with different types"
+
+ for prop in other.properties:
+ if prop in self._properties:
+ self._properties[prop].merge(other.properties[prop])
+ else:
+ self._properties[prop] = other.properties[prop]
+
+ if other._capabilities:
+ self._capabilities |= other._capabilities
+
+#------------------------------------------------------------------------------
+# Class: Requirement list
+#------------------------------------------------------------------------------
+
+class RequirementList(list):
+
+ def __init__(self,x=None):
+ super().__init__()
+ if x:
+ self.extend(x)
+
+ def append(self,x):
+ assert isinstance(x,Requirement)
+ # XXX O(n) right now, might be able to do better
+ for req in self:
+ if req.requirement_type == x.requirement_type:
+ req.merge(x)
+ return
+
+ super().append(x)
+
+ def extend(self, x):
+ for y in x:
+ self.append(y)
diff --git a/vicn/core/resource.py b/vicn/core/resource.py
new file mode 100644
index 00000000..53ad2181
--- /dev/null
+++ b/vicn/core/resource.py
@@ -0,0 +1,898 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+import copy
+import logging
+import operator
+import random
+import string
+import sys
+import traceback
+import types
+
+from abc import ABC, ABCMeta
+
+from netmodel.model.mapper import ObjectSpecification
+from netmodel.model.type import String, Bool, Integer, Dict
+from netmodel.model.type import BaseType, Self
+from netmodel.util.deprecated import deprecated
+from netmodel.util.singleton import Singleton
+from vicn.core.attribute import Attribute, Multiplicity, Reference
+from vicn.core.attribute import NEVER_SET
+from vicn.core.commands import ReturnValue
+from vicn.core.event import Event, AttributeChangedEvent
+from vicn.core.exception import VICNException, ResourceNotFound
+from vicn.core.resource_factory import ResourceFactory
+from vicn.core.requirement import Requirement, Property
+from vicn.core.sa_collections import InstrumentedList, _list_decorators
+from vicn.core.scheduling_algebra import SchedulingAlgebra
+from vicn.core.state import ResourceState, UUID
+from vicn.core.state import Operations, InstanceState
+from vicn.core.task import run_task, BashTask
+
+log = logging.getLogger(__name__)
+
+NAME_SEP = '-'
+
+# Warning and error messages
+
+W_UNK_ATTR = 'Ignored unknown attribute {} for resource {}'
+E_UNK_RES_NAME = 'Unknown resource name for attribute {} in {} ({}) : {}'
+E_GET_NON_LOCAL = 'Cannot get non-local attribute {} for resource {}'
+E_AUTO_UNM = 'Trying to auto-instanciate attribute {} on unmanaged resource {}'
+
+#------------------------------------------------------------------------------
+# Resource category
+#------------------------------------------------------------------------------
+
+# A base resource is not instanciated itself but uses delegates. Which one to
+# use is resolved during initialization
+class TopLevelResource: pass
+
+class FactoryResource(TopLevelResource): pass
+class CategoryResource(TopLevelResource): pass
+
+#------------------------------------------------------------------------------
+
+class ResourceMetaclass(ABCMeta):
+ def __init__(cls, class_name, parents, attrs):
+ """
+ Args:
+ cls: The class type we're registering.
+ class_name: A String containing the class_name.
+ parents: The parent class types of 'cls'.
+ attrs: The attribute (members) of 'cls'.
+ """
+ super().__init__(class_name, parents, attrs)
+
+ # We use the metaclass to create attributes for instance, even before
+ # the Resource Factory is called. They are needed both for initializing
+ # attributes and reverse attributes, in whatever order. Only class
+ # creation allow us to clear _attributes, otherwise, we will just add
+ # those from the parent, siblings, etc...
+ cls._sanitize()
+
+#------------------------------------------------------------------------------
+
+class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass):
+ """Base Resource class
+
+ The base Resource class implements all the logic related to resource
+ instances.
+
+ See also :
+ * ResourceManager : logic related to class instanciation
+ * Resource metaclass : logic related to class construction
+ * ResourceFactory : logic related to available classes and mapping from
+ name to type
+ """
+
+ __type__ = TopLevelResource
+
+ name = Attribute(String, description = 'Alias name for the resource')
+ managed = Attribute(Bool, description = 'Flag: resource is managed',
+ default = True)
+ owner = Attribute(Self, description = 'Owning resource', default = None)
+ data = Attribute(Dict, description = 'User data')
+
+ #---------------------------------------------------------------------------
+ # Constructor
+ #---------------------------------------------------------------------------
+
+ def __new__(cls, *args, **kwargs):
+ """
+ We implement a "factory method" design pattern in the constructor...
+ """
+ # Ensure the resource factory exists and has been initialized, and thus
+ # that Resource objects are fully created
+ from vicn.core.resource_mgr import ResourceManager
+
+ ResourceFactory()
+
+ delegate = ResourceManager().get_resource_with_capabilities(cls, set())
+ if not delegate:
+ log.error('No delegate for abstract resource : %s', cls.__name__)
+ raise VICNException
+
+ instance = super().__new__(delegate)
+
+ return instance
+
+ def __init__(self, *args, **kwargs):
+ from vicn.core.resource_mgr import ResourceManager
+
+ # Cache dependencies
+ self._deps = None
+
+ # Internal data tag for resources
+ self._internal_data = dict()
+
+ mandatory = { a.name for a in self.iter_attributes() if a.mandatory }
+
+ for key, value in kwargs.items():
+ attribute = self.get_attribute(key)
+ if attribute is None:
+ log.warning(W_UNK_ATTR.format(key, self.get_type()))
+ continue
+
+ if isinstance(value, Reference):
+ if value._resource is Self:
+ value = getattr(self, value._attribute)
+ else:
+ value = getattr(value._resource, value._attribute)
+
+ if value and issubclass(attribute.type, Resource):
+ if attribute.is_collection:
+ new_value = list()
+ for x in value:
+ if isinstance(x, str):
+ resource = ResourceManager().by_name(x)
+ elif isinstance(x, UUID):
+ resource = ResourceManager().by_uuid(x)
+ else:
+ resource = x
+ if not resource:
+ raise VICNException(E_UNK_RES_NAME.format(key,
+ self.name, self.__class__.__name__, x))
+ element = resource if isinstance(resource, Reference) \
+ else resource._state.uuid
+ new_value.append(element)
+ value = new_value
+ else:
+ if isinstance(value, str):
+ resource = ResourceManager().by_name(value)
+ elif isinstance(value, UUID):
+ resource = ResourceManager().by_uuid(value)
+ else:
+ resource = value
+ if not resource:
+ raise VICNException(E_UNK_RES_NAME.format(key,
+ self.name, self.__class__.__name__, value))
+ value = value if isinstance(resource, Reference) \
+ else resource._state.uuid
+ self.set(key, value, blocking=False)
+ mandatory -= { key }
+
+ # Check that all mandatory atttributes have been set
+ # Mandatory resource attributes will be marked as pending since they
+ # might be discovered
+ # Eventually, their absence will be discovered at runtime
+ if mandatory:
+ raise VICNException('Mandatory attributes not set: %r' % (mandatory,))
+
+ # Check requirements
+ for attr in self.iter_attributes():
+ if issubclass(attr.type, Resource) and attr.requirements:
+ for req in attr.requirements:
+ instance = self.get(attr.name)
+ if instance is None:
+ continue
+ ResourceManager().add_instance_requirement(instance, req)
+
+ self._subresources = None
+
+ def __after__(self):
+ return tuple()
+
+ def __after_init__(self):
+ return tuple()
+
+ def __subresources__(self):
+ return None
+
+ def set_subresources(self, subresources):
+ if not subresources:
+ return
+
+ # Add state to operators
+ for sr in subresources:
+ if not hasattr(sr, '_state'):
+ sr._state = InstanceState(self._state.manager, sr)
+
+ self._subresources = subresources
+
+ def get_uuid(self):
+ return self._state.uuid
+
+ def from_uuid(self, uuid):
+ return self._state.manager.by_uuid(uuid)
+
+ #--------------------------------------------------------------------------
+ # Object model
+ #--------------------------------------------------------------------------
+
+ def get(self, attribute_name, default=NEVER_SET, unref=True, resolve=True,
+ allow_never_set=True, blocking=True):
+
+ attribute = self.get_attribute(attribute_name)
+
+ # Handling Lambda attributes
+ if hasattr(attribute, 'func') and attribute.func:
+ value = attribute.func(self)
+ else:
+ if self.is_local_attribute(attribute.name):
+ value = vars(self).get(attribute.name, NEVER_SET)
+ else:
+ # A pending value has priority
+ value = self._state.dirty.get(attribute.name, NEVER_SET)
+ if value.value is not NEVER_SET:
+ value = value.value
+ else:
+ # otherwise, let's use a previously fetched value if it
+ # exists
+ value = vars(self).get(attribute.name, NEVER_SET)
+
+ if value is NEVER_SET:
+ if not allow_never_set:
+ log.error(E_GET_NON_LOCAL.format(attribute_name,
+ self._state.uuid))
+ raise NotImplementedError
+
+ if attribute.is_collection:
+ value = self.get_default_collection(attribute)
+ else:
+ if attribute.auto:
+ # Automatic instanciation
+ if attribute.requirements:
+ log.warning('Ignored requirements {}'.format(
+ attribute.requirements))
+ value = self.auto_instanciate(attribute)
+
+ if value is NEVER_SET:
+ value = self.get_default(attribute)
+
+ if self.is_local_attribute(attribute.name):
+ self.set(attribute.name, value)
+
+ if unref and isinstance(value, UUID):
+ value = self.from_uuid(value)
+
+ if resolve and isinstance(value, Reference):
+ if value._resource is Self:
+ value = getattr(self, value._attribute)
+ else:
+ value = getattr(value._resource, value._attribute)
+
+ return value
+
+ async def async_get(self, attribute_name, default=NEVER_SET, unref=True,
+ resolve=True, allow_never_set=False, blocking=True):
+ attribute = self.get_attribute(attribute_name)
+
+ # Handling Lambda attributes
+ if hasattr(attribute, 'func') and attribute.func:
+ value = self.func(self)
+ else:
+ if self.is_local_attribute(attribute.name):
+ value = vars(self).get(attribute.name, NEVER_SET)
+ else:
+
+ # A pending value has priority
+ value = self._state.dirty.get(attribute.name, NEVER_SET)
+ if value.value is not NEVER_SET:
+ value = value.value
+ else:
+ # otherwise, let's use a previously fetched value if it
+ # exists
+ value = vars(self).get(attribute.name, NEVER_SET)
+ if value is NEVER_SET:
+ await self._state.manager.attribute_get(self,
+ attribute_name, value)
+ value = vars(self).get(attribute.name, NEVER_SET)
+
+ # Handling NEVER_SET
+ if value is NEVER_SET:
+ if not allow_never_set:
+ log.error(E_GET_NON_LOCAL.format(attribute_name,
+ self._state.uuid))
+ raise NotImplementedError
+
+ if attribute.is_collection:
+ value = self.get_default_collection(attribute)
+ else:
+ if attribute.auto:
+ # Automatic instanciation
+ if attribute.requirements:
+ log.warning('Ignored requirements {}'.format(
+ attribute.requirements))
+ value = self.auto_instanciate(attribute)
+
+ if value is NEVER_SET:
+ value = self.get_default(attribute)
+
+ if value is self.is_local_attribute(attribute.name):
+ self.set(attribute.name, value)
+
+ if unref and isinstance(value, UUID):
+ value = self.from_uuid(value)
+
+ if resolve and isinstance(value, Reference):
+ if value._resource is Self:
+ value = getattr(self, value._attribute)
+ else:
+ value = getattr(value._resource, value._attribute)
+
+ return value
+
+ def _set(self, attribute_name, value, current=False, set_reverse=True):
+ """
+ Note that set does not automatically mark a resource dirty.
+ We might need a flag to avoid dirty by default, which will be useful
+ when a resource is modified by another resource: eg x.up, or
+ x.ip_address = y, ...
+ Returns : task that can be monitored (note that it is not scheduled)
+ """
+ attribute = self.get_attribute(attribute_name)
+
+ if set_reverse and attribute.reverse_name:
+ for base in self.__class__.mro():
+ if not hasattr(base, '_reverse_attributes'):
+ continue
+
+ for ra in base._reverse_attributes.get(attribute, list()):
+ # Value information : we need resources, not uuids
+ if attribute.is_collection:
+ lst = list()
+ if value:
+ for x in value:
+ if isinstance(x, UUID):
+ x = self.from_uuid(x)
+ lst.append(x)
+ value = InstrumentedList(lst)
+ value._attribute = attribute
+ value._instance = self
+ else:
+ if isinstance(value, UUID):
+ value = self.from_uuid(value)
+
+ if ra.multiplicity == Multiplicity.OneToOne:
+ if value is not None:
+ value.set(ra.name, self, set_reverse = False)
+ elif ra.multiplicity == Multiplicity.ManyToOne:
+ for element in value:
+ value.set(ra.name, self, set_reverse = False)
+ elif ra.multiplicity == Multiplicity.OneToMany:
+ if value is not None:
+ collection = value.get(ra.name)
+ collection.append(self)
+ else:
+ value is None
+ elif ra.multiplicity == Multiplicity.ManyToMany:
+ collection = value.get(ra.name)
+ value.extend(self)
+
+ # Handling value : we need uuids, not resources
+ if attribute.is_collection:
+ if not isinstance(value, InstrumentedList):
+ lst = list()
+ if value:
+ for x in value:
+ if isinstance(x, Resource):
+ x = x.get_uuid()
+ lst.append(x)
+
+ value = InstrumentedList(lst)
+ else:
+ value = InstrumentedList([])
+ value._attribute = attribute
+ value._instance = self
+ else:
+ if isinstance(value, Resource):
+ value = value.get_uuid()
+ return value
+
+ def set(self, attribute_name, value, current=False, set_reverse=True,
+ blocking = True):
+ value = self._set(attribute_name, value, current=current,
+ set_reverse=set_reverse)
+ if self.is_local_attribute(attribute_name) or current:
+ # super()
+ if value is None:
+ attribute = self.get_attribute(attribute_name)
+ vars(self)[attribute_name] = value
+
+ else:
+ fut = self._state.manager.attribute_set(self, attribute_name, value)
+ asyncio.ensure_future(fut)
+
+ async def async_set(self, attribute_name, value, current=False,
+ set_reverse=True, blocking=True):
+ """
+ Example:
+ - setting the ip address on a node's interface
+
+ We need to communicate our intention to the resource manager, which will
+ process our request in a centralized fashion, and do the necessary
+ steps for us to set the value properly.
+ """
+ value = self._set(attribute_name, value, current=current,
+ set_reverse=set_reverse)
+ await self._state.manager.attribute_set(self, attribute_name, value,
+ blocking=blocking)
+
+ def set_many(self, attribute_dict, current=False):
+ if not attribute_dict:
+ return
+ for k, v in attribute_dict.items():
+ self.set(k, v, current=current)
+
+ def is_set(self, attribute_name):
+ return attribute_name in vars(self)
+
+# def clean(self, attribute_name):
+# return self._state.manager.attribute_clean(self, attribute_name)
+
+ def is_local_attribute(self, attribute_name):
+ ACTIONS = ['get', 'set', 'add', 'remove']
+ for action in ACTIONS:
+ method = '_{}_{}'.format(action, attribute_name)
+ if hasattr(self, method) and getattr(self, method) is not None:
+ return False
+ return True
+
+ def get_default_collection(self, attribute):
+ if isinstance(attribute.default, types.FunctionType):
+ default = attribute.default(self)
+ elif isinstance(attribute.default, Reference):
+ if attribute.default._resource is Self:
+ default = getattr(self, attribute.default._attribute)
+ else:
+ default = getattr(attribute.default._resource,
+ attribute.default._attribute)
+ else:
+ default = attribute.default
+ value = InstrumentedList(default)
+ value._attribute = attribute
+ value._instance = self
+ return value
+
+ def get_default(self, attribute):
+ if isinstance(attribute.default, types.FunctionType):
+ value = attribute.default(self)
+ elif isinstance(attribute.default, Reference):
+ if attribute.default._resource is Self:
+ value = getattr(self, attribute.default._attribute)
+ else:
+ value = getattr(attribute.default._resource,
+ attribute.default._attribute)
+ else:
+ value = copy.deepcopy(attribute.default)
+ return value
+
+ def async_get_task(self, attribute_name):
+ task = getattr(self, '_get_{}'.format(attribute_name))()
+ assert not isinstance(task, tuple)
+ return task
+
+
+ def async_set_task(self, attribute_name, value):
+ raise NotImplementedError
+ return async_task(async_set_task, attribute_name, value)
+
+ @classmethod
+ def get_attribute(cls, key):
+ # Searchs if it is a recursive attribute
+ try:
+ pos = key.find('.')
+ if pos >= 0:
+ attr, subattr = key[0:pos], key[pos+1: len(key)]
+ return getattr(cls,attr).type.get_attribute(subattr)
+ return getattr(cls, key)
+ except AttributeError:
+ return None
+
+ @classmethod
+ def _sanitize(cls):
+ """Sanitize the object model to accomodate for multiple declaration
+ styles
+
+ In particular, this method:
+ - set names to all attributes
+ """
+ cls._reverse_attributes = dict()
+ cur_reverse_attributes = dict()
+ for name, obj in vars(cls).items():
+ if not isinstance(obj, ObjectSpecification):
+ continue
+ if isinstance(obj, Attribute):
+ obj.name = name
+
+ # Remember whether a reverse_name is defined before loading
+ # inherited properties from parent
+ has_reverse = bool(obj.reverse_name)
+
+ # Handle overloaded attributes
+ # By recursion, it is sufficient to look into the parent
+ for base in cls.__bases__:
+ if hasattr(base, name):
+ parent_attribute = getattr(base, name)
+ obj.merge(parent_attribute)
+ assert obj.type
+
+ # Handle reverse attribute
+ #
+ # NOTE: we need to do this after merging to be sure we get all
+ # properties inherited from parent (eg. multiplicity)
+ if has_reverse:
+ a = {
+ 'name' : obj.reverse_name,
+ 'description' : obj.reverse_description,
+ 'multiplicity' : Multiplicity.reverse(obj.multiplicity),
+ 'auto' : obj.reverse_auto,
+ }
+ reverse_attribute = Attribute(cls, **a)
+ reverse_attribute.is_aggregate = True
+
+ cur_reverse_attributes[obj.type] = reverse_attribute
+
+ #print('*** class backref ***', cls, obj, reverse_attribute)
+ if not obj in cls._reverse_attributes:
+ cls._reverse_attributes[obj] = list()
+ cls._reverse_attributes[obj].append(reverse_attribute)
+
+ for kls, a in cur_reverse_attributes.items():
+ setattr(kls, a.name, a)
+
+ @classmethod
+ def iter_attributes(cls, aggregates = False):
+ for name in dir(cls):
+ attribute = getattr(cls, name)
+ if not isinstance(attribute, Attribute):
+ continue
+ if attribute.is_aggregate and not aggregates:
+ continue
+
+ yield attribute
+
+ def iter_keys(self):
+ for attribute in self.iter_attributes():
+ if attribute.key == True:
+ yield attribute
+
+ def get_keys(self):
+ return list(self.iter_keys())
+
+ def auto_instanciate(self, attribute):
+ if self.managed is False:
+ raise ResourceNotFound(E_AUTO_UNM.format(attribute, self))
+ cstr_attributes = dict()
+
+ for a in attribute.type.iter_attributes():
+ if not a.mandatory:
+ continue
+
+ # Let's find attributes in the remote class that are of my
+ # class, and let's setup them to me
+ if issubclass(a.type, Resource) and isinstance(self, a.type):
+ cstr_attributes[a.name] = self
+ continue
+
+ if hasattr(self, a.name):
+ cstr_attributes[a.name] = getattr(self, a.name)
+
+ capabilities = set()
+ reqs = self._state.manager.get_instance_requirements(self)
+ for req in reqs:
+ if req._type != attribute.name:
+ continue
+
+ for attr_name, prop in req.properties.items():
+ value = next(iter(prop.value))
+ capabilities |= req._capabilities
+
+ # We need to find a subclass of self._resource with proper capabilities
+ cls = self._state.manager.get_resource_with_capabilities(
+ attribute.type, capabilities)
+
+ # Before creating a new instance of a class, let's check
+ resource = cls(**cstr_attributes)
+
+ self._state.manager.commit_resource(resource)
+ return resource
+
+ def get_attributes(self, aggregates = False):
+ return list(self.iter_attributes(aggregates = aggregates))
+
+ def get_attribute_names(self, aggregates = False):
+ return set(a.name
+ for a in self.iter_attributes(aggregates = aggregates))
+
+ def get_attribute_dict(self, field_names = None, aggregates = False,
+ uuid = True):
+ assert not field_names or field_names.is_star()
+ attributes = self.get_attributes(aggregates = aggregates)
+
+ ret = dict()
+ for a in attributes:
+ if not a.is_set(self):
+ continue
+ value = getattr(self, a.name)
+ if a.is_collection:
+ ret[a.name] = list()
+ for x in value:
+ if uuid and isinstance(x, Resource):
+ x = x._state.uuid._uuid
+ ret[a.name].append(x)
+ else:
+ if uuid and isinstance(value, Resource):
+ value = value._state.uuid._uuid
+ ret[a.name] = value
+ return ret
+
+ def get_tuple(self):
+ return (self.__class__, self._get_attribute_dict())
+
+ @property
+ def state(self):
+ return self._state.state
+
+ @state.setter
+ def state(self, state):
+ self._state.state = state
+
+ def get_types(self):
+ return [cls.__name__.lower() for cls in self.__class__.mro()
+ if cls.__name__ not in ('ABC', 'BaseType', 'object')]
+
+ def get_type(self):
+ return self.__class__.__name__.lower()
+
+ def has_type(self, typ):
+ return typ in self.get_types()
+
+ def __repr__(self):
+ # Showing aggregate attributes can cause infinite loops
+ name = self._state.uuid if self.name in (None, NEVER_SET) else self.name
+ return '<{}: {} {}>'.format(self.__class__.__name__, name,
+ ', '.join('{}={}'.format(k,v)
+ for k, v in self.get_attribute_dict().items()))
+
+ def __str__(self):
+ return self.__repr__()
+
+ #---------------------------------------------------------------------------
+ # Resource helpers
+ #---------------------------------------------------------------------------
+
+ def get_dependencies(self, allow_unresolved = False):
+ if not self._deps:
+ deps = set()
+ for a in self.iter_attributes():
+ if not issubclass(a.type, Resource):
+ continue
+ if a.is_aggregate:
+ continue
+
+ value = getattr(self, a.name)
+ if not value:
+ continue
+
+ if a.multiplicity in (Multiplicity.OneToOne,
+ Multiplicity.ManyToOne):
+ resource = value
+ if not resource:
+ log.warning('Null resource')
+ continue
+ if not resource.managed:
+ continue
+ uuid = resource._state.uuid
+ # Avoid considering oneself as a dependency due to
+ # ResourceAttribute(Self)
+ if uuid != self._state.uuid:
+ deps.add(uuid)
+ else:
+ resources = value
+ for cpt, resource in enumerate(resources):
+ if not resource:
+ log.warning('Null resource in collection')
+ continue
+ if not resource.managed:
+ continue
+ uuid = resource._state.uuid
+ deps.add(uuid)
+ self._deps = deps
+ return self._deps
+
+ def make_name(self, *args, type=True, id=True):
+ l = list()
+ if type:
+ l.append(self.__class__.__name__)
+ l.extend(list(args))
+ if id:
+ N = 3
+ uuid = ''.join(random.choice(string.ascii_uppercase +
+ string.digits) for _ in range(N))
+ l.append(uuid)
+ name = NAME_SEP.join(str(x) for x in l)
+ return name
+
+
+ def check_requirements(self):
+ for attr in self.iter_attributes():
+ if issubclass(attr.type, Resource) and attr.requirements:
+ for req in attr.requirements:
+ instance = getattr(self, attr.name)
+ req.check(instance)
+
+ #--------------------------------------------------------------------------
+ # Triggers
+ #--------------------------------------------------------------------------
+
+ @deprecated
+ def trigger(self, action, attribute_name, *args, **kwargs):
+ self._state.manager.trigger(self, action, attribute_name,
+ *args, **kwargs)
+
+ #--------------------------------------------------------------------------
+ # Object model
+ #
+ # Only assignment is implemented here, other operators are overloaded in
+ # the Attribute class (core.attribute.Attribute)
+ #--------------------------------------------------------------------------
+
+ def format(self, fmt):
+ return fmt.format(**self.get_attribute_dict(uuid = False))
+
+ def get_tag(self, tag_name, default = NEVER_SET):
+ """
+ A tag corresponds to a propery that is required by a class in all of
+ its inheritors. For instance, a service requires than a subclass
+ informs about the 'service_name' tag, which is a class member named
+ according to the following convention : __service_name__.
+ """
+ tag = '__{}__'.format(tag_name)
+ if not tag in vars(self.__class__):
+ if default is NEVER_SET:
+ return default
+ raise NotImplementedError('Missing tag {} in class {}'.format(tag,
+ self.__class__.__name__))
+ return getattr(self.__class__, tag)
+
+ def iter_backrefs(self):
+ for base in self.__class__.mro():
+ if not hasattr(base, '_reverse_attributes'):
+ continue
+ for attr, rattrs in base._reverse_attributes.items():
+ instances = self.get(attr.name, allow_never_set = True)
+ if instances in (None, NEVER_SET):
+ continue
+ if not attr.is_collection:
+ instances = [instances]
+ for instance in instances:
+ for rattr in rattrs:
+ yield instance, rattr
+
+ #---------------------------------------------------------------------------
+ # Accessors
+ #---------------------------------------------------------------------------
+
+ @classmethod
+ def has_attribute(cls, name):
+ return name in [a.name for a in cls.attributes()]
+
+ def has_callback(self, action, attribute):
+ return hasattr(self, '_{}_{}'.format(action, attribute.name))
+
+ def is_setup(self):
+ return self.state in (ResourceState.SETUP_PENDING,
+ ResourceState.SETUP, ResourceState.DIRTY)
+
+ __get__ = None
+ __create__ = None
+ __delete__ = None
+
+#-------------------------------------------------------------------------------
+# Helper functions
+#-------------------------------------------------------------------------------
+
+# The following Mixin are useful to convert an expresson of subresources into
+# an expression of tasks.
+
+class ConcurrentMixin:
+ async def async_commit_to_manager(self, manager):
+ await asyncio.gather(*[element.async_commit_to_manager(manager)
+ for element in self._elements])
+ await asyncio.gather(*[e._state.clean.wait() for e in self._elements])
+ self._state.clean.set()
+
+class SequentialMixin:
+ async def async_commit_to_manager(self, manager):
+ for element in self._elements:
+ await element.async_commit_to_manager(manager)
+ await element._state.clean.wait()
+ self._state.clean.set()
+
+class CompositionMixin:
+ async def async_commit_to_manager(self, manager):
+ for element in self._elements:
+ await element.async_commit_to_manager(manager)
+ await element._state.clean.wait()
+ self._state.clean.set()
+
+_Resource, EmptyResource = SchedulingAlgebra(BaseResource, ConcurrentMixin,
+ CompositionMixin, SequentialMixin)
+
+class ManagedResource(_Resource):
+ def __init__(self, *args, **kwargs):
+ from vicn.core.resource_mgr import ResourceManager
+ owner = kwargs.get('owner', None)
+ name = kwargs.get('name', None)
+
+ manager = ResourceManager()
+ self.register_to_manager(manager, name=name)
+
+ # Manager is needed for reference and reverse attributes
+ super().__init__(*args, **kwargs)
+
+ async def async_commit_to_manager(self, manager):
+ if not self.managed:
+ return
+ self._state.manager.commit_resource(self)
+
+ def register_to_manager(self, manager, name = None):
+ if not self.managed:
+ return
+ manager.add_resource(self, name = name)
+
+Resource = ManagedResource
+
+class BashResource(Resource):
+ """
+ __get__ : use return code of the bash command
+
+ Intermediate values and attributes: should be dict-like
+ Actually, we should collect attributes: dict update/remove, map/reduce
+ """
+ __node__ = None
+ __cmd_get__ = None
+ __cmd_create__ = None
+ __cmd_delete__ = None
+
+ def __get__(self):
+ assert self.__cmd_get__
+ return BashTask(self.node, self.__cmd_get__, {'self': self})
+
+ def __create__(self):
+ assert self.__cmd_create__
+ return BashTask(self.node, self.__cmd_create__, {'self': self})
+
+ def __delete__(self):
+ assert self.__cmd_delete__
+ return BashTask(self.node, self.__cmd_delete__, {'self': self})
+
diff --git a/vicn/core/resource_factory.py b/vicn/core/resource_factory.py
new file mode 100644
index 00000000..15504729
--- /dev/null
+++ b/vicn/core/resource_factory.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import copy
+import inspect
+import logging
+import pkgutil
+import traceback
+
+from netmodel.model.type import Type
+from netmodel.util.singleton import Singleton
+
+log = logging.getLogger(__name__)
+
+# Blacklist : resource that are temporarily disabled loaded
+RESOURCE_BLACKLIST = ['LinuxBridge']
+
+class ResourceFactory(metaclass=Singleton):
+ """
+ This manages classes, not instances
+ """
+
+ def __init__(self):
+ self._registry = dict()
+ self._register_all()
+
+ def _register_all(self):
+ log.info("Registering resources")
+ from vicn.core.resource import Resource
+
+ from vicn import resource as package
+ prefix = package.__name__ + "."
+
+ # Because aggregates might not be instanciated in order, we accumulate
+ # them and register them at the end
+ delayed_aggregates = dict()
+
+ # Explored modules are automatically imported by walk_modules + it
+ # allows to explore recursively resources/
+ # http://docs.python.org/2/library/pkgutil.html
+ for importer, modname, ispkg in pkgutil.walk_packages(package.__path__,
+ prefix, onerror = None):
+ try:
+ module = __import__(modname, fromlist = "dummy")
+
+ classes = [m[1] for m in inspect.getmembers(module,
+ inspect.isclass) if m[1].__module__ == modname]
+ for cls in classes:
+ if not issubclass(cls, Resource):
+ continue
+
+ if cls.__name__ in RESOURCE_BLACKLIST:
+ print('Skipped blacklisted resource ' + cls.__name__)
+ continue
+
+ # Register module to resource factory
+ self._registry[cls.__qualname__] = cls
+ Type._registry[cls.__qualname__.lower()] = cls
+
+ except Exception as e:
+ log.warning("Cannot load %s : %s: %s" % (modname, e,
+ traceback.format_exc()))
+
+ log.info("Registered resources are: {%s}" % ", ".join(sorted(
+ self._registry.keys())))
+
+ def get_available_resources(self):
+ return self._registry
+
diff --git a/vicn/core/resource_mgr.py b/vicn/core/resource_mgr.py
new file mode 100644
index 00000000..f6082488
--- /dev/null
+++ b/vicn/core/resource_mgr.py
@@ -0,0 +1,1436 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import sys, logging, asyncio, socket, functools
+import time
+
+# LXD workaround
+from pylxd.exceptions import NotFound as LxdNotFound, LXDAPIException
+
+from netmodel.model.filter import Filter
+from netmodel.model.query import Query, ACTION_SELECT, ACTION_INSERT
+from netmodel.model.query import ACTION_UPDATE, ACTION_SUBSCRIBE
+from netmodel.model.sql_parser import SQLParser
+from netmodel.network.packet import Packet
+from netmodel.network.router import Router
+from netmodel.util.toposort import toposort, toposort_flatten
+from netmodel.util.meta import inheritors
+from netmodel.util.singleton import Singleton
+from netmodel.util.misc import is_iterable
+from vicn.core.attribute import NEVER_SET
+from vicn.core.exception import VICNException, ResourceNotFound
+from vicn.core.resource_factory import ResourceFactory
+from vicn.core.resource import Resource, FactoryResource, EmptyResource
+from vicn.core.sa_collections import InstrumentedList
+from vicn.core.state import InstanceState, ResourceState
+from vicn.core.state import AttributeState, Operations, PendingValue
+from vicn.core.task import TaskManager, wait_task, task, async_task
+from vicn.core.task import EmptyTask, BashTask
+
+log = logging.getLogger(__name__)
+
+ENABLE_LXD_WORKAROUND = True
+
+# Monitoring queries
+
+Q_SUB_VPP = 'SUBSCRIBE SUM(*) FROM interface WHERE device_name INCLUDED [{}]'
+Q_SUB_IF = 'SUBSCRIBE * FROM interface WHERE device_name == "{}"'
+Q_SUB_STATS = 'SUBSCRIBE * FROM stats'
+Q_SUB_EMULATOR_IF = 'SUBSCRIBE * FROM interface WHERE id == "{}"'
+Q_SUB_EMULATOR = 'SUBSCRIBE * FROM interface WHERE device_name == "{}"'
+
+# Log messages
+
+S_WAIT_DEP = ' .. Waiting for dependency {}'
+S_WAIT_DEP_OK = ' .. Done waiting for dependency {}'
+S_WAIT = ' .. Waiting for {}'
+S_WAIT_OK = ' .. Done waiting for {}'
+S_WAIT_PRED = ' - Waiting for initialization of predecessors...'
+S_WAIT_SRS = ' - Waiting for subresources...'
+S_REG_SR = ' . Registering subresource to manager {}...'
+S_WAIT_SR = ' . Waiting for subresource: {}'
+S_WAIT_SR_OK = ' . Subresource is ready: {}'
+S_AFTER = ' . AFTER TYPE={}'
+
+S_INIT_DONE = 'INIT done. Resource exists. Process attribute dict {}'
+S_GET_DONE = 'GET done. Resource does not exist (exception was: {})'
+S_KEYS_OK = 'Keys initialized, resource can now be created.'
+S_CREATE_OK = 'CREATE success. Process attribute dict {}'
+
+#------------------------------------------------------------------------------
+# Helpers
+#------------------------------------------------------------------------------
+
+async def wait_resource(resource):
+ await resource._state.clean.wait()
+
+async def wait_resource_init(resource):
+ await resource._state.init.wait()
+
+async def wait_resources(resources):
+ await asyncio.gather(*[wait_resource(r) for r in resources])
+
+wait_resource_task = async_task(wait_resource)
+wait_resources_task = async_task(wait_resources)
+
+#------------------------------------------------------------------------------
+
+class ResourceManager(metaclass=Singleton):
+ """
+ A ResourceManager is in charge of managing resources, their lifecycle, and
+ interfaces to them.
+ """
+
+ def __init__(self, base, settings):
+
+ # Base directory for scenario
+ self._base = base
+
+ # Resources sorted via dependency (instances)
+ self._resources = dict()
+
+ self._deps = None
+
+ # Store resource requirements used for automatic instanciation
+ # instance -> attribute -> requirements
+ self._instance_requirements = dict()
+
+ self._dirty = set()
+ self._auto_commit = False
+
+ # class -> Requirements
+ self._class_requirements = dict()
+
+ self._map_uuid_name = dict()
+ self._map_name_uuid = dict()
+ self._map_str_uuid = dict()
+
+ # The task manager is used to schedule tasks used for resource
+ # synchronization
+ self._task_mgr = TaskManager()
+
+ # Store experiment settings
+ self._settings = settings
+
+ # Cache available resource types
+ _available = ResourceFactory().get_available_resources()
+ self._available = { k.lower(): v for k, v in _available.items() }
+
+ # API / user interface
+ self._router = Router(vicn_callback = self._on_vicn_command)
+ self._router.add_interface('unixserver')
+ self._router.add_interface('local', router = self._router)
+ self._router.add_interface('vicn', manager = self)
+
+ ws_port = self.get('websocket_port')
+ self._ws = self._router.add_interface('websocketserver',
+ port = ws_port)
+
+ # Monitoring
+ self._monitored = set()
+ self._pending_monitoring = set()
+
+ # For debug
+ self._committed = set()
+
+ def terminate(self):
+ self._router.terminate()
+
+ #--------------------------------------------------------------------------
+ # Settings
+ #--------------------------------------------------------------------------
+
+ def set_settings(self, settings):
+ if settings is None:
+ return
+ self._settings.update(settings)
+
+ def get(self, setting):
+ return self._settings[setting]
+
+ def set(self, setting, value):
+ self._settings[setting] = value
+
+ #--------------------------------------------------------------------------
+ # Monitoring
+ #
+ # XXX This code should be deprecated / moved into a separate module.
+ # Planned for a future release.
+ #--------------------------------------------------------------------------
+
+ def _on_vicn_command(self, command):
+ if command == 'setup':
+ self.setup()
+ elif command == 'teardown':
+ self.teardown()
+ elif command == 'monitor':
+ self.monitor()
+ elif command == 'terminate':
+ loop = asyncio.get_event_loop()
+ loop.stop()
+ else:
+ # open_terminal, ...
+ raise NotImplementedError
+
+ def _broadcast(self, query):
+ if not self._ws:
+ return
+ self._ws.execute(query)
+
+ def _broadcast_packet(self, packet):
+ self._broadcast(packet.to_query())
+
+ def _on_ns_record(self, packet):
+ query = packet.to_query()
+
+ if not query.object_name == 'interface':
+ return
+ q = Query(ACTION_UPDATE, 'channel', filter = query.filter,
+ params = query.params)
+ q.reply = True
+
+ self._ws.execute(q)
+ return None
+
+ def _on_netmon_record(self, packet):
+ query = packet.to_query()
+
+ # Find channel related to query
+ # NOTE: we update the channel twice, once for each interface...
+ if query.object_name == 'interface':
+ device_names = [value for key, op, value in query.filter.to_list()
+ if key == 'device_name']
+ if not device_names:
+ log.error('No device name in packet=', packet)
+ return
+ device_name = device_names[0]
+ node_name = query.params['node']
+ node = ResourceManager().by_name(node_name)
+ if node is None:
+ return None
+ for interface in node.interfaces:
+ if interface.device_name == device_name:
+ if interface.channel:
+ f = Filter.from_list([['id', '==',
+ interface.channel._state.uuid._uuid]])
+ q = Query(ACTION_UPDATE, 'channel', filter = f,
+ params = query.params)
+ q.reply = True
+ self._ws.execute(q)
+ return None
+ return None
+ return None
+ return None
+
+ def _on_netmon_channel_record(self, packet):
+ query = packet.to_query()
+ if query.object_name == 'interface':
+ device_names = [value for key, op, value in query.filter.to_list()
+ if key == 'device_name']
+ if not device_names:
+ log.error('No device name in packet=', packet)
+ return
+
+ device_name = device_names[0]
+
+ f = Filter.from_list([['id', '==', device_name]])
+ q = Query(ACTION_UPDATE, 'channel', filter = f,
+ params = query.params)
+ q.reply = True
+ self._ws.execute(q)
+ return None
+
+ return None
+
+ def _on_vpp_record(self, packet, pylink_id):
+ query = packet.to_query()
+ if query.object_name == 'interface':
+ device_names = [value for key, op, value in query.filter.to_list()
+ if key == 'device_name']
+ if not device_names:
+ log.error('No device name in packet=', packet)
+ return
+
+ # We might want to check if the query has SUM(*)
+ f = Filter.from_list([['id', '==', pylink_id]])
+ q = Query(ACTION_UPDATE, 'channel', filter = f,
+ params = query.params)
+ q.reply = True
+ self._ws.execute(q)
+ return None
+
+ print('discard packet in on_netmon_channel_record', query)
+ return None
+
+ #--------------------------------------------------------------------------
+ # Resource management
+ #--------------------------------------------------------------------------
+
+ def create_from_dict(self, **resource):
+ resource_type = resource.pop('type', None)
+ assert resource_type
+
+ return self.create(resource_type.lower(), **resource)
+
+ def create(self, resource_type, **attributes):
+ cls = self._available.get(resource_type)
+ if not cls:
+ raise Exception("Ignored resource with unknown type %s: %r" %
+ (resource_type, attributes))
+
+ resource = cls(**attributes)
+
+ name = attributes.get('name', None)
+ if name:
+ self._map_uuid_name[resource._state.uuid] = name
+ self._map_name_uuid[name] = resource._state.uuid
+
+ return resource
+
+ def get_resource_type_names(self):
+ return ResourceFactory().get_available_resources().keys()
+
+ def add_resource(self, instance, name = None):
+ instance._state = InstanceState(self, instance, name = name)
+
+ self._resources[instance._state.uuid] = instance
+ self._map_str_uuid[instance._state.uuid._uuid] = instance._state.uuid
+ self._deps = None
+
+ def commit_resource(self, resource):
+ """
+ Committing a resource creates an asyncio function implementing a state
+ management automaton.
+ """
+ asyncio.ensure_future(self._process_resource(resource))
+
+ def commit(self):
+ """
+ Commit all resource whose owner is not set, and mark unmanaged
+ resources as clean.
+
+ This function is used at initialization.
+ """
+
+ # Start FSM for all managed resources
+ for resource in self.get_resources():
+ if resource.owner is not None:
+ continue
+ if resource.managed == False:
+ asyncio.ensure_future(self._set_resource_state(resource,
+ ResourceState.CLEAN))
+ continue
+
+ self.commit_resource(resource)
+
+ def setup(self, commit=False):
+ """
+ This function is in charge of setting up all resources needed by the
+ experiment. Since it might be a long process, it should be asynchronous
+ at some point. So far, we let resources take care of this by themselves.
+ """
+ self._auto_commit = commit
+ if commit:
+ self.commit()
+
+ def get_resource_with_capabilities(self, cls, capabilities):
+ if '__type__' in cls.__dict__ and cls.__type__ == FactoryResource:
+ candidates = inheritors(cls)
+ if not candidates:
+ log.error('Abstract resource with no candidates: %s',
+ cls.__name__)
+ return None
+
+ for delegate in candidates:
+ if capabilities and (not '__capabilities__' in vars(delegate)
+ or not capabilities.issubset(delegate.__capabilities__)):
+ continue
+ log.info("Abstract resource %s, delegated %s among %r" % \
+ (cls.__name__, delegate.__name__, candidates))
+ return delegate
+ return None
+ else:
+ if capabilities and (not '__capabilities__' in vars(delegate) or
+ not capabilities.issubset(delegate.__capabilities__)):
+ log.error('Capabilities conflict for resource : %s',
+ cls.__name__)
+ raise VICNException
+ return cls
+
+ def find(self, resource_tuple):
+ cls, attr_dict = resource_tuple
+ for instance in self.by_type(cls):
+ cur_attr_dict = instance._get_attribute_dict()
+ common_keys = [k for k in cur_attr_dict.keys()
+ if k in attr_dict.keys()]
+ if all(attr_dict[k] == cur_attr_dict[k] for k in common_keys):
+ return instance
+ return None
+
+ def __iter__(self):
+ for resource in self._resources.values():
+ yield resource
+
+ def resources(self):
+ return list(self.__iter__())
+
+ def _sort_resources(self):
+ deps = {}
+ for instance in self.resources():
+ deps[instance._state.uuid] = \
+ instance.get_dependencies(allow_unresolved = True)
+
+ self._deps = toposort_flatten(deps)
+
+ def _sorted_resources(self):
+ """
+ Iterates on resources based on their dependencies
+ """
+ if not self._deps:
+ self._sort_resources()
+ for dep in self._deps:
+ try:
+ yield self._resources[dep]
+ except KeyError:
+ log.error('Dependency not found : {}'.format(dep))
+ raise InvalidResource
+
+ def sorted_resources(self):
+ return list(self._sorted_resources())
+
+ #--------------------------------------------------------------------------
+ # Queries
+ #--------------------------------------------------------------------------
+
+ def by_uuid(self, uuid):
+ return self._resources.get(uuid)
+
+ def by_uuid_str(self, uuid_str):
+ uuid = self._map_str_uuid.get(uuid_str)
+ return self._resources.get(uuid)
+
+ def by_name(self, name):
+ uuid = self._map_name_uuid.get(name)
+ return self.by_uuid(uuid)
+
+ def get_resources(self):
+ return self._resources.values()
+
+ def get_aggregates(self, resource_name, resource_cls):
+ """
+ Get aggregated object.
+ """
+ if not resource_name in self._aggregates:
+ return None
+ all_aggregates = self._aggregates[resource_name]
+
+ if not resource_cls in all_aggregates:
+ return None
+ aggregates = all_aggregates[resource_cls]
+
+ assert all(isinstance(x, resource_cls) for x in aggregates)
+ return aggregates
+
+ def get_aggregate(self, resource_name, resource_cls):
+ aggregates = self.get_aggregates(resource_name, resource_cls)
+ if not aggregates:
+ return None
+ assert len(aggregates) == 1
+ return next(aggregates)
+
+ def by_type(self, type):
+ return [r for r in self if isinstance(r, type)]
+
+ def by_type_str(self, typestr):
+ cls = self._available.get(typestr.lower())
+ if not cls:
+ return list()
+ return self.by_type(cls)
+
+ #--------------------------------------------------------------------------
+ # Requirements
+ #--------------------------------------------------------------------------
+
+ def add_instance_requirement(self, instance, requirement):
+ uuid = instance._state.uuid
+ if not uuid in self._instance_requirements:
+ self._instance_requirements[uuid] = list()
+ self._instance_requirements[uuid].append(requirement)
+
+ def get_instance_requirements(self, instance):
+ uuid = instance._state.uuid
+ return self._instance_requirements.get(uuid, dict())
+
+ def add_class_requirement(self, cls, requirement):
+ if not cls in self._class_requirements:
+ self._class_requirements[cls] = list()
+ self._class_requirements[cls].append(requirement)
+
+ #--------------------------------------------------------------------------
+ # Events
+ #--------------------------------------------------------------------------
+
+ def on(self, resource_name, event, action):
+ resource = self._resources.get(resource_name, None)
+ if not resource:
+ return
+
+ resource.on(event, action)
+
+ #--------------------------------------------------------------------------
+ # Task management
+ #--------------------------------------------------------------------------
+
+ def schedule(self, task):
+ if task is None or isinstance(task, EmptyTask):
+ return
+ self._task_mgr.schedule(task)
+
+ #--------------------------------------------------------------------------
+ # Asynchronous resource API
+ #
+ # The manager is the only one to submit tasks to the scheduler since it can
+ # store and share the results, manage concurrent access, etc.
+ # As many functions are not thread safe, we make sure that they are all
+ # executed in the manager's thread (=main thread).
+ #--------------------------------------------------------------------------
+
+ async def resource_exits(self, resource):
+ await self._resource_get()
+ await self.wait_resource_exists(resource)
+ return resource._state.exists
+
+ async def wait_attr_init(self, resource, attribute_name):
+ await resource._state.attr_init[attribute_name].wait()
+
+ async def wait_attr_clean(self, resource, attribute_name):
+ await resource._state.attr_clean[attribute_name].wait()
+
+ async def attribute_get(self, resource, attribute, value):
+ await self.wait_attr_init(resource, attribute)
+ return resource.get(attribute)
+
+ async def attribute_set(self, resource, attribute_name, value,
+ blocking=True):
+ with await resource._state.write_lock:
+ # Add the current operation to the pending list
+ # NOTE: collections are unordered and can be updated concurrently
+ #self._attribute_set_pending_value(resource, attribute_name)
+ resource._state.dirty[attribute_name].trigger(Operations.SET,
+ value)
+
+ attr_state = resource._state.attr_state[attribute_name]
+ if attr_state == AttributeState.CLEAN:
+ resource._state.attr_state[attribute_name] = \
+ AttributeState.DIRTY
+ elif attr_state in [
+ # Nothing to do since we know the attribute value will be
+ # processed later.
+ # If the attribute was not processed by default, we would have
+ # to change the state of the attribute so that it gets
+ # processed.
+ AttributeState.UNINITIALIZED,
+ AttributeState.INITIALIZED,
+ AttributeState.PENDING_INIT,
+ AttributeState.DIRTY]:
+ pass
+ else:
+ # We cannot have the lock for instance if the attribute is
+ # being updated.
+ raise RuntimeError
+
+ resource_state = resource._state.state
+ if resource_state == ResourceState.CLEAN:
+ resource._state.state = ResourceState.DIRTY
+ resource._state.change_event.set()
+ elif resource_state in [
+ ResourceState.UNINITIALIZED,
+ ResourceState.INITIALIZED,
+ ResourceState.PENDING_KEYS,
+ ResourceState.KEYS_OK,
+ ResourceState.PENDING_DEPS,
+ ResourceState.DEPS_OK,
+ ResourceState.PENDING_CREATE,
+ ResourceState.CREATED,
+ ResourceState.DIRTY,
+ ]:
+ pass # Nothing to do, the attribute will get processed
+ else:
+ # ResourceState.PENDING_UPDATE
+ # other
+ raise RuntimeError("Resource cannot be in state".format(
+ resource_state))
+
+ if blocking:
+ await self.wait_attr_clean(resource, attribute_name)
+
+ #---------------------------------------------------------------------------
+ # Resource dependency management
+ #---------------------------------------------------------------------------
+
+ async def _resource_wait_attributes(self, resource):
+ """Check dependencies and requirements
+
+ Inspect all attributes for referenced resources, and their eventual
+ requirements.
+ """
+ self.log(resource, ' - Waiting for attribute dependencies...')
+ for attr in resource.iter_attributes():
+ if issubclass(attr.type, Resource):
+ deps = resource.get(attr.name)
+ if deps is None:
+ # Not really a dependency, we expect mandatory to prevent
+ # us to continue if we should not
+ continue
+
+ if not attr.is_collection:
+ deps = [deps]
+
+ for dep in deps:
+ # XXX This could be done in parallel
+ if not dep.managed:
+ continue
+ dep_pfx = '{}:{}'.format(dep.get_type(), dep.get_uuid())
+ self.log(resource, S_WAIT_DEP. format(dep_pfx))
+ await wait_resource(dep)
+ self.log(resource, S_WAIT_DEP_OK. format(dep_pfx))
+
+ if not attr.requirements:
+ continue
+
+ for req in attr.requirements:
+ dep_attr_name = req.requirement_type
+ dep_attr = dep.get_attribute(dep_attr_name)
+ assert issubclass(dep_attr.type, Resource)
+ dep_attr_value = dep.get(dep_attr_name)
+
+ if not dep_attr_value:
+ dep_attr_value = dep.auto_instanciate(dep_attr)
+ setattr(dep, dep_attr_name, dep_attr_value)
+
+ dep_attr_value_pfx = '{}:{}'.format(
+ dep_attr_value.get_type(),
+ dep_attr_value.get_uuid())
+ self.log(resource,
+ S_WAIT_DEP.format(dep_attr_value_pfx))
+ await wait_resource(dep_attr_value)
+ self.log(resource,
+ S_WAIT_DEP_OK .format(dep_attr_value_pfx))
+
+ async def _resource_wait_predecessors(self, resource):
+ after = resource.__after__()
+ if after:
+ self.log(resource, ' - Waiting for predecessors...')
+ for resource_type in after:
+ self.log(resource, ' . AFTER TYPE={}'.format(resource_type))
+ befores = resource._state.manager.by_type_str(resource_type)
+ for before in befores:
+ if not before.managed:
+ continue
+ before_pfx = '{}:{}'.format(before.get_type(),
+ before.get_uuid())
+ self.log(resource, S_WAIT.format(before_pfx))
+ await wait_resource(before)
+ self.log(resource, S_WAIT_OK.format(before_pfx))
+
+ after_init = resource.__after_init__()
+ if after_init:
+ self.log(resource, S_WAIT_PRED)
+ for resource_type in after_init:
+ self.log(resource, S_AFTER.format(resource_type))
+ befores = resource._state.manager.by_type_str(resource_type)
+ for before in befores:
+ if not before.managed:
+ continue
+ before_pfx = '{}:{}'.format(before.get_type(),
+ before.get_uuid())
+ self.log(resource, S_WAIT.format(before_pfx))
+ await wait_resource_init(before)
+ self.log(resource, S_WAIT_OK.format(before_pfx))
+
+ async def _resource_wait_subresources(self, resource):
+ self.log(resource, S_WAIT_SRS)
+
+ # We should accumulate subresources through the hierarchy
+ sr = EmptyResource()
+ for base in reversed(resource.__class__.mro()):
+ if '__subresources__' not in vars(base):
+ continue
+ sr = sr > base.__subresources__(resource)
+
+ if sr is not None and not isinstance(sr, EmptyResource):
+ resource.set_subresources(sr)
+ pfx_sr = '{}:{}'.format(sr.get_type(), sr.get_uuid())
+ self.log(resource, S_REG_SR .format(pfx_sr))
+ await sr.async_commit_to_manager(self)
+ self.log(resource, S_WAIT_SR.format(pfx_sr))
+ await wait_resource(sr)
+ self.log(resource, S_WAIT_SR_OK.format(pfx_sr))
+
+ async def _resource_wait_dependencies(self, resource):
+ self.log(resource, 'Waiting for dependencies...')
+ await self._resource_wait_attributes(resource)
+ await self._resource_wait_predecessors(resource)
+ await self._resource_wait_subresources(resource)
+
+ def _task_resource_action(self, resource, action):
+ """Perform action: __get__, __create__, __delete__ on the full class
+ hierarchy.
+ """
+ task = EmptyTask()
+ for base in reversed(resource.__class__.mro()):
+ # To avoid adding several times the same task
+ if action not in vars(base):
+ continue
+ func = getattr(base, action, None)
+ if func is None:
+ continue
+ t = func(resource)
+
+ task = task > t
+
+ return task
+
+ #--------------------------------------------------------------------------
+ # Resource model
+ #--------------------------------------------------------------------------
+
+ def _task_attribute_op(self, resource, attribute, op):
+ return getattr(resource, '_{}_{}'.format(op, attribute.name))()
+
+ #--------------------------------------------------------------------------
+ # Attribute FSM
+ #--------------------------------------------------------------------------
+
+ def _attribute_is_dirty(self, resource, attribute):
+ """
+ Precondition:
+ Attribute has been retrieved
+ """
+ pending_value = resource._state.dirty[attribute.name]
+ return pending_value.value != NEVER_SET
+
+ async def __set_attribute_state(self, resource, attribute_name, state):
+ """Sets the resource state (no-lock version)
+
+ It is important to centralize state change since some states are
+ associated with Events().
+ """
+ resource._state.attr_state[attribute_name] = state
+ if state in [
+ AttributeState.INITIALIZED,
+ AttributeState.CLEAN,
+ AttributeState.DIRTY
+ ]:
+ resource._state.attr_init[attribute_name].set()
+ else:
+ raise RuntimeError("Inconsistent resource state {}".format(state))
+
+ if state in [AttributeState.CLEAN]:
+ resource._state.attr_clean[attribute_name].set()
+ elif state in [
+ AttributeState.INITIALIZED,
+ AttributeState.DIRTY
+ ]:
+ resource._state.attr_clean[attribute_name].clear()
+ else:
+ raise RuntimeError
+
+ async def _set_attribute_state(self, resource, attribute_name, state):
+ """Sets the attribute state (lock version)
+ """
+ with await resource._state.attr_lock[attribute_name]:
+ await self.__set_attribute_state(resource, attribute_name, state)
+
+ def _trigger_attr_state_change(self, resource, attribute, fut):
+ try:
+ ret = fut.result()
+ resource._state.attr_change_success[attribute.name] = True
+ resource._state.attr_change_value[attribute.name] = ret
+ except Exception as e:
+ resource._state.attr_change_success[attribute.name] = False
+ resource._state.attr_change_value[attribute.name] = e
+ resource._state.attr_change_event[attribute.name].set()
+
+ async def attribute_process(self, resource, attribute):
+ """
+ Temporary FSM executing in parallel for attribute management. Those FSM
+ are under the responsability of the main resource FSM.
+
+ Precondition:
+ Attribute state is initialized
+ """
+ self.attr_log(resource, attribute,
+ 'Starting attribute FSM for {}'.format(attribute.name))
+
+ new_state = None
+ while new_state != AttributeState.CLEAN:
+ #with await resource._state.attr_lock[attribute.name]:
+ state = resource._state.attr_state[attribute.name]
+ self.attr_log(resource, attribute,
+ 'Current state is {}'.format(state))
+
+ if resource._state.attr_change_success == False:
+ log.error('Attribute error')
+ e = resource._state.attr_change_value[attribute.name]
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+
+ # Signal update errors to the parent resource
+ resource._state.attr_change_event[attribute.name].set()
+
+ elif state == AttributeState.UNINITIALIZED:
+ pending_state = AttributeState.PENDING_INIT
+ elif state in AttributeState.INITIALIZED:
+ pending_state = AttributeState.PENDING_UPDATE
+ elif state == AttributeState.DIRTY:
+ pending_state = AttributeState.PENDING_UPDATE
+ elif state in [
+ AttributeState.PENDING_INIT,
+ AttributeState.PENDING_UPDATE
+ ]:
+ # Nothing to do
+ pending_state = None
+ elif state == AttributeState.CLEAN:
+ return
+ else:
+ raise RuntimeError
+
+ if pending_state is None:
+ self.attr_log(resource, attribute,
+ 'Nothing to do. Waiting for event...')
+ await resource._state.attr_change_event[attribute.name].wait()
+ resource._state.attr_change_event[attribute.name].clear()
+ self.attr_log(resource, attribute, 'Wake up from event')
+ continue
+
+ if pending_state == AttributeState.PENDING_INIT:
+ task = self._task_attribute_op(resource, attribute, 'get')
+ elif pending_state == AttributeState.PENDING_UPDATE:
+ pending_value = resource._state.dirty[attribute.name]
+
+ if pending_value.value == NEVER_SET:
+ assert len(pending_value.operations) == 0
+ task = EmptyTask()
+ else:
+ try:
+ task = self._task_attribute_op(resource, attribute,
+ Operations.SET)
+ except Exception as e:
+ log.warning('No attribute setter attribute {}'.format(
+ attribute))
+ task = EmptyTask()
+ else:
+ raise RuntimeError
+
+ if task is not None and not isinstance(task, EmptyTask):
+ state_change = functools.partial( \
+ self._trigger_attr_state_change, resource, attribute)
+ task.add_done_callback(state_change)
+ self.attr_log(resource, attribute,
+ 'Trigger {} -> {}. Waiting task completion'.format(
+ state, pending_state))
+ self.schedule(task)
+
+ await resource._state.attr_change_event[attribute.name].wait()
+ resource._state.attr_change_event[attribute.name].clear()
+ self.attr_log(resource, attribute,
+ 'Completed {} -> {}. Success = {}'.format(
+ state, pending_state,
+ resource._state.attr_change_success[attribute.name]))
+ else:
+ # If this value is not reset, attributes get updated many times
+ resource._state.attr_change_value[attribute.name] = NEVER_SET
+
+ if pending_state == AttributeState.PENDING_INIT:
+ if resource._state.attr_change_success[attribute.name] == True:
+ attrs = resource._state.attr_change_value[attribute.name]
+ self.attr_log(resource, attribute,
+ 'INIT success. Value = {}'.format(attrs))
+ found = self._process_attr_dict(resource, attribute, attrs)
+ if not found:
+ log.error('Attribute missing return attrs: {}'.format(
+ attrs))
+ found = self._process_attr_dict(resource, attribute,
+ attrs)
+ new_state = AttributeState.INITIALIZED
+ else:
+ attrs = resource._state.attr_change_value[attribute.name]
+ self.attr_log(resource, attribute,
+ 'INIT gave no value. Value = {}'.format(attrs))
+ new_state = AttributeState.INITIALIZED
+
+ elif pending_state == AttributeState.PENDING_UPDATE:
+ if resource._state.attr_change_success[attribute.name] == True:
+ attrs = resource._state.attr_change_value[attribute.name]
+ self.attr_log(resource, attribute,
+ 'UPDATE success. Value = {}. Attribute is CLEAN'.format(attrs))
+ if attrs != NEVER_SET:
+ # None could be interpreted as the return value. Also,
+ # we need not to overwrite the value from get
+ self._process_attr_dict(resource, attribute, attrs)
+
+ # We might do this for all returned attributes
+ cur_value = vars(resource)[attribute.name]
+ if attribute.is_collection:
+ tmp = InstrumentedList(pending_value.value)
+ tmp._attribute = cur_value._attribute
+ tmp._instance = cur_value._instance
+ else:
+ tmp = pending_value.value
+ vars(resource)[attribute.name] = tmp
+ pending_value.clear()
+
+ new_state = AttributeState.CLEAN
+ else:
+ log.error('Attribute error')
+ e = resource._state.attr_change_value[attribute.name]
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+
+ else:
+ raise RuntimeError
+
+ # Setting attribute state
+ await self._set_attribute_state(resource, attribute.name,
+ new_state)
+
+ #--------------------------------------------------------------------------
+ # Resource FSM
+ #--------------------------------------------------------------------------
+
+ def parse_query(self, line):
+ dic = SQLParser().parse(line)
+ if not dic:
+ raise RuntimeError("Can't parse input command: %s" % command)
+
+ return Query.from_dict(dic)
+
+ def _monitor_netmon(self, resource):
+ ip = resource.node.host_interface.ip_address
+ if not ip:
+ log.error('IP of monitored Node is None')
+ import os; os._exit(1)
+
+ ws = self._router.add_interface('websocketclient', address=ip,
+ hook=self._on_netmon_record)
+
+ node = resource.node
+ for interface in node.interfaces:
+ if not interface.monitored:
+ continue
+
+ if interface.get_type() == 'dpdkdevice' and hasattr(node,'vpp'):
+
+ # Check if vICN has already subscribed for one interface in
+ # the channel
+ if hasattr(interface.channel,'already_subscribed'):
+ continue
+
+ channel_id = interface.channel._state.uuid._uuid
+
+ update_vpp = functools.partial(self._on_vpp_record,
+ pylink_id = channel_id)
+ ws_vpp = self._router.add_interface('websocketclient',
+ address=ip, hook=update_vpp)
+
+ aggregate_interfaces = list()
+ for _interface in node.interfaces:
+ if not _interface.get_type() == 'dpdkdevice' and \
+ _interface.monitored:
+ aggregate_interfaces.append('"' +
+ _interface.device_name + '"')
+
+ q_str = Q_SUB_VPP.format(','.join(aggregate_interfaces))
+ q = self.parse_query(q_str)
+ packet = Packet.from_query(q)
+ self._router._flow_table.add(packet, None, ws_vpp)
+ ws_vpp.send(packet)
+
+ # Prevent vICN to subscribe to other interfaces of the same
+ # channel
+ interface.channel.already_subscribed = True
+
+ else:
+ q_str = Q_SUB_IF.format(interface.device_name)
+ q = self.parse_query(q_str)
+ packet = Packet.from_query(q)
+ self._router._flow_table.add(packet, None, ws)
+ ws.send(packet)
+
+ def _monitor_emulator(self, resource):
+ ns = resource
+ ip = ns.node.bridge.ip_address # host_interface.ip_address
+
+ ws_ns = self._router.add_interface('websocketclient', address = ip,
+ port = ns.control_port,
+ hook = self._on_ns_record)
+ ws = self._router.add_interface('websocketclient', address = ip,
+ hook = self._on_netmon_channel_record)
+
+ for station in ns.stations:
+ if not station.managed:
+ interface = [i for i in station.interfaces if i.channel == ns]
+ assert len(interface) == 1
+ interface = interface[0]
+ identifier = interface.name
+ else:
+ iface = ns._sta_ifs[station]
+ identifier = iface._state.uuid._uuid
+
+ # Monitor the wireless channel for position and link rate
+ q_str = Q_SUB_EMULATOR_IF.format(identifier)
+ q = self.parse_query(q_str)
+ packet = Packet.from_query(q)
+ self._router._flow_table.add(packet, None, ws_ns)
+ ws_ns.send(packet)
+
+ # We also need to subscribe on the node for the tap interfaces
+ # for individual bandwidth monitoring
+ tap = ns._sta_taps[station]
+ q_str = Q_SUB_EMULATOR.format(tap.device_name)
+ q = self.parse_query(q_str)
+ packet = Packet.from_query(q)
+ self._router._flow_table.add(packet, None, ws)
+ ws.send(packet)
+
+ def _monitor(self, resource):
+ if resource.get_type() == 'centralip':
+ for uuid in self._pending_monitoring:
+ pending = self.by_uuid(uuid)
+ self._monitor(pending)
+ self._pending_monitoring.clear()
+ return
+
+ central_ip = self.by_type_str('centralip')
+ if not central_ip:
+ raise NotImplementedError('Missing CentralIP in experiment')
+ central_ip = central_ip[0]
+
+ uuid = resource.get_uuid()
+
+ if central_ip._state.state != ResourceState.CLEAN:
+ self._pending_monitoring.add(uuid)
+ return
+
+ if uuid in self._monitored:
+ return
+ self._monitored.add(uuid)
+
+ if resource.get_type() == 'netmon':
+ if resource.node.get_type() != 'lxccontainer':
+ return
+ self._monitor_netmon(resource)
+
+ elif resource.has_type('emulatedchannel'):
+ self._monitor_emulator(resource)
+
+ async def __set_resource_state(self, resource, state):
+ """Sets the resource state (no-lock version)
+
+ It is important to centralize state change since some states are
+ associated with Events().
+ """
+ resource._state.state = state
+ if state == ResourceState.CLEAN:
+ # Monitoring hook
+ self._monitor(resource)
+ resource._state.clean.set()
+ else:
+ resource._state.clean.clear()
+ if state == ResourceState.INITIALIZED:
+ resource._state.init.set()
+
+ async def _set_resource_state(self, resource, state):
+ """Sets the resource state (lock version)
+ """
+ with await resource._state.lock:
+ await self.__set_resource_state(resource, state)
+
+ def _trigger_state_change(self, resource, fut):
+ try:
+ ret = fut.result()
+ resource._state.change_success = True
+ resource._state.change_value = ret
+ except Exception as e:
+ resource._state.change_success = False
+ resource._state.change_value = e
+ resource._state.change_event.set()
+
+ def _process_attr_dict(self, resource, attribute, attrs):
+ if not isinstance(attrs, dict):
+ if attribute is None:
+ return False
+ attrs = {attribute.name: attrs}
+ resource.set_many(attrs, current=True)
+ return True
+
+ async def _task_resource_update(self, resource):
+ # Monitor all FSM one by one and inform about errors.
+ futs = list()
+ attrs = list()
+ for attr in resource.iter_attributes():
+ if resource.is_local_attribute(attr.name):
+ continue
+ if attr.key:
+ # Those attributes are already done
+ continue
+
+ attrs.append(attr)
+ fut = self.attribute_process(resource, attr)
+ futs.append(fut)
+
+ if not futs:
+ self.log(resource, 'No attribute to update')
+ return None
+
+ await asyncio.gather(*futs)
+
+ # Inform the resource about the outcome of the update process
+ # Error if at least one attribute failed.
+ resource._state.change_success = all(
+ resource._state.attr_change_success[attr.name]
+ for attr in attrs)
+ self.log(resource,
+ 'All attributes FSM terminated with success={}'.format(
+ resource._state.change_success))
+
+ if resource._state.change_success:
+ ret = [ resource._state.attr_change_value[attr.name]
+ for attr in attrs]
+ return ret
+ else:
+ raise NotImplementedError('At least one attribute failed')
+
+ async def _task_resource_keys(self, resource):
+ # Monitor all FSM one by one and inform about errors.
+ futs = list()
+ attrs = list()
+ for attr in resource.get_keys():
+ if resource.is_local_attribute(attr.name):
+ continue
+ attrs.append(attr)
+ fut = self.attribute_process(resource, attr)
+ futs.append(fut)
+
+ if not futs:
+ self.log(resource, 'No key attribute to update')
+ return None
+
+ await asyncio.gather(*futs)
+
+ # Inform the resource about the outcome of the update process
+ # Error if at least one attribute failed.
+ resource._state.change_success = all(
+ resource._state.attr_change_success[attr.name]
+ for attr in attrs)
+ self.log(resource,
+ 'KEY attributes FSM terminated with success={}'.format(
+ resource._state.change_success))
+
+ if resource._state.change_success:
+ ret = resource._state.attr_change_value
+ return ret
+ else:
+ raise NotImplementedError('At least one attribute failed')
+
+ #--------------------------------------------------------------------------
+ # Logging
+ #--------------------------------------------------------------------------
+
+ def log(self, resource, msg=None):
+ resource._state.log.append(msg)
+
+ # Display on screen
+ #pfx = '[{}] {}: '.format(resource.get_type(), resource.get_uuid())
+ #print(pfx, msg)
+
+ def attr_log(self, resource, attribute, msg):
+ resource._state.attr_log[attribute.name].append(msg)
+
+ # Display on screen
+ #pfx = '[{}] {} / {}: '.format(resource.get_type(), resource.get_uuid(),
+ # attribute.name)
+ #print(pfx, msg)
+
+ #--------------------------------------------------------------------------
+
+ async def _process_resource(self, resource):
+ """
+ We need to schedule the first set of subresources, knowing others will
+ be orchestrated by the operators
+ - subresources need to enter the system in order
+ -> we just add them to the manager in time
+ - but they need to be managed by the system
+ - in particular, the owner waits for the system to complete
+ subresoruces: this is the implementation of __get__ __create__
+ __delete__ in the base resource
+ """
+ pfx = '[{}] {}: '.format(resource.get_type(), resource.get_uuid())
+
+ self.log(resource, 'Starting FSM...')
+
+ # When a resource is managed, it will get automatically monitored by
+ # adding the netmon resource on it.
+ from vicn.resource.node import Node
+ if resource.get_type() == 'lxccontainer':
+ self.log(resource,
+ 'Associating monitoring to lxc container resource...')
+ instance = self.create('netmon', node=resource)
+ self.commit_resource(instance)
+
+ # FIXME
+ elif resource.get_type() == 'physical' and resource.managed and \
+ len(self.by_type_str('emulatedchannel')) > 0:
+ self.log(resource,
+ 'Associating monitoring to physical node resource...')
+ instance = self.create('netmon', node=resource)
+ self.commit_resource(instance)
+
+ state = None
+
+ while True:
+ with await resource._state.lock:
+
+ # FSM implementation
+ state = resource._state.state
+ self.log(resource, 'Current state is {}'.format(state))
+
+ if resource._state.change_success == False:
+ e = resource._state.change_value
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+
+ elif state == ResourceState.UNINITIALIZED:
+ pending_state = ResourceState.PENDING_DEPS
+ elif state == ResourceState.DEPS_OK:
+ pending_state = ResourceState.PENDING_INIT
+
+ elif state == ResourceState.INITIALIZED:
+ pending_state = ResourceState.PENDING_GET
+
+ elif state == ResourceState.GET_DONE:
+ if resource.get_keys():
+ pending_state = ResourceState.PENDING_KEYS
+ else:
+ pending_state = ResourceState.PENDING_CREATE
+
+ elif state == ResourceState.KEYS_OK:
+ pending_state = ResourceState.PENDING_CREATE
+
+ elif state in [ResourceState.CREATED, ResourceState.DIRTY]:
+ pending_state = ResourceState.PENDING_UPDATE
+
+ elif state == ResourceState.DELETED:
+ raise NotImplementedError
+ # Nothing to do unless explicitely requested
+ pending_state = None
+
+ elif state in [
+ ResourceState.PENDING_DEPS,
+ ResourceState.PENDING_INIT,
+ ResourceState.PENDING_CREATE,
+ ResourceState.PENDING_DELETE,
+ ResourceState.CLEAN
+ ]:
+ # Nothing to do
+ pending_state = None
+ else:
+ raise RuntimeError
+
+ # Implement state changes
+ #
+ # If a task is already pending, we simply wait for it to complete
+ if pending_state is None:
+ # Wait for an external change
+ self.log(resource, 'Nothing to do. Waiting for event...')
+ await resource._state.change_event.wait()
+ self.log(resource, 'Wake up from event')
+ resource._state.change_event.clear()
+ continue
+
+ if pending_state == ResourceState.PENDING_DEPS:
+ # XXX Maybe for any action, we need to wait for dependencies to
+ # be up to date
+ task = async_task(functools.partial(self._resource_wait_dependencies, resource))()
+
+ elif pending_state == ResourceState.PENDING_INIT:
+ task = self._task_resource_action(resource, '__initialize__')
+
+ elif pending_state == ResourceState.PENDING_GET:
+ task = self._task_resource_action(resource, '__get__')
+ if isinstance(task, BashTask):
+ task.set_default_parse_for_get()
+
+ elif pending_state == ResourceState.PENDING_KEYS:
+ task = async_task(functools.partial(self._task_resource_keys, resource))()
+
+ elif pending_state == ResourceState.PENDING_CREATE:
+ task = self._task_resource_action(resource, '__create__')
+
+ elif pending_state == ResourceState.PENDING_UPDATE:
+ # Instead of tasks, we wait for many smaller autoamtons to
+ # terminate
+ await resource._state.write_lock.acquire()
+ task = async_task(functools.partial(self._task_resource_update, resource))()
+
+ elif pending_state == ResourceState.PENDING_DELETE:
+ task = self._task_resource_action(resource, '__delete__')
+
+ else:
+ raise RuntimeError
+
+ if task is not None and not isinstance(task, EmptyTask):
+ state_change = functools.partial(self._trigger_state_change, resource)
+ task.add_done_callback(state_change)
+ self.schedule(task)
+
+ self.log(resource, 'Trigger {} -> {}. Waiting task completion'.format(
+ state, pending_state))
+ await resource._state.change_event.wait()
+ resource._state.change_event.clear()
+ self.log(resource, 'Completed {} -> {}. Success = {}'.format(
+ state, pending_state, resource._state.change_success))
+
+ # If no task, can assume there is an instant switch to the next step...
+
+ # Update state based on task results
+ if pending_state == ResourceState.PENDING_DEPS:
+ # XXX NO CHANGE SUCCESS TEST ??
+ new_state = ResourceState.DEPS_OK
+
+ elif pending_state == ResourceState.PENDING_INIT:
+ if resource._state.change_success == True:
+ attrs = resource._state.change_value
+ self.log(resource, 'INIT done.')
+ new_state = ResourceState.INITIALIZED
+ else:
+ e = resource._state.change_value
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+
+ elif pending_state == ResourceState.PENDING_GET:
+ if resource._state.change_success == True:
+ attrs = resource._state.change_value
+ self.log(resource, S_INIT_DONE.format(attrs))
+ self._process_attr_dict(resource, None, attrs)
+ new_state = ResourceState.CREATED
+ else:
+ e = resource._state.change_value
+ if ENABLE_LXD_WORKAROUND and \
+ resource.get_type() != 'lxccontainer' and \
+ isinstance(e, LxdNotFound):
+ # "not found" is the normal exception when the container
+ # does not exists. anyways the bug should only occur
+ # with container.execute(), not container.get()
+ log.error('LXD Fix (not found). Reset resource')
+ new_state = ResourceState.UNINITIALIZED
+ elif ENABLE_LXD_WORKAROUND and isinstance(e, LXDAPIException):
+ # "not found" is the normal exception when the container
+ # does not exists. anyways the bug should only occur
+ # with container.execute(), not container.get()
+ log.error('LXD Fix (API error). Reset resource')
+ new_state = ResourceState.UNINITIALIZED
+ elif isinstance(e, ResourceNotFound):
+ # The resource does not exist
+ self.log(resource, S_GET_DONE.format(
+ resource._state.change_value))
+ new_state = ResourceState.GET_DONE
+ resource._state.change_value = None
+ else:
+ e = resource._state.change_value
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+ resource._state.change_success = True
+
+ elif pending_state == ResourceState.PENDING_KEYS:
+ if resource._state.change_success == True:
+ new_state = ResourceState.KEYS_OK
+ self.log(resource, S_KEYS_OK)
+ else:
+ e = resource._state.change_value
+ self.log(resource, 'KEYS failed: {}'.format(e))
+
+ if ENABLE_LXD_WORKAROUND and isinstance(e, LxdNotFound):
+ log.error('LXD Fix (not found). Reset resource')
+ new_state = ResourceState.CREATED
+ resource._state.change_success = True
+ else:
+ e = resource._state.change_value
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+
+ elif pending_state == ResourceState.PENDING_CREATE:
+ if resource._state.change_success == True:
+ attrs = resource._state.change_value
+ self.log(resource, S_CREATE_OK.format(attrs))
+ self._process_attr_dict(resource, None, attrs)
+ new_state = ResourceState.CREATED
+ else:
+ e = resource._state.change_value
+
+ if ENABLE_LXD_WORKAROUND and isinstance(e, LxdNotFound):
+ log.error('LXD Fix (not found). Reset resource')
+ new_state = ResourceState.UNINITIALIZED
+ resource._state.change_success = True
+ elif ENABLE_LXD_WORKAROUND and \
+ isinstance(e, LXDAPIException):
+ log.error('LXD Fix (API error). Reset resource')
+ new_state = ResourceState.UNINITIALIZED
+ resource._state.change_success = True
+ elif 'File exists' in str(e):
+ new_state = ResourceState.CREATED
+ resource._state.change_success = True
+ elif 'dpkg --configure -a' in str(e):
+ resource._dpkg_configure_a = True
+ new_state = ResourceState.UNINITIALIZED
+ resource._state.change_success = True
+ else:
+ self.log(resource, 'CREATE failed: {}'.format(e))
+ e = resource._state.change_value
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+
+ elif pending_state == ResourceState.PENDING_UPDATE:
+ if resource._state.change_success == True:
+ self.log(resource, 'Update finished, resource is CLEAN.')
+ new_state = ResourceState.CLEAN
+ resource._state.write_lock.release()
+ else:
+ e = resource._state.change_value
+ self.log(resource, 'UPDATE failed: {}'.format(e))
+
+ if ENABLE_LXD_WORKAROUND and isinstance(e, LxdNotFound):
+ log.error('LXD Fix (not found). Reset resource')
+ new_state = ResourceState.CREATED
+ resource._state.change_success = True
+ resource._state.write_lock.release()
+ else:
+ e = resource._state.change_value
+ resource._state.write_lock.release()
+ import traceback; traceback.print_tb(e.__traceback__)
+ raise NotImplementedError
+
+ elif pending_state == ResourceState.PENDING_DELETE:
+ raise NotImplementedError
+ new_state = None
+ else:
+ raise RuntimeError
+
+ await self._set_resource_state(resource, new_state)
+
diff --git a/vicn/core/sa_collections.py b/vicn/core/sa_collections.py
new file mode 100644
index 00000000..e627caa5
--- /dev/null
+++ b/vicn/core/sa_collections.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# This module is derived from code from SQLAlchemy
+#
+# orm/collections.py
+# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+#
+
+import logging
+
+from vicn.core.sa_compat import py2k
+from vicn.core.exception import VICNListException
+
+log = logging.getLogger(__name__)
+
+def _list_decorators():
+ """Tailored instrumentation wrappers for any list-like class."""
+
+ def _tidy(fn):
+ fn._sa_instrumented = True
+ fn.__doc__ = getattr(list, fn.__name__).__doc__
+
+ def append(fn):
+ def append(self, item):
+ try:
+ item = self._attribute.do_list_add(self._instance, item)
+ fn(self, item)
+ except VICNListException as e:
+ pass
+ _tidy(append)
+ return append
+
+ def remove(fn):
+ def remove(self, value):
+ # testlib.pragma exempt:__eq__
+ try:
+ self._attribute.do_list_remove(self._instance, value)
+ fn(self, value)
+ except : pass
+ _tidy(remove)
+ return remove
+
+ def insert(fn):
+ def insert(self, index, value):
+ try:
+ value = self._attribute.do_list_add(self._instance, item)
+ fn(self, index, value)
+ except : pass
+ _tidy(insert)
+ return insert
+
+ def __getitem__(fn):
+ def __getitem__(self, index):
+ item = fn(self, index)
+ return self._attribute.handle_getitem(self._instance, item)
+ _tidy(__getitem__)
+ return __getitem__
+
+ def __setitem__(fn):
+ def __setitem__(self, index, value):
+ if not isinstance(index, slice):
+ existing = self[index]
+ if existing is not None:
+ try:
+ self._attribute.do_list_remove(self._instance, existing)
+ except: pass
+ try:
+ value = self._attribute.do_list_add(self._instance, value)
+ fn(self, index, value)
+ except: pass
+ else:
+ # slice assignment requires __delitem__, insert, __len__
+ step = index.step or 1
+ start = index.start or 0
+ if start < 0:
+ start += len(self)
+ if index.stop is not None:
+ stop = index.stop
+ else:
+ stop = len(self)
+ if stop < 0:
+ stop += len(self)
+
+ if step == 1:
+ for i in range(start, stop, step):
+ if len(self) > start:
+ del self[start]
+
+ for i, item in enumerate(value):
+ self.insert(i + start, item)
+ else:
+ rng = list(range(start, stop, step))
+ if len(value) != len(rng):
+ raise ValueError(
+ "attempt to assign sequence of size %s to "
+ "extended slice of size %s" % (len(value),
+ len(rng)))
+ for i, item in zip(rng, value):
+ self.__setitem__(i, item)
+ _tidy(__setitem__)
+ return __setitem__
+
+ def __delitem__(fn):
+ def __delitem__(self, index):
+ if not isinstance(index, slice):
+ item = self[index]
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ fn(self, index)
+ except : pass
+ else:
+ # slice deletion requires __getslice__ and a slice-groking
+ # __getitem__ for stepped deletion
+ # note: not breaking this into atomic dels
+ has_except = False
+ for item in self[index]:
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ except : has_except = True
+ if not has_except:
+ fn(self, index)
+ _tidy(__delitem__)
+ return __delitem__
+
+ if py2k:
+ def __setslice__(fn):
+ def __setslice__(self, start, end, values):
+ has_except = False
+ for value in self[start:end]:
+ try:
+ self._attribute.do_list_remove(self._instance, value)
+ except : has_except = True
+ #values = [self._attribute.do_list_add(self._instance, value) for value in values]
+ _values = list()
+ for value in values:
+ try:
+ _values.append(self._attribute.do_list_add(self._instance, value))
+ except: has_except = True
+ if not has_except:
+ fn(self, start, end, _values)
+ _tidy(__setslice__)
+ return __setslice__
+
+ def __delslice__(fn):
+ def __delslice__(self, start, end):
+ has_except = False
+ for value in self[start:end]:
+ try:
+ self._attribute.do_list_remove(self._instance, value)
+ except : has_except = True
+ if not has_except:
+ fn(self, start, end)
+ _tidy(__delslice__)
+ return __delslice__
+
+ def extend(fn):
+ def extend(self, iterable):
+ for value in iterable:
+ self.append(value)
+ _tidy(extend)
+ return extend
+
+ def __iadd__(fn):
+ def __iadd__(self, iterable):
+ # list.__iadd__ takes any iterable and seems to let TypeError
+ # raise as-is instead of returning NotImplemented
+ for value in iterable:
+ self.append(value)
+ return self
+ _tidy(__iadd__)
+ return __iadd__
+
+ def pop(fn):
+ def pop(self, index=-1):
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ item = fn(self, index)
+ return item
+ except : return None
+ _tidy(pop)
+ return pop
+
+ def __iter__(fn):
+ def __iter__(self):
+ for item in fn(self):
+ yield self._attribute.handle_getitem(self._instance, item)
+ _tidy(__iter__)
+ return __iter__
+
+ def __repr__(fn):
+ def __repr__(self):
+ return '<Collection {} {}>'.format(id(self), list.__repr__(self))
+ _tidy(__repr__)
+ return __repr__
+
+ __str__ = __repr__
+ #def __str__(fn):
+ # def __str__(self):
+ # return str(list(self))
+ # _tidy(__str__)
+ # return __str__
+
+ if not py2k:
+ def clear(fn):
+ def clear(self, index=-1):
+ has_except = False
+ for item in self:
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ except : has_except = True
+ if not has_except:
+ fn(self)
+ _tidy(clear)
+ return clear
+
+ # __imul__ : not wrapping this. all members of the collection are already
+ # present, so no need to fire appends... wrapping it with an explicit
+ # decorator is still possible, so events on *= can be had if they're
+ # desired. hard to imagine a use case for __imul__, though.
+
+ l = locals().copy()
+ l.pop('_tidy')
+ return l
+
+def _instrument_list(cls):
+ # inspired by sqlalchemy
+ for method, decorator in _list_decorators().items():
+ fn = getattr(cls, method, None)
+ if fn:
+ #if (fn and method not in methods and
+ # not hasattr(fn, '_sa_instrumented')):
+ setattr(cls, method, decorator(fn))
+
+class InstrumentedList(list):
+
+ def __contains__(self, key):
+ from vicn.core.resource import Resource
+ if isinstance(key, Resource):
+ key = key.get_uuid()
+ return list.__contains__(self, key)
+
+ def __lshift__(self, item):
+ self.append(item)
+
+_instrument_list(InstrumentedList)
diff --git a/vicn/core/sa_compat.py b/vicn/core/sa_compat.py
new file mode 100644
index 00000000..34211455
--- /dev/null
+++ b/vicn/core/sa_compat.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# This module originates from SQLAlchemy
+#
+# util/compat.py
+# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+#
+
+"""Handle Python version/platform incompatibilities."""
+
+import sys
+
+try:
+ import threading
+except ImportError:
+ import dummy_threading as threading
+
+py36 = sys.version_info >= (3, 6)
+py33 = sys.version_info >= (3, 3)
+py32 = sys.version_info >= (3, 2)
+py3k = sys.version_info >= (3, 0)
+py2k = sys.version_info < (3, 0)
+py265 = sys.version_info >= (2, 6, 5)
+jython = sys.platform.startswith('java')
+pypy = hasattr(sys, 'pypy_version_info')
+win32 = sys.platform.startswith('win')
+cpython = not pypy and not jython # TODO: something better for this ?
+
+import collections
+next = next
+
+if py3k:
+ import pickle
+else:
+ try:
+ import cPickle as pickle
+ except ImportError:
+ import pickle
+
+# work around http://bugs.python.org/issue2646
+if py265:
+ safe_kwarg = lambda arg: arg
+else:
+ safe_kwarg = str
+
+ArgSpec = collections.namedtuple("ArgSpec",
+ ["args", "varargs", "keywords", "defaults"])
+
+if py3k:
+ import builtins
+
+ from inspect import getfullargspec as inspect_getfullargspec
+ from urllib.parse import (quote_plus, unquote_plus,
+ parse_qsl, quote, unquote)
+ import configparser
+ from io import StringIO
+
+ from io import BytesIO as byte_buffer
+
+ def inspect_getargspec(func):
+ return ArgSpec(
+ *inspect_getfullargspec(func)[0:4]
+ )
+
+ string_types = str,
+ binary_types = bytes,
+ binary_type = bytes
+ text_type = str
+ int_types = int,
+ iterbytes = iter
+
+ def u(s):
+ return s
+
+ def ue(s):
+ return s
+
+ def b(s):
+ return s.encode("latin-1")
+
+ if py32:
+ callable = callable
+ else:
+ def callable(fn):
+ return hasattr(fn, '__call__')
+
+ def cmp(a, b):
+ return (a > b) - (a < b)
+
+ from functools import reduce
+
+ print_ = getattr(builtins, "print")
+
+ import_ = getattr(builtins, '__import__')
+
+ import itertools
+ itertools_filterfalse = itertools.filterfalse
+ itertools_filter = filter
+ itertools_imap = map
+ from itertools import zip_longest
+
+ import base64
+
+ def b64encode(x):
+ return base64.b64encode(x).decode('ascii')
+
+ def b64decode(x):
+ return base64.b64decode(x.encode('ascii'))
+
+else:
+ from inspect import getargspec as inspect_getfullargspec
+ inspect_getargspec = inspect_getfullargspec
+ from urllib import quote_plus, unquote_plus, quote, unquote
+ from urlparse import parse_qsl
+ import ConfigParser as configparser
+ from StringIO import StringIO
+ from cStringIO import StringIO as byte_buffer
+
+ string_types = basestring,
+ binary_types = bytes,
+ binary_type = str
+ text_type = unicode
+ int_types = int, long
+
+ def iterbytes(buf):
+ return (ord(byte) for byte in buf)
+
+ def u(s):
+ # this differs from what six does, which doesn't support non-ASCII
+ # strings - we only use u() with
+ # literal source strings, and all our source files with non-ascii
+ # in them (all are tests) are utf-8 encoded.
+ return unicode(s, "utf-8")
+
+ def ue(s):
+ return unicode(s, "unicode_escape")
+
+ def b(s):
+ return s
+
+ def import_(*args):
+ if len(args) == 4:
+ args = args[0:3] + ([str(arg) for arg in args[3]],)
+ return __import__(*args)
+
+ callable = callable
+ cmp = cmp
+ reduce = reduce
+
+ import base64
+ b64encode = base64.b64encode
+ b64decode = base64.b64decode
+
+ def print_(*args, **kwargs):
+ fp = kwargs.pop("file", sys.stdout)
+ if fp is None:
+ return
+ for arg in enumerate(args):
+ if not isinstance(arg, basestring):
+ arg = str(arg)
+ fp.write(arg)
+
+ import itertools
+ itertools_filterfalse = itertools.ifilterfalse
+ itertools_filter = itertools.ifilter
+ itertools_imap = itertools.imap
+ from itertools import izip_longest as zip_longest
+
+
+import time
+if win32 or jython:
+ time_func = time.clock
+else:
+ time_func = time.time
+
+from collections import namedtuple
+from operator import attrgetter as dottedgetter
+
+
+if py3k:
+ def reraise(tp, value, tb=None, cause=None):
+ if cause is not None:
+ assert cause is not value, "Same cause emitted"
+ value.__cause__ = cause
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+else:
+ # not as nice as that of Py3K, but at least preserves
+ # the code line where the issue occurred
+ exec("def reraise(tp, value, tb=None, cause=None):\n"
+ " if cause is not None:\n"
+ " assert cause is not value, 'Same cause emitted'\n"
+ " raise tp, value, tb\n")
+
+
+def raise_from_cause(exception, exc_info=None):
+ if exc_info is None:
+ exc_info = sys.exc_info()
+ exc_type, exc_value, exc_tb = exc_info
+ cause = exc_value if exc_value is not exception else None
+ reraise(type(exception), exception, tb=exc_tb, cause=cause)
+
+if py3k:
+ exec_ = getattr(builtins, 'exec')
+else:
+ def exec_(func_text, globals_, lcl=None):
+ if lcl is None:
+ exec('exec func_text in globals_')
+ else:
+ exec('exec func_text in globals_, lcl')
+
+
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass.
+
+ Drops the middle class upon creation.
+
+ Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/
+
+ """
+
+ class metaclass(meta):
+ __call__ = type.__call__
+ __init__ = type.__init__
+
+ def __new__(cls, name, this_bases, d):
+ if this_bases is None:
+ return type.__new__(cls, name, (), d)
+ return meta(name, bases, d)
+ return metaclass('temporary_class', None, {})
+
+
+from contextlib import contextmanager
+
+try:
+ from contextlib import nested
+except ImportError:
+ # removed in py3k, credit to mitsuhiko for
+ # workaround
+
+ @contextmanager
+ def nested(*managers):
+ exits = []
+ vars = []
+ exc = (None, None, None)
+ try:
+ for mgr in managers:
+ exit = mgr.__exit__
+ enter = mgr.__enter__
+ vars.append(enter())
+ exits.append(exit)
+ yield vars
+ except:
+ exc = sys.exc_info()
+ finally:
+ while exits:
+ exit = exits.pop()
+ try:
+ if exit(*exc):
+ exc = (None, None, None)
+ except:
+ exc = sys.exc_info()
+ if exc != (None, None, None):
+ reraise(exc[0], exc[1], exc[2])
diff --git a/vicn/core/scheduling_algebra.py b/vicn/core/scheduling_algebra.py
new file mode 100644
index 00000000..207856c0
--- /dev/null
+++ b/vicn/core/scheduling_algebra.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+def SchedulingAlgebra(cls, concurrent_mixin=object, composition_mixin=object,
+ sequential_mixin=object): # allow_none = True
+
+ class BaseElement(cls):
+ def __default__(cls, *elements):
+ elts = [e for e in elements
+ if e is not None and not isinstance(e, Empty)]
+ if len(elts) == 0:
+ # The first is always Empty
+ assert len(elements) != 0
+ return elements[0]
+ elif len(elts) == 1:
+ return elts[0]
+ return cls(*elts)
+
+ def __concurrent__(*elements):
+ return BaseElement.__default__(Concurrent, *elements)
+
+ def __composition__(*elements):
+ return BaseElement.__default__(Composition, *elements)
+
+ def __sequential__(*elements):
+ return BaseElement.__default__(Sequential, *elements)
+
+ # Operator: |
+ __or__ = __concurrent__
+
+ # Operator: >
+ __gt__ = __sequential__
+
+ # Operator: @
+ __matmul__ = __composition__
+
+ class Element(BaseElement):
+ def __iter__(self):
+ yield self
+
+ class Operator(BaseElement):
+ def __init__(self, *elements):
+ super().__init__()
+ self._elements = list(elements)
+
+ def __iter__(self):
+ yield self
+ for element in self._elements:
+ for x in element:
+ yield x
+
+ class Concurrent(Operator, concurrent_mixin):
+ # Algebraic rule : ((A // B) // C) ~ (A // B // C)
+ def __concurrent__(self, other):
+ self._elements.append(other)
+ return self
+
+ def __repr__(self):
+ return '<Concurrent {}>'.format(self._elements)
+
+ class Composition(Operator, composition_mixin):
+ def __repr__(self):
+ return '<Composition {}>'.format(self._elements)
+
+ class Sequential(Operator, sequential_mixin):
+ def __repr__(self):
+ return '<Sequential {}>'.format(self._elements)
+
+ class Empty(Element):
+ def __concurrent__(self, other):
+ return other
+
+ def __composition__(self, other):
+ return other
+
+ def __sequential__(self, other):
+ return other
+
+ def __repr__(self):
+ return '<Empty>'
+
+ return Element, Empty
diff --git a/vicn/core/state.py b/vicn/core/state.py
new file mode 100644
index 00000000..d5069b24
--- /dev/null
+++ b/vicn/core/state.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+import random
+import string
+
+class NEVER_SET:
+ pass
+
+# Separator for components of the UUID
+UUID_SEP = '-'
+
+# Length of the random component of the UUID
+UUID_LEN = 5
+
+class ResourceState:
+ UNINITIALIZED = 'UNINITIALIZED'
+ PENDING_DEPS = 'PENDING_DEPS'
+ DEPS_OK = 'DEPS_OK'
+ PENDING_INIT = 'PENDING_INIT'
+ INITIALIZED = 'INITIALIZED'
+ PENDING_GET = 'PENDING_GET'
+ GET_DONE = 'GET_DONE'
+ PENDING_KEYS = 'PENDING_KEYS'
+ KEYS_OK = 'KEYS_OK'
+ PENDING_CREATE = 'PENDING_CREATE'
+ CREATED = 'CREATED'
+ DIRTY = 'DIRTY'
+ CLEAN = 'CLEAN'
+ PENDING_UPDATE = 'PENDING_UPDATE'
+ PENDING_DELETE = 'PENDING_DELETE'
+ DELETED = 'DELETED'
+
+class AttributeState:
+ UNINITIALIZED = 'UNINITIALIZED'
+ INITIALIZED = 'INITIALIZED'
+ DIRTY = 'DIRTY'
+ PENDING_INIT = 'PENDING_INIT'
+ PENDING_UPDATE = 'PENDING_UPDATE'
+ CLEAN = 'CLEAN'
+
+class Operations:
+ SET = 'set'
+ LIST_ADD = 'add'
+ LIST_REMOVE = 'remove'
+ LIST_CLEAR = 'clear'
+
+class UUID:
+ def __init__(self, name, cls):
+ self._uuid = self._make_uuid(name, cls)
+
+ def _make_uuid(self, name, cls):
+ """Generate a unique resource identifier
+
+ The UUID consists in the type of the resource, to which is added a
+ random identifier of length UUID_LEN. Components of the UUID are
+ separated by UUID_SEP.
+ """
+ uuid = ''.join(random.choice(string.ascii_uppercase + string.digits)
+ for _ in range(UUID_LEN))
+ if name:
+ uuid = name # + UUID_SEP + uuid
+ return UUID_SEP.join([cls.__name__, uuid])
+
+ def __repr__(self):
+ return '<UUID {}>'.format(self._uuid)
+
+ def __lt__(self, other):
+ return self._uuid < other._uuid
+
+ __str__ = __repr__
+
+class PendingValue:
+ def __init__(self, value = None):
+ self.clear(value)
+
+ def clear(self, value=NEVER_SET):
+ self.value = NEVER_SET
+ self.operations = list()
+
+ def trigger(self, action, value, cur_value = None):
+
+ if self.value is NEVER_SET:
+ if cur_value is not None:
+ self.value = cur_value
+
+ if action == Operations.SET:
+ self.value = value
+ self.operations = [(Operations.SET, value)]
+ elif action == Operations.LIST_CLEAR:
+ self.value = list()
+ self.operations = [(Operations.LIST_CLEAR, None)]
+ else:
+ if action == Operations.LIST_ADD:
+ self.value.append(value)
+ elif action == Operations.LIST_REMOVE:
+ self.value.remove(value)
+ else:
+ raise RuntimeError
+ self.operations.append((action, value))
+
+class InstanceState:
+ def __init__(self, manager, instance, name = None):
+
+ # Unique identifier for the instance. This is useful for relation
+ # between resources
+ self.uuid = UUID(name, instance.__class__)
+ self.instance = instance
+
+ # Instance manager
+ self.manager = manager
+
+ # Events
+ self.events = dict()
+
+ # Stores the requested value : attribute_name -> requested operations =
+ # LIST set add remove clear
+ self.dirty = dict()
+
+
+ # Initialize resource state
+ self.lock = asyncio.Lock()
+ self.write_lock = asyncio.Lock()
+ self.state = ResourceState.UNINITIALIZED
+ self.clean = asyncio.Event()
+ self.clean.clear()
+ self.init = asyncio.Event()
+ self.init.clear()
+ self.change_event = asyncio.Event()
+ self.change_event.clear()
+ self.change_success = None
+ self.change_value = None
+ self.log = list()
+
+ self.attr_lock = dict()
+ self.attr_init = dict()
+ self.attr_clean = dict()
+ self.attr_state = dict()
+ self.attr_change_event = dict()
+ self.attr_change_success = dict()
+ self.attr_change_value= dict()
+ self.attr_log = dict()
+ # Initialize attribute state
+ for attribute in instance.iter_attributes():
+ self.attr_lock[attribute.name] = asyncio.Lock()
+ self.attr_init[attribute.name] = asyncio.Event()
+ self.attr_clean[attribute.name] = asyncio.Event()
+ self.attr_state[attribute.name] = AttributeState.UNINITIALIZED
+ self.attr_change_event[attribute.name] = asyncio.Event()
+ self.attr_change_event[attribute.name].clear()
+ self.attr_change_success[attribute.name] = None
+ self.attr_change_value[attribute.name] = None
+ self.dirty[attribute.name] = PendingValue(NEVER_SET)
+ self.attr_log[attribute.name] = list()
+
+ def set_dirty(self, attr_name):
+ self.attr_dirty.add(attr_name)
+ self.manager.set_dirty(self.uuid)
+
+ def trigger(self, attribute_name, action, value):
+ self.dirty[attribute_name].trigger(action, value)
diff --git a/vicn/core/task.py b/vicn/core/task.py
new file mode 100644
index 00000000..2e9bc275
--- /dev/null
+++ b/vicn/core/task.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+import concurrent.futures
+import functools
+import logging
+import shlex
+import subprocess
+
+from vicn.core.scheduling_algebra import SchedulingAlgebra
+from vicn.core.commands import ReturnValue
+from vicn.core.exception import ResourceNotFound
+from vicn.core.commands import Command, SequentialCommands
+from netmodel.util.process import execute_local
+
+log = logging.getLogger(__name__)
+
+EXECUTOR=concurrent.futures.ThreadPoolExecutor
+#EXECUTOR=concurrent.futures.ProcessPoolExecutor
+
+MAX_WORKERS=50 # None
+
+class BaseTask:
+ """Base class for all tasks
+ """
+
+ def __init__(self):
+ self._future = asyncio.Future()
+
+ def terminate(self):
+ pass
+
+ def start(self):
+ pass
+
+ def stop(self):
+ pass
+
+ def get_future(self):
+ return self._future
+
+ def add_done_callback(self, cb):
+ self._future.add_done_callback(cb)
+
+ def __repr__(self):
+ return '<BaseTask>'
+
+class ConcurrentMixin:
+ async def execute(self):
+ try:
+ for t in self._elements:
+ await t.execute()
+ rets = await asyncio.gather(*[t.get_future()
+ for t in self._elements])
+
+ # The result value is the "union" of all result values
+ # In case of tasks setting the same attributes, they are merged
+ # into a list
+
+ dic = dict()
+ for ret in rets:
+ # Ideally we should get all attribute names, and properly
+ # insert Nones. So far we assume all dicts are present and
+ # complete.
+ if not isinstance(ret, dict):
+ continue
+ for k, v in ret.items():
+ if k in dic:
+ if not isinstance(dic[k], list):
+ dic[k] = [dic[k]]
+ dic[k].append(v)
+ else:
+ dic[k] = [v]
+ self.get_future().set_result(dic)
+ except Exception as e:
+ self.get_future().set_exception(e)
+
+class SequentialMixin:
+ async def execute(self):
+ try:
+ for t in self._elements:
+ await t.execute()
+ await t.get_future()
+ self.get_future().set_result(None)
+ except Exception as e:
+ self.get_future().set_exception(e)
+
+class CompositionMixin:
+ async def execute(self):
+ try:
+ ret = None
+ for t in self._elements:
+ ret = (ret,) if ret is not None else tuple()
+ await t.execute(*ret)
+ ret = await t.get_future()
+ self.get_future().set_result(ret)
+ except Exception as e:
+ print('we need to cancel tasks not executed...')
+ self.get_future().set_exception(e)
+
+Task, EmptyTask = SchedulingAlgebra(BaseTask, ConcurrentMixin,
+ CompositionMixin, SequentialMixin)
+
+def task(fn):
+ def decorator(*args, **kwargs):
+ return PythonTask(fn, *args, **kwargs)
+ return decorator
+
+def async_task(fn, *t_args, **t_kwargs):
+ def decorator(*args, **kwargs):
+ all_args = tuple() + t_args + args
+ all_kwargs = dict()
+ all_kwargs.update(t_kwargs)
+ all_kwargs.update(kwargs)
+ return PythonAsyncTask(fn, *args, **kwargs)
+ return decorator
+
+def inline_task(fn):
+ def decorator(*args, **kwargs):
+ return PythonInlineTask(fn, *args, **kwargs)
+ return decorator
+
+async def wait_task(task):
+ return await task.get_future()
+
+async def run_task(task, manager):
+ manager.schedule(task)
+ ret = await wait_task(task)
+ return ret
+
+async def wait_concurrent_tasks(tasks):
+ await wait_task(Task.__concurrent__(*tasks))
+
+wait_task_task = async_task(wait_task)
+
+def get_attribute_task(resource, attribute_name):
+ @async_task
+ async def func():
+ return await resource.async_get(attribute_name)
+ return func()
+
+def set_attributes_task(resource, attribute_dict):
+ # The difficulty is in setting the pending value without triggering the
+ # manager, and executing the task by ourselves !
+ raise NotImplementedError
+
+def get_attributes_task(resource, attribute_names):
+ assert len(attribute_names) == 1
+ attribute_name = attribute_names[0]
+
+ @async_task
+ async def func():
+ await resource._state.manager.wait_attr_init(resource, attribute_name)
+ ret = await resource.async_get(attribute_name)
+ return {attribute_name: ret}
+ return func()
+
+class PythonTask(Task):
+ def __init__(self, func, *args, **kwargs):
+ super().__init__()
+ self._func = func
+ self._args = args
+ self._kwargs = kwargs
+
+ def _done_callback(self, fut):
+ try:
+ self._future.set_result(fut.result())
+ except Exception as e:
+ self._future.set_exception(e)
+
+ async def execute(self, *args, **kwargs):
+ all_args = self._args + args
+ all_kwargs = dict()
+ all_kwargs.update(self._kwargs)
+ all_kwargs.update(kwargs)
+
+ partial = functools.partial(self._func, *all_args, **all_kwargs)
+
+ loop = asyncio.get_event_loop()
+ fut = loop.run_in_executor(None, partial)
+ fut.add_done_callback(self._done_callback)
+
+ def __repr__(self):
+ return '<Task[py] {} / {} {}>'.format(self._func, self._args,
+ self._kwargs)
+
+class PythonAsyncTask(PythonTask):
+ async def execute(self, *args, **kwargs):
+ all_args = self._args + args
+ all_kwargs = dict()
+ all_kwargs.update(self._kwargs)
+ all_kwargs.update(kwargs)
+
+ partial = asyncio.coroutine(self._func)(*all_args, **all_kwargs)
+
+ fut = asyncio.ensure_future(partial)
+ fut.add_done_callback(self._done_callback)
+
+ def __repr__(self):
+ return '<Task[apy]>'
+
+class PythonInlineTask(PythonTask):
+ async def execute(self, *args, **kwargs):
+ all_args = self._args + args
+ all_kwargs = dict()
+ all_kwargs.update(self._kwargs)
+ all_kwargs.update(kwargs)
+
+ try:
+ ret = self._func(*all_args, **all_kwargs)
+ self._future.set_result(ret)
+ except Exception as e:
+ self._future.set_exception(e)
+ return self._future
+
+class BashTask(Task):
+ def __init__(self, node, cmd, parameters=None, parse=None, as_root=False,
+ output=False, pre=None, post=None, lock=None):
+ super().__init__()
+ self._node = node
+ self._cmd = cmd
+ self._params = parameters if parameters else dict()
+ self._parse = parse
+ self._pre = pre
+ self._post = post
+ self._lock = lock
+
+ self._output = output
+ self._as_root = as_root
+
+ def _default_parse_for_get(self, rv):
+ if not bool(rv):
+ raise ResourceNotFound
+
+ def set_default_parse_for_get(self):
+ if self._parse is None:
+ self._parse = self._default_parse_for_get
+
+ def _done_callback(self, fut):
+ """
+ Note: extends the functionality of the parent _done_callback
+ """
+ try:
+ rv = fut.result()
+ if self._parse is None and rv.return_value != 0:
+ raise Exception('Bash command failed', self.get_full_cmd(), rv)
+ if self._post:
+ self._post()
+ if self._parse:
+ rv = self._parse(rv)
+ self._future.set_result(rv)
+ except Exception as e:
+ self._future.set_exception(e)
+ if self._lock:
+ self._lock.release()
+
+ def get_full_cmd(self):
+ c = SequentialCommands()
+ desc = None
+ for line in self._cmd.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith('#'):
+ desc = line[1:].strip()
+ continue
+ c << Command(line, description = desc)
+ desc = None
+
+ c.parameters = self._params
+ return c.command.full_commandline
+
+ async def execute(self, *args, **kwargs):
+ """Execute the task, enforcing any eventual locking.
+
+ Returns:
+ asyncio.Future
+
+ Upon completion (eventually error), the Task's future is set.
+ """
+ if len(args) == 1:
+ dic, = args
+ if isinstance(dic, dict):
+ self._params.update(dic)
+ if self._pre:
+ self._pre()
+
+ func = self._node.execute if self._node else execute_local
+ # It is important that the command is contructed only just before it is
+ # executed, so that any object passed as parameters is deferenced right
+ # on time.
+ cmd = self.get_full_cmd()
+ partial = functools.partial(func, cmd, output = bool(self._parse))
+
+ node_str = self._node.name if self._node else '(LOCAL)'
+ cmd_str = cmd[:77] + '...' if len(cmd) > 80 else cmd
+ log.info('Execute: {} - {}'.format(node_str, cmd_str))
+
+ if self._lock:
+ # We need to do lock/unlock around the task execution
+ # Locking now will early block other tasks, but will at the same
+ # time delay them entering the executor queue; so this is
+ # equivalent
+ await self._lock.acquire()
+
+ loop = asyncio.get_event_loop()
+ fut = loop.run_in_executor(None, partial)
+ fut.add_done_callback(self._done_callback)
+
+ def execute_blocking(self):
+ rv = self._node.execute(self.get_full_cmd(), output=True)
+ if self._parse:
+ rv = self._parse(rv)
+ return rv
+
+ def __repr__(self):
+ return '<Task[bash] {} / {}>'.format(self._cmd, self._params)
+
+class TaskManager:
+ def __init__(self):
+ executor = EXECUTOR() if MAX_WORKERS is None \
+ else EXECUTOR(max_workers=MAX_WORKERS)
+ loop = asyncio.get_event_loop()
+ loop.set_default_executor(executor)
+
+ def schedule(self, task):
+ """All instances of BaseTask can be scheduled
+
+ Here we might decide to do more advanced scheduling, like merging bash
+ tasks, etc. thanks to the task algebra.
+ """
+ asyncio.ensure_future(task.execute())
+
+@task
+def ParseRegexTask(rv):
+ return [m.groupdict() for m in rx.finditer(rv.stdout)]
diff --git a/vicn/helpers/resource_definition.py b/vicn/helpers/resource_definition.py
new file mode 100644
index 00000000..b49a2a1f
--- /dev/null
+++ b/vicn/helpers/resource_definition.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.resource import Resource
+from vicn.core.attribute import Attribute, Reference, Multiplicity
+from netmodel.model.type import String, Integer, Bool
+from vicn.core.exception import ResourceNotFound
diff --git a/vicn/helpers/tasks.py b/vicn/helpers/tasks.py
new file mode 100644
index 00000000..6805691d
--- /dev/null
+++ b/vicn/helpers/tasks.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import inline_task, task, async_task
+from vicn.core.task import EmptyTask, BashTask
diff --git a/vicn/resource/__init__.py b/vicn/resource/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/__init__.py
diff --git a/vicn/resource/application.py b/vicn/resource/application.py
new file mode 100644
index 00000000..f5341f2b
--- /dev/null
+++ b/vicn/resource/application.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import Resource
+from vicn.resource.node import Node
+
+class Application(Resource):
+ node = Attribute(Node,
+ description = 'Node on which the application is installed',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'applications',
+ reverse_description = 'Applications installed on node')
diff --git a/vicn/resource/central.py b/vicn/resource/central.py
new file mode 100644
index 00000000..73a43478
--- /dev/null
+++ b/vicn/resource/central.py
@@ -0,0 +1,789 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import networkx as nx
+import logging
+
+from netmodel.model.type import String
+from netmodel.util.misc import pairwise
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import async_task, inline_task
+from vicn.core.task import EmptyTask, BashTask
+from vicn.resource.channel import Channel
+from vicn.resource.ip.route import IPRoute
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.icn.face import L2Face, L4Face, FaceProtocol
+from vicn.resource.icn.producer import Producer
+from vicn.resource.icn.route import Route as ICNRoute
+from vicn.resource.linux.file import TextFile
+from vicn.resource.lxd.lxc_container import LxcContainer
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+TMP_DEFAULT_PORT = 6363
+
+CMD_CONTAINER_SET_DNS = 'echo "nameserver {ip_dns}" | ' \
+ 'resolvconf -a {interface_name}'
+
+# For host
+CMD_NAT = '\n'.join([
+ 'iptables -t nat -A POSTROUTING -o {bridge_name} -s {network} ' \
+ '! -d {network} -j MASQUERADE',
+ 'echo 1 > /proc/sys/net/ipv4/ip_forward'
+])
+
+# For containers
+CMD_IP_FORWARD = 'echo 1 > /proc/sys/net/ipv4/ip_forward'
+
+HOST_FILE_PATH = "/etc/hosts.vicn"
+
+#------------------------------------------------------------------------------
+# Routing strategies
+#------------------------------------------------------------------------------
+
+def routing_strategy_spt(G, origins, weight_key = None):
+ """Routing strategy : Shortest path tree
+
+ This routing strategy uses the Dijkstra algorithm on an undirected graph
+ to build the shortest path tree towards all origin prefixes.
+
+ NOTE: weights are currently unsupported by this strategy.
+
+ Args:
+ G (nx.Graph): network graph
+ origins (dict): dictionary mapping nodes to the set of prefixes they
+ are origins for
+ weight_key (str): key corresponding to weight key in edge data. None
+ assumes all weights have unit cost
+
+ Returns:
+ generator : returning triplets (source, prefix, next hop)
+ """
+ assert weight_key is None
+
+ origin_nodes = origins.keys()
+ seen = set()
+ for dst_node in origin_nodes:
+ sssp = nx.shortest_path(G, target = dst_node)
+ # Notes from the documentation:
+ # - If only the target is specified, return a dictionary keyed by
+ # sources with a list of nodes in a shortest path from one of the
+ # sources to the target.
+ # - All returned paths include both the source and target in the
+ # path.
+ for _, path in sssp.items():
+ if len(path) == 1:
+ # Local prefix
+ continue
+ for s, d in pairwise(path):
+ for prefix in origins[dst_node]:
+ t = (s, prefix, d)
+ if t in seen:
+ continue
+ seen.add(t)
+ yield t
+
+def routing_strategy_max_flow(G, origins, weight_key = 'capacity'):
+ """Routing strategy : Maximum Flow
+
+ TODO
+
+ Args:
+ G (nx.Graph): network graph
+ origins (dict): dictionary mapping nodes to the set of prefixes they
+ are origins for
+ weight_key (str): key corresponding to weight key in edge data. None
+ assumes all weights have unit cost
+
+ Returns:
+ generator : returning triplets (source, prefix, next hop)
+ """
+ assert weight_key is None
+
+ origin_nodes = origins.keys()
+ for dst_node in origin_nodes:
+ for src_node in G.nodes:
+ if src_node == dst_node:
+ continue
+ _, flow_dict = nx.maximum_flow(G, src_node, dst_node,
+ capacity=weight_key)
+
+ # Notes from the documentation:
+ # https://networkx.github.io/documentation/networkx-1.10/reference/
+ # generated/networkx.algorithms.flow.maximum_flow.html
+ # - flow_dict (dict) – A dictionary containing the value of the
+ # flow that went through each edge.
+ for s, d_map in flow_dict.items():
+ for d, flow in d_map.items():
+ if flow == 0:
+ continue
+ for prefix in origins[dst_node]:
+ yield s, prefix, d
+
+MAP_ROUTING_STRATEGY = {
+ 'spt' : routing_strategy_spt,
+ 'max_flow' : routing_strategy_max_flow,
+}
+
+#------------------------------------------------------------------------------
+# L2 and L4/ICN graphs
+#------------------------------------------------------------------------------
+
+def _get_l2_graph(manager, with_managed = False):
+ G = nx.Graph()
+ for node in manager.by_type(Node):
+ G.add_node(node._state.uuid)
+
+ for channel in manager.by_type(Channel):
+ if channel.has_type('emulatedchannel'):
+ src = channel._ap_if
+ for dst in channel._sta_ifs.values():
+ if not with_managed and (not src.managed or not dst.managed):
+ continue
+ if G.has_edge(src.node._state.uuid, dst.node._state.uuid):
+ continue
+
+ map_node_interface = { src.node._state.uuid : src._state.uuid,
+ dst.node._state.uuid: dst._state.uuid}
+ G.add_edge(src.node._state.uuid, dst.node._state.uuid,
+ map_node_interface = map_node_interface)
+ else:
+ # This is for a normal Channel
+ for src_it in range(0,len(channel.interfaces)):
+ src = channel.interfaces[src_it]
+
+ # Iterate over the remaining interface to create all the
+ # possible combination
+ for dst_it in range(src_it+1,len(channel.interfaces)):
+ dst = channel.interfaces[dst_it]
+
+ if not with_managed and (not src.managed or
+ not dst.managed):
+ continue
+ if G.has_edge(src.node._state.uuid, dst.node._state.uuid):
+ continue
+ map_node_interface = {
+ src.node._state.uuid : src._state.uuid,
+ dst.node._state.uuid: dst._state.uuid}
+ G.add_edge(src.node._state.uuid, dst.node._state.uuid,
+ map_node_interface = map_node_interface)
+ return G
+
+def _get_icn_graph(manager):
+ G = nx.Graph()
+ for forwarder in manager.by_type(Forwarder):
+ node = forwarder.node
+ G.add_node(node._state.uuid)
+ for face in forwarder.faces:
+ other_face = manager.by_uuid(face._internal_data['sibling_face'])
+ other_node = other_face.node
+ if G.has_edge(node._state.uuid, other_node._state.uuid):
+ continue
+ map_node_face = { node._state.uuid: face._state.uuid,
+ other_node._state.uuid: other_face._state.uuid }
+ G.add_edge(node._state.uuid, other_node._state.uuid,
+ map_node_face = map_node_face)
+
+ return G
+
+#-------------------------------------------------------------------------------
+
+class IPAssignment(Resource):
+ """
+ Resource: IPAssignment
+ """
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ('Interface',)
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __subresources__(self):
+ self.host_file = TextFile(node = None, filename = HOST_FILE_PATH,
+ overwrite = True)
+ return self.host_file
+
+ def __create__(self):
+ """
+ IP assignment strategy /32: assign /32 IP address to each interface
+
+ We might use different subnets for resources involved in experiment,
+ and supporting resources, and to minimize routing tables.
+ """
+ log.info('Assigment of IP addresses')
+ tasks = EmptyTask()
+
+ # We sort nodes by names for IP assignment. This code ensures that
+ # interfaces on the same channel get consecutive IP addresses. That
+ # way, we can assign /31 on p2p channels.
+ channels = sorted(self._state.manager.by_type(Channel),
+ key = lambda x : x.get_sortable_name())
+ channels.extend(sorted(self._state.manager.by_type(Node),
+ key = lambda node : node.name))
+
+ host_file_content = ""
+
+ # Dummy code to start IP addressing on an even number for /31
+ ip = AddressManager().get_ip(None)
+ if int(ip[-1]) % 2 == 0:
+ ip = AddressManager().get_ip("dummy object")
+
+ for channel in channels:
+ # Sort interfaces in a deterministic order to ensure consistent
+ # addressing across restarts of the tool
+ interfaces = sorted(channel.interfaces,
+ key = lambda x : x.device_name)
+
+ for interface in interfaces:
+ if interface.ip_address is None:
+ ip = AddressManager().get_ip(interface)
+
+ @async_task
+ async def set_ip(interface, ip):
+ await interface.async_set('ip_address', ip)
+
+ if interface.managed:
+ tasks = tasks | set_ip(interface, ip)
+ else:
+ ip = interface.ip_address
+
+ # Note: interface.ip_address should still be None at this stage
+ # since we have not made the assignment yet
+
+ if isinstance(channel, Node):
+ host_file_content += '# {} {} {}\n'.format(
+ interface.node.name, interface.device_name, ip)
+ if interface == interface.node.host_interface:
+ host_file_content += '{} {}\n'.format(ip,
+ interface.node.name)
+ self.host_file.content = host_file_content
+
+ return tasks
+
+ __delete__ = None
+
+#-------------------------------------------------------------------------------
+
+class IPRoutes(Resource):
+ """
+ Resource: IPRoutes
+
+ Centralized IP route computation.
+ """
+ routing_strategy = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ @inline_task
+ def __create__(self):
+ routes = list()
+ pre_routes, routes = self._get_ip_routes()
+ routes.extend(pre_routes)
+ routes.extend(routes)
+ for route in routes:
+ route.node.routing_table.routes << route
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_ip_origins(self):
+ origins = dict()
+ for node in self._state.manager.by_type(Node):
+ node_uuid = node._state.uuid
+ if not node_uuid in origins:
+ origins[node_uuid] = list()
+ for interface in node.interfaces:
+ origins[node_uuid].append(interface.ip_address)
+ return origins
+
+ def _get_ip_routes(self):
+ if self.routing_strategy == 'pair':
+ return [], self._get_pair_ip_routes()
+
+ strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy)
+
+ G = _get_l2_graph(self._state.manager)
+ origins = self._get_ip_origins()
+
+ # node -> list(origins for which we have routes)
+ ip_routes = dict()
+
+ pre_routes = list()
+ routes = list()
+ for src, prefix, dst in strategy(G, origins):
+ data = G.get_edge_data(src, dst)
+
+ map_ = data['map_node_interface']
+ next_hop_interface = map_[src]
+
+
+ next_hop_ingress = self._state.manager.by_uuid(map_[dst])
+ src_node = self._state.manager.by_uuid(src)
+
+ mac_addr = None
+ if ((hasattr(next_hop_ingress, 'vpp') and
+ next_hop_ingress.vpp is not None) or
+ (hasattr(src_node, 'vpp') and src_node.vpp is not None)):
+ mac_addr = next_hop_ingress.mac_address
+
+ # Avoid duplicate routes due to multiple paths in the network
+ if not src_node in ip_routes:
+ ip_routes[src_node] = list()
+ if prefix in ip_routes[src_node]:
+ continue
+
+ if prefix == next_hop_ingress.ip_address:
+ # Direct route on src_node.name :
+ # route add [prefix] dev [next_hop_interface_.device_name]
+ route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = prefix,
+ mac_address = mac_addr,
+ interface = next_hop_interface)
+ else:
+ # We need to be sure we have a route to the gw from the node
+ if not next_hop_ingress.ip_address in ip_routes[src_node]:
+ pre_route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = next_hop_ingress.ip_address,
+ mac_address = mac_addr,
+ interface = next_hop_interface)
+ ip_routes[src_node].append(next_hop_ingress.ip_address)
+ pre_routes.append(pre_route)
+
+ # Route on src_node.name:
+ # route add [prefix] dev [next_hop_interface_.device_name]
+ # via [next_hop_ingress.ip_address]
+ route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = prefix,
+ interface = next_hop_interface,
+ mac_address = mac_addr,
+ gateway = next_hop_ingress.ip_address)
+ ip_routes[src_node].append(prefix)
+ routes.append(route)
+ return pre_routes, routes
+
+ def _get_pair_ip_routes(self):
+ """
+ IP routing strategy : direct routes only
+ """
+ routes = list()
+ G = _get_l2_graph(self._state.manager)
+ for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True):
+ src_node = self._state.manager.by_uuid(src_node_uuid)
+ dst_node = self._state.manager.by_uuid(dst_node_uuid)
+
+ map_ = data['map_node_interface']
+ src = self._state.manager.by_uuid(map_[src_node_uuid])
+ dst = self._state.manager.by_uuid(map_[dst_node_uuid])
+
+ log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format(
+ src_node.name, src.device_name, src.ip_address,
+ dst_node.name, dst.device_name, dst.ip_address))
+ log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format(
+ dst_node.name, dst.device_name, dst.ip_address,
+ src_node.name, src.device_name, src.ip_address))
+
+ route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = dst.ip_address,
+ mac_address = dst.mac_address,
+ interface = src)
+ routes.append(route)
+
+ route = IPRoute(node = dst_node,
+ managed = False,
+ owner = self,
+ ip_address = src.ip_address,
+ mac_address = src.mac_address,
+ interface = dst)
+ routes.append(route)
+
+ return routes
+
+#-------------------------------------------------------------------------------
+
+class ICNFaces(Resource):
+ """
+ Resource: ICNFaces
+
+ Centralized ICN face creation.
+ """
+ protocol_name = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound # always create faces
+
+ @inline_task
+ def __create__(self):
+ icn_faces = self._get_faces()
+ for face in icn_faces:
+ face.node.forwarder.faces << face
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_faces(self):
+ """
+ Face creation (heuristic: facemgr)
+
+ Requires: at least direct IP links
+ """
+ protocol = FaceProtocol.from_string(self.protocol_name)
+
+ faces = list()
+ G = _get_l2_graph(self._state.manager)
+ for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True):
+ src_node = self._state.manager.by_uuid(src_node_uuid)
+ dst_node = self._state.manager.by_uuid(dst_node_uuid)
+
+ map_ = data['map_node_interface']
+ src = self._state.manager.by_uuid(map_[src_node_uuid])
+ dst = self._state.manager.by_uuid(map_[dst_node_uuid])
+
+ log.debug('{} -> {} ({} -> {})'.format(src_node_uuid,
+ dst_node_uuid, src.device_name, dst.device_name))
+
+ if protocol == FaceProtocol.ether:
+ src_face = L2Face(node = src_node,
+ owner = self,
+ protocol = protocol,
+ src_nic = src,
+ dst_mac = dst.mac_address)
+ dst_face = L2Face(node = dst_node,
+ owner = self,
+ protocol = protocol,
+ src_nic = dst,
+ dst_mac = src.mac_address)
+
+ elif protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6,
+ FaceProtocol.udp4, FaceProtocol.udp6):
+ src_face = L4Face(node = src_node,
+ owner = self,
+ protocol = protocol,
+ src_ip = src.ip_address,
+ dst_ip = dst.ip_address,
+ src_port = TMP_DEFAULT_PORT,
+ dst_port = TMP_DEFAULT_PORT)
+ dst_face = L4Face(node = dst_node,
+ owner = self,
+ protocol = protocol,
+ src_ip = dst.ip_address,
+ dst_ip = src.ip_address,
+ src_port = TMP_DEFAULT_PORT,
+ dst_port = TMP_DEFAULT_PORT)
+ else:
+ raise NotImplementedError
+
+ # We key the sibling face for easier building of the ICN graph
+ src_face._internal_data['sibling_face'] = dst_face._state.uuid
+ dst_face._internal_data['sibling_face'] = src_face._state.uuid
+
+ faces.append(src_face)
+ faces.append(dst_face)
+
+ return faces
+
+#------------------------------------------------------------------------------
+
+class ICNRoutes(Resource):
+ """
+ Resource: ICNRoutes
+
+ Centralized ICN route computation.
+ """
+
+ routing_strategy = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound # always create routes
+
+ @inline_task
+ def __create__(self):
+ icn_routes = self._get_icn_routes()
+ for route in icn_routes:
+ route.node.forwarder.routes << route
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_prefix_origins(self):
+ origins = dict()
+ for producer in self._state.manager.by_type(Producer):
+ node_uuid = producer.node._state.uuid
+ if not node_uuid in origins:
+ origins[node_uuid] = list()
+ origins[node_uuid].extend(producer.prefixes)
+ return origins
+
+ def _get_icn_routes(self):
+ strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy)
+
+ G = _get_icn_graph(self._state.manager)
+ origins = self._get_prefix_origins()
+
+ routes = list()
+ for src, prefix, dst in strategy(G, origins):
+ data = G.get_edge_data(src, dst)
+
+ map_ = data['map_node_face']
+ next_hop_face = map_[src]
+
+ route = ICNRoute(node = src,
+ owner = self,
+ prefix = prefix,
+ face = next_hop_face)
+ routes.append(route)
+ return routes
+
+#------------------------------------------------------------------------------
+
+class DnsServerEntry(Resource):
+ """
+ Resource: DnsServerEntry
+
+ Setup of DNS resolver for LxcContainers
+
+ Todo:
+ - This should be merged into the LxcContainer resource
+ """
+
+ node = Attribute(String)
+ ip_address = Attribute(String)
+ interface_name = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CONTAINER_SET_DNS,
+ {'ip_dns': self.ip_address,
+ 'interface_name': self.interface_name})
+
+ def __delete__(self):
+ raise NotImplementedError
+
+#------------------------------------------------------------------------------
+
+class ContainerSetup(Resource):
+ """
+ Resource: ContainerSetup
+
+ Setup of container networking
+
+ Todo:
+ - This should be merged into the LxcContainer resource
+ """
+
+ container = Attribute(LxcContainer)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+
+ # a) routes: host -> container
+ # . container interfaces
+ # . container host (main) interface
+ # route add -host {ip_address} dev {bridge_name}
+ route = IPRoute(node = self.container.node,
+ managed = False,
+ owner = self,
+ ip_address = self.container.host_interface.ip_address,
+ interface = self.container.node.bridge)
+ route.node.routing_table.routes << route
+
+ # b) route: container -> host
+ # route add {ip_gateway} dev {interface_name}
+ # route add default gw {ip_gateway} dev {interface_name}
+ route = IPRoute(node = self.container,
+ owner = self,
+ managed = False,
+ ip_address = self.container.node.bridge.ip_address,
+ interface = self.container.host_interface)
+ route.node.routing_table.routes << route
+ route_gw = IPRoute(node = self.container,
+ managed = False,
+ owner = self,
+ ip_address = 'default',
+ interface = self.container.host_interface,
+ gateway = self.container.node.bridge.ip_address)
+ route_gw.node.routing_table.routes << route_gw
+
+ # c) dns
+ dns_server_entry = DnsServerEntry(node = self.container,
+ owner = self,
+ ip_address = self.container.node.bridge.ip_address,
+ interface_name = self.container.host_interface.device_name)
+
+ return dns_server_entry
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ return BashTask(self.container.node, CMD_IP_FORWARD)
+
+#------------------------------------------------------------------------------
+
+class ContainersSetup(Resource):
+ """
+ Resource: ContainersSetup
+
+ Setup of LxcContainers (main resource)
+
+ Todo:
+ - This should be merged into the LxcContainer resource
+ """
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ containers = self._state.manager.by_type(LxcContainer)
+ if len(containers) == 0:
+ return None
+
+ container_resources = [ContainerSetup(owner = self, container = c)
+ for c in containers]
+
+ return Resource.__concurrent__(*container_resources)
+
+#------------------------------------------------------------------------------
+
+class CentralIP(Resource):
+ """
+ Resource: CentralIP
+
+ Central IP management (main resource)
+ """
+
+ ip_routing_strategy = Attribute(String, description = 'IP routing strategy',
+ default = 'pair') # spt, pair
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after_init__(self):
+ return ('Node', 'Channel', 'Interface')
+
+ def __subresources__(self):
+ ip_assign = IPAssignment(owner=self)
+ containers_setup = ContainersSetup(owner=self)
+ ip_routes = IPRoutes(owner = self,
+ routing_strategy = self.ip_routing_strategy)
+
+ return ip_assign > (ip_routes | containers_setup)
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ __delete__ = None
+
+#------------------------------------------------------------------------------
+
+class CentralICN(Resource):
+ """
+ Resource: CentralICN
+
+ Central ICN management (main resource)
+ """
+
+ # Choices: spt, max_flow
+ icn_routing_strategy = Attribute(String,
+ description = 'ICN routing strategy',
+ default = 'spt')
+ face_protocol = Attribute(String,
+ description = 'Protocol used to create faces',
+ default = 'ether')
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ """
+ We need to wait for IP configuration in order to be able to build
+ overload ICN faces, and producers for prefix origins.
+ """
+ return ('CentralIP',)
+
+ def __subresources__(self):
+ icn_faces = ICNFaces(owner = self, protocol_name = self.face_protocol)
+ icn_routes = ICNRoutes(owner = self,
+ routing_strategy = self.icn_routing_strategy)
+ return icn_faces > icn_routes
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ __delete__ = None
diff --git a/vicn/resource/channel.py b/vicn/resource/channel.py
new file mode 100644
index 00000000..cd64b641
--- /dev/null
+++ b/vicn/resource/channel.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.resource import Resource
+from vicn.core.attribute import Attribute
+
+class Channel(Resource):
+ """
+ Resource: Channel
+ """
+
+ #--------------------------------------------------------------------------
+ # Public API
+ #--------------------------------------------------------------------------
+
+ def get_remote_name(self, name):
+ if len(self._interfaces) != 2:
+ return None
+ return next(x for x in self._interfaces if x.get_name() != name)
+
+ def get_sortable_name(self):
+ """
+ This method is used to sort channel during IP assignment. This is
+ necessary to get the same IP configuration on the same experiment.
+ """
+ ret = "{:03}".format(len(self.interfaces))
+ ret = ret + ''.join(sorted(map(lambda x : x.node.name, self.interfaces)))
+ return ret
diff --git a/vicn/resource/dns_server.py b/vicn/resource/dns_server.py
new file mode 100644
index 00000000..3fbe89f9
--- /dev/null
+++ b/vicn/resource/dns_server.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import FactoryResource
+from vicn.resource.application import Application
+
+class DnsServer(Application):
+ """
+ Resource: DnsServer
+ """
+
+ __type__ = FactoryResource
+
+ node = Attribute(
+ reverse_name = 'dns_server',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
diff --git a/vicn/resource/gui.py b/vicn/resource/gui.py
new file mode 100644
index 00000000..3ded7a5a
--- /dev/null
+++ b/vicn/resource/gui.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.helpers.resource_definition import *
+
+class GUI(Resource):
+ """
+ Resource: GUI
+
+ This resource is empty on purpose. It is a temporary resource used as a
+ placeholder for controlling the GUI and should be deprecated in future
+ releases.
+ """
+ pass
diff --git a/vicn/resource/icn/__init__.py b/vicn/resource/icn/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/icn/__init__.py
diff --git a/vicn/resource/icn/ccnx_consumer_producer_test.py b/vicn/resource/icn/ccnx_consumer_producer_test.py
new file mode 100644
index 00000000..f682657d
--- /dev/null
+++ b/vicn/resource/icn/ccnx_consumer_producer_test.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask
+from vicn.resource.icn.icn_application import ICN_SUITE_CCNX_1_0
+from vicn.resource.icn.consumer import Consumer
+from vicn.resource.icn.producer import Producer
+from vicn.resource.node import Node
+
+class CcnxConsumerTest(Consumer):
+ """
+ Resource: CcnxConsumerTest
+
+ Test consumer exchanging dummy data.
+ """
+
+ __package_names__ = ["libconsumer-producer-ccnx"]
+
+ prefixes = Attribute(String,
+ description = "Name served by the producer server test",
+ default = lambda self: self.default_name(),
+ mandatory = False,
+ multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ requirements=[
+ Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0']),
+ properties = {"protocol_suites" : ICN_SUITE_CCNX_1_0})
+ ])
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/ccnxtest']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ template = ["consumer-test", " ccnx:{prefix}"]
+ params = {'prefix' : self.prefixes[0]}
+ return BashTask(self.node, ' '.join(template), parameters = params)
+
+ def __method_stop__(self):
+ raise NotImplementedError
+
+#------------------------------------------------------------------------------
+
+class CcnxProducerTest(Producer):
+ """
+ Resource: CcnxConsumerTest
+
+ Test producer exchanging dummy data.
+ """
+
+ __package_names__ = ["libconsumer-producer-ccnx"]
+
+ node = Attribute(Node,
+ requirements = [Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0']),
+ properties = {"protocol_suites" : ICN_SUITE_CCNX_1_0})])
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/ccnxtest']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ template = ["producer-test", " ccnx:{prefix}"]
+ params = {'prefix' : self.prefixes[0]}
+
+ return BashTask(self.node, ' '.join(template), parameters = params)
+
+ def __method_stop__(self):
+ raise NotImplementedError
+
diff --git a/vicn/resource/icn/ccnx_keystore.py b/vicn/resource/icn/ccnx_keystore.py
new file mode 100644
index 00000000..ddd87019
--- /dev/null
+++ b/vicn/resource/icn/ccnx_keystore.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String, Integer
+from vicn.core.attribute import Attribute, Reference
+from vicn.core.task import BashTask
+from vicn.resource.linux.file import File
+from vicn.resource.linux.package_manager import Packages
+
+METIS_KEYSTORE_CREATE = ('parc-publickey -c {filename} {password} '
+ '{subject_name} {size} {validity}')
+
+# FIXME default passwords, not very sensitive
+DEFAULT_KEYSTORE_FILE = "keystore.pkcs12"
+DEFAULT_KEYSTORE_PASSWD = "password"
+DEFAULT_KEYSTORE_VALIDITY = 365
+DEFAULT_KEYSTORE_SUBJ = "password"
+DEFAULT_KEYSTORE_KEYLENGTH = 2048
+
+class MetisKeystore(File):
+ """
+ Resource: MetisKeystore
+ """
+
+ filename = Attribute(String, description = "File containing the keystore",
+ default = DEFAULT_KEYSTORE_FILE, mandatory=False)
+ password = Attribute(String,
+ description = "Password for the keystore file",
+ default = DEFAULT_KEYSTORE_PASSWD)
+ subject_name = Attribute(String,
+ description = "Subject name for the keystore",
+ default = DEFAULT_KEYSTORE_SUBJ)
+ validity = Attribute(String,
+ description = "Validity period of the keystore",
+ default = DEFAULT_KEYSTORE_VALIDITY)
+ size = Attribute(Integer, description = 'Length of the keys',
+ default = DEFAULT_KEYSTORE_KEYLENGTH)
+
+ __package_names__ = ['libparc']
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ packages = Packages(node=Reference(self, 'node'),
+ names=self._get_package_names(), owner=self)
+ return packages
+
+ def __create__(self):
+ args = {'filename' : self.filename, 'password' : self.password,
+ 'subject_name' : self.subject_name, 'validity' : self.validity,
+ 'size' : self.size}
+ return BashTask(self.node, METIS_KEYSTORE_CREATE, args)
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_package_names(self):
+ package_names = list()
+ for base in self.__class__.mro():
+ if not '__package_names__' in vars(base):
+ continue
+ package_names.extend(getattr(base, '__package_names__'))
+ return package_names
+
+
+ def format_baseline(self, baseline):
+ return baseline.format(keystore_file=self.filename, password=self.password)
+
+
diff --git a/vicn/resource/icn/ccnx_metis.py b/vicn/resource/icn/ccnx_metis.py
new file mode 100644
index 00000000..ead9b9bf
--- /dev/null
+++ b/vicn/resource/icn/ccnx_metis.py
@@ -0,0 +1,368 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from functools import wraps
+
+from netmodel.model.type import String, Integer, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import BashTask, EmptyTask, task
+from vicn.resource.icn.ccnx_keystore import MetisKeystore
+from vicn.resource.icn.face import L2Face, L4Face, FaceProtocol
+from vicn.resource.icn.face import DEFAULT_ETHER_PROTO
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.linux.file import TextFile
+from vicn.resource.linux.service import Service
+
+METIS_CONTROL_BASELINE = (
+ 'metis_control --keystore {keystore_file} --password {password}')
+
+CMD_ADD_LISTENER_ETHER = (
+ 'add listener ether ether{conn_id} {listener.src_nic.device_name} '
+ '{listener.ether_proto}')
+CMD_ADD_LISTENER_L4 = 'add listener {protocol} transport{conn_id} {infos}'
+CMD_ADD_CONNECTION_ETHER = ('add connection ether {face.id} {face.dst_mac} '
+ '{face.src_nic.device_name}')
+CMD_ADD_CONNECTION_L4 = ('add connection {protocol} {face.id} {face.dst_ip} '
+ '{face.dst_port} {face.src_ip} {face.src_port}')
+CMD_ADD_ROUTE = 'add route {route.face.id} ccnx:{route.prefix} {route.cost}'
+METIS_DAEMON_BOOTSTRAP = (
+ 'metis_daemon --port {port} --daemon --log-file {log_file} '
+ '--capacity {cs_size} --config {config}')
+METIS_DAEMON_STOP = "killall -9 metis_daemon"
+
+BASE_CONN_NAME = "conn"
+
+METIS_DEFAULT_PORT = 9596
+
+METIS_ETC_DEFAULT = "/etc/default/metis-forwarder"
+
+#------------------------------------------------------------------------------
+# Listeners
+#------------------------------------------------------------------------------
+
+class MetisListener:
+
+ def __init__(self, protocol):
+ self.protocol = protocol
+
+ @staticmethod
+ def listener_from_face(face):
+ if face.protocol is FaceProtocol.ether:
+ return MetisEtherListener(face.protocol, face.src_nic,
+ face.ether_proto)
+ elif face.protocol in [FaceProtocol.tcp4, FaceProtocol.tcp6,
+ FaceProtocol.udp4, FaceProtocol.udp6]:
+ return MetisL4Listener(face.protocol, face.src_ip, face.src_port)
+ else:
+ raise ValueError("Metis only supports Ethernet and TCP/UDP faces")
+
+class MetisEtherListener(MetisListener):
+
+ def __init__(self, protocol, src_nic, ether_proto=DEFAULT_ETHER_PROTO):
+ super().__init__(protocol)
+ self.src_nic = src_nic
+ self.ether_proto = ether_proto
+
+ def get_setup_command(self, conn_id):
+ return CMD_ADD_LISTENER_ETHER.format(listener = self,
+ conn_id = conn_id)
+
+ def __eq__(self, other):
+ return (isinstance(other, MetisEtherListener)
+ and (other.src_nic == self.src_nic)
+ and (other.ether_proto == self.ether_proto))
+
+ def __ne__(self, other):
+ return ((not isinstance(other, MetisEtherListener))
+ or (other.src_nic != self.src_nic)
+ or (other.ether_proto != self.ether_proto))
+
+class MetisL4Listener(MetisListener):
+
+ def __init__(self, protocol, src_ip, src_port):
+ super().__init__(protocol)
+ self.src_ip = src_ip
+ self.src_port = src_port
+
+ def _get_proto_as_str(self):
+ if self.protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6):
+ return "tcp"
+ elif self.protocol in (FaceProtocol.udp4, FaceProtocol.udp6):
+ return "udp"
+
+ def get_setup_command(self, conn_id):
+ infos = '{} {}'.format(self.src_ip, self.src_port)
+
+ return CMD_ADD_LISTENER_L4.format(protocol = self._get_proto_as_str(),
+ conn_id = conn_id, infos = infos)
+
+ def __eq__(self, other):
+ return (isinstance(other, MetisL4Listener) and
+ self.protocol == other.protocol and
+ self.src_ip == other.src_ip and
+ self.src_port == other.src_port)
+
+#------------------------------------------------------------------------------
+
+class MetisForwarder(Forwarder, Service):
+ __capabilities__ = set(['ICN_SUITE_CCNX_1_0'])
+ __package_names__ = ['metis-forwarder']
+ __service_name__ = "metis-forwarder"
+
+ log_file = Attribute(String, description = 'File for metis logging',
+ default = '/tmp/ccnx-metis.log') # '/dev/null')
+ port = Attribute(Integer, description = 'TCP port for metis',
+ default = 9695)
+ gen_config = Attribute(Bool,
+ description = 'Set to record all metis commands in a config file',
+ default = True)
+ config_file = Attribute(String, default = '/root/.ccnx_metis.conf')
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._nb_conn = 0
+ self._listeners = []
+ self._listeners_idx = 0
+ self.keystore = None
+
+ # Cache
+ self._faces = set()
+ self._routes = set()
+
+ # Internal subresources
+ self._config = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ('CentralICN',)
+
+ def __subresources__(self):
+ self.keystore = MetisKeystore(node = self.node, owner = self)
+ self.env_file = self._write_environment_file()
+ return self.keystore | self.env_file
+
+ @task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+
+ # Alternatively, we might put all commands in a configuration file
+ # before starting the forwarder. In that case, we need to restart it if
+ # it is already started.
+ # XXX Need to schedule subresource before and after some other tasks
+
+ _, faces = self._cmd_create_faces()
+ _, routes = self._cmd_create_routes()
+
+ cfg = list()
+ cfg.append('add listener tcp local0 127.0.0.1 9695')
+ cfg.extend(faces)
+ cfg.extend(routes)
+
+ self._config = TextFile(filename = self.config_file,
+ node = self.node,
+ owner = self,
+ content = '\n'.join(cfg),
+ overwrite = True)
+
+ self._state.manager.commit_resource(self._config)
+
+ start_or_restart = self.__method_restart__()
+
+ return wait_resource_task(self._config) > start_or_restart
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ # Force local management of faces and routes
+
+ _add_faces = None
+ _remove_faces = None
+ _get_faces = None
+ _set_faces = None
+
+ _add_routes = None
+ _remove_routes = None
+ _get_routes = None
+ _set_routes = None
+
+ #--------------------------------------------------------------------------
+ # Method helpers
+ #--------------------------------------------------------------------------
+
+ def _start_as_daemon(self):
+ """
+ Start the metis forwarder as normal daemon
+ """
+
+ command = METIS_DAEMON_BOOTSTRAP
+ args = {'port' : self.port, 'log_file' : self.log_file,
+ 'cs_size' : self.cache_size, 'config' : self.config_file}
+ return BashTask(self.node, command, parameters = args)
+
+ def _restart_as_daemon(self):
+ """
+ Restart the metis forwarder as normal daemon
+ """
+
+ command = METIS_DAEMON_STOP + '; ' + METIS_DAEMON_BOOTSTRAP
+ args = {'port' : self.port, 'log_file' : self.log_file,
+ 'cs_size' : self.cache_size, 'config' : self.config_file}
+ return BashTask(self.node, command, parameters = args)
+
+ def _start_as_service(self):
+ """
+ Start the metis forwarder as service managed by systemd
+ """
+ return super().__method_start__()
+
+ def _restart_as_service(self):
+ """
+ Restart the metis forwarder as service managed by systemd
+ """
+ return super().__method_restart__()
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ return self._start_as_service()
+
+ def __method_restart__(self):
+ return self._restart_as_service()
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _cmd_create_faces(self):
+ """Returns the list of commands used to update faces (delete and
+ create).
+
+ We need two lists because some delete might need to occur before create.
+ This function is used to populate the config file and also alter the
+ configuration of the forwarder live. It might be possible to further
+ optimize but keeping the two separate seems important, since delete is
+ only used for an already running metis.
+ """
+ create_cmds = list()
+ delete_cmds = list()
+
+ for face in self.faces:
+ listener = MetisListener.listener_from_face(face)
+ if listener not in self._listeners:
+ self._listeners.append(listener)
+ conn_id = self._listeners_idx
+ self._listeners_idx += 1
+ cmd = listener.get_setup_command(conn_id)
+ create_cmds.append(cmd)
+
+ face.id = 'conn{}'.format(self._nb_conn)
+ self._nb_conn += 1
+
+ if face.protocol is FaceProtocol.ether:
+ assert isinstance(face, L2Face), \
+ 'Ethernet face should be instance of L2Face'
+ cmd = CMD_ADD_CONNECTION_ETHER.format(face = face)
+
+ elif face.protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6):
+ assert isinstance(face, L4Face), \
+ "TCP/UDP face should be instance of L4Face"
+ cmd = CMD_ADD_CONNECTION_L4.format(face = face,
+ protocol = 'tcp')
+
+ elif face.protocol in (FaceProtocol.udp4, FaceProtocol.udp6):
+ assert isinstance(face, L4Face), \
+ 'TCP/UDP face should be instance of L4Face'
+ cmd = CMD_ADD_CONNECTION_L4.format(face = face,
+ protocol = 'udp')
+
+ else:
+ raise ValueError('Unsupported face type for Metis')
+
+ create_cmds.append(cmd)
+
+ return (delete_cmds, create_cmds)
+
+ def _cmd_create_routes(self):
+ create_cmds = list()
+ delete_cmds = list()
+ for route in self.routes:
+ cmd = CMD_ADD_ROUTE.format(route = route)
+ create_cmds.append(cmd)
+ return (delete_cmds, create_cmds)
+
+ def _task_create_faces(self):
+ delete_cmds, create_cmds = self._cmd_create_faces()
+
+ delete_task = EmptyTask()
+ if len(delete_cmds) > 0:
+ cmds = '\n'.join('{} {}'.format(self._baseline, command)
+ for command in delete_cmds)
+ delete_task = BashTask(self.node, cmds)
+
+ create_task = EmptyTask()
+ if len(create_cmds) > 0:
+ cmds = '\n'.join('{} {}'.format(self._baseline, command)
+ for command in create_cmds)
+ create_task = BashTask(self.node, cmds)
+
+ return delete_task > create_task
+
+ def _task_create_routes(self):
+ delete_cmds, create_cmds = self._cmd_create_routes()
+
+ delete_task = EmptyTask()
+ if len(delete_cmds) > 0:
+ delete_task = BashTask(self.node, "\n".join(delete_cmds))
+
+ create_task = EmptyTask()
+ if len(create_cmds) > 0:
+ create_task = BashTask(self.node, '\n'.join(create_cmds))
+
+ return delete_task > create_task
+
+ def _write_environment_file(self):
+ param_port = "PORT={port}"
+ param_log_file = "LOG_FILE={log_file}"
+ param_cs_capacity = "CS_SIZE={cs_size}"
+ param_config = "CONFIG={config}"
+
+ env = [param_port.format(port = self.port),
+ param_log_file.format(log_file = self.log_file),
+ param_cs_capacity.format(cs_size = self.cache_size),
+ param_config.format(config = self.config_file)]
+
+ environment_file = TextFile(filename = METIS_ETC_DEFAULT,
+ node = self.node,
+ owner = self,
+ overwrite = True,
+ content = '\n'.join(env))
+ return environment_file
diff --git a/vicn/resource/icn/ccnx_simpleTrafficGenerator.py b/vicn/resource/icn/ccnx_simpleTrafficGenerator.py
new file mode 100644
index 00000000..221298fc
--- /dev/null
+++ b/vicn/resource/icn/ccnx_simpleTrafficGenerator.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import Resource, EmptyResource
+from vicn.core.task import EmptyTask
+from vicn.resource.icn.icn_application import ICN_SUITE_CCNX_1_0
+from vicn.resource.node import Node
+
+from vicn.resource.icn.ccnx_consumer_producer_test import CcnxConsumerTest
+from vicn.resource.icn.ccnx_consumer_producer_test import CcnxProducerTest
+
+class CcnxSimpleTrafficGenerator(Resource):
+
+ prefix = Attribute(String,
+ description = "Routable prefix for the applications",
+ default = lambda self: self.default_name(),
+ mandatory = False)
+ consumers = Attribute(Node,
+ multiplicity = Multiplicity.OneToMany)
+ producers = Attribute(Node,
+ multiplicity = Multiplicity.OneToMany)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._sr = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ """
+ Create the list of consumers and producers.
+ For each of them, assign a different namespace under the same prefix.
+ """
+
+ sr = EmptyResource()
+ for producer in self.producers:
+ producer = CcnxProducerTest(node = producer,
+ owner = self,
+ prefixes = [self.prefix])
+ sr = sr | producer
+ for consumer in self.consumers:
+ full_prefix = self.prefix
+ consumer = CcnxConsumerTest(node = consumer,
+ owner = self,
+ prefixes = [full_prefix])
+ sr = sr | consumer
+ self._sr = sr
+ return sr
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ if self._sr is None:
+ return
+
+ tasks = EmptyTask()
+ for sr in self._sr:
+ sr_task = sr.__method_start__()
+ tasks = tasks | sr_task
+ return tasks
+
+ def __method_stop__(self):
+ if self._sr is None:
+ return
+
+ tasks = EmptyTask()
+ for sr in self._sr:
+ sr_task = sr.__method_stop__()
+ tasks = tasks | sr_task
+ return tasks
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/ccnxtest']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
diff --git a/vicn/resource/icn/consumer.py b/vicn/resource/icn/consumer.py
new file mode 100644
index 00000000..8c4c5e76
--- /dev/null
+++ b/vicn/resource/icn/consumer.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.icn.icn_application import ICNApplication
+
+class Consumer(ICNApplication):
+ """
+ Resource: Consumer
+ """
+ pass
diff --git a/vicn/resource/icn/face.py b/vicn/resource/icn/face.py
new file mode 100644
index 00000000..db72730d
--- /dev/null
+++ b/vicn/resource/icn/face.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.requirement import Requirement
+from vicn.core.resource import Resource
+from vicn.resource.node import Node
+from vicn.resource.interface import Interface
+
+DEFAULT_ETHER_PROTO = 0x0801
+FMT_L4FACE = '{protocol.name}://{dst_ip}:{dst_port}/'
+FMT_L2FACE = '{protocol.name}://[{dst_mac}]/{src_nic.device_name}'
+
+class FaceProtocol(Enum):
+ ether = 0
+ ip4 = 1
+ ip6 = 2
+ tcp4 = 3
+ tcp6 = 4
+ udp4 = 5
+ udp6 = 7
+ app = 8
+
+ @staticmethod
+ def from_string(protocol):
+ return getattr(FaceProtocol, protocol)
+
+#------------------------------------------------------------------------------
+
+class Face(Resource):
+ """
+ Resource: Face
+ """
+
+ node = Attribute(Node, mandatory = True,
+ requirements = [
+ Requirement('forwarder')
+ ])
+ protocol = Attribute(String,
+ description = 'Face underlying protocol',
+ mandatory = True)
+ id = Attribute(String, description = 'Local face ID',
+ ro = True)
+
+ # Cisco's extensions
+ wldr = Attribute(Bool, description = 'flag: WLDR enabled',
+ default = False)
+ x2 = Attribute(Bool, description = 'flag: X2 face',
+ default = False)
+
+ # NFD extensions
+ permanent = Attribute(Bool, description = 'flag: permanent face',
+ default = True)
+ nfd_uri = Attribute(String, description = 'Face uri',
+ func = lambda self : self._lambda_nfd_uri())
+ nfdc_flags = Attribute(String,
+ description = 'Flags for face creation with NFDC',
+ func = lambda self : self._lambda_nfdc_flags())
+
+ def __repr__(self):
+ flags = ''
+ if self.permanent:
+ flags += 'permanent '
+ if self.wldr:
+ flags += 'wldr '
+ if self.x2:
+ flags += 'x2 '
+ sibling_face_name = self.data.get('sibling_face', None)
+ sibling_face = self._state.manager.by_name(sibling_face_name) \
+ if sibling_face_name else None
+ dst_node = sibling_face.node.name if sibling_face else None
+ return '<Face {} {} on node {} -- to node {}>'.format(
+ self.nfd_uri, flags, self.node.name, dst_node)
+
+ __str__ = __repr__
+
+ # NFD specifics
+
+ def _lambda_nfd_uri(self):
+ raise NotImplementedError
+
+ def _lambda_nfdc_flags(self):
+ flags = ''
+ if self.permanent:
+ flags += '-P '
+ if self.wldr:
+ flags += '-W '
+ if self.x2:
+ flags += '-X '
+ return flags
+
+#------------------------------------------------------------------------------
+
+class L2Face(Face):
+
+ src_nic = Attribute(Interface,
+ description = "Name of the network interface linked to the face",
+ mandatory=True)
+ dst_mac = Attribute(String, description = "destination MAC address",
+ mandatory=True)
+ ether_proto = Attribute(String,
+ description = "Ethernet protocol number used by the face",
+ default=DEFAULT_ETHER_PROTO)
+
+ def _lambda_nfd_uri(self):
+ return self.format(FMT_L2FACE)
+
+#------------------------------------------------------------------------------
+
+class L4Face(Face):
+
+ ip_version = Attribute(Integer, description = "IPv4 or IPv6", default = 4)
+ src_ip = Attribute(String, description = "local IP address",
+ mandatory = True)
+ src_port = Attribute(Integer, description = "local TCP/UDP port")
+ dst_ip = Attribute(String, descrition = "remote IP address",
+ mandatory=True)
+ dst_port = Attribute(Integer, description = "remote TCP/UDP port",
+ mandatory=True)
+
+ def _lambda_nfd_uri(self):
+ return self.format(FMT_L4FACE)
diff --git a/vicn/resource/icn/forwarder.py b/vicn/resource/icn/forwarder.py
new file mode 100644
index 00000000..a719caf7
--- /dev/null
+++ b/vicn/resource/icn/forwarder.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from abc import ABC
+from enum import Enum
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import FactoryResource
+from vicn.resource.icn.icn_application import ICNApplication
+from vicn.resource.icn.face import Face
+from vicn.resource.icn.route import Route
+
+DEFAULT_CACHE_SIZE = 1000 # pk
+DEFAULT_CACHE_POLICY = 'LRU'
+DEFAULT_STRATEGY = 'best-route'
+
+class Forwarder(ICNApplication, ABC):
+ """
+ Resource: Forwarder
+ """
+
+ __type__ = FactoryResource
+
+ faces = Attribute(Face, description = 'ICN ffaces of the forwarder',
+ multiplicity = Multiplicity.OneToMany,
+ reverse_name = 'forwarder')
+ routes = Attribute(Route, description = 'Routes in the ICN FIB',
+ multiplicity = Multiplicity.OneToMany,
+ reverse_name = 'forwarder')
+ cache_size = Attribute(Integer,
+ description = 'Size of the cache (in chunks)',
+ default = DEFAULT_CACHE_SIZE)
+ cache_policy = Attribute(String, description = 'Cache policy',
+ default = DEFAULT_CACHE_POLICY)
+ strategy = Attribute(String, description = 'Forwarding Strategy',
+ default = DEFAULT_STRATEGY)
+ config_file = Attribute(String, description = 'Configuration file')
+ port = Attribute(Integer, description = 'Default listening port',
+ default = lambda self: self._get_default_port())
+ log_file = Attribute(String, description = 'Log file')
+
+ # Overloaded attributes
+
+ node = Attribute(
+ reverse_name = 'forwarder',
+ reverse_description = 'ICN forwarder attached to the node',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
diff --git a/vicn/resource/icn/icn_application.py b/vicn/resource/icn/icn_application.py
new file mode 100644
index 00000000..5abee3c5
--- /dev/null
+++ b/vicn/resource/icn/icn_application.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.application import LinuxApplication
+from vicn.core.attribute import Attribute
+from netmodel.model.type import Integer
+
+ICN_SUITE_CCNX_1_0=0
+ICN_SUITE_NDN=1
+
+class ICNApplication(LinuxApplication):
+ """
+ Resource: ICNApplication
+ """
+
+ protocol_suites = Attribute(Integer,
+ description = 'Protocol suites supported by the application',
+ default = lambda self: self._def_protocol_suite())
+
+ def _def_protocol_suite(self):
+ return -1
+
diff --git a/vicn/resource/icn/icn_tools.py b/vicn/resource/icn/icn_tools.py
new file mode 100644
index 00000000..54823719
--- /dev/null
+++ b/vicn/resource/icn/icn_tools.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.icn.icn_application import ICNApplication
+
+class ICNTools(ICNApplication):
+ """
+ Resource: ICNTools
+ """
+
+ __package_names__ = ['libconsumer-producer-ccnx']
diff --git a/vicn/resource/icn/iping.py b/vicn/resource/icn/iping.py
new file mode 100644
index 00000000..0e04eadc
--- /dev/null
+++ b/vicn/resource/icn/iping.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask
+from vicn.resource.icn.icn_application import ICNApplication
+from vicn.resource.icn.icn_application import ICN_SUITE_CCNX_1_0
+from vicn.resource.icn.producer import Producer
+from vicn.resource.icn.consumer import Consumer
+from vicn.resource.node import Node
+
+DEFAULT_PING_PAYLOAD_SIZE = 64
+DEFAULT_PING_COUNT = 100
+
+class IPing(ICNApplication):
+ """
+ Resource: IPingClient
+ """
+
+ __package_names__ = ["libicnet"]
+
+ prefixes = Attribute(String,
+ description = "name served by the ping server",
+ default = lambda self: self.default_name(),
+ mandatory = False,
+ multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ requirements=[
+ Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0']),
+ properties = {"protocol_suites" : ICN_SUITE_CCNX_1_0})
+ ])
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ return self._build_command()
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/iping']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
+#------------------------------------------------------------------------------
+
+class IPingClient(IPing, Producer):
+ """
+ Resource: IPingClient
+ """
+
+ flood = Attribute(Bool, description = 'enable flood mode',
+ default = False)
+ count = Attribute(Integer, description = 'number of ping to send')
+ interval = Attribute(Integer,
+ description = 'interval between interests in ping mode')
+ size = Attribute(Integer, description = 'size of the interests')
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _build_command(self):
+ template = ["iPing_Client", "-l ccnx:{prefix}"]
+ params={'prefix' : self.prefixes[0]}
+
+ if self.flood:
+ template.append("-f")
+ else:
+ template.append("-p") #Ping mode
+
+ if self.count:
+ template.append("-c {count}")
+ params["count"] = self.count
+ if self.size:
+ template.append("-s {size}")
+ params['size'] = self.size
+ if self.interval:
+ template.append("-i {interval}")
+ params['interval'] = self.interval
+
+ return BashTask(self.node, ' '.join(template), parameters=params)
+
+#------------------------------------------------------------------------------
+
+class IPingServer(IPing, Consumer):
+
+ size = Attribute(Integer, description = "size of the payload")
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _build_command(self):
+ template = ["iPing_Server", "-l ccnx:{prefix}"]
+ params={'prefix' : self.prefixes[0]}
+
+ if self.size:
+ template.append("-s {size}")
+ params['size'] = self.size
+
+ return BashTask(self.node, ' '.join(template), parameters=params)
diff --git a/vicn/resource/icn/ndnpingserver.py b/vicn/resource/icn/ndnpingserver.py
new file mode 100644
index 00000000..da13f59b
--- /dev/null
+++ b/vicn/resource/icn/ndnpingserver.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask
+from vicn.resource.icn.producer import Producer
+from vicn.resource.linux.service import Service
+
+TPL_DEFAULT_PREFIX='/ndn/{node.name}'
+
+FN_ETC_DEFAULT='/etc/default/ndnping'
+
+TPL_ETC_DEFAULT='''
+# defaults for ndnping server
+
+# Prefix should be set to a valid value
+PREFIX="/ndn/server"
+
+FLAGS=""
+'''
+
+CMD_START = 'ndnpingserver {prefix} &'
+
+class NDNPingServerBase(Producer):
+ """NDNPingServer Resource
+
+ This NDNPingServer resource wraps a NDN ping server
+
+ Attributes:
+ prefixes (List[str]) : (overloaded) One-element list containing the
+ prefix on which the ping server is listening.
+
+ TODO:
+ - ndnpingserver only supports a single prefix.
+ """
+ prefixes = Attribute(String,
+ default = lambda self: self._default_prefixes())
+
+ node = Attribute(requirements = [
+ Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0'])) ])
+
+ __package_names__ = ['ndnping']
+
+ def _default_prefixes(self):
+ return [self.format(TPL_DEFAULT_PREFIX)]
+
+#------------------------------------------------------------------------------
+
+class NDNPingServer(NDNPingServerBase):
+
+ def __method_start__(self):
+ return BashTask(self.node, CMD_START)
+
+#------------------------------------------------------------------------------
+
+class NDNPingService(NDNPingServerBase, Service):
+ __package_names__ = ['ndnping']
+ __service_name__ = 'ndnping'
diff --git a/vicn/resource/icn/nfd.py b/vicn/resource/icn/nfd.py
new file mode 100644
index 00000000..c65fdeb8
--- /dev/null
+++ b/vicn/resource/icn/nfd.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import re
+
+from vicn.core.exception import ResourceNotFound
+from vicn.core.task import inline_task, BashTask
+from vicn.core.task import ParseRegexTask
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.icn.icn_application import ICN_SUITE_NDN
+
+log = logging.getLogger(__name__)
+
+NFD_CONF_FILE = "/etc/ndn/nfd.conf"
+
+CMD_SET_STRATEGY_CACHE = '\n'.join([
+ 'sed -i "s/^.*cs_max_packets .*$/ cs_max_packets {nfd.cache_size}/" ' \
+ '{conf_file}',
+ 'sed -i "0,/\/ / s/\/localhost\/nfd\/strategy\/.*/' \
+ '\/localhost\/nfd\/strategy\/{nfd.fw_strategy}/" {conf_file}',
+ 'service nfd restart'])
+CMD_RESET_CACHE = '''
+sed -i "s/^.*cs_max_packets .*$/ cs_max_packets 65536/" {conf_file}
+service nfd restart
+'''
+
+CMD_ADD_ROUTE = 'nfdc register {route.prefix} {route.face.nfd_uri}'
+# or: nfdc register {route.prefix} {route.face.id}
+
+CMD_REMOVE_ROUTE = 'nfdc unregister {route.prefix} {route.face.nfd_uri}'
+# or: nfdc unregister {route.prefix} {route.face.id}
+
+CMD_ADD_FACE = 'nfdc create {face.nfdc_flags} {face.nfd_uri}'
+
+CMD_REMOVE_FACE = 'nfdc destroy {face.id}'
+# or: nfdc destroy {face.nfd_uri}
+
+# FIXME redundant with Forwarder.FaceType
+layer_2_protocols = ["udp", "udp4", "tcp", "tcp4", "ether"]
+
+NFD_DEFAULT_PORT = 6363
+
+# Regular expressions used for parsing nfdc results
+STR_ADD_FACE = ('Face creation succeeded: ControlParameters\(FaceId: '
+ '(?P<id>.*?), Uri: (?P<face_uri>.*?), \)')
+RX_ADD_FACE = re.compile(STR_ADD_FACE)
+
+class NFD(Forwarder):
+ """
+ Resource: NFD
+ """
+
+ __capabilities__ = set(['ICN_SUITE_NDN'])
+ __service_name__ = 'nfd'
+ __package_names__ = ['nfd']
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ # NFD is assumed not to exist
+ raise ResourceNotFound
+
+ def __create__(self):
+ # Modify the configuration file before running the forwarder service
+ conf = BashTask(self.node, CMD_SET_STRATEGY_CACHE, {'nfd': self})
+ forwarder = Forwarder.__create__(self)
+ return conf.then(forwarder)
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def _get_routes(self):
+ return {'routes': list()}
+
+ @inline_task
+ def _add_routes(self, route):
+ return BashTask(self.node, CMD_ADD_ROUTE, {'route': route})
+
+ @inline_task
+ def _remove_routes(self, route):
+ return BashTask(self.node, CMD_REMOVE_ROUTE, {'route': route})
+
+ @inline_task
+ def _get_faces(self):
+ return {'faces': list()}
+
+ @inline_task
+ def _add_faces(self, face):
+ add_face = BashTask(self.node, CMD_ADD_FACE, {'face': face})
+ set_face_id = ParseRegexTask(RX_ADD_FACE)
+ return add_face.compose(set_face_id)
+
+ @inline_task
+ def _remove_faces(self, face):
+ return BashTask(self.node, CMD_REMOVE_FACE, {'face': face})
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_reset_cache__(self, conf_file):
+ return BashTask(self.node, CMD_RESET_CACHE, {'conf_file': conf_file})
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_default_port(self):
+ return NFD_DEFAULT_PORT
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_NDN
diff --git a/vicn/resource/icn/producer.py b/vicn/resource/icn/producer.py
new file mode 100644
index 00000000..23434ebd
--- /dev/null
+++ b/vicn/resource/icn/producer.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.resource.icn.icn_application import ICNApplication
+from vicn.core.attribute import Attribute, Multiplicity
+
+class Producer(ICNApplication):
+ """
+ Resource: Producer
+ """
+
+ prefixes = Attribute(String, description = 'List of served prefixes',
+ multiplicity = Multiplicity.OneToMany)
diff --git a/vicn/resource/icn/repo-ng.py b/vicn/resource/icn/repo-ng.py
new file mode 100644
index 00000000..7b654a6a
--- /dev/null
+++ b/vicn/resource/icn/repo-ng.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.service import Service
+
+class RepoNG(Service):
+ """
+ Resource: RepoNG
+ """
+ __service_name__ = 'repo-ng'
diff --git a/vicn/resource/icn/route.py b/vicn/resource/icn/route.py
new file mode 100644
index 00000000..0dc2ed2f
--- /dev/null
+++ b/vicn/resource/icn/route.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute
+from vicn.core.resource import Resource
+from vicn.resource.icn.face import Face
+from vicn.resource.node import Node
+
+class Route(Resource):
+ node = Attribute(Node, mandatory = True)
+ prefix = Attribute(String, mandatory = True)
+ face = Attribute(Face, description = "face used to forward interests",
+ mandatory=True)
+ cost = Attribute(Integer, default=1)
+
+ def __repr__(self):
+ return '<Route {} {} on node {}>'.format(self.prefix, self.face,
+ self.node.name)
+
+ __str__ = __repr__
diff --git a/vicn/resource/icn/virtual-repo.py b/vicn/resource/icn/virtual-repo.py
new file mode 100644
index 00000000..8cb306d9
--- /dev/null
+++ b/vicn/resource/icn/virtual-repo.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String, Integer
+from vicn.core.attribute import Attribute
+from vicn.resource.icn.producer import Producer
+
+DEFAULT_CHUNK_SIZE = 1300
+
+class VirtualRepo(Producer):
+ """
+ Resource: VirtualRepo
+
+ Note:
+ ndn-virtual-repo {self.folder} -s {self.chunk_size}
+ """
+
+ __package_names__ = ['ndn-virtual-repo']
+
+ folder = Attribute(String, description = "Folder")
+ chunk_size = Attribute(Integer, description = "Chunk size",
+ default = DEFAULT_CHUNK_SIZE)
diff --git a/vicn/resource/icn/webserver.py b/vicn/resource/icn/webserver.py
new file mode 100644
index 00000000..8b8e2ef3
--- /dev/null
+++ b/vicn/resource/icn/webserver.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.icn.producer import Producer
+
+class WebServer(Producer):
+ """
+ Resource: WebServer
+
+ CCNX Webserver
+ """
+
+ __package_names__ = ['webserver-ccnx']
+ __service_name__ = 'webserver-ccnx'
diff --git a/vicn/resource/interface.py b/vicn/resource/interface.py
new file mode 100644
index 00000000..db5f5427
--- /dev/null
+++ b/vicn/resource/interface.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from netmodel.model.type import Bool
+from vicn.core.resource import Resource
+from vicn.resource.node import Node
+from vicn.resource.channel import Channel
+
+class Interface(Resource):
+ """
+ Resource: Interface
+ """
+
+ node = Attribute(Node, description = 'Node to which the interface belongs',
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'interfaces',
+ mandatory = True)
+ channel = Attribute(Channel, description = 'Channel to which the interface is attached',
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'interfaces')
+ promiscuous = Attribute(Bool, description = 'Promiscuous mode',
+ default = False)
+ up = Attribute(Bool, description = 'Interface up/down status',
+ default = True)
+ monitored = Attribute(Bool, default = True)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Flag to check whether vpp uses that interface
+ self.has_vpp_child = False
diff --git a/vicn/resource/ip/__init__.py b/vicn/resource/ip/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/ip/__init__.py
diff --git a/vicn/resource/ip/route.py b/vicn/resource/ip/route.py
new file mode 100644
index 00000000..f073e426
--- /dev/null
+++ b/vicn/resource/ip/route.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.resource.node import Node
+from vicn.core.attribute import Attribute
+from vicn.core.resource import Resource
+from vicn.resource.interface import Interface
+
+class IPRoute(Resource):
+ node = Attribute(Node, mandatory = True)
+ ip_address = Attribute(String, mandatory = True)
+ interface = Attribute(Interface, mandatory = True)
+ gateway = Attribute(String)
+
+ # FIXME Temp hack for VPP, migrate this to an ARP table resource
+ mac_address = Attribute(String)
diff --git a/vicn/resource/ip/routing_table.py b/vicn/resource/ip/routing_table.py
new file mode 100644
index 00000000..52b81794
--- /dev/null
+++ b/vicn/resource/ip/routing_table.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import EmptyTask, BashTask
+from vicn.resource.ip.route import IPRoute
+from vicn.resource.node import Node
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ROUTE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ROUTE_GW
+
+CMD_ADD_ROUTE = ('ip route add {route.ip_address} '
+ 'dev {route.interface.device_name} || true')
+CMD_ADD_ROUTE_GW = ('ip route add {route.ip_address} '
+ 'dev {route.interface.device_name} via {route.gateway} || true')
+CMD_DEL_ROUTE = ('ip route del {route.ip_address} '
+ 'dev {route.interface.device_name}')
+CMD_SHOW_ROUTES = 'ip route show'
+
+CMD_ADD_ARP_ENTRY = 'arp -s {route.ip_address} {route.mac_address}'
+
+# Populate arp table too. The current configuration with one single bridge
+# connecting every container and vpp nodes seem to create loops that prevent
+# vpp from netmodel.network.interface for routing ip packets.
+
+VPP_ARP_FIX = True
+
+def _iter_routes(out):
+ for line in out.splitlines():
+ toks = line.strip().split()
+ route = {'ip_address': toks[0]}
+ for pos in range(1, len(toks)):
+ if toks[pos] == '':
+ pos+=1
+ elif toks[pos] == 'dev':
+ route['interface_name'] = toks[pos+1]
+ pos+=2
+ elif toks[pos] in ['src', 'proto', 'scope', 'metric']:
+ pos+=2
+ elif toks[pos] == 'via':
+ route['gateway'] = toks[pos+1]
+ pos+=2
+ elif toks[pos] in ['linkdown', 'onlink']:
+ pos+=1
+ yield route
+
+#------------------------------------------------------------------------------
+
+class RoutingTable(Resource):
+ """
+ Resource: RoutingTable
+
+ IP Routing Table management
+
+ """
+
+ node = Attribute(Node,
+ mandatory = True,
+ reverse_name = 'routing_table',
+ reverse_description = 'Routing table of the node',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
+
+ routes = Attribute(IPRoute,
+ multiplicity = Multiplicity.OneToMany)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self._routes = dict()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ('CentralIP', 'VPPInterface')
+
+ def __get__(self):
+ def cache(rv):
+ for route in _iter_routes(rv.stdout):
+ self._routes[route['ip_address']] = route
+
+ # Force routing update
+ raise ResourceNotFound
+ return BashTask(self.node, CMD_SHOW_ROUTES, parse=cache)
+
+ def __create__(self):
+ """
+ Create a single BashTask for all routes
+ """
+
+ done = set()
+ routes_cmd = list()
+ routes_via_cmd = list()
+ arp_cmd = list()
+
+ # vppctl lock
+ # NOTE: we currently lock vppctl during the whole route update
+ routes_lock = None
+ routes_via_lock = None
+
+ for route in self.routes:
+ if route.ip_address in self._routes:
+ continue
+ if route.ip_address in done:
+ continue
+ done.add(route.ip_address)
+
+ # TODO VPP should provide its own implementation of routing table
+ # on the node
+ if not route.interface.has_vpp_child:
+ if route.gateway is None:
+ cmd = CMD_ADD_ROUTE.format(route = route)
+ routes_cmd.append(cmd)
+ else:
+ cmd = CMD_ADD_ROUTE_GW.format(route = route)
+ routes_via_cmd.append(cmd)
+ if VPP_ARP_FIX and route.mac_address:
+ if route.ip_address != "default":
+ cmd = CMD_ADD_ARP_ENTRY.format(route = route)
+ arp_cmd.append(cmd)
+ else:
+ if route.gateway is None:
+ cmd = CMD_VPP_ADD_ROUTE.format(route = route)
+ routes_cmd.append(cmd)
+ routes_lock = route.node.vpp.vppctl_lock
+ else:
+ cmd = CMD_VPP_ADD_ROUTE_GW.format(route = route)
+ routes_via_cmd.append(cmd)
+ routes_via_lock = route.node.vpp.vppctl_lock
+
+ # TODO: checks
+ clean_routes_task = EmptyTask()
+
+ if len(routes_cmd) > 0:
+ routes_task = BashTask(self.node, '\n'.join(routes_cmd),
+ lock = routes_lock)
+ else:
+ routes_task = EmptyTask()
+
+ if len(routes_via_cmd) > 0:
+ routes_via_task = BashTask(self.node, '\n'.join(routes_via_cmd),
+ lock = routes_via_lock)
+ else:
+ routes_via_task = EmptyTask()
+
+ if len(arp_cmd) > 0:
+ arp_task = BashTask(self.node, '\n'.join(arp_cmd))
+ else:
+ arp_task = EmptyTask()
+
+ return ((clean_routes_task > routes_task) > routes_via_task) > arp_task
+
+ def __delete__(self):
+ raise NotImplementedError
diff --git a/vicn/resource/linux/__init__.py b/vicn/resource/linux/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/linux/__init__.py
diff --git a/vicn/resource/linux/application.py b/vicn/resource/linux/application.py
new file mode 100644
index 00000000..d2b5139e
--- /dev/null
+++ b/vicn/resource/linux/application.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Reference
+from vicn.core.resource import Resource, EmptyResource
+from vicn.resource.application import Application
+from vicn.resource.linux.package_manager import Packages
+
+
+class LinuxApplication(Application):
+ """
+ Resource: Linux Application
+
+ This resource ensures that the application is present on the system, and
+ installs it during setup if necessary.
+ """
+
+ def __subresources__(self):
+ package_names = self._get_package_names()
+ if package_names:
+ packages = Packages(node=Reference(self, 'node'),
+ names=package_names,
+ owner=self)
+ else:
+ packages = EmptyResource()
+
+ process = None
+
+ return packages > process
+
+ #--------------------------------------------------------------------------
+ # Private methods
+ #--------------------------------------------------------------------------
+
+ def _get_package_names(self):
+ package_names = list()
+ for base in self.__class__.mro():
+ if not '__package_names__' in vars(base):
+ continue
+ package_names.extend(getattr(base, '__package_names__'))
+ return package_names
diff --git a/vicn/resource/linux/bridge.py b/vicn/resource/linux/bridge.py
new file mode 100644
index 00000000..882f0226
--- /dev/null
+++ b/vicn/resource/linux/bridge.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.task import inline_task
+from vicn.resource.channel import Channel
+from vicn.resource.linux.bridge_mgr import BridgeManager
+from vicn.resource.linux.net_device import BaseNetDevice
+
+log = logging.getLogger(__name__)
+
+# FIXME This should use the AddressManager to get allocated a name that does
+# not exist
+DEFAULT_BRIDGE_NAME = 'br0'
+
+class Bridge(Channel, BaseNetDevice):
+ """
+ Resource: Bridge
+ """
+ node = Attribute(
+ reverse_name = 'bridge',
+ reverse_description = 'Main bridge',
+ reverse_auto = 'true',
+ multiplicity = Multiplicity.OneToOne,
+ requirements = [
+ Requirement('bridge_manager')
+ ])
+ device_name = Attribute(
+ default = DEFAULT_BRIDGE_NAME,
+ mandatory = False)
+
+ #--------------------------------------------------------------------------
+ # Constructor / Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'br'
+ self.netdevice_type = 'bridge'
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ # FIXME we currently force the recreation of the bridge, delegating the
+ # check to the creation function
+ raise ResourceNotFound
+
+ def __create__(self):
+ # FIXME : reserves .1 IP address for the bridge, provided no other
+ # class uses this trick
+ AddressManager().get_ip(self)
+ return self.node.bridge_manager.add_bridge(self.device_name)
+
+ # Everything should be handled by BaseNetDevice
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def _add_interface(self, interface, vlan=None):
+ """
+ Returns:
+ Task
+ """
+ return self.node.bridge_manager.add_interface(self.device_name,
+ interface.device_name, vlan)
+
+ def __method_add_interface__(self, interface, vlan=None):
+ return self._add_interface(interface, vlan)
+
+ def _remove_interface(self, interface):
+ """
+ Returns:
+ Task
+ """
+ log.info('Removing interface {} from bridge {}'.format(
+ interface.device_name, self.name))
+ return self.node.bridge_manager.del_interface(self.device_name,
+ interface.device_name)
+
diff --git a/vicn/resource/linux/bridge_mgr.py b/vicn/resource/linux/bridge_mgr.py
new file mode 100644
index 00000000..b7035221
--- /dev/null
+++ b/vicn/resource/linux/bridge_mgr.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.application import LinuxApplication
+from vicn.core.resource import FactoryResource
+from vicn.core.attribute import Attribute, Multiplicity
+
+class BridgeManager(LinuxApplication):
+ """
+ Resource: Bridge Manager
+
+ A bridge manager is responsible to manage bridges on a node.
+ """
+ __type__ = FactoryResource
+
+ # Overloaded reverse attribute
+ node = Attribute(reverse_name = 'bridge_manager',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
diff --git a/vicn/resource/linux/certificate.py b/vicn/resource/linux/certificate.py
new file mode 100644
index 00000000..e8750dff
--- /dev/null
+++ b/vicn/resource/linux/certificate.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os.path
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity, Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import task, inline_task, BashTask
+from vicn.resource.linux.file import File
+from vicn.resource.node import Node
+
+DEFAULT_RSA_LENGTH = '4096'
+DEFAULT_SUBJECT = '/CN=www.cisco.com/L=Paris/O=Cisco/C=FR'
+
+CMD_CREATE='\n'.join([
+ '# Generate a new certificate',
+ 'openssl req -x509 -newkey rsa:' + DEFAULT_RSA_LENGTH +
+ ' -keyout {self.key} -out {self.cert} -subj ' + DEFAULT_SUBJECT + ' -nodes'
+])
+
+class Certificate(Resource):
+ """
+ Resource: Certificate
+
+ Implements a SSL certificate.
+
+ Todo:
+ - ideally, this should be implemented as a pair of tightly coupled files.
+ """
+ node = Attribute(Node,
+ description = 'Node on which the certificate is created',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne)
+ cert = Attribute(String, description = 'Certificate path',
+ mandatory = True)
+ key = Attribute(String, description = 'Key path',
+ mandatory = True)
+
+ @inline_task
+ def __initialize__(self):
+ self._cert_file = File(node = Reference(self, 'node'),
+ filename = Reference(self, 'cert'),
+ managed = False)
+ self._key_file = File(node = Reference(self, 'node'),
+ filename = Reference(self, 'key'),
+ managed = False)
+
+ def __get__(self):
+ return self._cert_file.__get__() | self._key_file.__get__()
+
+ def __create__(self):
+ return BashTask(None, CMD_CREATE, {'self': self})
+
+ def __delete__(self):
+ return self._cert_file.__delete__() | self._key_file.__delete__()
+
+
diff --git a/vicn/resource/linux/dnsmasq.py b/vicn/resource/linux/dnsmasq.py
new file mode 100644
index 00000000..e18f750f
--- /dev/null
+++ b/vicn/resource/linux/dnsmasq.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import ipaddress
+import logging
+
+from string import Template
+
+from netmodel.model.type import String, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.resource import EmptyResource
+from vicn.resource.dns_server import DnsServer
+from vicn.resource.interface import Interface
+from vicn.resource.linux.file import TextFile
+from vicn.resource.linux.service import Service
+
+log = logging.getLogger(__name__)
+
+FN_CONF='/etc/dnsmasq.conf'
+
+TPL_CONF='''
+# Configuration file for dnsmasq.
+#
+# Format is one option per line, legal options are the same
+# as the long options legal on the command line. See
+# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details.
+
+interface=$interface
+dhcp-range=$dhcp_range
+dhcp-host=00:0e:c6:81:79:01,192.168.128.200,12h
+
+#server=$server
+$flags
+'''
+
+DHCP_OFFSET = 195
+
+class DnsMasq(Service, DnsServer):
+ """
+
+ Todo:
+ - Currently, a single interface is supported.
+ - DHCP range is hardcoded
+ """
+ __package_names__ = ['dnsmasq']
+ __service_name__ = 'dnsmasq'
+
+ interface = Attribute(Interface,
+ description = 'Interface on which to listen')
+ lease_interval = Attribute(String,
+ default = '12h')
+ server = Attribute(String)
+ dhcp_authoritative = Attribute(Bool,
+ description = 'Flag: DHCP authoritative',
+ default = True)
+ log_queries = Attribute(Bool, description = 'Flag: log DNS queries',
+ default = True)
+ log_dhcp = Attribute(Bool, description = 'Flag: log DHCP queries',
+ default = True)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if not self.interface:
+ if self.node.bridge:
+ self.interface = self.node.bridge
+ else:
+ self.interface = self.node.host_interface
+
+ def __subresources__(self):
+ # Overwrite configuration file
+ flags = list()
+ if self.dhcp_authoritative:
+ flags.append('dhcp-authoritative')
+ if self.log_queries:
+ flags.append('log-queries')
+ if self.log_dhcp:
+ flags.append('log-dhcp')
+ network = self._state.manager.get('network')
+ network = ipaddress.ip_network(network, strict=False)
+
+ dhcp_range = '{},{},{},{},{}'.format(
+ self.interface.device_name,
+ str(network[DHCP_OFFSET]),
+ str(network[DHCP_OFFSET + 5]), # eg. .253
+ "255.255.255.0",
+ self.lease_interval)
+
+ t_dict = {
+ 'interface' : self.interface.device_name,
+ 'dhcp_range': dhcp_range,
+ 'server' : str(network[-2]), # unused so far
+ 'flags' : '\n'.join(flags)
+ }
+
+ t = Template(TPL_CONF)
+ conf = t.substitute(t_dict)
+
+ return TextFile(node = self.node, owner = self, filename = FN_CONF,
+ content = conf, overwrite = True)
diff --git a/vicn/resource/linux/file.py b/vicn/resource/linux/file.py
new file mode 100644
index 00000000..cddda8ed
--- /dev/null
+++ b/vicn/resource/linux/file.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String, Bool
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, inline_task
+from vicn.resource.node import Node
+
+CREATE_DIR_CMD = "mkdir -p {dir}"
+CREATE_FILE_CMD = "mkdir -p $(dirname {file.filename}) && touch {file.filename}"
+DELETE_FILE_CMD = "rm -f {file.filename}"
+
+GET_FILE_CMD = 'test -f {file.filename} && readlink -e {file.filename}'
+
+GREP_FILE_CMD = "cat {file.filename}"
+
+CMD_PRINT_TO_FILE = 'echo -n "{file.content}" > {file.filename}'
+
+class File(Resource):
+ """
+ Resource: File
+ """
+ filename = Attribute(String, description = 'Path to the file',
+ mandatory = True)
+ node = Attribute(Node, description = 'Node on which the file is created',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'files',
+ reverse_description = 'Files created on the node')
+ overwrite = Attribute(Bool,
+ description = 'Determines whether an existing file is overwritten',
+ default = False)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __get__(self):
+
+ # UGLY
+ @inline_task
+ def not_found():
+ raise ResourceNotFound
+
+ if self.overwrite:
+ return not_found()
+
+ def is_path (rv):
+ if rv is None or rv.stdout is None or len(rv.stdout) == 0 or \
+ rv.return_value != 0:
+ raise ResourceNotFound
+ return {} # 'filename': rv.stdout}
+
+ test = BashTask(self.node, GET_FILE_CMD, {"file": self}, parse=is_path)
+ return test
+
+ def __create__(self):
+ ctask = BashTask(self.node, CREATE_FILE_CMD, {"file": self})
+ if self.overwrite:
+ ctask = BashTask(self.node, DELETE_FILE_CMD, {'file': self}) > ctask
+ return ctask
+
+ def __delete__(self):
+ return BashTask(self.node, DELETE_FILE_CMD, { "file" : self})
+
+#------------------------------------------------------------------------------
+
+class TextFile(File):
+ """
+ Resource: TextFile
+
+ A file with text content.
+ """
+
+ content = Attribute(String, default='')
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_PRINT_TO_FILE, {'file': self})
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _set_content(self):
+ return self.__create__()
+
+ def _get_content(self):
+ return BashTask(self.node, GREP_FILE_CMD, {'file': self},
+ parse =( lambda x : x.stdout))
diff --git a/vicn/resource/linux/iperf.py b/vicn/resource/linux/iperf.py
new file mode 100644
index 00000000..a0780a1c
--- /dev/null
+++ b/vicn/resource/linux/iperf.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from abc import ABC
+
+from netmodel.model.type import Integer
+from vicn.core.attribute import Attribute
+from vicn.resource.linux.application import LinuxApplication
+
+class Iperf3(LinuxApplication, ABC):
+ __package_names__ = ['iperf3']
+
diff --git a/vicn/resource/linux/link.py b/vicn/resource/linux/link.py
new file mode 100644
index 00000000..4304a948
--- /dev/null
+++ b/vicn/resource/linux/link.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import random
+import string
+import logging
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute, Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.state import ResourceState, AttributeState
+from vicn.core.task import inline_task, async_task, run_task
+from vicn.core.task import get_attributes_task, BashTask
+from vicn.resource.channel import Channel
+from vicn.resource.interface import Interface
+from vicn.resource.linux.net_device import NonTapBaseNetDevice
+from vicn.resource.node import Node
+
+# FIXME remove VPP specific code
+from vicn.resource.vpp.interface import VPPInterface
+
+log = logging.getLogger(__name__)
+
+CMD_DELETE_IF_EXISTS='ip link show {interface.device_name} && ' \
+ 'ip link delete {interface.device_name} || true'
+
+CMD_CREATE='''
+# Create veth pair in the host node
+ip link add name {tmp_src} type veth peer name {tmp_dst}
+ip link set dev {tmp_src} netns {pid[0]} name {interface.src.device_name}
+ip link set dev {tmp_dst} netns {pid[1]} name {interface.dst.device_name}
+'''
+CMD_UP='''
+ip link set dev {interface.device_name} up
+'''
+
+class Link(Channel):
+ """
+ Resource: Link
+
+ Implements a virtual wired link between containers. It is a VethPair, both
+ sides of which sit inside a different container.
+
+ Because of this, the resource only supports passing source and destination
+ containers, and not interfaces. It also explains the relative complexity of
+ the current implementation.
+ """
+
+ src = Attribute(Interface, description = 'Source interface')
+ dst = Attribute(Interface, description = 'Destination interface')
+
+ capacity = Attribute(Integer, description = 'Link capacity (Mb/s)')
+ delay = Attribute(String, description = 'Link propagation delay')
+
+ src_node = Attribute(Node, description = 'Source node',
+ mandatory = True)
+ dst_node = Attribute(Node, description = 'Destination node',
+ mandatory = True)
+
+ def __init__(self, *args, **kwargs):
+ assert 'src' not in kwargs and 'dst' not in kwargs
+ assert 'src_node' in kwargs and 'dst_node' in kwargs
+ super().__init__(*args, **kwargs)
+
+ @inline_task
+ def __initialize__(self):
+
+ # We create two managed net devices that are pre-setup
+ # but the resource manager has to take over for IP addresses etc.
+ # Being done in initialize, those attributes won't be considered as
+ # dependencies and will thus not block the resource state machine.
+ self.src = NonTapBaseNetDevice(node = self.src_node,
+ device_name = self.dst_node.name,
+ channel = self,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.dst = NonTapBaseNetDevice(node = self.dst_node,
+ device_name = self.src_node.name,
+ channel = self,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.dst.remote = self.src
+ self.src.remote = self.dst
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ async def _commit(self):
+ manager = self._state.manager
+
+ # We mark the src and dst interfaces created because we are pre-setting
+ # them up in __create__ using a VethPair
+ # We go through both INITIALIZED and CREATED stats to raise the proper
+ # events and satisfy any eventual wait_* command.
+ await manager._set_resource_state(self.src, ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.dst, ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.src, ResourceState.CREATED)
+ await manager._set_resource_state(self.dst, ResourceState.CREATED)
+
+ # We mark the attribute clean so that it is not updated
+ await manager._set_attribute_state(self, 'src', AttributeState.CLEAN)
+ await manager._set_attribute_state(self, 'dst', AttributeState.CLEAN)
+
+ manager.commit_resource(self.src)
+ manager.commit_resource(self.dst)
+
+ # Disable rp_filtering
+ # self.src.rp_filter = False
+ # self.dst.rp_filter = False
+
+ #XXX VPP
+ if hasattr(self.src_node, 'vpp') and not self.src_node.vpp is None:
+ vpp_src = VPPInterface(parent = self.src,
+ vpp = self.src_node.vpp,
+ ip_address = Reference(self.src, 'ip_address'),
+ device_name = 'vpp' + self.src.device_name)
+ manager.commit_resource(vpp_src)
+
+ if hasattr(self.dst_node, 'vpp') and not self.dst_node.vpp is None:
+ vpp_dst = VPPInterface(parent = self.dst,
+ vpp = self.dst_node.vpp,
+ ip_address = Reference(self.dst, 'ip_address'),
+ device_name = 'vpp' + self.dst.device_name)
+ manager.commit_resource(vpp_dst)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @async_task
+ async def __get__(self):
+ manager = self._state.manager
+
+ try:
+ await run_task(self.src.__get__(), manager)
+ await run_task(self.dst.__get__(), manager)
+ except ResourceNotFound:
+ # This is raised if any of the two side of the VethPair is missing
+ raise ResourceNotFound
+
+ # We always need to commit the two endpoints so that their attributes
+ # are correctly updated
+ await self._commit()
+
+ def __create__(self):
+ assert self.src_node.get_type() == 'lxccontainer'
+ assert self.dst_node.get_type() == 'lxccontainer'
+
+ src_host = self.src_node.node
+ dst_host = self.dst_node.node
+
+ assert src_host == dst_host
+ host = src_host
+
+ # Sometimes a down interface persists on one side
+ delif_src = BashTask(self.src_node, CMD_DELETE_IF_EXISTS,
+ {'interface': self.src})
+ delif_dst = BashTask(self.dst_node, CMD_DELETE_IF_EXISTS,
+ {'interface': self.dst})
+
+ pid_src = get_attributes_task(self.src_node, ['pid'])
+ pid_dst = get_attributes_task(self.dst_node, ['pid'])
+
+ tmp_src = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase +
+ string.digits) for _ in range(5))
+ tmp_dst = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase +
+ string.digits) for _ in range(5))
+
+ create = BashTask(host, CMD_CREATE, {'interface': self,
+ 'tmp_src': tmp_src, 'tmp_dst': tmp_dst})
+
+ up_src = BashTask(self.src_node, CMD_UP, {'interface': self.src})
+ up_dst = BashTask(self.dst_node, CMD_UP, {'interface': self.dst})
+
+ @async_task
+ async def set_state():
+ # We always need to commit the two endpoints so that their attributes
+ # are correctly updated
+ await self._commit()
+
+ delif = delif_src | delif_dst
+ up = up_src | up_dst
+ pid = pid_src | pid_dst
+ return ((delif > (pid @ create)) > up) > set_state()
+
diff --git a/vicn/resource/linux/macvlan.py b/vicn/resource/linux/macvlan.py
new file mode 100644
index 00000000..ea9c37c1
--- /dev/null
+++ b/vicn/resource/linux/macvlan.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.task import BashTask
+from vicn.resource.linux.net_device import SlaveBaseNetDevice
+
+CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \
+ 'link {netdevice.parent.device_name} ' \
+ 'type {netdevice.netdevice_type} mode {netdevice.mode}'
+
+class MacVlan(SlaveBaseNetDevice):
+ """
+ Resource: MacVlan
+
+ Implements a MacVlan interface.
+ """
+
+ mode = Attribute(String, description = 'MACVLAN mode',
+ default = 'bridge')
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'macvlan'
+ self.netdevice_type = 'macvlan'
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self})
diff --git a/vicn/resource/linux/macvtap.py b/vicn/resource/linux/macvtap.py
new file mode 100644
index 00000000..82002e02
--- /dev/null
+++ b/vicn/resource/linux/macvtap.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.task import BashTask
+from vicn.resource.linux.net_device import SlaveBaseNetDevice
+
+CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \
+ 'link {netdevice.parent.device_name} ' \
+ 'type {netdevice.netdevice_type} mode {netdevice.mode}'
+
+class MacVtap(SlaveBaseNetDevice):
+ """
+ Resource: MacVtap
+
+ Implements a MacVtap interface.
+ """
+
+ mode = Attribute(String, description = 'MACVTAP mode',
+ default = 'bridge'),
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'macvtap'
+ self.netdevice_type = 'macvtap'
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self})
diff --git a/vicn/resource/linux/net_device.py b/vicn/resource/linux/net_device.py
new file mode 100644
index 00000000..f0a08991
--- /dev/null
+++ b/vicn/resource/linux/net_device.py
@@ -0,0 +1,519 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import re
+import math
+import random
+import string
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import BaseResource
+from vicn.core.task import BashTask, task, EmptyTask
+from vicn.resource.application import Application
+from vicn.resource.interface import Interface
+
+# parse_ip_addr inspired from:
+# From: https://github.com/ohmu/poni/blob/master/poni/cloud_libvirt.py
+
+LXD_FIX = lambda cmd: 'sleep 1 && {}'.format(cmd)
+
+MAX_DEVICE_NAME_SIZE = 15
+
+CMD_FLUSH_IP = 'ip addr flush dev {device_name}'
+
+CMD_INTERFACE_LIST = 'ip link show | grep -A 1 @{}'
+RX_INTERFACE_LIST = '.*?(?P<ifname>[^ ]*)@{}:'
+CMD_INTERFACE_GET = 'ip link show | grep -A 1 {}@{}'
+RX_INTERFACE_GET = '.*?(?P<ifname>{})@{}:'
+
+log = logging.getLogger(__name__)
+
+CMD_GET = LXD_FIX('ip link show {netdevice.device_name}')
+CMD_CREATE = 'ip link add name {netdevice.device_name} ' \
+ 'type {netdevice.netdevice_type}'
+CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \
+ 'link {netdevice.parent.device_name} ' \
+ 'type {netdevice.netdevice_type}'
+CMD_DELETE = 'ip link delete {netdevice.device_name}'
+CMD_SET_MAC_ADDRESS = 'ip link set dev {netdevice.device_name} ' \
+ 'address {netdevice.mac_address}'
+CMD_GET_IP_ADDRESS = 'ip addr show {netdevice.device_name}'
+CMD_SET_IP_ADDRESS = 'ip addr add dev {netdevice.device_name} ' \
+ '{netdevice.ip_address} brd + || true'
+CMD_SET_PROMISC = 'ip link set dev {netdevice.device_name} promisc {on_off}'
+CMD_SET_UP = 'ip link set {netdevice.device_name} {up_down}'
+CMD_SET_CAPACITY='\n'.join([
+ 'tc qdisc del dev {netdevice.device_name} root || true',
+ 'tc qdisc add dev {netdevice.device_name} root handle 1: tbf rate '
+ '{netdevice.capacity}Mbit burst {burst}kb latency 70ms'
+ 'tc qdisc add dev {netdevice.device_name} parent 1:1 codel',
+])
+CMD_GET_PCI_ADDRESS='ethtool -i {netdevice.device_name} | ' \
+ "sed -n '/bus-info/{{s/.*: [^:]*:\(.*\)/\\1/p}}'"
+CMD_GET_OFFLOAD='ethtool -k {netdevice.device_name} | ' \
+ 'grep rx-checksumming | cut -d " " -f 2'
+CMD_SET_OFFLOAD='ethtool -K {netdevice.device_name} rx on tx on'
+CMD_UNSET_OFFLOAD='ethtool -K {netdevice.device_name} rx off tx off'
+
+CMD_UNSET_RP_FILTER = '''
+sysctl -w net.ipv4.conf.all.rp_filter=0
+sysctl -w net.ipv4.conf.{netdevice.device_name}.rp_filter=0
+'''
+CMD_SET_RP_FILTER = 'sysctl -w ' \
+ 'net.ipv4.conf.{netdevice.device_name}.rp_filter=1'
+CMD_GET_RP_FILTER = '''
+sysctl net.ipv4.conf.all.rp_filter
+sysctl net.ipv4.conf.{netdevice.device_name}.rp_filter
+'''
+
+#-------------------------------------------------------------------------------
+
+# FIXME GPL code
+
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Utility to parse 'ip link [show]'.
+
+Example dictionary returned by parse_ip_link():
+
+{u'eth0': {u'flags': set([u'BROADCAST', u'LOWER_UP', u'MULTICAST', u'UP']),
+ u'index': 2,
+ u'mac': u'80:fa:5c:0d:43:5e',
+ u'name': u'eth0',
+ u'settings': {u'group': u'default',
+ u'mode': u'DEFAULT',
+ u'mtu': u'1500',
+ u'qdisc': u'pfifo_fast',
+ u'qlen': u'1000',
+ u'state': u'UP'}},
+ u'lo': {u'flags': set([u'LOOPBACK', u'LOWER_UP', u'UP']),
+ u'index': 1,
+ u'name': u'lo',
+ u'settings': {u'group': u'default',
+ u'mode': u'DEFAULT',
+ u'mtu': u'65536',
+ u'qdisc': u'noqueue',
+ u'state': u'UNKNOWN'}}}
+
+The dictionary above is generated given the following input:
+
+ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN \
+mode DEFAULT group default
+ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
+ 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast \
+state UP mode DEFAULT group default qlen 1000
+ link/ether 80:fa:5c:0d:43:5e brd ff:ff:ff:ff:ff:ff
+"""
+
+def _get_settings_dict(settings_line):
+ """
+ Given a string of the format:
+ "[[<key1> <value1>] <key2> <value2>][...]"
+ Returns a dictionary mapping each key to its corresponding value.
+ :param settings_line: unicode
+ :return: dict
+ """
+ settings = settings_line.strip().split()
+ num_tokens = len(settings)
+ assert num_tokens % 2 == 0
+ return {
+ settings[2 * i]: settings[2 * i + 1] for i in range(num_tokens // 2)
+ }
+
+
+def _parse_interface_definition(line):
+ """Given a string of the format:
+ <interface_index>: <interface_name>: <flags> <settings>
+ Returns a dictionary containing the component parts.
+ :param line: unicode
+ :return: dict
+ :raises: ValueError if a malformed interface definition line is presented
+ """
+ interface = {}
+
+ # This line is in the format:
+ # <interface_index>: <interface_name>: <properties>
+ [index, name, properties] = map(
+ lambda s: s.strip(), line.split(':'))
+
+ interface['index'] = int(index)
+ if '@' in name:
+ name, parent = name.split('@')
+ interface['name'] = name
+ interface['parent'] = parent
+ else:
+ interface['name'] = name
+ interface['parent'] = None
+
+ # Now parse the <properties> part from above.
+ # This will be in the form "<FLAG1,FLAG2> key1 value1 key2 value2 ..."
+ matches = re.match(r"^<(.*)>(.*)", properties)
+ if matches:
+ flags = matches.group(1)
+ if len(flags) > 0:
+ flags = flags.split(',')
+ else:
+ flags = []
+ interface['flags'] = set(flags)
+ interface['settings'] = _get_settings_dict(matches.group(2))
+ else:
+ raise ValueError("Malformed 'ip link' line (%s)" % line)
+ return interface
+
+
+def _add_additional_interface_properties(interface, line):
+ """
+ Given the specified interface and a specified follow-on line containing
+ more interface settings, adds any additional settings to the interface
+ dictionary. (currently, the only relevant setting is the interface MAC.)
+ :param interface: dict
+ :param line: unicode
+ """
+ settings = _get_settings_dict(line)
+ # We only care about the MAC address for Ethernet interfaces.
+ mac = settings.get('link/ether')
+ if mac is not None:
+ interface['mac'] = mac
+
+
+def parse_ip_link(output):
+ """
+ Given the full output from 'ip link [show]', parses it and returns a
+ dictionary mapping each interface name to its settings.
+ :param output: string or unicode
+ :return: dict
+ """
+ interfaces = {}
+ interface = None
+ for line in output.splitlines():
+ if re.match(r'^[0-9]', line):
+ interface = _parse_interface_definition(line)
+ if interface is not None:
+ interfaces[interface['name']] = interface
+ else:
+ if interface is not None:
+ _add_additional_interface_properties(interface, line)
+ return interfaces
+
+#------------------------------------------------------------------------------
+
+_IP_ADDR_SPLIT_RE = re.compile("^[0-9]+: ", flags=re.MULTILINE)
+
+def parse_ip_addr(data):
+ """
+ Parse addresses from 'ip addr' output
+ """
+
+ for iface in _IP_ADDR_SPLIT_RE.split(data.strip()):
+ if not iface:
+ continue
+ lines = [l.strip() for l in iface.splitlines()]
+ # XXX @ in name not supported
+ name = lines.pop(0).partition(":")[0]
+ info = {
+ "ip-addresses": [],
+ "hardware-address": None,
+ }
+ if '@' in name:
+ name, parent = name.split('@')
+ info['name'] = name
+ info['parent'] = parent
+ else:
+ info['name'] = name
+ info['parent'] = None
+
+ for line in lines:
+ words = line.split()
+ if words[0].startswith("link/") and len(words) >= 2:
+ info["hardware-address"] = words[1]
+ elif words[0] in ("inet", "inet6"):
+ addrtype = "ipv6" if words[0] == "inet6" else "ipv4"
+ addr, _, prefix = words[1].partition("/")
+ if prefix == '':
+ prefix = 128 if addrtype == "ipv6" else 32
+ info["ip-addresses"].append({"ip-address-type": addrtype,
+ "ip-address": addr, "prefix": int(prefix)})
+ yield info
+
+#------------------------------------------------------------------------------
+
+class BaseNetDevice(Interface, Application):
+ __type__ = BaseResource
+
+ # XXX note: ethtool only required if we need to get the pci address
+ __package_names__ = ['ethtool']
+
+ device_name = Attribute(String, description = 'Name of the NetDevice',
+ default = lambda x : x._default_device_name(),
+ max_size = MAX_DEVICE_NAME_SIZE)
+ capacity = Attribute(Integer,
+ description = 'Capacity for interface shaping (Mb/s)')
+ mac_address = Attribute(String, description = 'Mac address of the device')
+ ip_address = Attribute(String, description = 'IP address of the device')
+ pci_address = Attribute(String,
+ description = 'PCI bus address of the device',
+ ro = True)
+ promiscuous = Attribute(Bool, description = 'Promiscuous', default = False)
+ up = Attribute(Bool, description = 'Promiscuous', default = True)
+ netdevice_type = Attribute(String, description = 'Type of the netdevice',
+ ro = True)
+ prefix = Attribute(String, default = 'dev')
+ rp_filter = Attribute(Bool, description = 'Reverse-path filtering enabled',
+ default = True)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Dummy member to store the other side of a VethPair
+ # We use it to disable offloading on interfaces connected to VPP
+ self.remote = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __get__(self):
+ def check(rv):
+ if not bool(rv):
+ raise ResourceNotFound
+ return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True,
+ parse=check)
+
+ __create__ = None
+
+ def __delete__(self):
+ return BashTask(self.node, CMD_DELETE, {'netdevice': self})
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_mac_address(self):
+ # Merge into parse_ip_link
+ def parse(rv):
+ assert rv is not None
+
+ nds = parse_ip_link(rv.stdout)
+ # This will raise an exception is the interface does not exist
+ nd = nds[self.device_name]
+ attrs = { 'mac_address': nd['mac'], }
+ return attrs
+
+ return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True,
+ parse=parse)
+
+ def _set_mac_address(self):
+ return BashTask(self.node, CMD_SET_MAC_ADDRESS, {'netdevice': self})
+
+ def _get_ip_address(self):
+ """
+ NOTE: Incidently, this will also give the MAC address, as well as other
+ attributes...
+ """
+ def parse(rv):
+ attrs = dict()
+
+ assert rv is not None
+
+ nds = list(parse_ip_addr(rv.stdout))
+
+ assert nds
+ assert len(nds) <= 1
+
+ nd = nds[0]
+
+ assert nd['name'] == self.device_name
+
+ attrs['mac_address'] = nd['hardware-address']
+
+ # We assume a single IPv4 address for now...
+ ips = [ip for ip in nd['ip-addresses']
+ if ip['ip-address-type'] == 'ipv4']
+ if len(ips) >= 1:
+ if len(ips) > 1:
+ log.warning('Keeping only first of many IP addresses...')
+ ip = ips[0]
+ attrs['ip_address'] = ip['ip-address']
+ else:
+ attrs['ip_address'] = None
+ return attrs
+
+ return BashTask(self.node, CMD_GET_IP_ADDRESS,
+ {'netdevice': self}, parse=parse)
+
+ def _set_ip_address(self):
+ if self.ip_address is None:
+ # Unset IP
+ return BashTask(self.node, CMD_FLUSH_IP,
+ {'device_name': self.device_name})
+ return BashTask(self.node, CMD_SET_IP_ADDRESS,
+ {'netdevice': self})
+
+ @task
+ def _get_promiscuous(self):
+ return {'promiscuous': False}
+
+ def _set_promiscuous(self):
+ on_off = 'on' if self.promiscuous else 'off'
+ return BashTask(self.node, CMD_SET_PROMISC,
+ {'netdevice': self, 'on_off' : on_off})
+
+ @task
+ def _get_up(self):
+ return {'up': False}
+
+ def _set_up(self):
+ up_down = 'up' if self.up else 'down'
+ return BashTask(self.node, CMD_SET_UP,
+ {'netdevice': self, 'up_down': up_down})
+
+ @task
+ def _get_capacity(self):
+ return {'capacity': None}
+
+ def _set_capacity(self):
+ if self.capacity is None:
+ log.warning('set_capacity(None) not implemented')
+ return EmptyTask()
+
+ # http://unix.stackexchange.com/questions/100785/bucket-size-in-tbf
+ MBPS = 1000000
+ KBPS = 1024
+ BYTES = 8
+ HZ = 250
+
+ # Round to power of two... see manpage
+ burst = math.ceil((((self.capacity * MBPS) / HZ) / BYTES) / KBPS)
+ burst = 1 << (burst - 1).bit_length()
+
+ return BashTask(self.node, CMD_SET_CAPACITY,
+ {'netdevice': self, 'burst': burst})
+
+ def _get_rp_filter(self):
+ def parse(rv):
+ lines = rv.stdout.splitlines()
+ return (int(lines[0][-1]) + int(lines[1][-1]) > 0)
+ return BashTask(self.node, CMD_GET_RP_FILTER, {'netdevice' :self},
+ parse = parse)
+
+ def _set_rp_filter(self):
+ cmd = CMD_SET_RP_FILTER if self.rp_filter else CMD_UNSET_RP_FILTER
+ return BashTask(self.node, cmd, {'netdevice' : self})
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _remote_node_name(self):
+ remote_interface = self._remote_interface()
+ if remote_interface:
+ return remote_interface.node.name
+ else:
+ rnd = ''.join(random.choice(string.ascii_uppercase + string.digits)
+ for _ in range(3))
+ return 'unk{}'.format(rnd)
+
+ def _remote_interface(self):
+ if not self.channel:
+ return None
+ interfaces = self.channel.interfaces
+ for interface in interfaces:
+ if interface == self:
+ continue
+ return interface
+
+ def _default_device_name(self):
+ remote_node_name = self._remote_node_name()
+ if remote_node_name:
+ return remote_node_name
+ else:
+ return AddressManager().get('device_name', self,
+ prefix = self.prefix, scope = self.prefix)
+
+#------------------------------------------------------------------------------
+
+class NonTapBaseNetDevice(BaseNetDevice):
+ # Tap devices for instance don't have offload
+ offload = Attribute(Bool, description = 'Offload', default=True)
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_offload(self):
+ return BashTask(self.node, CMD_GET_OFFLOAD, {'netdevice': self},
+ parse = lambda rv : rv.stdout.strip() == 'on')
+
+ def _set_offload(self):
+ cmd = None
+ if self.offload:
+ cmd = CMD_SET_OFFLOAD
+ else:
+ cmd = CMD_UNSET_OFFLOAD
+ return BashTask(self.node, cmd, {'netdevice' : self})
+
+#------------------------------------------------------------------------------
+
+class NetDevice(NonTapBaseNetDevice):
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE, {'netdevice': self})
+
+#------------------------------------------------------------------------------
+
+class SlaveBaseNetDevice(BaseNetDevice):
+ parent = Attribute(NetDevice, description = 'Parent NetDevice')
+
+ host = Attribute(NetDevice, description = 'Host interface',
+ default = lambda x : x._default_host())
+
+ def _default_host(self):
+ if self.node.__class__.__name__ == 'LxcContainer':
+ host = self.node.node
+ else:
+ host = self.node
+ max_len = MAX_DEVICE_NAME_SIZE - len(self.node.name) - 1
+ device_name = self.device_name[:max_len]
+
+ return NetDevice(node = host,
+ device_name = '{}-{}'.format(self.node.name, device_name),
+ managed = False)
+
+#------------------------------------------------------------------------------
+
+class SlaveNetDevice(SlaveBaseNetDevice):
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self})
diff --git a/vicn/resource/linux/netmon.py b/vicn/resource/linux/netmon.py
new file mode 100644
index 00000000..8472f308
--- /dev/null
+++ b/vicn/resource/linux/netmon.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.service import Service
+
+class NetMon(Service):
+ """
+ Resource: NetMon
+
+ Generic network monitoring daemon, used internally by VICN for resource
+ monitoring.
+ """
+ __package_names__ = ['netmon']
+ __service_name__ = 'netmon'
diff --git a/vicn/resource/linux/numa_mgr.py b/vicn/resource/linux/numa_mgr.py
new file mode 100644
index 00000000..632264ce
--- /dev/null
+++ b/vicn/resource/linux/numa_mgr.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import re
+from itertools import cycle
+
+from netmodel.model.type import BaseType
+from vicn.core.resource import Resource
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.task import BashTask
+from vicn.resource.node import Node
+
+PATTERN_LSCPU_NUMA = 'NUMA node[0-9]+ CPU\(s\)'
+CMD_LSCPU = 'lscpu'
+
+class CycleType(BaseType, cycle):
+ """
+ Type: CycleType
+ """
+ pass
+
+#------------------------------------------------------------------------------
+
+def parse_lscpu_line(line):
+ #Format: NUMA node0 CPU(s): 0-17,36-53
+ line = line.split(':')[1]
+ #line = 0-17,36,53
+
+ def limits_to_list(string):
+ limits = string.split('-')
+ lower_limit = int(limits[0])
+ #Removes core 0 as it is used the most often by the kernl
+ if lower_limit is 0 : lower_limit = 1
+ return cycle(range(lower_limit, int(limits[1])))
+ return cycle(map(limits_to_list, line.split(',')))
+
+def parse_lscpu_rv(rv):
+ ret = []
+ for line in rv.stdout.splitlines():
+ if re.search(PATTERN_LSCPU_NUMA, line):
+ ret.append(parse_lscpu_line(line))
+ return ret
+
+#------------------------------------------------------------------------------
+
+class NumaManager(Resource):
+ """
+ Resource: NumaManager
+ """
+
+ node = Attribute(Node,
+ mandatory = True,
+ multiplicity = Multiplicity.OneToOne,
+ reverse_auto = True,
+ reverse_name = 'numa_mgr')
+ numa_repartitor = Attribute(CycleType,
+ description = 'Tool to separate cores/CPUs/sockets',
+ multiplicity = Multiplicity.OneToMany,
+ ro = True)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ __create__ = None
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.current_numa_node = 0
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_numa_repartitor(self):
+ return BashTask(self.node, CMD_LSCPU, parse=parse_lscpu_rv)
+
+ #--------------------------------------------------------------------------
+ # Public API
+ #--------------------------------------------------------------------------
+
+ def get_numa_core(self, numa_node=None):
+ if numa_node is None:
+ numa_node = self.current_numa_node
+ self.current_numa_node = (self.current_numa_node+1) % \
+ len(self.numa_repartitor)
+ numa_list = self.numa_repartitor[numa_node]
+
+ socket = next(numa_list)
+ return numa_node, next(socket)
+
+ def get_number_of_numa(self):
+ return len(self.numa_repartitor)
diff --git a/vicn/resource/linux/ovs.py b/vicn/resource/linux/ovs.py
new file mode 100644
index 00000000..d67e4bca
--- /dev/null
+++ b/vicn/resource/linux/ovs.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.task import BashTask
+from vicn.resource.linux.bridge_mgr import BridgeManager
+
+CMD_ADD_BRIDGE = '''
+ovs-vsctl --may-exist add-br {bridge_name}
+ip link set dev {bridge_name} up
+'''
+
+CMD_DEL_BRIDGE = 'ovs-vsctl --if-exists del-br {bridge_name}'
+
+CMD_ADD_INTERFACE = 'ovs-vsctl --may-exist add-port {bridge_name} ' \
+ '{interface_name}'
+CMD_ADD_INTERFACE_VLAN = CMD_ADD_INTERFACE + ' tag={vlan}'
+CMD_DEL_INTERFACE = 'ovs-vsctl --if-exists del-port {bridge_name} ' \
+ '{interface_name}'
+
+class OVS(BridgeManager):
+ """
+ Resource: OVS
+
+ OpenVSwitch bridge manager
+ """
+
+ __package_names__ = ['openvswitch-switch']
+
+ #---------------------------------------------------------------------------
+ # BridgeManager API
+ #---------------------------------------------------------------------------
+
+ def add_bridge(self, bridge_name):
+ return BashTask(self.node, CMD_ADD_BRIDGE,
+ {'bridge_name': bridge_name},
+ output = False, as_root = True)
+
+ def del_bridge(self, bridge_name):
+ return BashTask(self.node, CMD_DEL_BRIDGE,
+ {'bridge_name': bridge_name},
+ output = False, as_root = True)
+
+ def add_interface(self, bridge_name, interface_name, vlan=None):
+ cmd = CMD_ADD_INTERFACE_VLAN if vlan is not None else CMD_ADD_INTERFACE
+ return BashTask(self.node, cmd, {'bridge_name': bridge_name,
+ 'interface_name': interface_name, 'vlan': vlan},
+ output = False, as_root = True)
+
+ def del_interface(self, bridge_name, interface_name, vlan=None):
+ return BashTask(self.node, CMD_DEL_INTERFACE,
+ {'bridge_name': bridge_name, 'interface_name': interface_name,
+ 'vlan': vlan},
+ output = False, as_root = True)
diff --git a/vicn/resource/linux/package_manager.py b/vicn/resource/linux/package_manager.py
new file mode 100644
index 00000000..cb149ac6
--- /dev/null
+++ b/vicn/resource/linux/package_manager.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+import logging
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, EmptyTask, async_task
+from vicn.core.task import inline_task, run_task
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+CMD_APT_GET_KILL = 'kill -9 $(pidof apt-get) || true'
+CMD_DPKG_CONFIGURE_A = 'dpkg --configure -a'
+
+CMD_APT_GET_UPDATE = '''
+# Force IPv4
+echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4
+# Update package repository on node {node}
+apt-get update
+'''
+
+# We need to double { } we want to preserve
+CMD_PKG_TEST='dpkg -s {self.package_name}'
+
+CMD_PKG_INSTALL='''
+# Installing package {package_name}
+apt-get -y --allow-unauthenticated install {package_name}
+'''
+
+CMD_PKG_UNINSTALL='''
+# Uninstalling package {self.package_name}
+apt-get remove {self.package_name}
+'''
+
+CMD_SETUP_REPO = '''
+# Initialize package repository {repository.repo_name} on node {self.node.name}
+echo "{deb_source}" > {path}
+'''
+
+class PackageManager(Resource):
+ """
+ Resource: PackageManager
+
+ APT package management wrapper.
+
+ Todo:
+ - We assume a package manager is always installed on every machine.
+ - Currently, we limit ourselves to debian/ubuntu, and voluntarily don't
+ subclass this as we have (so far) no code for selecting the right
+ subclass, eg choising dynamically between DebRepositoryManager and
+ RpmRepositoryManager.
+ - We currently don't use package version numbers, which means a package
+ can be installed but not be up to date.
+ """
+
+ node = Attribute(Node,
+ reverse_name = 'package_manager',
+ reverse_auto = True,
+ mandatory = True,
+ multiplicity = Multiplicity.OneToOne)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._up_to_date = False
+ self.apt_lock = asyncio.Lock()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ if self.node.__class__.__name__ == 'Physical':
+ # UGLY : This blocking code is currently needed
+ task = self.node.host_interface._get_ip_address()
+ ip_dict = task.execute_blocking()
+ self.node.host_interface.ip_address = ip_dict['ip_address']
+ return ('Repository',)
+ else:
+ return ('Repository', 'CentralIP', 'RoutingTable')
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ repos = EmptyTask()
+ for repository in self._state.manager.by_type_str('Repository'):
+ deb_source = self._get_deb_source(repository)
+ path = self._get_path(repository)
+ repo = BashTask(self.node, CMD_SETUP_REPO,
+ {'deb_source': deb_source, 'path': path})
+ repos = repos | repo
+
+ return repos
+
+ #---------------------------------------------------------------------------
+ # Methods
+ #---------------------------------------------------------------------------
+
+ def __method_update__(self):
+ kill = BashTask(self.node, CMD_APT_GET_KILL, {'node': self.node.name},
+ lock = self.apt_lock)
+
+ # Setup during a reattempt
+ if hasattr(self, '_dpkg_configure_a'):
+ dpkg_configure_a = BashTask(self.node, CMD_DPKG_CONFIGURE_A,
+ lock = self.apt_lock)
+ else:
+ dpkg_configure_a = EmptyTask()
+
+ if not self.node.package_manager._up_to_date:
+ update = BashTask(self.node, CMD_APT_GET_UPDATE, {'node': self.node.name},
+ lock = self.apt_lock, post = self._mark_updated)
+ else:
+ update = EmptyTask()
+
+ return (kill > dpkg_configure_a) > update
+
+ def __method_install__(self, package_name):
+ update = self.__method_update__()
+ install = BashTask(self.node, CMD_PKG_INSTALL, {'package_name':
+ package_name}, lock = self.apt_lock)
+ return update > install
+
+ #---------------------------------------------------------------------------
+ # Internal methods
+ #---------------------------------------------------------------------------
+
+ def _mark_updated(self):
+ self._up_to_date = True
+
+ def _get_path(self, repository):
+ return '/etc/apt/sources.list.d/{}.list'.format(repository.repo_name)
+
+ def _get_deb_source(self, repository):
+ path = repository.node.host_interface.ip_address + '/'
+ if repository.directory:
+ path += repository.directory + '/'
+ return 'deb http://{} {}/'.format(path, self.node.dist)
+
+#------------------------------------------------------------------------------
+
+class Package(Resource):
+ """
+ Resource: Package
+
+ deb package support
+ """
+
+ package_name = Attribute(String, mandatory = True)
+ node = Attribute(Node,
+ mandatory = True,
+ requirements=[
+ Requirement('package_manager')
+ ])
+
+ #---------------------------------------------------------------------------
+ # Resource lifecycle
+ #---------------------------------------------------------------------------
+
+ def __get__(self):
+ return BashTask(self.node, CMD_PKG_TEST, {'self': self})
+
+ def __create__(self):
+ return self.node.package_manager.__method_install__(self.package_name)
+
+ @async_task
+ async def __delete__(self):
+ with await self.node.package_manager._lock:
+ task = BashTask(self.node, CMD_PKG_UNINSTALL, {'self': self})
+ ret = await run_task(task, self._state.manager)
+ return ret
+
+#------------------------------------------------------------------------------
+
+class Packages(Resource):
+ """
+ Resource: Packages
+
+ Todo:
+ - The number of concurrent subresources is not dynamically linked to the
+ nodes. We may need to link subresources to the attribute in general, but
+ since package_names are static for a resource, this is not a problem here.
+ """
+ names = Attribute(String, multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ mandatory = True,
+ requirements=[
+ Requirement('package_manager')
+ ])
+
+ #---------------------------------------------------------------------------
+ # Resource lifecycle
+ #---------------------------------------------------------------------------
+
+ def __subresources__(self):
+ """
+ Note: Although packages are (rightfully) specified concurrent, apt tasks
+ will be exlusive thanks to the use of a lock in the package manager.
+ """
+ if self.names:
+ packages = [Package(node=self.node, package_name=name, owner=self)
+ for name in self.names]
+ return Resource.__concurrent__(*packages)
+ else:
+ return None
+
diff --git a/vicn/resource/linux/phy_interface.py b/vicn/resource/linux/phy_interface.py
new file mode 100644
index 00000000..c1aef27e
--- /dev/null
+++ b/vicn/resource/linux/phy_interface.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.resource import BaseResource
+from vicn.resource.interface import Interface
+
+class PhyInterface(Interface):
+ """
+ Resource: PhyInterface
+
+ Physical network interface.
+ """
+
+ __type__ = BaseResource
+
+ device_name = Attribute(String, description = 'Name of the DpdkDevice',
+ mandatory = True)
+ pci_address = Attribute(String, description = "Device's PCI bus address",
+ mandatory = True)
+ mac_address = Attribute(String, description = "Device's MAC address",
+ mandatory=True)
+ ip_address = Attribute(String, description = "Device's IP address")
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ if not self.name:
+ self.name = self.node.name + '-' + self.device_name
diff --git a/vicn/resource/linux/phy_link.py b/vicn/resource/linux/phy_link.py
new file mode 100644
index 00000000..878cf7c6
--- /dev/null
+++ b/vicn/resource/linux/phy_link.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute
+from vicn.core.task import inline_task
+from vicn.resource.channel import Channel
+from vicn.resource.linux.phy_interface import PhyInterface
+
+class PhyLink(Channel):
+ """
+ Resource: PhyLink
+
+ Physical Link to inform the orchestrator about Layer2 connectivity.
+ """
+
+ src = Attribute(PhyInterface, description = 'Source interface',
+ mandatory = True)
+ dst = Attribute(PhyInterface, description = 'Destination interface',
+ mandatory = True)
+
+ @inline_task
+ def __initialize__(self):
+ self.src.set('channel', self)
+ self.dst.set('channel', self)
diff --git a/vicn/resource/linux/physical.py b/vicn/resource/linux/physical.py
new file mode 100644
index 00000000..e5eba2d3
--- /dev/null
+++ b/vicn/resource/linux/physical.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import stat
+import logging
+import subprocess
+import shlex
+
+from netmodel.model.type import String, Integer
+from netmodel.util.misc import is_local_host
+from netmodel.util.socket import check_port
+from vicn.core.attribute import Attribute
+from vicn.core.commands import Command, ReturnValue
+from vicn.core.exception import ResourceNotFound
+from vicn.core.task import Task, task
+from vicn.resource.node import Node, DEFAULT_USERNAME
+from vicn.resource.node import DEFAULT_SSH_PUBLIC_KEY
+from vicn.resource.node import DEFAULT_SSH_PRIVATE_KEY
+
+log = logging.getLogger(__name__)
+
+CMD_SSH_COPY_ID = 'ssh-copy-id {ssh_options} -i {public_key} -p {port} ' \
+ '{user}@{host}'
+CMD_SSH = 'ssh {ssh_options} -i {private_key} -p {port} ' \
+ '{user}@{host} {command}'
+CMD_SSH_NF = 'ssh -n -f {ssh_options} -i {private_key} -p {port} ' \
+ '{user}@{host} {command}'
+
+class Physical(Node):
+ """
+ Resource: Physical
+
+ Physical node
+ """
+
+ hostname = Attribute(String, description = 'Hostname or IP address',
+ mandatory = True)
+ ssh_port = Attribute(Integer, description = 'SSH port number',
+ default = 22)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.node_with_kernel = self
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @task
+ def __get__(self, attributes=None):
+ if not check_port(self.hostname, self.ssh_port):
+ raise ResourceNotFound
+
+ def __create__(self):
+ tasks = list()
+ modes = (True, False) if DEFAULT_USERNAME != 'root' else (True,)
+ for as_root in modes:
+ tasks.append(self._setup_ssh_key(as_root))
+ return Task.__concurrent__(*tasks)
+
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ @task
+ def _setup_ssh_key(self, as_root):
+ os.chmod(DEFAULT_SSH_PUBLIC_KEY, stat.S_IRUSR | stat.S_IWUSR)
+ os.chmod(DEFAULT_SSH_PRIVATE_KEY, stat.S_IRUSR | stat.S_IWUSR)
+ cmd_params = {
+ 'public_key' : DEFAULT_SSH_PUBLIC_KEY,
+ 'ssh_options' : '',
+ 'port' : self.ssh_port,
+ 'user' : 'root' if as_root else DEFAULT_USERNAME,
+ 'host' : self.hostname,
+ }
+
+ c = Command(CMD_SSH_COPY_ID, parameters = cmd_params)
+
+ return self._do_execute_process(c.full_commandline, output=False)
+
+ #--------------------------------------------------------------------------
+ # Public API
+ #--------------------------------------------------------------------------
+
+ def _do_execute_process(self, command, output = False):
+ p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
+ if output:
+ out, err = p.communicate()
+ return ReturnValue(p.returncode, stdout=out, stderr=err)
+
+ p.wait()
+ return ReturnValue(p.returncode)
+
+ def _do_execute_ssh(self, command, output=False, as_root=False,
+ ssh_options=None):
+ if not ssh_options:
+ ssh_options = dict()
+ cmd_params = {
+ 'private_key' : DEFAULT_SSH_PRIVATE_KEY,
+ 'ssh_options' : ' '.join(['-o {}={}'.format(k, v)
+ for k, v in ssh_options.items()]),
+ 'port' : self.ssh_port,
+ 'user' : 'root' if as_root else DEFAULT_USERNAME,
+ 'host' : self.hostname,
+ 'command' : shlex.quote(command),
+ }
+
+ if (command[-1] != '&'):
+ c = Command(CMD_SSH, parameters = cmd_params)
+ else:
+ c = Command(CMD_SSH_NF, parameters = cmd_params)
+
+ return self._do_execute_process(c.full_commandline_nobashize, output)
+
+ def execute(self, command, output=False, as_root=False):
+ if is_local_host(self.hostname):
+ rv = self._do_execute_process(command, output = output)
+ else:
+ rv = self._do_execute_ssh(command, output = output,
+ as_root = as_root)
+ return rv
diff --git a/vicn/resource/linux/repository.py b/vicn/resource/linux/repository.py
new file mode 100644
index 00000000..f3e70565
--- /dev/null
+++ b/vicn/resource/linux/repository.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.resource.application import Application
+
+class Repository(Application):
+ """
+ Resource: Repository
+
+ deb package repository
+
+ Note: As PackageManager uses a Repository, this resource cannot be a
+ LinuxApplication resource. We have no package to install since they are
+ part of any basic distribution install.
+ """
+
+ repo_name = Attribute(String, description = 'Name of the repository',
+ default = 'vicn')
+ directory = Attribute(String, description = 'Directory holding packages',
+ default = '')
+ distributions = Attribute(String,
+ description = 'List of distributions served by this repository',
+ multiplicity = Multiplicity.ManyToMany,
+ default = ['sid', 'trusty', 'xenial'])
diff --git a/vicn/resource/linux/service.py b/vicn/resource/linux/service.py
new file mode 100644
index 00000000..3eb753fc
--- /dev/null
+++ b/vicn/resource/linux/service.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import CategoryResource
+from vicn.core.task import inline_task, BashTask, EmptyTask
+from vicn.resource.linux.application import LinuxApplication
+
+log = logging.getLogger(__name__)
+
+CMD_START = 'service {service_name} start'
+CMD_STOP = 'service {service_name} stop'
+CMD_RESTART = 'service {service_name} restart'
+CMD_STOP_START = 'service {service_name} stop && sleep 1 && ' \
+ 'service {service_name} start'
+
+class Service(LinuxApplication):
+ """Service resource
+
+ This resource wraps a Linux Service, and ensure the service is started
+ (resp. stopped) during setup (resp. teardown).
+
+ Required tags:
+ __service_name__ (str): all classes that inherit from Service should
+ inform this tag which gives the name of the service known to the
+ system.
+
+ TODO:
+ * Support for upstart, sysvinit and systemd services.
+ * Start and Stop method
+ * Status attribute
+ """
+
+ __type__ = CategoryResource
+
+
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __method_restart__(self):
+ return BashTask(self.node, CMD_RESTART,
+ {'service_name': self.__service_name__})
+
+ def __method_start__(self):
+ return BashTask(self.node, CMD_START,
+ {'service_name': self.__service_name__})
+
+
+ def __create__(self):
+ if self.__service_name__ == 'lxd':
+ log.warning('Not restarting LXD')
+ return EmptyTask()
+
+ if self.__service_name__ == 'dnsmasq':
+ return BashTask(self.node, CMD_STOP_START,
+ {'service_name': self.__service_name__})
+
+ return self.__method_restart__()
+
+
+ def __delete__(self):
+ return BashTask(self.node, CMD_STOP,
+ {'service_name': self.__service_name__})
+
diff --git a/vicn/resource/linux/sym_veth_pair.py b/vicn/resource/linux/sym_veth_pair.py
new file mode 100644
index 00000000..bf79a69b
--- /dev/null
+++ b/vicn/resource/linux/sym_veth_pair.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import random
+import string
+
+from netmodel.model.type import Integer
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.state import ResourceState, AttributeState
+from vicn.core.task import BashTask, get_attributes_task
+from vicn.core.task import async_task, task, inline_task
+from vicn.core.task import run_task
+from vicn.resource.interface import Interface
+from vicn.resource.node import Node
+from vicn.resource.linux.net_device import NonTapBaseNetDevice
+from vicn.resource.linux.link import CMD_DELETE_IF_EXISTS
+from vicn.resource.linux.link import CMD_UP
+
+CMD_CREATE='''
+# Create veth pair in the host node
+ip link add name {tmp_side1} type veth peer name {tmp_side2}
+ip link set dev {tmp_side1} netns {pid[0]} name {side1_device_name}
+ip link set dev {tmp_side2} netns {pid[1]} name {side2_device_name}
+'''
+
+class SymVethPair(Resource):
+ """
+ Resource: SymVethPair
+
+ This resource is used in VPPBridge. The main difference with the Link
+ resource is that is it not a channel.
+ """
+
+ node1 = Attribute(Node,
+ description = 'Node on which one side of the veth will sit',
+ mandatory = True)
+ node2 = Attribute(Node,
+ description = 'Node on which the other side of the veth will sit',
+ mandatory = True)
+ capacity = Attribute(Integer,
+ description = 'Capacity of the veth pair (Mb/s)')
+ side1 = Attribute(Interface, description = 'Source interface')
+ side2 = Attribute(Interface, description = 'Destination interface')
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ async def _commit(self):
+ # see link.py for explanations
+ manager = self._state.manager
+ await manager._set_resource_state(self.side1,
+ ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.side2,
+ ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.side1, ResourceState.CREATED)
+ await manager._set_resource_state(self.side2, ResourceState.CREATED)
+ await manager._set_attribute_state(self, 'side1', AttributeState.CLEAN)
+ await manager._set_attribute_state(self, 'side2', AttributeState.CLEAN)
+ manager.commit_resource(self.side1)
+ manager.commit_resource(self.side2)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __initialize__(self):
+ self.side1 = NonTapBaseNetDevice(node = self.node1,
+ device_name = self.node2.name,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.side2 = NonTapBaseNetDevice(node = self.node2,
+ device_name = self.node1.name,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.side1.remote = self.side2
+ self.side2.remote = self.side1
+
+ @async_task
+ async def __get__(self):
+ manager = self._state.manager
+
+ try:
+ await run_task(self.side1.__get__(), manager)
+ await run_task(self.side2.__get__(), manager)
+ except ResourceNotFound:
+ raise ResourceNotFound
+
+ await self._commit()
+
+ def __create__(self):
+ assert self.node1.get_type() == 'lxccontainer'
+ assert self.node2.get_type() == 'lxccontainer'
+
+ node1_host = self.node1.node
+ node2_host = self.node2.node
+
+ assert node1_host == node2_host
+ host = node1_host
+
+ # Sometimes a down interface persists on one side
+ delif_side1 = BashTask(self.node1, CMD_DELETE_IF_EXISTS,
+ {'interface': self.side1})
+ delif_side2 = BashTask(self.node2, CMD_DELETE_IF_EXISTS,
+ {'interface': self.side2})
+
+ pid_node1 = get_attributes_task(self.node1, ['pid'])
+ pid_node2 = get_attributes_task(self.node2, ['pid'])
+
+ tmp_side1 = 'tmp-veth-' + ''.join(random.choice(
+ string.ascii_uppercase + string.digits) for _ in range(5))
+ tmp_side2 = 'tmp-veth-' + ''.join(random.choice(
+ string.ascii_uppercase + string.digits) for _ in range(5))
+
+ create = BashTask(host, CMD_CREATE,
+ {'side1_device_name': self.side1.device_name,
+ 'side2_device_name': self.side2.device_name,
+ 'tmp_side1': tmp_side1, 'tmp_side2': tmp_side2})
+
+ up_side1 = BashTask(self.node1, CMD_UP, {'interface': self.side1})
+ up_side2 = BashTask(self.node2, CMD_UP, {'interface': self.side2})
+
+ @async_task
+ async def set_state():
+ await self._commit()
+
+ delif = delif_side1 | delif_side2
+ up = up_side1 | up_side2
+ pid = pid_node1 | pid_node2
+ return ((delif > (pid @ create)) > up) > set_state()
+
+ def __delete__(self):
+ raise NotImplementedError
diff --git a/vicn/resource/linux/tap_device.py b/vicn/resource/linux/tap_device.py
new file mode 100644
index 00000000..68c0b9c7
--- /dev/null
+++ b/vicn/resource/linux/tap_device.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.task import BashTask
+from vicn.resource.linux.net_device import BaseNetDevice
+
+CMD_CREATE='ip tuntap add name {netdevice.device_name} mode tap'
+CMD_FLUSH_IP='ip addr flush dev {device_name}'
+CMD_SET_IP_ADDRESS='ip addr add dev {netdevice.device_name} 0.0.0.0'
+
+class TapDevice(BaseNetDevice):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'tap'
+ self.netdevice_type = 'tap'
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE, {'netdevice': self})
+
+ def _set_ip_address(self):
+ if self.ip_address is None:
+ # Unset IP
+ return BashTask(self.node, CMD_FLUSH_IP,
+ {'device_name': self.device_name})
+ return BashTask(self.node, CMD_SET_IP_ADDRESS,
+ {'netdevice': self})
+
+class TapChannel(TapDevice):
+ station_name = Attribute(String)
+ channel_name = Attribute(String)
diff --git a/vicn/resource/linux/traceroute.py b/vicn/resource/linux/traceroute.py
new file mode 100644
index 00000000..2f8cb2c9
--- /dev/null
+++ b/vicn/resource/linux/traceroute.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.application import LinuxApplication
+
+class Traceroute(LinuxApplication):
+ __package_names__ = ['traceroute']
+
diff --git a/vicn/resource/linux/veth_pair.py b/vicn/resource/linux/veth_pair.py
new file mode 100644
index 00000000..53fa9bf8
--- /dev/null
+++ b/vicn/resource/linux/veth_pair.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import random
+import string
+
+from vicn.resource.linux.net_device import SlaveBaseNetDevice
+from vicn.core.task import BashTask, get_attributes_task
+
+# ip link add veth0 type veth peer name veth1
+
+CMD_CREATE='''
+# Create veth pair in the host node
+ip link add name {interface.host.device_name} type veth peer name {tmp_name}
+# The host interface will always be up...
+ip link set dev {interface.host.device_name} up
+# Move interface into container and rename it
+ip link set dev {tmp_name} netns {pid} name {interface.device_name}
+'''
+CMD_UP='''
+ip link set dev {interface.device_name} up
+'''
+
+# see:
+# http://stackoverflow.com/questions/22780927/lxc-linux-containers-add-new-network-interface-without-restarting
+
+class VethPair(SlaveBaseNetDevice):
+ # Do not need the parent attribute...
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'veth'
+ self.netdevice_type = 'veth'
+
+ def __create__(self):
+ assert self.node.__class__.__name__ == 'LxcContainer'
+ host = self.node.node
+ pid = get_attributes_task(self.node, ['pid'])
+ tmp_name = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase \
+ + string.digits) for _ in range(5))
+ create = BashTask(host, CMD_CREATE, {'tmp_name': tmp_name,
+ 'interface': self})
+ up = BashTask(self.node, CMD_UP, {'interface': self})
+ bridge = host.bridge_manager.add_interface(host.bridge.device_name,
+ self.host.device_name)
+ return ((pid @ create) > up) > bridge
+
+ # ... IP and UP missing...
diff --git a/vicn/resource/lxd/__init__.py b/vicn/resource/lxd/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/lxd/__init__.py
diff --git a/vicn/resource/lxd/lxc_container.py b/vicn/resource/lxd/lxc_container.py
new file mode 100644
index 00000000..afa64aba
--- /dev/null
+++ b/vicn/resource/lxd/lxc_container.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import shlex
+import time
+
+# Suppress logging from pylxd dependency on ws4py
+# (this needs to be included before pylxd)
+from ws4py import configure_logger
+configure_logger(level=logging.ERROR)
+import pylxd
+
+from netmodel.model.type import String, Integer, Bool, Self
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute, Reference, Multiplicity
+from vicn.core.commands import ReturnValue
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import task, inline_task, BashTask
+from vicn.resource.linux.net_device import NetDevice
+from vicn.resource.node import Node
+from vicn.resource.vpp.scripts import APPARMOR_VPP_PROFILE
+
+log = logging.getLogger(__name__)
+
+# Default name of VICN management/monitoring interface
+DEFAULT_LXC_NETDEVICE = 'eth0'
+
+# Default remote server (pull mode only)
+DEFAULT_SOURCE_URL = 'https://cloud-images.ubuntu.com/releases/'
+
+# Default protocol used to download images (lxd or simplestreams)
+DEFAULT_SOURCE_PROTOCOL = 'simplestreams'
+
+# Commands used to interact with LXD (in addition to pylxd bindings)
+CMD_GET_PID='lxc info {container.name} | grep Pid | cut -d " " -f 2'
+
+# Type: ContainerName
+ContainerName = String(max_size = 64, ascii = True,
+ forbidden = ('/', ',', ':'))
+
+class LxcContainer(Node):
+ """
+ Resource: LxcContainer
+
+ Todo:
+ - Remove VPP dependency
+ - The bridge is not strictly needed, but we currently have no automated
+ way to determine whether we need it or not
+ - The management interface should be added by VICN, not part of the
+ resource, and its name should be determined automatically.
+ """
+
+ architecture = Attribute(String, description = 'Architecture',
+ default = 'x86_64')
+ container_name = Attribute(ContainerName,
+ description = 'Name of the container',
+ default = Reference(Self, 'name'))
+ ephemeral = Attribute(Bool, description = 'Ephemeral container flag',
+ default = False)
+ node = Attribute(Node,
+ description = 'Node on which the container is running',
+ mandatory = True,
+ requirements = [
+ # We need the hypervisor setup to be able to check for the
+ # container; more generally, all dependencies
+ Requirement('lxd_hypervisor'), # not null
+ # The bridge is not strictly needed, but we currently have
+ # no automated way to determine whether we need it or not
+ Requirement('bridge'),
+ # A DNS server is required to provide internet connectivity to
+ # the containers
+ Requirement('dns_server'),
+ ])
+ profiles = Attribute(String, multiplicity = Multiplicity.OneToMany,
+ default = ['default'])
+ image = Attribute(String, description = 'image', default = None)
+ is_image = Attribute(Bool, defaut = False)
+ pid = Attribute(Integer, description = 'PID of the container')
+
+ #--------------------------------------------------------------------------
+ # Constructor / Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._container = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __initialize__(self):
+ """
+ We need to intanciate VPPHost before container creation.
+ """
+ self.node_with_kernel = Reference(self, 'node')
+
+ # We automatically add the management/monitoring interface
+ self._host_interface = NetDevice(node = self,
+ owner = self,
+ monitored = False,
+ device_name = DEFAULT_LXC_NETDEVICE)
+ self._state.manager.commit_resource(self._host_interface)
+
+ for iface in self.interfaces:
+ if iface.get_type() == "dpdkdevice":
+ self.node.vpp_host.dpdk_devices.append(iface.pci_address)
+
+ if 'vpp' in self.profiles:
+ dummy = self.node.vpp_host.uio_devices
+
+ @task
+ def __get__(self):
+ client = self.node.lxd_hypervisor.client
+ try:
+ self._container = client.containers.get(self.name)
+ except pylxd.exceptions.NotFound:
+ raise ResourceNotFound
+
+ def __create__(self):
+ """
+ Make sure vpp_host is instanciated before starting the container.
+ """
+ wait_vpp_host = wait_resource_task(self.node.vpp_host)
+ create = self._create_container()
+ start = self.__method_start__()
+ return wait_vpp_host > (create > start)
+
+ @task
+ def _create_container(self):
+ container = self._get_container_description()
+ log.debug('Container description: {}'.format(container))
+ client = self.node.lxd_hypervisor.client
+ self._container = client.containers.create(container, wait=True)
+ self._container.start(wait = True)
+
+ def _get_container_description(self):
+ # Base configuration
+ container = {
+ 'name' : self.container_name,
+ 'architecture' : self.architecture,
+ 'ephemeral' : self.ephemeral,
+ 'profiles' : ['default'],
+ 'config' : {},
+ 'devices' : {},
+ }
+
+ # DEVICES
+
+ devices = {}
+ # FIXME Container profile support is provided by setting changes into
+ # configuration (currently only vpp profile is supported)
+ for profile in self.profiles:
+ if profile == 'vpp':
+ # Set the new apparmor profile. This will be created in VPP
+ # application
+ # Mount hugetlbfs in the container.
+ container['config']['raw.lxc'] = APPARMOR_VPP_PROFILE
+ container['config']['security.privileged'] = 'true'
+
+ for device in self.node.vpp_host.uio_devices:
+ container['devices'][device] = {
+ 'path' : '/dev/{}'.format(device),
+ 'type' : 'unix-char' }
+
+ # NETWORK (not for images)
+
+ if not self.is_image:
+ container['config']['user.network_mode'] = 'link-local'
+ device = {
+ 'type' : 'nic',
+ 'name' : self.host_interface.device_name,
+ 'nictype' : 'bridged',
+ 'parent' : self.node.bridge.device_name,
+ }
+ device['hwaddr'] = AddressManager().get_mac(self)
+ prefix = 'veth-{}'.format(self.container_name)
+ device['host_name'] = AddressManager().get('device_name', self,
+ prefix = prefix, scope = prefix)
+
+ container['devices'][device['name']] = device
+
+
+ # SOURCE
+
+ image_names = [alias['name'] for alias in self.node.lxd_hypervisor.aliases]
+ image_exists = self.image is not None and self.image in image_names
+
+ if image_exists:
+ container['source'] = {
+ 'type' : 'image',
+ 'mode' : 'local',
+ 'alias' : self.image,
+ }
+ else:
+ container['source'] = {
+ 'type' : 'image',
+ 'mode' : 'pull',
+ 'server' : DEFAULT_SOURCE_URL,
+ 'protocol' : DEFAULT_SOURCE_PROTOCOL,
+ 'alias' : self.dist,
+ }
+
+ log.info('Creating container: {}'.format(container))
+ return container
+
+ @task
+ def __delete__(self):
+ log.info("Delete container {}".format(self.container_name))
+ self.node.lxd_hypervisor.client.containers.remove(self.name)
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_pid(self):
+ """
+ Attribute: pid (getter)
+ """
+ return BashTask(self.node, CMD_GET_PID, {'container': self},
+ parse = lambda rv: {'pid': rv.stdout.strip()})
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ @task
+ def __method_start__(self):
+ """
+ Method: Start the container
+ """
+ self._container.start(wait = True)
+
+ @task
+ def __method_stop__(self):
+ """
+ Method: Stop the container
+ """
+ self._container.stop(wait = True)
+
+ @task
+ def __method_to_image__(self):
+ """
+ Returns:
+ Image metadata as returned by LXD REST API.
+ """
+ publish_description = {
+ "public": True,
+ "properties": {
+ "os": "Ubuntu",
+ "architecture": "x86_64",
+ "description": "Image generated from container {}".format(
+ self.container_name),
+ },
+ "source": {
+ "type": "container", # One of "container" or "snapshot"
+ "name": 'image-{}'.format(self.container_name),
+ }
+ }
+ return self.node.lxd_hypervisor.publish_image(publish_description)
+
+ #--------------------------------------------------------------------------
+ # Node API
+ #--------------------------------------------------------------------------
+
+ def execute(self, command, output = False, as_root = False):
+ """
+ Executes a command on the node
+
+ Params:
+ output (bool) : Flag determining whether the method should return
+ the output value.
+ as_root (bool) : Flag telling whether the command should be
+ executed as root.
+
+ Returns:
+ ReturnValue containing exit code, and eventually stdout and stderr.
+
+ Raises
+ Exception in case of error
+
+ The node exposes an interface allowing command execution through LXD.
+ We don't currently use an eventually available SSH connection.
+ """
+
+ ret = self._container.execute(shlex.split(command))
+
+ # NOTE: pylxd documents the return value as a tuple, while it is in
+ # fact a ContainerExecuteResult object
+ if not hasattr(ret, "exit_code"):
+ log.error("LXD return value does not have an exit code. "
+ "Try installing pylxd>=2.2.2 with pip3")
+ import sys; sys.exit(1)
+
+ args = (ret.exit_code,)
+ if output:
+ args += (ret.stdout, ret.stderr)
+ return ReturnValue(*args)
diff --git a/vicn/resource/lxd/lxc_image.py b/vicn/resource/lxd/lxc_image.py
new file mode 100644
index 00000000..2cc7220d
--- /dev/null
+++ b/vicn/resource/lxd/lxc_image.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import time
+
+from netmodel.model.type import Self
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource import Resource
+from vicn.core.task import task, inline_task
+from vicn.resource.linux.application import LinuxApplication as Application
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+class LxcImage(Resource):
+ """
+ Resource: LxcImage
+ """
+
+ node = Attribute(Node, description = 'Node on which the image is stored',
+ mandatory = True,
+ requirements = [
+ Requirement('lxd_hypervisor')
+ ])
+ image = Attribute(Self, description = 'image', default = None)
+ applications = Attribute(Application, multiplicity = Multiplicity.OneToMany)
+
+ #---------------------------------------------------------------------------
+ # Constructor / Accessors
+ #---------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ self.fingerprint = None
+ self._tmp_container = None
+ super().__init__(*args, **kwargs)
+
+ #---------------------------------------------------------------------------
+ # Resource lifecycle
+ #---------------------------------------------------------------------------
+
+ @task
+ def __get__(self):
+ aliases = [alias['name'] for images in self.node.lxd_hypervisor.client.images.all()
+ for alias in images.aliases]
+ if not self.image in aliases:
+ raise ResourceNotFound
+
+ @inline_task
+ def __create__(self):
+ log.warning('Image creation is currently disabled')
+ return
+
+
+ @task
+ def __create_DISABLED__(self):
+ """
+ Image creation consists in setting up a temporary container, stopping
+ it, publishing an image of it, setting an alias, and deleting it.
+ """
+
+
+ tmp_container.setup()
+
+ print("TODO: Installing applications...")
+ for application in self.applications:
+ print('Installing application on image')
+ application.setup()
+
+ # XXX stop() hangs if run to early wrt container start
+ # - is it related to ZFS ? is it a more general problem ?
+ time.sleep(5)
+
+ print("I: Stopping container")
+ tmp_container.stop()
+
+ print("I: Publishing image")
+ image_metadata = tmp_container.publish_image() # METHOD !
+ print("MD=", image_metadata)
+ self.fingerprint = image_metadata['fingerprint']
+ self.set_alias()
+
+ tmp_container.delete()
+
+ @task
+ def __delete__(self):
+ self.node.lxd_hypervisor.client.images.delete(self.name)
+
+ #---------------------------------------------------------------------------
+ # Public methods
+ #---------------------------------------------------------------------------
+
+ def set_alias(self):
+ alias_dict = {
+ "description": "Ubuntu 14.04 image with ICN software already installed",
+ "target": self.fingerprint,
+ "name": self.name
+ }
+ self.node.lxd_hypervisor.set_alias(alias_dict)
diff --git a/vicn/resource/lxd/lxd_hypervisor.py b/vicn/resource/lxd/lxd_hypervisor.py
new file mode 100644
index 00000000..328f3fdf
--- /dev/null
+++ b/vicn/resource/lxd/lxd_hypervisor.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#-------------------------------------------------------------------------------
+# NOTES
+#-------------------------------------------------------------------------------
+# - lxd >= 2.0.4 is required
+# daemon/container: Remember the return code in the non wait-for-websocket
+# case (Issue #2243)
+# - Reference: https://github.com/lxc/lxd/tree/master/doc
+#-------------------------------------------------------------------------------
+
+import logging
+import os
+from pylxd import Client
+from pylxd.exceptions import LXDAPIException
+
+from netmodel.model.type import String, Integer
+from vicn.core.attribute import Attribute, Multiplicity, Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, task
+from vicn.resource.linux.application import LinuxApplication as Application
+from vicn.resource.linux.service import Service
+from vicn.resource.linux.certificate import Certificate
+
+# Suppress non-important logging messages from requests and urllib3
+logging.getLogger("requests").setLevel(logging.WARNING)
+logging.getLogger("urllib3").setLevel(logging.WARNING)
+log = logging.getLogger(__name__)
+
+# FIXME use system-wide files
+DEFAULT_CERT_PATH = os.path.join(os.path.dirname(__file__),
+ '..', '..', '..', 'config', 'lxd_client_cert', 'client_cert.pem')
+DEFAULT_KEY_PATH = os.path.join(os.path.dirname(__file__),
+ '..', '..', '..', 'config', 'lxd_client_cert', 'client_key.pem')
+
+# FIXME hardcoded password for LXD server
+DEFAULT_TRUST_PASSWORD = 'vicn'
+
+DEFAULT_LXD_STORAGE = 100 # GB
+
+# Commands used to interact with the LXD hypervisor
+CMD_LXD_CHECK_INIT = 'lsof -i:{lxd.lxd_port}'
+
+CMD_LXD_INIT_BASE = 'lxd init --auto '
+CMD_LXD_INIT='''
+{base}
+lxc profile unset default environment.http_proxy
+lxc profile unset default user.network_mode
+'''
+
+#------------------------------------------------------------------------------
+# Subresources
+#------------------------------------------------------------------------------
+
+class LxdInit(Application):
+ __package_names__ = ['lxd', 'zfsutils-linux', 'lsof']
+
+ def __get__(self):
+ return BashTask(self.owner.node, CMD_LXD_CHECK_INIT,
+ {'lxd': self.owner})
+
+ def __create__(self):
+ cmd_params = {
+ 'storage-backend' : self.owner.storage_backend,
+ 'network-port' : self.owner.lxd_port,
+ 'network-address' : '0.0.0.0',
+ 'trust-password' : DEFAULT_TRUST_PASSWORD,
+ }
+
+ if self.owner.storage_backend == 'zfs':
+ cmd_params['storage-pool'] = self.owner.zfs_pool
+
+ # zpool list -H -o name,cap
+ # don't create it if it exists
+ zfs_pool_exists = True
+
+ if zfs_pool_exists:
+ cmd_params['storage-create-loop'] = self.owner.storage_size
+ elif self.owner.storage_backend == 'dir':
+ raise NotImplementedError
+ else:
+ raise NotImplementedError
+ cmd = CMD_LXD_INIT_BASE + ' '.join('--{}={}'.format(k, v)
+ for k, v in cmd_params.items())
+
+ # error: Failed to create the ZFS pool: The ZFS modules are not loaded.
+ # Try running '/sbin/modprobe zfs' as root to load them.
+ # zfs-dkms in the host
+ return BashTask(self.owner.node, CMD_LXD_INIT, {'base': cmd},
+ as_root = True)
+
+ def __delete__(self):
+ raise NotImplementedError
+
+class LxdInstallCert(Resource):
+ certificate = Attribute(Certificate, mandatory = True)
+
+ @task
+ def __get__(self):
+ try:
+ self.owner.client.certificates.all()
+ except LXDAPIException as e:
+ if e.response.raw.status == 403:
+ raise ResourceNotFound
+ raise
+ except Exception:
+ # Missing certificates raises an exception
+ raise ResourceNotFound
+
+
+ @task
+ def __create__(self):
+ """
+ Some operations with containers requires the client to be trusted by
+ the server. So at the beginning we have to upload a (self signed)
+ client certificate for the LXD daemon.
+ """
+ log.info('Adding certificate on LXD')
+ self.owner.client.authenticate(DEFAULT_TRUST_PASSWORD)
+ if not self.owner.client.trusted:
+ raise Exception
+
+#------------------------------------------------------------------------------
+
+class LxdHypervisor(Service):
+ """
+ Resource: LxdHypervisor
+
+ Manages a LXD hypervisor, accessible through a REST API.
+ """
+ __service_name__ = 'lxd'
+
+ lxd_port = Attribute(Integer, description = 'LXD REST API port',
+ default = 8443)
+ storage_backend = Attribute(String, description = 'Storage backend',
+ default = 'zfs',
+ choices = ['zfs'])
+ storage_size = Attribute(Integer, description = 'Storage size',
+ default = DEFAULT_LXD_STORAGE) # GB
+ zfs_pool = Attribute(String, description = 'ZFS pool',
+ default='vicn')
+
+ # Just overload attribute with a new reverse
+ node = Attribute(
+ reverse_name = 'lxd_hypervisor',
+ reverse_description = 'LXD hypervisor',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
+
+ #--------------------------------------------------------------------------
+ # Constructor / Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._client = None
+ self._images = None
+
+ @property
+ def client(self):
+ if not self._client:
+ self._client = Client(endpoint = self._get_server_url(),
+ cert=(DEFAULT_CERT_PATH, DEFAULT_KEY_PATH),
+ verify=False)
+ return self._client
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ lxd_init = LxdInit(owner=self, node = self.node)
+ lxd_local_cert = Certificate(node = Reference(self, 'node'),
+ cert = DEFAULT_CERT_PATH,
+ key = DEFAULT_KEY_PATH,
+ owner = self)
+ lxd_cert_install = LxdInstallCert(node = Reference(self, 'node'),
+ certificate = lxd_local_cert,
+ owner = self)
+
+ return (lxd_init | lxd_local_cert) > lxd_cert_install
+
+ #--------------------------------------------------------------------------
+ # Private methods
+ #--------------------------------------------------------------------------
+
+ def _get_server_url(self):
+ return 'https://{0}:{1}'.format(self.node.hostname, self.lxd_port)
+
+ #--------------------------------------------------------------------------
+ # Public interface
+ #--------------------------------------------------------------------------
+
+ @property
+ def images(self):
+ """
+ This method caches available images to minimize the number of queries
+ done when creating multiple containers.
+ """
+ if not self._images:
+ self._images = self.node.lxd_hypervisor.client.images.all()
+ return self._images
+
+ @property
+ def aliases(self):
+ return [alias for image in self.images for alias in image.aliases]
diff --git a/vicn/resource/node.py b/vicn/resource/node.py
new file mode 100644
index 00000000..bfb2f9ec
--- /dev/null
+++ b/vicn/resource/node.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import os
+
+from netmodel.model.type import Double, String, Self
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute
+from vicn.core.resource import Resource
+
+log = logging.getLogger(__name__)
+
+DEFAULT_USERNAME = 'root'
+DEFAULT_SSH_PRIVATE_KEY = os.path.join(os.path.dirname(__file__),
+ '..', '..', 'config', 'ssh_client_cert', 'ssh_client_key')
+DEFAULT_SSH_PUBLIC_KEY = os.path.join(os.path.dirname(__file__),
+ '..', '..', 'config', 'ssh_client_cert', 'ssh_client_key.pub')
+
+class Node(Resource):
+ """
+ Resource: Node
+ """
+
+ x = Attribute(Double, description = 'X coordinate',
+ default = 0.0)
+ y = Attribute(Double, description = 'Y coordinate',
+ default = 0.0)
+ category = Attribute(String)
+ os = Attribute(String, description = 'OS',
+ default = 'ubuntu',
+ choices = ['debian', 'ubuntu'])
+ dist = Attribute(String, description = 'Distribution name',
+ default = 'xenial',
+ choices = ['trusty', 'xenial', 'sid'])
+ arch = Attribute(String, description = 'Architecture',
+ default = 'amd64',
+ choices = ['amd64'])
+ node_with_kernel = Attribute(Self,
+ description = 'Node on which the kernel sits',
+ ro = True)
+
+ #---------------------------------------------------------------------------
+ # Constructor and Accessors
+ #---------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._host_interface = None
+
+ #---------------------------------------------------------------------------
+ # Public API
+ #---------------------------------------------------------------------------
+
+ @property
+ def host_interface(self):
+ """
+ We assume that any unmanaged interface associated to the host is the
+ main host interface. It should thus be declared in the JSON topology.
+ We might later perform some kind of auto discovery.
+
+ This unmanaged interface is only required to get the device_name:
+ - to create Veth (need a parent)
+ - to ssh a node, get its ip address (eg for the repo)
+ - to avoid loops in type specification
+
+ It is used for all nodes to provide network connectivity.
+ """
+
+ for interface in self.interfaces:
+ if not interface.managed or interface.owner is not None:
+ return interface
+
+ raise Exception('Cannot find host interface for node {}: {}'.format(
+ self, self.interfaces))
+
+ def execute(self, command, output = False, as_root = False):
+ raise NotImplementedError
diff --git a/vicn/resource/ns3/__init__.py b/vicn/resource/ns3/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/ns3/__init__.py
diff --git a/vicn/resource/ns3/emulated_channel.py b/vicn/resource/ns3/emulated_channel.py
new file mode 100644
index 00000000..08d7a14b
--- /dev/null
+++ b/vicn/resource/ns3/emulated_channel.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import random
+
+from netmodel.model.type import Integer
+from netmodel.util.socket import check_port
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource import BaseResource
+from vicn.core.resource_mgr import wait_resources
+from vicn.core.task import inline_task, async_task, task
+from vicn.core.task import BashTask, run_task
+from vicn.resource.channel import Channel
+from vicn.resource.linux.application import LinuxApplication as Application
+from vicn.resource.linux.net_device import NetDevice
+from vicn.resource.linux.tap_device import TapDevice
+from vicn.resource.linux.veth_pair import VethPair
+from vicn.resource.lxd.lxc_container import LxcContainer
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+class EmulatedChannel(Channel, Application):
+ """EmulatedChannel resource
+
+ This resources serves as a base class for wireless channels emulated by
+ means of ns3 simulation.
+
+ Attributes:
+ ap (Reference[node]): Reference to the AP node
+ stations (Reference[node]): Reference to the list of stations.
+ control_port (int): Port used to communicate with the management
+ interface of the simulation.
+
+ Implementation notes:
+ - Both AP and stations are allocated a separate VLAN to isolate broadcast
+ traffic and prevent loops on the bridge.
+ - We also need that all interfaces related to ap and stations are created
+ before we run the commandline (currently, dynamically adding/removing
+ AP and stations is not supported by the emulator). This is made
+ possible thanks to the key=True parameter, which makes sure the
+ attributes are processed before the __create__ is called.
+
+ Todo:
+ - Retrieve the process PID to kill it during __delete__
+ """
+
+ __resource_type__ = BaseResource
+
+ ap = Attribute(Node, description = 'AP', key = True)
+ stations = Attribute(Node, description = 'List of stations',
+ multiplicity = Multiplicity.OneToMany, key = True)
+ control_port = Attribute(Integer,
+ description = 'Control port for the simulation')
+
+ # Overloaded attributes
+ node = Attribute(requirements = [
+ Requirement('bridge')
+ ])
+
+ #--------------------------------------------------------------------------
+ # Constructor
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # AP (resp. stations) interfaces (for external connectivity) and
+ # tap_devices (for connection to emulator)
+ self._ap_if = None
+ self._ap_tap = None
+ self._sta_ifs = dict()
+ self._sta_taps = dict()
+
+ # Device names to be attached to the bridge (differs according to the
+ # node type, Physical or LxcContainer, and eventually None for an
+ # unmanaged stations)
+ self._ap_bridged = None
+ self._sta_bridged = dict()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ # NOTE: http://stackoverflow.com/questions/21141352/python-subprocess-
+ # calling-a-script-which-runs-a-background-process-hanging
+ # The output of the background scripts is still going to the same file
+ # descriptor as the child script, thus the parent script waits for it
+ # to finish.
+ cmd = '(' + self.__app_name__ + ' ' + self._get_cmdline_params() + \
+ '>/dev/null 2>&1) &'
+ return BashTask(self.node, cmd)
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Attribute handlers
+ #--------------------------------------------------------------------------
+
+ @async_task
+ async def _set_ap(self, ap=None):
+ if ap is None:
+ ap = self.ap
+ if ap is None:
+ log.info('Ignored setting ap to None...')
+ return
+
+ # Add a WiFi interface for the AP...
+ interfaces = list()
+ if isinstance(ap, LxcContainer):
+ # Ideally, We need to create a VethPair for each station
+ # This should be monitored for the total channel bw
+ host = NetDevice(node = ap.node,
+ device_name='vhh-' + ap.name + '-' + self.name,
+ monitored = False,
+ managed = False)
+ self._ap_if = VethPair(node = self.ap,
+ name = 'vh-' + ap.name + '-' + self.name,
+ device_name = 'vh-' + ap.name + '-' + self.name,
+ host = host,
+ owner = self)
+ self._ap_bridged = self._ap_if.host
+ else:
+ raise NotImplementedError
+ self._state.manager.commit_resource(self._ap_if)
+
+ interfaces.append(self._ap_if)
+
+ # Add a tap interface for the AP...
+ self._ap_tap = TapDevice(node = self.node,
+ owner = self,
+ device_name = 'tap-' + ap.name + '-' + self.name,
+ up = True,
+ promiscuous = True,
+ monitored = False)
+ self._state.manager.commit_resource(self._ap_tap)
+ interfaces.append(self._ap_tap)
+
+ # Wait for interfaces to be setup
+ await wait_resources(interfaces)
+
+ # NOTE: only set channel after the resource is created or it might
+ # create loops which, at this time, are not handled
+ self._ap_if.set('channel', self)
+
+ # Add interfaces to bridge
+ vlan = AddressManager().get('vlan', self, tag='ap')
+
+ # AS the container has created the VethPair already without Vlan, we
+ # need to delete and recreate it
+ task = self.node.bridge._remove_interface(self._ap_bridged)
+ await run_task(task, self._state.manager)
+ task = self.node.bridge._add_interface(self._ap_bridged, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(self._ap_tap, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ print('/!\ pass information to the running simulation')
+
+ @inline_task
+ def _get_ap(self):
+ return {'ap': None}
+
+ @inline_task
+ def _get_stations(self):
+ return {'stations': list()}
+
+ @async_task
+ async def _set_stations(self, stations=None):
+ print('adding stations...')
+ if stations is None:
+ stations = self.stations
+
+ for station in stations:
+ await self._add_station(station)
+
+ def _add_stations(self, stations):
+ raise NotImplementedError
+
+ @inline_task
+ def _remove_stations(self, station):
+ raise NotImplementedError
+
diff --git a/vicn/resource/ns3/emulated_lte_channel.py b/vicn/resource/ns3/emulated_lte_channel.py
new file mode 100644
index 00000000..8c7382cb
--- /dev/null
+++ b/vicn/resource/ns3/emulated_lte_channel.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.address_mgr import AddressManager
+from vicn.core.resource_mgr import wait_resources
+from vicn.core.task import run_task
+from vicn.resource.ns3.emulated_channel import EmulatedChannel
+from vicn.resource.linux.net_device import NetDevice
+
+DEFAULT_FADING_ENABLED = True
+DEFAULT_TW_BUFFER = 800000
+DEFAULT_NETMASK = 24
+
+class EmulatedLteChannel(EmulatedChannel):
+ """
+ Resource: EmulatedLteChannel
+
+ This resource uses ns3 based emulation to emulate a lte subnet with one pgw,
+ one enode B, and multiple UEs.
+
+ NOTE:
+ This model needs to be extended with LTE specific features like "network
+ resource" in the future. Currently it works in same way as wifi emulator
+ with no lte specific features.
+
+ Attributes:
+ ap (Reference[node]): Reference to the AP/pgw node
+ stations (Reference[node]): Reference to the list of UE.
+ control_port (int): Port used to communicate with the management
+ interface of the simulation.
+ """
+
+ __package_names__ = ['lte-emulator']
+ __app_name__ = 'lte_emulator'
+
+ #---------------------------------------------------------------------------
+ # Attribute handlers
+ #---------------------------------------------------------------------------
+
+ async def _add_station(self, station):
+ from vicn.resource.lxd.lxc_container import LxcContainer
+ from vicn.resource.linux.veth_pair import VethPair
+ from vicn.resource.linux.tap_device import TapChannel
+
+ interfaces = list()
+ # ... and each station
+ if not station.managed:
+ sta_if = None
+ else:
+ if isinstance(station, LxcContainer):
+ host = NetDevice(node = station.node,
+ device_name='vhh-' + station.name + '-' + self.name,
+ managed = False)
+ sta_if = VethPair(node = station,
+ name = 'vh-' + station.name + '-' + self.name,
+ device_name = 'vh-' + station.name + '-' + self.name,
+ host = host,
+ owner = self)
+ bridged_sta = sta_if.host
+ else:
+ raise NotImplementedError
+
+ if sta_if:
+ self._sta_ifs[station] = sta_if
+ self._sta_bridged[station] = bridged_sta
+ interfaces.append(sta_if)
+ self._state.manager.commit_resource(sta_if)
+
+ sta_tap = TapChannel(node = self.node,
+ owner = self,
+ device_name = 'tap-' + station.name + '-' + self.name,
+ up = True,
+ promiscuous = True,
+ station_name = station.name,
+ channel_name = self.name)
+ self._sta_taps[station] = sta_tap
+ interfaces.append(sta_tap)
+ self._state.manager.commit_resource(sta_tap)
+
+ # Wait for interfaces to be setup
+ await wait_resources(interfaces)
+
+ # Add interfaces to bridge
+ # One vlan per station is needed to avoid broadcast loops
+ vlan = AddressManager().get('vlan', sta_tap)
+ if sta_if:
+ sta_if.set('channel', self)
+
+ task = self.node.bridge._remove_interface(bridged_sta)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(bridged_sta,
+ vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(sta_tap, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ def _get_cmdline_params(self):
+
+ # IP have not been assign, use AddressManager for simplicity since it
+ # will remember the assignment
+ # NOTE: here the IP address passed to emulator program is hardcoded with
+ # a /24 mask(even if the associated IP with the station does not have a
+ # /24 mask). This is not a problem at all because the netmask passed to
+ # the emulator program has no impact on configuration in the emulator
+ # program. Indeed, the IP routing table in the emulator program are
+ # configured on a per address basis(one route per IP address) instead of
+ # on a per prefix basis(one route per prefix). This guarantees the IP
+ # routing will not change regardless of what netmask is. That is why we
+ # can always safely pass a hardcoded /24 mask to the emulator program.
+
+ sta_list = list() # list of identifiers
+ sta_macs = list() # list of macs
+ sta_taps = list()
+ sta_ips = list()
+ for station in self.stations:
+ if not station.managed:
+ interface = [i for i in station.interfaces if i.channel == self]
+ assert len(interface) == 1
+ interface = interface[0]
+
+ sta_list.append(interface.name)
+ sta_macs.append(interface.mac_address)
+ sta_ips.append(interface.ip_address + '/24')
+ else:
+ identifier = self._sta_ifs[station]._state.uuid._uuid
+ sta_list.append(identifier)
+
+ mac = self._sta_ifs[station].mac_address
+ sta_macs.append(mac)
+
+ # Preallocate IP address
+ ip = AddressManager().get_ip(self._sta_ifs[station]) + '/24'
+ sta_ips.append(ip)
+
+ tap = self._sta_taps[station].device_name
+ sta_taps.append(tap)
+
+ params = {
+ # Name of the tap between NS3 and the base station
+ 'bs-tap' : self._ap_tap.device_name,
+ # Number of stations
+ 'n-sta' : len(self._sta_taps),
+ # List of the stations of the simulation
+ 'sta-list' : ','.join(sta_list),
+ # List of the taps between NS3 and the mobile stations
+ 'sta-taps' : ','.join(sta_taps),
+ # List of the macs of the mobile stations
+ 'sta-macs' : ','.join(sta_macs),
+ # X position of the Base Station
+ 'bs-x' : 0, #self.ap.x,
+ # Y position of the Base Station
+ 'bs-y' : 0, #self.ap.y,
+ # Experiment ID
+ 'experiment-id' : 'vicn',
+ # Index of the base station
+ 'bs-name' : self._ap_tap.device_name,
+ # Base station IP address
+ 'bs-mac' : self._ap_if.mac_address,
+ # Control port for dynamically managing the stations movement
+ 'control-port' : self.control_port,
+ # Coma-separated list of stations' IP/netmask len
+ 'sta-ips' : ','.join(sta_ips),
+ # Base station IP/netmask len
+ 'bs-ip' : AddressManager().get_ip(self._ap_if) +
+ DEFAULT_NETMASK,
+ 'txBuffer' : '800000',
+ 'isFading' : 'true' if DEFAULT_FADING_ENABLED else 'false',
+ }
+
+ return ' '.join(['--{}={}'.format(k, v) for k, v in params.items()])
+
diff --git a/vicn/resource/ns3/emulated_wifi_channel.py b/vicn/resource/ns3/emulated_wifi_channel.py
new file mode 100644
index 00000000..088d4444
--- /dev/null
+++ b/vicn/resource/ns3/emulated_wifi_channel.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.ns3.emulated_channel import EmulatedChannel
+from vicn.resource.linux.net_device import NetDevice
+from vicn.core.address_mgr import AddressManager
+from vicn.core.resource_mgr import wait_resources
+from vicn.core.task import async_task, run_task
+
+class EmulatedWiFiChannel(EmulatedChannel):
+ """EmulatedWiFiChannel Resource
+
+ This resource uses the ns3 simulator to emulate an 80211n wireless channel.
+ It assume no interference between base stations, and thus the emulated
+ channel corresponds to a single base station.
+ """
+ __package_names__ = ['wifi-emulator']
+ __app_name__ = 'wifi_emulator'
+
+ #---------------------------------------------------------------------------
+ # Attribute handlers
+ #---------------------------------------------------------------------------
+
+ async def _add_station(self, station):
+ from vicn.resource.lxd.lxc_container import LxcContainer
+ from vicn.resource.linux.veth_pair import VethPair
+ from vicn.resource.linux.tap_device import TapChannel
+ from vicn.resource.linux.macvlan import MacVlan
+
+ interfaces = list()
+ if not station.managed:
+ sta_if = None
+ else:
+ if isinstance(station, LxcContainer):
+ host = NetDevice(node = station.node,
+ device_name='vhh-' + station.name + '-' + self.name,
+ managed = False)
+ sta_if = VethPair(node = station,
+ name = 'vh-' + station.name + '-' + self.name,
+ device_name = 'vh-' + station.name + '-' + self.name,
+ host = host,
+ owner = self)
+ bridged_sta = sta_if.host
+ else:
+ raise NotImplementedError
+
+ if sta_if:
+ self._sta_ifs[station] = sta_if
+ self._sta_bridged[station] = bridged_sta
+ interfaces.append(sta_if)
+ self._state.manager.commit_resource(sta_if)
+
+ sta_tap = TapChannel(node = self.node,
+ owner = self,
+ device_name = 'tap-' + station.name + '-' + self.name,
+ up = True,
+ promiscuous = True,
+ station_name = station.name,
+ channel_name = self.name)
+ self._sta_taps[station] = sta_tap
+ interfaces.append(sta_tap)
+ self._state.manager.commit_resource(sta_tap)
+
+ # Wait for interfaces to be setup
+ await wait_resources(interfaces)
+
+ # Add interfaces to bridge
+ # One vlan per station is needed to avoid broadcast loops
+ vlan = AddressManager().get('vlan', sta_tap)
+ # sta_tap choosen because always there
+ if sta_if:
+ sta_if.set('channel', self)
+
+ task = self.node.bridge._remove_interface(bridged_sta)
+ await run_task(task, self._state.manager)
+ task = self.node.bridge._add_interface(bridged_sta,
+ vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(sta_tap, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+
+ def _get_cmdline_params(self, ):
+
+ # sta-macs and sta-list for unmanaged stations
+ sta_list = list() # list of identifiers
+ sta_macs = list() # list of macs
+ sta_taps = list()
+ for station in self.stations:
+ if not station.managed:
+ interface = [i for i in station.interfaces if i.channel == self]
+ assert len(interface) == 1
+ interface = interface[0]
+
+ sta_list.append(interface.name)
+ sta_macs.append(interface.mac_address)
+ else:
+ identifier = self._sta_ifs[station]._state.uuid._uuid
+ sta_list.append(identifier)
+
+ mac = self._sta_ifs[station].mac_address
+ sta_macs.append(mac)
+
+ tap = self._sta_taps[station].device_name
+ sta_taps.append(tap)
+
+ params = {
+ # Name of the tap between NS3 and the base station
+ 'bs-tap' : self._ap_tap.device_name,
+ # Number of stations
+ 'n-sta' : len(self._sta_taps),
+ # List of the stations of the simulation # identifiers
+ 'sta-list' : ','.join(sta_list),
+ # List of the taps between NS3 and the mobile stations
+ 'sta-taps' : ','.join(sta_taps),
+ # List of the macs of the mobile stations
+ 'sta-macs' : ','.join(sta_macs),
+ # X position of the Base Station
+ 'bs-x' : 0, #self.ap.x,
+ # Y position of the Base Station
+ 'bs-y' : 0, #self.ap.y,
+ # Experiment ID
+ 'experiment-id' : 'vicn',
+ # Index of the base station
+ 'bs-name' : self._ap_tap.device_name,
+ # Base station MAC address
+ 'bs-mac' : self._ap_if.mac_address,
+ # Control port for dynamically managing the stations movement
+ 'control-port' : self.control_port,
+ }
+
+ return ' '.join(['--{}={}'.format(k, v) for k, v in params.items()])
diff --git a/vicn/resource/script.py b/vicn/resource/script.py
new file mode 100644
index 00000000..30196b21
--- /dev/null
+++ b/vicn/resource/script.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.helpers.resource_definition import *
+
+class Script(Resource):
+ """
+ Resource: Script
+
+ This resource is empty on purpose. It is a temporary resource used as a
+ placeholder for controlling the tool from GUI and should be deprecated in
+ future releases.
+ """
+
+ id = Attribute(String)
+ name = Attribute(String)
diff --git a/vicn/resource/vpp/__init__.py b/vicn/resource/vpp/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/vpp/__init__.py
diff --git a/vicn/resource/vpp/cicn.py b/vicn/resource/vpp/cicn.py
new file mode 100644
index 00000000..be523a6c
--- /dev/null
+++ b/vicn/resource/vpp/cicn.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import re
+
+from netmodel.model.type import Integer, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import async_task, task, BashTask, EmptyTask
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.node import Node
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ENABLE_PLUGIN
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_GET
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ICN_FACE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ICN_ROUTE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_GET_CACHE_SIZE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_SET_CACHE_SIZE
+
+_ADD_FACE_RETURN_FORMAT = "Face ID: [0-9]+"
+
+def check_face_id_return_format(data):
+ prog = re.compile(_ADD_FACE_RETURN_FORMAT)
+ return prog.match(data)
+
+def parse_face_id(data):
+ return data.partition(':')[2]
+
+class CICNForwarder(Forwarder):
+ """
+ NOTE: Based on the Vagrantfile, we recommend a node with mem=4096, cpu=4
+ """
+
+ node = Attribute(Node,
+ mandatory=True,
+ requirements = [Requirement('vpp')],
+ reverse_name='cicn')
+ numa_node = Attribute(Integer,
+ description = 'Numa node on which vpp will run',
+ default = None)
+ core = Attribute(Integer,
+ description = 'Core belonging the numa node on which vpp will run',
+ default = None)
+ enable_worker = Attribute(Bool,
+ description = 'Enable one worker for packet processing',
+ default = False)
+
+ #__packages__ = ['vpp-plugin-cicn']
+
+ def __after__(self):
+ return ['CentralICN']
+
+ def __get__(self):
+ def parse(rv):
+ if rv.return_value > 0 or 'cicn: not enabled' in rv.stdout:
+ raise ResourceNotFound
+ return BashTask(self.node, CMD_VPP_CICN_GET,
+ lock = self.node.vpp.vppctl_lock, parse=parse)
+
+ def __create__(self):
+
+ #self.node.vpp.plugins.append("cicn")
+ lock = self.node.vpp.vppctl_lock
+ create_task = BashTask(self.node, CMD_VPP_ENABLE_PLUGIN,
+ {'plugin' : 'cicn'}, lock = lock)
+
+ face_task = EmptyTask()
+ route_task = EmptyTask()
+
+ def parse_face(rv, face):
+ if check_face_id_return_format(rv.stdout):
+ face.id = parse_face_id(rv.stdout)
+ return {}
+
+ for face in self.faces:
+ face_task = face_task > BashTask(self.node, CMD_VPP_ADD_ICN_FACE,
+ {'face':face},
+ parse = (lambda x : parse_face(x, face)), lock = lock)
+
+ if not self.routes:
+ from vicn.resource.icn.route import Route
+ for route in self._state.manager.by_type(Route):
+ if route.node is self.node:
+ self.routes.append(route)
+ for route in self.routes:
+ route_task = route_task > BashTask(self.node,
+ CMD_VPP_ADD_ICN_ROUTE, {'route' : route}, lock = lock)
+
+ return (wait_resource_task(self.node.vpp) > create_task) > (face_task > route_task)
+
+ # Nothing to do
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ # Force local update
+
+ _add_faces = None
+ _remove_faces = None
+ _get_faces = None
+ _set_faces = None
+
+ _add_routes = None
+ _remove_routes = None
+ _get_routes = None
+ _set_routes = None
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _set_cache_size(self):
+ return BashTask(self.node, CMD_VPP_CICN_SET_CACHE_SIZE, {'self': self},
+ lock = self.node.vpp.vppctl_lock)
+
+ def _get_cache_size(self):
+ def parse(rv):
+ return int(rv.stdout)
+ return BashTask(self.node, CMD_VPP_CICN_GET_CACHE_SIZE, parse=parse,
+ lock = self.node.vpp.vppctl_lock)
diff --git a/vicn/resource/vpp/dpdk_device.py b/vicn/resource/vpp/dpdk_device.py
new file mode 100644
index 00000000..69449e48
--- /dev/null
+++ b/vicn/resource/vpp/dpdk_device.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute
+from vicn.resource.linux.phy_interface import PhyInterface
+
+class DpdkDevice(PhyInterface):
+ """
+ Resource: DpdkDevice
+
+ A DpdkDevice is a physical net device supported by Dpdk and with parameters
+ specific to VPP.
+ """
+ numa_node = Attribute(Integer,
+ description = 'NUMA node on the same PCI bus as the DPDK card')
+ socket_mem = Attribute(Integer,
+ description = 'Memory used by the vpp forwarder',
+ default = 512)
+ mac_address = Attribute(String)
diff --git a/vicn/resource/vpp/interface.py b/vicn/resource/vpp/interface.py
new file mode 100644
index 00000000..efe4fe5a
--- /dev/null
+++ b/vicn/resource/vpp/interface.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.resource import Resource
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.task import inline_task, BashTask, task
+from vicn.core.task import EmptyTask
+from vicn.resource.interface import Interface
+from vicn.resource.linux.net_device import NonTapBaseNetDevice
+from vicn.resource.vpp.vpp import VPP
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CREATE_IFACE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_SET_IP, CMD_VPP_SET_UP
+
+class VPPInterface(Resource):
+ """
+ Resource: VPPInterface
+
+ An interface representation in VPP
+ """
+
+ vpp = Attribute(VPP,
+ description = 'Forwarder to which this interface belong to',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne,
+ key = True,
+ reverse_name = 'interfaces')
+ parent = Attribute(Interface, description = 'parent',
+ mandatory = True, reverse_name = 'vppinterface')
+ ip_address = Attribute(String)
+ prefix_len = Attribute(Integer, default = 31)
+ up = Attribute(Bool, description = 'Interface up/down status')
+ monitored = Attribute(Bool, default = True)
+
+ device_name = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ """
+ We need CentralIP to get the parent interface IP address
+ """
+ return ['CentralIP']
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ # We must control what is the type of the parent netDevice (currently
+ # supported only veths, physical nics are coming)
+ create_task = EmptyTask()
+
+ # We must let the routing algorithm know that the parent interface
+ # belongs to vpp
+ self.parent.has_vpp_child = True
+
+ self.ip_address = self.parent.ip_address
+ self.up = True
+
+ if isinstance(self.parent,NonTapBaseNetDevice):
+ # Remove ip address in the parent device, it must only be set in
+ # the vpp interface otherwise vpp and the linux kernel will reply
+ # to non-icn request (e.g., ARP replies, port ureachable etc)
+
+ self.device_name = 'host-' + self.parent.device_name
+ create_task = BashTask(self.vpp.node, CMD_VPP_CREATE_IFACE,
+ {'vpp_interface': self},
+ lock = self.vpp.vppctl_lock)
+
+ self.parent.set('ip_address', None)
+ self.parent.set('offload', False)
+ self.parent.remote.set('offload', False)
+
+ elif self.parent.get_type() == 'dpdkdevice':
+ self.device_name = self.parent.device_name
+ else :
+ # Currently assume naively that everything else will be a physical
+ # NIC for VPP
+ #
+ # Before initialization, we need to make sure that the parent
+ # interface is down (vpp will control the nic)
+ self.device_name = 'host-' + self.parent.device_name
+ self.parent.set('up', False)
+
+ return create_task
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _set_ip_address(self):
+ if self.ip_address:
+ return BashTask(self.vpp.node, CMD_VPP_SET_IP, {'netdevice': self},
+ lock = self.vpp.vppctl_lock)
+
+ def _set_up(self):
+ return BashTask(self.vpp.node, CMD_VPP_SET_UP, {'netdevice': self},
+ lock = self.vpp.vppctl_lock)
+
+ @task
+ def _get_up(self):
+ return {'up' : False}
+
+ @task
+ def _get_ip_address(self):
+ return {'ip_address' : None}
diff --git a/vicn/resource/vpp/scripts.py b/vicn/resource/vpp/scripts.py
new file mode 100644
index 00000000..3a3d5e8f
--- /dev/null
+++ b/vicn/resource/vpp/scripts.py
@@ -0,0 +1,287 @@
+FN_APPARMOR_DPDK_SCRIPT='/etc/apparmor.d/lxc/lxc-dpdk'
+
+TPL_APPARMOR_DPDK_SCRIPT='''
+profile lxc-dpdk flags=(attach_disconnected,mediate_deleted) {
+ ### Base profile
+ capability,
+ dbus,
+ file,
+ network,
+ umount,
+
+ # Allow us to receive signals from anywhere.
+ signal (receive),
+
+ # Allow us to send signals to ourselves
+ signal peer=@{profile_name},
+
+ # Allow other processes to read our /proc entries, futexes, perf tracing and
+ # kcmp for now (they will need 'read' in the first place). Administrators can
+ # override with:
+ # deny ptrace (readby) ...
+ ptrace (readby),
+
+ # Allow other processes to trace us by default (they will need 'trace' in
+ # the first place). Administrators can override with:
+ # deny ptrace (tracedby) ...
+ ptrace (tracedby),
+
+ # Allow us to ptrace ourselves
+ ptrace peer=@{profile_name},
+
+ # ignore DENIED message on / remount
+ deny mount options=(ro, remount) -> /,
+ deny mount options=(ro, remount, silent) -> /,
+
+ # allow tmpfs mounts everywhere
+ mount fstype=tmpfs,
+
+ # allow hugetlbfs mounts everywhere
+ mount fstype=hugetlbfs,
+
+ # allow mqueue mounts everywhere
+ mount fstype=mqueue,
+
+ # allow fuse mounts everywhere
+ mount fstype=fuse,
+ mount fstype=fuse.*,
+
+ # deny access under /proc/bus to avoid e.g. messing with pci devices directly
+ deny @{PROC}/bus/** wklx,
+
+ # deny writes in /proc/sys/fs but allow binfmt_misc to be mounted
+ mount fstype=binfmt_misc -> /proc/sys/fs/binfmt_misc/,
+ deny @{PROC}/sys/fs/** wklx,
+
+ # allow efivars to be mounted, writing to it will be blocked though
+ mount fstype=efivarfs -> /sys/firmware/efi/efivars/,
+
+ # block some other dangerous paths
+ deny @{PROC}/kcore rwklx,
+ deny @{PROC}/kmem rwklx,
+ deny @{PROC}/mem rwklx,
+ deny @{PROC}/sysrq-trigger rwklx,
+
+ # deny writes in /sys except for /sys/fs/cgroup, also allow
+ # fusectl, securityfs and debugfs to be mounted there (read-only)
+ mount fstype=fusectl -> /sys/fs/fuse/connections/,
+ mount fstype=securityfs -> /sys/kernel/security/,
+ mount fstype=debugfs -> /sys/kernel/debug/,
+ deny mount fstype=debugfs -> /var/lib/ureadahead/debugfs/,
+ mount fstype=proc -> /proc/,
+ mount fstype=sysfs -> /sys/,
+ mount options=(rw, nosuid, nodev, noexec, remount) -> /sys/,
+ deny /sys/firmware/efi/efivars/** rwklx,
+ # note, /sys/kernel/security/** handled below
+ mount options=(move) /sys/fs/cgroup/cgmanager/ -> /sys/fs/cgroup/cgmanager.lower/,
+ mount options=(ro, nosuid, nodev, noexec, remount, strictatime) -> /sys/fs/cgroup/,
+
+ mount options=(ro, nosuid, nodev, noexec, remount, strictatime) -> /sys,
+
+ # deny reads from debugfs
+ deny /sys/kernel/debug/{,**} rwklx,
+
+ # allow paths to be made slave, shared, private or unbindable
+ # FIXME: This currently doesn't work due to the apparmor parser treating those as allowing all mounts.
+# mount options=(rw,make-slave) -> **,
+# mount options=(rw,make-rslave) -> **,
+# mount options=(rw,make-shared) -> **,
+# mount options=(rw,make-rshared) -> **,
+# mount options=(rw,make-private) -> **,
+# mount options=(rw,make-rprivate) -> **,
+# mount options=(rw,make-unbindable) -> **,
+# mount options=(rw,make-runbindable) -> **,
+
+ # allow bind-mounts of anything except /proc, /sys and /dev
+ mount options=(rw,bind) /[^spd]*{,/**},
+ mount options=(rw,bind) /d[^e]*{,/**},
+ mount options=(rw,bind) /de[^v]*{,/**},
+ mount options=(rw,bind) /dev/.[^l]*{,/**},
+ mount options=(rw,bind) /dev/.l[^x]*{,/**},
+ mount options=(rw,bind) /dev/.lx[^c]*{,/**},
+ mount options=(rw,bind) /dev/.lxc?*{,/**},
+ mount options=(rw,bind) /dev/[^.]*{,/**},
+ mount options=(rw,bind) /dev?*{,/**},
+ mount options=(rw,bind) /p[^r]*{,/**},
+ mount options=(rw,bind) /pr[^o]*{,/**},
+ mount options=(rw,bind) /pro[^c]*{,/**},
+ mount options=(rw,bind) /proc?*{,/**},
+ mount options=(rw,bind) /s[^y]*{,/**},
+ mount options=(rw,bind) /sy[^s]*{,/**},
+ mount options=(rw,bind) /sys?*{,/**},
+
+ # allow moving mounts except for /proc, /sys and /dev
+ mount options=(rw,move) /[^spd]*{,/**},
+ mount options=(rw,move) /d[^e]*{,/**},
+ mount options=(rw,move) /de[^v]*{,/**},
+ mount options=(rw,move) /dev/.[^l]*{,/**},
+ mount options=(rw,move) /dev/.l[^x]*{,/**},
+ mount options=(rw,move) /dev/.lx[^c]*{,/**},
+ mount options=(rw,move) /dev/.lxc?*{,/**},
+ mount options=(rw,move) /dev/[^.]*{,/**},
+ mount options=(rw,move) /dev?*{,/**},
+ mount options=(rw,move) /p[^r]*{,/**},
+ mount options=(rw,move) /pr[^o]*{,/**},
+ mount options=(rw,move) /pro[^c]*{,/**},
+ mount options=(rw,move) /proc?*{,/**},
+ mount options=(rw,move) /s[^y]*{,/**},
+ mount options=(rw,move) /sy[^s]*{,/**},
+ mount options=(rw,move) /sys?*{,/**},
+
+ # generated by: lxc-generate-aa-rules.py container-rules.base
+ deny /proc/sys/[^kn]*{,/**} wklx,
+ deny /proc/sys/k[^e]*{,/**} wklx,
+ deny /proc/sys/ke[^r]*{,/**} wklx,
+ deny /proc/sys/ker[^n]*{,/**} wklx,
+ deny /proc/sys/kern[^e]*{,/**} wklx,
+ deny /proc/sys/kerne[^l]*{,/**} wklx,
+ deny /proc/sys/kernel/[^smhd]*{,/**} wklx,
+ deny /proc/sys/kernel/d[^o]*{,/**} wklx,
+ deny /proc/sys/kernel/do[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/dom[^a]*{,/**} wklx,
+ deny /proc/sys/kernel/doma[^i]*{,/**} wklx,
+ deny /proc/sys/kernel/domai[^n]*{,/**} wklx,
+ deny /proc/sys/kernel/domain[^n]*{,/**} wklx,
+ deny /proc/sys/kernel/domainn[^a]*{,/**} wklx,
+ deny /proc/sys/kernel/domainna[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/domainnam[^e]*{,/**} wklx,
+ deny /proc/sys/kernel/domainname?*{,/**} wklx,
+ deny /proc/sys/kernel/h[^o]*{,/**} wklx,
+ deny /proc/sys/kernel/ho[^s]*{,/**} wklx,
+ deny /proc/sys/kernel/hos[^t]*{,/**} wklx,
+ deny /proc/sys/kernel/host[^n]*{,/**} wklx,
+ deny /proc/sys/kernel/hostn[^a]*{,/**} wklx,
+ deny /proc/sys/kernel/hostna[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/hostnam[^e]*{,/**} wklx,
+ deny /proc/sys/kernel/hostname?*{,/**} wklx,
+ deny /proc/sys/kernel/m[^s]*{,/**} wklx,
+ deny /proc/sys/kernel/ms[^g]*{,/**} wklx,
+ deny /proc/sys/kernel/msg*/** wklx,
+ deny /proc/sys/kernel/s[^he]*{,/**} wklx,
+ deny /proc/sys/kernel/se[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/sem*/** wklx,
+ deny /proc/sys/kernel/sh[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/shm*/** wklx,
+ deny /proc/sys/kernel?*{,/**} wklx,
+ deny /proc/sys/n[^e]*{,/**} wklx,
+ deny /proc/sys/ne[^t]*{,/**} wklx,
+ deny /proc/sys/net?*{,/**} wklx,
+ deny /sys/[^fdck]*{,/**} wklx,
+ deny /sys/c[^l]*{,/**} wklx,
+ deny /sys/cl[^a]*{,/**} wklx,
+ deny /sys/cla[^s]*{,/**} wklx,
+ deny /sys/clas[^s]*{,/**} wklx,
+ deny /sys/class/[^nu]*{,/**} wklx,
+ deny /sys/class/n[^e]*{,/**} wklx,
+ deny /sys/class/ne[^t]*{,/**} wklx,
+ deny /sys/class/net?*{,/**} wklx,
+ deny /sys/class/u[^i]*{,/**} wklx,
+ deny /sys/class/ui[^o]*{,/**} wklx,
+ deny /sys/class?*{,/**} wklx,
+ deny /sys/d[^e]*{,/**} wklx,
+ deny /sys/de[^v]*{,/**} wklx,
+ deny /sys/dev[^i]*{,/**} wklx,
+ deny /sys/devi[^c]*{,/**} wklx,
+ deny /sys/devic[^e]*{,/**} wklx,
+ deny /sys/device[^s]*{,/**} wklx,
+# deny /sys/devices/[^vu]*{,/**} wklx,
+# deny /sys/devices/v[^i]*{,/**} wklx,
+# deny /sys/devices/vi[^r]*{,/**} wklx,
+# deny /sys/devices/vir[^t]*{,/**} wklx,
+# deny /sys/devices/virt[^u]*{,/**} wklx,
+# deny /sys/devices/virtu[^a]*{,/**} wklx,
+# deny /sys/devices/virtua[^l]*{,/**} wklx,
+# deny /sys/devices/virtual/[^n]*{,/**} wklx,
+# deny /sys/devices/virtual/n[^e]*{,/**} wklx,
+# deny /sys/devices/virtual/ne[^t]*{,/**} wklx,
+# deny /sys/devices/virtual/net?*{,/**} wklx,
+# deny /sys/devices/virtual?*{,/**} wklx,
+# deny /sys/devices?*{,/**} wklx,
+ deny /sys/f[^s]*{,/**} wklx,
+ deny /sys/fs/[^c]*{,/**} wklx,
+ deny /sys/fs/c[^g]*{,/**} wklx,
+ deny /sys/fs/cg[^r]*{,/**} wklx,
+ deny /sys/fs/cgr[^o]*{,/**} wklx,
+ deny /sys/fs/cgro[^u]*{,/**} wklx,
+ deny /sys/fs/cgrou[^p]*{,/**} wklx,
+ deny /sys/fs/cgroup?*{,/**} wklx,
+ deny /sys/fs?*{,/**} wklx,
+
+ ### Feature: unix
+ # Allow receive via unix sockets from anywhere
+ unix (receive),
+
+ # Allow all unix in the container
+ unix peer=(label=@{profile_name}),
+
+ ### Feature: cgroup namespace
+ mount fstype=cgroup -> /sys/fs/cgroup/**,
+
+ ### Feature: apparmor stacking
+
+ ### Configuration: apparmor loading disabled in privileged containers
+ deny /sys/k[^e]*{,/**} rwklx,
+ deny /sys/ke[^r]*{,/**} rwklx,
+ deny /sys/ker[^n]*{,/**} rwklx,
+ deny /sys/kern[^e]*{,/**} rwklx,
+ deny /sys/kerne[^l]*{,/**} rwklx,
+ deny /sys/kernel/[^sm]*{,/**} rwklx,
+ deny /sys/kernel/s[^e]*{,/**} rwklx,
+ deny /sys/kernel/se[^c]*{,/**} rwklx,
+ deny /sys/kernel/sec[^u]*{,/**} rwklx,
+ deny /sys/kernel/secu[^r]*{,/**} rwklx,
+ deny /sys/kernel/secur[^i]*{,/**} rwklx,
+ deny /sys/kernel/securi[^t]*{,/**} rwklx,
+ deny /sys/kernel/securit[^y]*{,/**} rwklx,
+ deny /sys/kernel/security/[^a]*{,/**} rwklx,
+ deny /sys/kernel/security/a[^p]*{,/**} rwklx,
+ deny /sys/kernel/security/ap[^p]*{,/**} rwklx,
+ deny /sys/kernel/security/app[^a]*{,/**} rwklx,
+ deny /sys/kernel/security/appa[^r]*{,/**} rwklx,
+ deny /sys/kernel/security/appar[^m]*{,/**} rwklx,
+ deny /sys/kernel/security/apparm[^o]*{,/**} rwklx,
+ deny /sys/kernel/security/apparmo[^r]*{,/**} rwklx,
+ deny /sys/kernel/security/apparmor?*{,/**} rwklx,
+ deny /sys/kernel/security?*{,/**} rwklx,
+ deny /sys/kernel?*{,/**} rwklx,
+}'''
+
+FN_VPP_DPDK_SCRIPT='/etc/vpp/startup.conf'
+
+TPL_VPP_DPDK_DAEMON_SCRIPT='''
+unix {
+ nodaemon
+ log /tmp/vpp.log
+ full-coredump
+}
+
+api-trace {
+ on
+}
+
+api-segment {
+ gid vpp
+}
+
+'''
+
+TPL_VPP_DPDK_SCRIPT='''
+unix {
+ log /tmp/vpp.log
+ full-coredump
+}
+
+api-trace {
+ on
+}
+
+api-segment {
+ gid vpp
+}
+
+'''
+
+APPARMOR_VPP_PROFILE = '''
+lxc.aa_profile = lxc-dpdk
+lxc.mount.entry = hugetlbfs dev/hugepages hugetlbfs rw,relatime,create=dir 0 0
+lxc.mount.auto = sys:rw'''
diff --git a/vicn/resource/vpp/vpp.py b/vicn/resource/vpp/vpp.py
new file mode 100644
index 00000000..f9d10703
--- /dev/null
+++ b/vicn/resource/vpp/vpp.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+
+from netmodel.model.type import String, Integer, Bool
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, task, inline_task
+from vicn.resource.lxd.lxc_container import LxcContainer
+from vicn.resource.node import Node
+from vicn.resource.linux.file import TextFile
+from vicn.resource.vpp.dpdk_device import DpdkDevice
+from vicn.resource.vpp.scripts import FN_VPP_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import TPL_VPP_DPDK_DAEMON_SCRIPT
+from vicn.resource.vpp.vpp_commands import CMD_VPP_DISABLE, CMD_VPP_STOP
+from vicn.resource.vpp.vpp_commands import CMD_VPP_START
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ENABLE_PLUGIN
+
+#------------------------------------------------------------------------------
+# VPP forwarder
+#------------------------------------------------------------------------------
+
+CMD_GET = 'killall -0 vpp_main'
+CMD_DISABLE_IP_FORWARD = 'sysctl -w net.ipv4.ip_forward=0'
+
+class VPP(Resource):
+ """
+ Todo:
+ - make VPP an application with package install
+ - vpp should be a service (hence a singleton) for which we override the
+ start and stop commands
+ """
+
+ #__package_names__ = ['vpp', 'vpp-dbg', 'vpp-dpdk-dev']
+
+ plugins = Attribute(String,
+ multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ multiplicity = Multiplicity.OneToOne,
+ reverse_name = 'vpp')
+ numa_node = Attribute(Integer,
+ description = 'Numa node on which vpp will run')
+ core = Attribute(Integer,
+ description = 'Core belonging the numa node on which vpp will run')
+ enable_worker = Attribute(Bool,
+ description = 'Enable one worker for packet processing',
+ default = False)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.vppctl_lock = asyncio.Lock()
+
+ self.dpdk_setup_file = None
+ if isinstance(self.node, LxcContainer):
+ if not 'vpp' in self.node.profiles:
+ self.node.profiles.append('vpp')
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ['BaseNetDevice']
+
+ def __get__(self):
+ return BashTask(self.node, CMD_GET)
+
+ def __subresources__(self):
+ self.dpdk_setup_file = TextFile(node = self.node,
+ filename = FN_VPP_DPDK_SCRIPT,
+ overwrite = True)
+ return self.dpdk_setup_file
+
+ def __create__(self):
+ socket_mem = dict()
+ numa_mgr = self.node.node_with_kernel.numa_mgr
+
+ for interface in self.node.interfaces:
+ if isinstance(interface, DpdkDevice):
+ # Assign as numa node the first numa node specified in a
+ # physical card (if any). If multiple nics connected to
+ # different numa nodes are assigned to this vpp memory access
+ # will be inefficient for the nics sitting in the other numa
+ # node.
+ socket_mem[interface.numa_node] = interface.socket_mem
+
+ for iface in self.interfaces:
+ if isinstance(iface.parent, DpdkDevice) and \
+ not iface.parent.numa_node is None:
+ self.numa_node = iface.parent.numa_node
+ break
+ if self.numa_node is None or self.core is None:
+ self.numa_node, self.core = \
+ numa_mgr.get_numa_core(numa_node = self.numa_node)
+
+ dpdk_list = list()
+
+ # On numa architecture socket-mem requires to set the amount of memory
+ # to be reserved on each numa node
+ socket_mem_str = 'socket-mem '
+ for numa in range (0,numa_mgr.get_number_of_numa()):
+ if numa in socket_mem:
+ socket_mem_str = socket_mem_str + str(socket_mem[numa])
+ else:
+ socket_mem_str = socket_mem_str + '0'
+
+ if numa < numa_mgr.get_number_of_numa()-1:
+ socket_mem_str = socket_mem_str + ','
+
+ dpdk_list.append(socket_mem_str)
+
+ for interface in self.node.interfaces:
+ if isinstance(interface, DpdkDevice):
+ dpdk_list.append('dev ' + interface.pci_address)
+
+ # Add the core on which running vpp and the dpdk parameters
+ setup = TPL_VPP_DPDK_DAEMON_SCRIPT + 'cpu {'
+
+ setup = setup + ''' \n main-core ''' + str(self.core)
+
+ if self.enable_worker:
+ self.numa_node, cpu_worker =numa_mgr.get_numa_core(self.numa_node)
+ setup = setup + '''\n corelist-workers ''' + str(cpu_worker)
+
+ setup = setup + '''\n}\n\n dpdk { '''
+
+ for dpdk_dev in dpdk_list:
+ setup = setup + ''' \n ''' + dpdk_dev
+
+ setup = setup + '\n}'
+
+
+ if any([isinstance(interface,DpdkDevice) for interface in self.node.interfaces]):
+ self.dpdk_setup_file.content = setup
+ else:
+ self.dpdk_setup_file.content = TPL_VPP_DPDK_DAEMON_SCRIPT
+
+ lock = self.node.node_with_kernel.vpp_host.vppstart_lock
+
+ vpp_disable = BashTask(self.node, CMD_VPP_DISABLE, lock = lock)
+ vpp_stop = BashTask(self.node, CMD_VPP_STOP, lock = lock)
+ enable_ip_forward = BashTask(self.node, CMD_DISABLE_IP_FORWARD)
+ start_vpp = BashTask(self.node, CMD_VPP_START, lock = lock)
+
+ return ((vpp_disable > vpp_stop) | enable_ip_forward) > start_vpp
+
+ def __delete__(self):
+ return BashTask(self.node, CMD_VPP_STOP)
+
+ def _add_plugins(self, plugin):
+ return BashTask(self.node, CMD_VPP_ENABLE_PLUGIN, {'plugin': plugin})
+
+ def _set_plugins(self):
+ cmd = None
+ for plugin in self.plugins:
+ cmd = cmd > BashTask(self.node, CMD_VPP_ENABLE_PLUGIN,
+ {'plugin' : plugin})
+ return cmd
+
+ def _remove_plugins(self, plugin):
+ raise NotImplementedError
+
+ @inline_task
+ def _get_plugins(self):
+ return {'plugins' : []}
diff --git a/vicn/resource/vpp/vpp_bridge.py b/vicn/resource/vpp/vpp_bridge.py
new file mode 100644
index 00000000..c7a70c02
--- /dev/null
+++ b/vicn/resource/vpp/vpp_bridge.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.attribute import Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.resource import Resource
+from vicn.core.task import task, BashTask, EmptyTask
+from vicn.resource.channel import Channel
+from vicn.resource.linux.application import LinuxApplication
+from vicn.resource.linux.sym_veth_pair import SymVethPair
+from vicn.resource.linux.sym_veth_pair import SymVethPair
+from vicn.resource.node import Node
+from vicn.resource.vpp.dpdk_device import DpdkDevice
+from vicn.resource.vpp.interface import VPPInterface
+from vicn.resource.vpp.vpp import VPP
+
+CMD_ADD_INTERFACE_TO_BR = ('vppctl set interface l2 bridge '
+ '{interface.device_name} {br_domain}')
+
+class VPPBridge(Channel, LinuxApplication):
+ """
+ Resource: VPPBridge
+
+ VPPBridge instantiate a vpp resource and set it as a vpp bridge.
+
+ This resource requires to be run within a LxcContainer which will have VPP.
+ Every interface in the lxc_container (i.e., the ones contained in
+ self.node.interfaces) will be added to the vpp bridge. To connect other vpp
+ node to the bridge, the corresponding dpdkdevice must be added as an
+ interface to the channel.
+ """
+
+ # The vpp bridge _USES_ a VPP forwarder on the node
+ node = Attribute(Node, mandatory=True,
+ description = 'Node on which vpp is running',
+ requirements = [Requirement('vpp')])
+
+ connected_nodes = Attribute(Node, multiplicity = Multiplicity.OneToMany,
+ description = 'List of nodes to connect to the bridge')
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._vpp_interfaces = list()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__ (self):
+ # We don't need any reference to the list of SymVethPair because each
+ # side of a veth will be included in the node.interfaces list
+ self._veths = [SymVethPair(node1 = self.node, node2 = node,
+ owner = self) for node in self.connected_nodes]
+
+ return Resource.__concurrent__(*self._veths)
+
+ @task
+ def __initialize__ (self):
+ # Add the veth side on the connected_nodes to the set of interfaces of
+ # the channel
+ self.interfaces.extend([veth.side2 for veth in self._veths])
+
+ @task
+ def __get__(self):
+ # Forces creation
+ raise ResourceNotFound
+
+ # Nothing to do
+ __delete__ = None
+
+ def __create__(self):
+ manager = self._state.manager
+
+ # Create a VPPInterface for each interface in the node. These will be
+ # the interfaces we will connect to the vpp bridge process
+ vpp_interfaces = list()
+ for interface in self.node.interfaces:
+ # FIXME harcoded value
+ if interface.device_name == 'eth0':
+ continue
+
+ vpp_interface = VPPInterface(vpp = self.node.vpp,
+ parent = interface,
+ ip_address = Reference(interface, 'ip_address'),
+ device_name = 'host-' + interface.device_name)
+ vpp_interfaces.append(vpp_interface)
+ manager.commit_resource(vpp_interface)
+
+ tasks = EmptyTask()
+
+ for vpp_interface in vpp_interfaces:
+ tasks = tasks > (wait_resource_task(vpp_interface) >
+ self._add_interface(vpp_interface,0))
+
+ return wait_resource_task(self.node.vpp) > tasks
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _add_interface(self, interface, br_domain):
+ return BashTask(self.node, CMD_ADD_INTERFACE_TO_BR,
+ {'interface': interface, 'br_domain': br_domain})
+
+ def _del_interface(self, interface, br_domain):
+ raise NotImplementedError('Interface removal not supported')
+
diff --git a/vicn/resource/vpp/vpp_commands.py b/vicn/resource/vpp/vpp_commands.py
new file mode 100644
index 00000000..8ee64bf6
--- /dev/null
+++ b/vicn/resource/vpp/vpp_commands.py
@@ -0,0 +1,41 @@
+##### VPP SETUP #####
+
+CMD_VPP_STOP_SERVICE = 'systemctl stop vpp.service'
+CMD_VPP_DISABLE = 'systemctl disable vpp.service'
+
+# 'sleep 1' ensures that VPP has enough time to start
+CMD_VPP_START = '''
+systemctl start vpp
+sleep 1
+'''
+CMD_VPP_STOP = '''
+systemctl stop vpp
+killall -9 vpp_main || true
+'''
+CMD_VPP_ENABLE_PLUGIN = 'vppctl {plugin} enable'
+
+##### VPP INTERFACES #####
+
+CMD_VPP_CREATE_IFACE = '''
+# Create vpp interface from netmodel.network.interface.device_name} with mac {self.parent.mac_address}
+vppctl create host-interface name {vpp_interface.parent.device_name} hw-addr {vpp_interface.parent.mac_address}
+vppctl set interface state {vpp_interface.device_name} up
+'''
+CMD_VPP_SET_IP = 'vppctl set int ip address {netdevice.device_name} {netdevice.ip_address}/{netdevice.prefix_len}'
+CMD_VPP_SET_UP = 'vppctl set int state {netdevice.device_name} up'
+
+##### VPP IP ROUTING #####
+
+CMD_VPP_ADD_ROUTE = 'vppctl set ip arp static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}'
+CMD_VPP_DEL_ROUTE = 'vppctl set ip arp del static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}'
+CMD_VPP_ADD_ROUTE_GW = 'vppctl ip route add {route.ip_address}/32 via {route.gateway} {route.interface.vppinterface.device_name}'
+CMD_VPP_DEL_ROUTE_GW = 'vppctl ip route del {route.ip_address}/32 via {route.gateway} {route.interface.vppinterface.device_name}'
+
+##### VPP CICN PLUGIN #####
+
+CMD_VPP_CICN_GET = "timeout 1 vppctl cicn show" #We timeout if vpp is not started
+CMD_VPP_ADD_ICN_ROUTE = 'vppctl cicn cfg fib add prefix {route.prefix} face {route.face.id}'
+CMD_VPP_ADD_ICN_FACE = 'vppctl cicn cfg face add local {face.src_ip}:{face.src_port} remote {face.dst_ip}:{face.dst_port}'
+
+CMD_VPP_CICN_GET_CACHE_SIZE = 'vppctl cicn show | grep "CS entries" | grep -E "[0-9]+"'
+CMD_VPP_CICN_SET_CACHE_SIZE = 'vppctl cicn control param cs size {self.cache_size}'
diff --git a/vicn/resource/vpp/vpp_host.py b/vicn/resource/vpp/vpp_host.py
new file mode 100644
index 00000000..134e65b0
--- /dev/null
+++ b/vicn/resource/vpp/vpp_host.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask, task, EmptyTask
+from vicn.resource.linux.application import LinuxApplication
+from vicn.resource.linux.file import TextFile
+from vicn.resource.node import Node
+from vicn.resource.vpp.scripts import FN_APPARMOR_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import TPL_APPARMOR_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import FN_VPP_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import TPL_VPP_DPDK_DAEMON_SCRIPT
+from vicn.resource.vpp.vpp_commands import CMD_VPP_DISABLE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_STOP_SERVICE
+
+CMD_INSERT_MODULES = 'modprobe uio && modprobe igb_uio'
+CMD_APP_ARMOR_RELOAD = '''
+# Force apparmor to reload profiles to include the new profile
+/etc/init.d/apparmor reload
+'''
+CMD_SYSCTL_HUGEPAGES = 'sysctl -w vm.nr_hugepages={nb_hp}'
+DEFAULT_NB_HUGEPAGES = 1024
+CMD_GREP_UIO_DEV = 'ls /dev | grep uio'
+CMD_CREATE_UIO_DEVICES = "dpdk_nic_bind --bind=igb_uio {pci_address}"
+
+class VPPHost(LinuxApplication):
+ """
+ Resource: VPPHost
+
+ Only used for container deployment
+
+ Packages required on the host
+ - vpp : sysctl configuration
+ - vpp-dpdk-dkms : kernel modules
+
+ Host must be configured to let vpp to work into container:
+ - install new apparmor profile (to let the container to read
+ hugepages info in /sys/kernel/mm/hugepages)
+ - set hugepages into the host
+ """
+
+ node = Attribute(Node,
+ description = 'Node on which the application is installed',
+ mandatory = True,
+ multiplicity = Multiplicity.OneToOne,
+ reverse_name = 'vpp_host',
+ reverse_description = 'Setup for hosting vpp containers',
+ reverse_auto = True,
+ requirements = [Requirement('numa_mgr')])
+ uio_devices = Attribute(String,
+ description = 'uio devices on the node',
+ multiplicity = Multiplicity.OneToMany,
+ ro = True)
+ dpdk_devices = Attribute(String,
+ description = 'Dpdk devices on the node',
+ multiplicity = Multiplicity.OneToMany)
+
+ __package_names__ = ['dpdk', 'vpp', 'vpp-dpdk-dkms']
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.vppstart_lock = asyncio.Lock()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ app_armor_file = TextFile(node = self.node,
+ filename = FN_APPARMOR_DPDK_SCRIPT,
+ content = TPL_APPARMOR_DPDK_SCRIPT,
+ overwrite = True)
+ startup_conf = TextFile(node = self.node,
+ filename = FN_VPP_DPDK_SCRIPT,
+ content = TPL_VPP_DPDK_DAEMON_SCRIPT,
+ overwrite = True)
+ return app_armor_file | startup_conf
+
+ @task
+ def __get__(self):
+ """
+ This method always assumes the resource does not exist, since it is not
+ an issue to perform the modprobe call everytime.
+ """
+ raise ResourceNotFound
+
+ def __create__(self):
+ modules = BashTask(self.node, CMD_INSERT_MODULES)
+ app_armor_reload = BashTask(self.node, CMD_APP_ARMOR_RELOAD)
+ sysctl_hugepages = BashTask(self.node, CMD_SYSCTL_HUGEPAGES,
+ {'nb_hp': DEFAULT_NB_HUGEPAGES})
+
+ # Hook
+ # The following is needed to create uio devices in /dev. They are
+ # required to let vpp to use dpdk (or other compatibles) nics. From a
+ # container, vpp cannot create those devices, therefore we need to
+ # create them in the host and then mount them on each container running
+ # vpp (and using a physical nic)
+ stop_vpp = BashTask(self.node, CMD_VPP_STOP_SERVICE)
+ disable_vpp = BashTask(self.node, CMD_VPP_DISABLE)
+ disable_vpp = stop_vpp > disable_vpp
+
+ create_uio = EmptyTask()
+ for device in self.dpdk_devices:
+ create_uio = create_uio > BashTask(self.node,
+ CMD_CREATE_UIO_DEVICES, {'pci_address' : device})
+
+ return ((modules | app_armor_reload) | sysctl_hugepages) > \
+ (disable_vpp > create_uio)
+
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_uio_devices(self):
+ def parse(rv):
+ return rv.stdout.splitlines()
+ return BashTask(self.node, CMD_GREP_UIO_DEV, parse = parse)