From 85a341d645b57b7cd88a26ed2ea0a314704240ea Mon Sep 17 00:00:00 2001 From: Jordan Augé Date: Fri, 24 Feb 2017 14:58:01 +0100 Subject: Initial commit: vICN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I7ce66c4e84a6a1921c63442f858b49e083adc7a7 Signed-off-by: Jordan Augé --- vicn/__init__.py | 0 vicn/__main__.py | 21 + vicn/bin/__init__.py | 0 vicn/bin/vicn.py | 104 ++ vicn/clients/__init__.py | 0 vicn/clients/command_line.py | 322 +++++ vicn/core/__init__.py | 0 vicn/core/address_mgr.py | 181 +++ vicn/core/api.py | 118 ++ vicn/core/attribute.py | 270 ++++ vicn/core/command_helpers.py | 48 + vicn/core/commands.py | 376 ++++++ vicn/core/event.py | 25 + vicn/core/exception.py | 39 + vicn/core/requirement.py | 229 ++++ vicn/core/resource.py | 898 ++++++++++++++ vicn/core/resource_factory.py | 84 ++ vicn/core/resource_mgr.py | 1436 ++++++++++++++++++++++ vicn/core/sa_collections.py | 249 ++++ vicn/core/sa_compat.py | 270 ++++ vicn/core/scheduling_algebra.py | 97 ++ vicn/core/state.py | 177 +++ vicn/core/task.py | 352 ++++++ vicn/helpers/resource_definition.py | 22 + vicn/helpers/tasks.py | 21 + vicn/resource/__init__.py | 0 vicn/resource/application.py | 29 + vicn/resource/central.py | 789 ++++++++++++ vicn/resource/channel.py | 44 + vicn/resource/dns_server.py | 33 + vicn/resource/gui.py | 29 + vicn/resource/icn/__init__.py | 0 vicn/resource/icn/ccnx_consumer_producer_test.py | 109 ++ vicn/resource/icn/ccnx_keystore.py | 87 ++ vicn/resource/icn/ccnx_metis.py | 368 ++++++ vicn/resource/icn/ccnx_simpleTrafficGenerator.py | 106 ++ vicn/resource/icn/consumer.py | 25 + vicn/resource/icn/face.py | 140 +++ vicn/resource/icn/forwarder.py | 64 + vicn/resource/icn/icn_application.py | 37 + vicn/resource/icn/icn_tools.py | 26 + vicn/resource/icn/iping.py | 125 ++ vicn/resource/icn/ndnpingserver.py | 76 ++ vicn/resource/icn/nfd.py | 136 ++ vicn/resource/icn/producer.py | 29 + vicn/resource/icn/repo-ng.py | 25 + vicn/resource/icn/route.py | 36 + vicn/resource/icn/virtual-repo.py | 37 + vicn/resource/icn/webserver.py | 29 + vicn/resource/interface.py | 47 + vicn/resource/ip/__init__.py | 0 vicn/resource/ip/route.py | 32 + vicn/resource/ip/routing_table.py | 175 +++ vicn/resource/linux/__init__.py | 0 vicn/resource/linux/application.py | 56 + vicn/resource/linux/bridge.py | 104 ++ vicn/resource/linux/bridge_mgr.py | 34 + vicn/resource/linux/certificate.py | 74 ++ vicn/resource/linux/dnsmasq.py | 118 ++ vicn/resource/linux/file.py | 110 ++ vicn/resource/linux/iperf.py | 27 + vicn/resource/linux/link.py | 201 +++ vicn/resource/linux/macvlan.py | 52 + vicn/resource/linux/macvtap.py | 52 + vicn/resource/linux/net_device.py | 519 ++++++++ vicn/resource/linux/netmon.py | 29 + vicn/resource/linux/numa_mgr.py | 113 ++ vicn/resource/linux/ovs.py | 68 + vicn/resource/linux/package_manager.py | 232 ++++ vicn/resource/linux/phy_interface.py | 49 + vicn/resource/linux/phy_link.py | 39 + vicn/resource/linux/physical.py | 144 +++ vicn/resource/linux/repository.py | 41 + vicn/resource/linux/service.py | 83 ++ vicn/resource/linux/sym_veth_pair.py | 151 +++ vicn/resource/linux/tap_device.py | 48 + vicn/resource/linux/traceroute.py | 23 + vicn/resource/linux/veth_pair.py | 62 + vicn/resource/lxd/__init__.py | 0 vicn/resource/lxd/lxc_container.py | 317 +++++ vicn/resource/lxd/lxc_image.py | 116 ++ vicn/resource/lxd/lxd_hypervisor.py | 223 ++++ vicn/resource/node.py | 93 ++ vicn/resource/ns3/__init__.py | 0 vicn/resource/ns3/emulated_channel.py | 209 ++++ vicn/resource/ns3/emulated_lte_channel.py | 188 +++ vicn/resource/ns3/emulated_wifi_channel.py | 148 +++ vicn/resource/script.py | 31 + vicn/resource/vpp/__init__.py | 0 vicn/resource/vpp/cicn.py | 138 +++ vicn/resource/vpp/dpdk_device.py | 35 + vicn/resource/vpp/interface.py | 125 ++ vicn/resource/vpp/scripts.py | 287 +++++ vicn/resource/vpp/vpp.py | 187 +++ vicn/resource/vpp/vpp_bridge.py | 130 ++ vicn/resource/vpp/vpp_commands.py | 41 + vicn/resource/vpp/vpp_host.py | 144 +++ 97 files changed, 12813 insertions(+) create mode 100644 vicn/__init__.py create mode 100644 vicn/__main__.py create mode 100644 vicn/bin/__init__.py create mode 100755 vicn/bin/vicn.py create mode 100644 vicn/clients/__init__.py create mode 100755 vicn/clients/command_line.py create mode 100644 vicn/core/__init__.py create mode 100644 vicn/core/address_mgr.py create mode 100644 vicn/core/api.py create mode 100644 vicn/core/attribute.py create mode 100644 vicn/core/command_helpers.py create mode 100644 vicn/core/commands.py create mode 100644 vicn/core/event.py create mode 100644 vicn/core/exception.py create mode 100644 vicn/core/requirement.py create mode 100644 vicn/core/resource.py create mode 100644 vicn/core/resource_factory.py create mode 100644 vicn/core/resource_mgr.py create mode 100644 vicn/core/sa_collections.py create mode 100644 vicn/core/sa_compat.py create mode 100644 vicn/core/scheduling_algebra.py create mode 100644 vicn/core/state.py create mode 100644 vicn/core/task.py create mode 100644 vicn/helpers/resource_definition.py create mode 100644 vicn/helpers/tasks.py create mode 100644 vicn/resource/__init__.py create mode 100644 vicn/resource/application.py create mode 100644 vicn/resource/central.py create mode 100644 vicn/resource/channel.py create mode 100644 vicn/resource/dns_server.py create mode 100644 vicn/resource/gui.py create mode 100644 vicn/resource/icn/__init__.py create mode 100644 vicn/resource/icn/ccnx_consumer_producer_test.py create mode 100644 vicn/resource/icn/ccnx_keystore.py create mode 100644 vicn/resource/icn/ccnx_metis.py create mode 100644 vicn/resource/icn/ccnx_simpleTrafficGenerator.py create mode 100644 vicn/resource/icn/consumer.py create mode 100644 vicn/resource/icn/face.py create mode 100644 vicn/resource/icn/forwarder.py create mode 100644 vicn/resource/icn/icn_application.py create mode 100644 vicn/resource/icn/icn_tools.py create mode 100644 vicn/resource/icn/iping.py create mode 100644 vicn/resource/icn/ndnpingserver.py create mode 100644 vicn/resource/icn/nfd.py create mode 100644 vicn/resource/icn/producer.py create mode 100644 vicn/resource/icn/repo-ng.py create mode 100644 vicn/resource/icn/route.py create mode 100644 vicn/resource/icn/virtual-repo.py create mode 100644 vicn/resource/icn/webserver.py create mode 100644 vicn/resource/interface.py create mode 100644 vicn/resource/ip/__init__.py create mode 100644 vicn/resource/ip/route.py create mode 100644 vicn/resource/ip/routing_table.py create mode 100644 vicn/resource/linux/__init__.py create mode 100644 vicn/resource/linux/application.py create mode 100644 vicn/resource/linux/bridge.py create mode 100644 vicn/resource/linux/bridge_mgr.py create mode 100644 vicn/resource/linux/certificate.py create mode 100644 vicn/resource/linux/dnsmasq.py create mode 100644 vicn/resource/linux/file.py create mode 100644 vicn/resource/linux/iperf.py create mode 100644 vicn/resource/linux/link.py create mode 100644 vicn/resource/linux/macvlan.py create mode 100644 vicn/resource/linux/macvtap.py create mode 100644 vicn/resource/linux/net_device.py create mode 100644 vicn/resource/linux/netmon.py create mode 100644 vicn/resource/linux/numa_mgr.py create mode 100644 vicn/resource/linux/ovs.py create mode 100644 vicn/resource/linux/package_manager.py create mode 100644 vicn/resource/linux/phy_interface.py create mode 100644 vicn/resource/linux/phy_link.py create mode 100644 vicn/resource/linux/physical.py create mode 100644 vicn/resource/linux/repository.py create mode 100644 vicn/resource/linux/service.py create mode 100644 vicn/resource/linux/sym_veth_pair.py create mode 100644 vicn/resource/linux/tap_device.py create mode 100644 vicn/resource/linux/traceroute.py create mode 100644 vicn/resource/linux/veth_pair.py create mode 100644 vicn/resource/lxd/__init__.py create mode 100644 vicn/resource/lxd/lxc_container.py create mode 100644 vicn/resource/lxd/lxc_image.py create mode 100644 vicn/resource/lxd/lxd_hypervisor.py create mode 100644 vicn/resource/node.py create mode 100644 vicn/resource/ns3/__init__.py create mode 100644 vicn/resource/ns3/emulated_channel.py create mode 100644 vicn/resource/ns3/emulated_lte_channel.py create mode 100644 vicn/resource/ns3/emulated_wifi_channel.py create mode 100644 vicn/resource/script.py create mode 100644 vicn/resource/vpp/__init__.py create mode 100644 vicn/resource/vpp/cicn.py create mode 100644 vicn/resource/vpp/dpdk_device.py create mode 100644 vicn/resource/vpp/interface.py create mode 100644 vicn/resource/vpp/scripts.py create mode 100644 vicn/resource/vpp/vpp.py create mode 100644 vicn/resource/vpp/vpp_bridge.py create mode 100644 vicn/resource/vpp/vpp_commands.py create mode 100644 vicn/resource/vpp/vpp_host.py (limited to 'vicn') diff --git a/vicn/__init__.py b/vicn/__init__.py new file mode 100644 index 00000000..e69de29b 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 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 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 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 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 : 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 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 ''.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 ''.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 '' 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 ''.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 "".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 ''.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 ''.format(self._elements) + + class Composition(Operator, composition_mixin): + def __repr__(self): + return ''.format(self._elements) + + class Sequential(Operator, sequential_mixin): + def __repr__(self): + return ''.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 '' + + 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 ''.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 '' + +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 ''.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 '' + +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 ''.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 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 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 ''.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.*?), Uri: (?P.*?), \)') +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 ''.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 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 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[^ ]*)@{}:' +CMD_INTERFACE_GET = 'ip link show | grep -A 1 {}@{}' +RX_INTERFACE_GET = '.*?(?P{})@{}:' + +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: 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: 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: + "[[ ] ][...]" + 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: + : : + 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: + # : : + [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 part from above. + # This will be in the form " 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 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 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 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) -- cgit 1.2.3-korg