aboutsummaryrefslogtreecommitdiffstats
path: root/vicn/resource
diff options
context:
space:
mode:
authorJordan Augé <jordan.auge+fdio@email.com>2017-02-24 14:58:01 +0100
committerJordan Augé <jordan.auge+fdio@cisco.com>2017-02-24 18:36:29 +0000
commit85a341d645b57b7cd88a26ed2ea0a314704240ea (patch)
treebdda2b35003aae20103a796f86daced160b8a730 /vicn/resource
parent9b30fc10fb1cbebe651e5a107e8ca5b24de54675 (diff)
Initial commit: vICN
Change-Id: I7ce66c4e84a6a1921c63442f858b49e083adc7a7 Signed-off-by: Jordan Augé <jordan.auge+fdio@cisco.com>
Diffstat (limited to 'vicn/resource')
-rw-r--r--vicn/resource/__init__.py0
-rw-r--r--vicn/resource/application.py29
-rw-r--r--vicn/resource/central.py789
-rw-r--r--vicn/resource/channel.py44
-rw-r--r--vicn/resource/dns_server.py33
-rw-r--r--vicn/resource/gui.py29
-rw-r--r--vicn/resource/icn/__init__.py0
-rw-r--r--vicn/resource/icn/ccnx_consumer_producer_test.py109
-rw-r--r--vicn/resource/icn/ccnx_keystore.py87
-rw-r--r--vicn/resource/icn/ccnx_metis.py368
-rw-r--r--vicn/resource/icn/ccnx_simpleTrafficGenerator.py106
-rw-r--r--vicn/resource/icn/consumer.py25
-rw-r--r--vicn/resource/icn/face.py140
-rw-r--r--vicn/resource/icn/forwarder.py64
-rw-r--r--vicn/resource/icn/icn_application.py37
-rw-r--r--vicn/resource/icn/icn_tools.py26
-rw-r--r--vicn/resource/icn/iping.py125
-rw-r--r--vicn/resource/icn/ndnpingserver.py76
-rw-r--r--vicn/resource/icn/nfd.py136
-rw-r--r--vicn/resource/icn/producer.py29
-rw-r--r--vicn/resource/icn/repo-ng.py25
-rw-r--r--vicn/resource/icn/route.py36
-rw-r--r--vicn/resource/icn/virtual-repo.py37
-rw-r--r--vicn/resource/icn/webserver.py29
-rw-r--r--vicn/resource/interface.py47
-rw-r--r--vicn/resource/ip/__init__.py0
-rw-r--r--vicn/resource/ip/route.py32
-rw-r--r--vicn/resource/ip/routing_table.py175
-rw-r--r--vicn/resource/linux/__init__.py0
-rw-r--r--vicn/resource/linux/application.py56
-rw-r--r--vicn/resource/linux/bridge.py104
-rw-r--r--vicn/resource/linux/bridge_mgr.py34
-rw-r--r--vicn/resource/linux/certificate.py74
-rw-r--r--vicn/resource/linux/dnsmasq.py118
-rw-r--r--vicn/resource/linux/file.py110
-rw-r--r--vicn/resource/linux/iperf.py27
-rw-r--r--vicn/resource/linux/link.py201
-rw-r--r--vicn/resource/linux/macvlan.py52
-rw-r--r--vicn/resource/linux/macvtap.py52
-rw-r--r--vicn/resource/linux/net_device.py519
-rw-r--r--vicn/resource/linux/netmon.py29
-rw-r--r--vicn/resource/linux/numa_mgr.py113
-rw-r--r--vicn/resource/linux/ovs.py68
-rw-r--r--vicn/resource/linux/package_manager.py232
-rw-r--r--vicn/resource/linux/phy_interface.py49
-rw-r--r--vicn/resource/linux/phy_link.py39
-rw-r--r--vicn/resource/linux/physical.py144
-rw-r--r--vicn/resource/linux/repository.py41
-rw-r--r--vicn/resource/linux/service.py83
-rw-r--r--vicn/resource/linux/sym_veth_pair.py151
-rw-r--r--vicn/resource/linux/tap_device.py48
-rw-r--r--vicn/resource/linux/traceroute.py23
-rw-r--r--vicn/resource/linux/veth_pair.py62
-rw-r--r--vicn/resource/lxd/__init__.py0
-rw-r--r--vicn/resource/lxd/lxc_container.py317
-rw-r--r--vicn/resource/lxd/lxc_image.py116
-rw-r--r--vicn/resource/lxd/lxd_hypervisor.py223
-rw-r--r--vicn/resource/node.py93
-rw-r--r--vicn/resource/ns3/__init__.py0
-rw-r--r--vicn/resource/ns3/emulated_channel.py209
-rw-r--r--vicn/resource/ns3/emulated_lte_channel.py188
-rw-r--r--vicn/resource/ns3/emulated_wifi_channel.py148
-rw-r--r--vicn/resource/script.py31
-rw-r--r--vicn/resource/vpp/__init__.py0
-rw-r--r--vicn/resource/vpp/cicn.py138
-rw-r--r--vicn/resource/vpp/dpdk_device.py35
-rw-r--r--vicn/resource/vpp/interface.py125
-rw-r--r--vicn/resource/vpp/scripts.py287
-rw-r--r--vicn/resource/vpp/vpp.py187
-rw-r--r--vicn/resource/vpp/vpp_bridge.py130
-rw-r--r--vicn/resource/vpp/vpp_commands.py41
-rw-r--r--vicn/resource/vpp/vpp_host.py144
72 files changed, 7474 insertions, 0 deletions
diff --git a/vicn/resource/__init__.py b/vicn/resource/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/__init__.py
diff --git a/vicn/resource/application.py b/vicn/resource/application.py
new file mode 100644
index 00000000..f5341f2b
--- /dev/null
+++ b/vicn/resource/application.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import Resource
+from vicn.resource.node import Node
+
+class Application(Resource):
+ node = Attribute(Node,
+ description = 'Node on which the application is installed',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'applications',
+ reverse_description = 'Applications installed on node')
diff --git a/vicn/resource/central.py b/vicn/resource/central.py
new file mode 100644
index 00000000..73a43478
--- /dev/null
+++ b/vicn/resource/central.py
@@ -0,0 +1,789 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import networkx as nx
+import logging
+
+from netmodel.model.type import String
+from netmodel.util.misc import pairwise
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import async_task, inline_task
+from vicn.core.task import EmptyTask, BashTask
+from vicn.resource.channel import Channel
+from vicn.resource.ip.route import IPRoute
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.icn.face import L2Face, L4Face, FaceProtocol
+from vicn.resource.icn.producer import Producer
+from vicn.resource.icn.route import Route as ICNRoute
+from vicn.resource.linux.file import TextFile
+from vicn.resource.lxd.lxc_container import LxcContainer
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+TMP_DEFAULT_PORT = 6363
+
+CMD_CONTAINER_SET_DNS = 'echo "nameserver {ip_dns}" | ' \
+ 'resolvconf -a {interface_name}'
+
+# For host
+CMD_NAT = '\n'.join([
+ 'iptables -t nat -A POSTROUTING -o {bridge_name} -s {network} ' \
+ '! -d {network} -j MASQUERADE',
+ 'echo 1 > /proc/sys/net/ipv4/ip_forward'
+])
+
+# For containers
+CMD_IP_FORWARD = 'echo 1 > /proc/sys/net/ipv4/ip_forward'
+
+HOST_FILE_PATH = "/etc/hosts.vicn"
+
+#------------------------------------------------------------------------------
+# Routing strategies
+#------------------------------------------------------------------------------
+
+def routing_strategy_spt(G, origins, weight_key = None):
+ """Routing strategy : Shortest path tree
+
+ This routing strategy uses the Dijkstra algorithm on an undirected graph
+ to build the shortest path tree towards all origin prefixes.
+
+ NOTE: weights are currently unsupported by this strategy.
+
+ Args:
+ G (nx.Graph): network graph
+ origins (dict): dictionary mapping nodes to the set of prefixes they
+ are origins for
+ weight_key (str): key corresponding to weight key in edge data. None
+ assumes all weights have unit cost
+
+ Returns:
+ generator : returning triplets (source, prefix, next hop)
+ """
+ assert weight_key is None
+
+ origin_nodes = origins.keys()
+ seen = set()
+ for dst_node in origin_nodes:
+ sssp = nx.shortest_path(G, target = dst_node)
+ # Notes from the documentation:
+ # - If only the target is specified, return a dictionary keyed by
+ # sources with a list of nodes in a shortest path from one of the
+ # sources to the target.
+ # - All returned paths include both the source and target in the
+ # path.
+ for _, path in sssp.items():
+ if len(path) == 1:
+ # Local prefix
+ continue
+ for s, d in pairwise(path):
+ for prefix in origins[dst_node]:
+ t = (s, prefix, d)
+ if t in seen:
+ continue
+ seen.add(t)
+ yield t
+
+def routing_strategy_max_flow(G, origins, weight_key = 'capacity'):
+ """Routing strategy : Maximum Flow
+
+ TODO
+
+ Args:
+ G (nx.Graph): network graph
+ origins (dict): dictionary mapping nodes to the set of prefixes they
+ are origins for
+ weight_key (str): key corresponding to weight key in edge data. None
+ assumes all weights have unit cost
+
+ Returns:
+ generator : returning triplets (source, prefix, next hop)
+ """
+ assert weight_key is None
+
+ origin_nodes = origins.keys()
+ for dst_node in origin_nodes:
+ for src_node in G.nodes:
+ if src_node == dst_node:
+ continue
+ _, flow_dict = nx.maximum_flow(G, src_node, dst_node,
+ capacity=weight_key)
+
+ # Notes from the documentation:
+ # https://networkx.github.io/documentation/networkx-1.10/reference/
+ # generated/networkx.algorithms.flow.maximum_flow.html
+ # - flow_dict (dict) – A dictionary containing the value of the
+ # flow that went through each edge.
+ for s, d_map in flow_dict.items():
+ for d, flow in d_map.items():
+ if flow == 0:
+ continue
+ for prefix in origins[dst_node]:
+ yield s, prefix, d
+
+MAP_ROUTING_STRATEGY = {
+ 'spt' : routing_strategy_spt,
+ 'max_flow' : routing_strategy_max_flow,
+}
+
+#------------------------------------------------------------------------------
+# L2 and L4/ICN graphs
+#------------------------------------------------------------------------------
+
+def _get_l2_graph(manager, with_managed = False):
+ G = nx.Graph()
+ for node in manager.by_type(Node):
+ G.add_node(node._state.uuid)
+
+ for channel in manager.by_type(Channel):
+ if channel.has_type('emulatedchannel'):
+ src = channel._ap_if
+ for dst in channel._sta_ifs.values():
+ if not with_managed and (not src.managed or not dst.managed):
+ continue
+ if G.has_edge(src.node._state.uuid, dst.node._state.uuid):
+ continue
+
+ map_node_interface = { src.node._state.uuid : src._state.uuid,
+ dst.node._state.uuid: dst._state.uuid}
+ G.add_edge(src.node._state.uuid, dst.node._state.uuid,
+ map_node_interface = map_node_interface)
+ else:
+ # This is for a normal Channel
+ for src_it in range(0,len(channel.interfaces)):
+ src = channel.interfaces[src_it]
+
+ # Iterate over the remaining interface to create all the
+ # possible combination
+ for dst_it in range(src_it+1,len(channel.interfaces)):
+ dst = channel.interfaces[dst_it]
+
+ if not with_managed and (not src.managed or
+ not dst.managed):
+ continue
+ if G.has_edge(src.node._state.uuid, dst.node._state.uuid):
+ continue
+ map_node_interface = {
+ src.node._state.uuid : src._state.uuid,
+ dst.node._state.uuid: dst._state.uuid}
+ G.add_edge(src.node._state.uuid, dst.node._state.uuid,
+ map_node_interface = map_node_interface)
+ return G
+
+def _get_icn_graph(manager):
+ G = nx.Graph()
+ for forwarder in manager.by_type(Forwarder):
+ node = forwarder.node
+ G.add_node(node._state.uuid)
+ for face in forwarder.faces:
+ other_face = manager.by_uuid(face._internal_data['sibling_face'])
+ other_node = other_face.node
+ if G.has_edge(node._state.uuid, other_node._state.uuid):
+ continue
+ map_node_face = { node._state.uuid: face._state.uuid,
+ other_node._state.uuid: other_face._state.uuid }
+ G.add_edge(node._state.uuid, other_node._state.uuid,
+ map_node_face = map_node_face)
+
+ return G
+
+#-------------------------------------------------------------------------------
+
+class IPAssignment(Resource):
+ """
+ Resource: IPAssignment
+ """
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ('Interface',)
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __subresources__(self):
+ self.host_file = TextFile(node = None, filename = HOST_FILE_PATH,
+ overwrite = True)
+ return self.host_file
+
+ def __create__(self):
+ """
+ IP assignment strategy /32: assign /32 IP address to each interface
+
+ We might use different subnets for resources involved in experiment,
+ and supporting resources, and to minimize routing tables.
+ """
+ log.info('Assigment of IP addresses')
+ tasks = EmptyTask()
+
+ # We sort nodes by names for IP assignment. This code ensures that
+ # interfaces on the same channel get consecutive IP addresses. That
+ # way, we can assign /31 on p2p channels.
+ channels = sorted(self._state.manager.by_type(Channel),
+ key = lambda x : x.get_sortable_name())
+ channels.extend(sorted(self._state.manager.by_type(Node),
+ key = lambda node : node.name))
+
+ host_file_content = ""
+
+ # Dummy code to start IP addressing on an even number for /31
+ ip = AddressManager().get_ip(None)
+ if int(ip[-1]) % 2 == 0:
+ ip = AddressManager().get_ip("dummy object")
+
+ for channel in channels:
+ # Sort interfaces in a deterministic order to ensure consistent
+ # addressing across restarts of the tool
+ interfaces = sorted(channel.interfaces,
+ key = lambda x : x.device_name)
+
+ for interface in interfaces:
+ if interface.ip_address is None:
+ ip = AddressManager().get_ip(interface)
+
+ @async_task
+ async def set_ip(interface, ip):
+ await interface.async_set('ip_address', ip)
+
+ if interface.managed:
+ tasks = tasks | set_ip(interface, ip)
+ else:
+ ip = interface.ip_address
+
+ # Note: interface.ip_address should still be None at this stage
+ # since we have not made the assignment yet
+
+ if isinstance(channel, Node):
+ host_file_content += '# {} {} {}\n'.format(
+ interface.node.name, interface.device_name, ip)
+ if interface == interface.node.host_interface:
+ host_file_content += '{} {}\n'.format(ip,
+ interface.node.name)
+ self.host_file.content = host_file_content
+
+ return tasks
+
+ __delete__ = None
+
+#-------------------------------------------------------------------------------
+
+class IPRoutes(Resource):
+ """
+ Resource: IPRoutes
+
+ Centralized IP route computation.
+ """
+ routing_strategy = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ @inline_task
+ def __create__(self):
+ routes = list()
+ pre_routes, routes = self._get_ip_routes()
+ routes.extend(pre_routes)
+ routes.extend(routes)
+ for route in routes:
+ route.node.routing_table.routes << route
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_ip_origins(self):
+ origins = dict()
+ for node in self._state.manager.by_type(Node):
+ node_uuid = node._state.uuid
+ if not node_uuid in origins:
+ origins[node_uuid] = list()
+ for interface in node.interfaces:
+ origins[node_uuid].append(interface.ip_address)
+ return origins
+
+ def _get_ip_routes(self):
+ if self.routing_strategy == 'pair':
+ return [], self._get_pair_ip_routes()
+
+ strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy)
+
+ G = _get_l2_graph(self._state.manager)
+ origins = self._get_ip_origins()
+
+ # node -> list(origins for which we have routes)
+ ip_routes = dict()
+
+ pre_routes = list()
+ routes = list()
+ for src, prefix, dst in strategy(G, origins):
+ data = G.get_edge_data(src, dst)
+
+ map_ = data['map_node_interface']
+ next_hop_interface = map_[src]
+
+
+ next_hop_ingress = self._state.manager.by_uuid(map_[dst])
+ src_node = self._state.manager.by_uuid(src)
+
+ mac_addr = None
+ if ((hasattr(next_hop_ingress, 'vpp') and
+ next_hop_ingress.vpp is not None) or
+ (hasattr(src_node, 'vpp') and src_node.vpp is not None)):
+ mac_addr = next_hop_ingress.mac_address
+
+ # Avoid duplicate routes due to multiple paths in the network
+ if not src_node in ip_routes:
+ ip_routes[src_node] = list()
+ if prefix in ip_routes[src_node]:
+ continue
+
+ if prefix == next_hop_ingress.ip_address:
+ # Direct route on src_node.name :
+ # route add [prefix] dev [next_hop_interface_.device_name]
+ route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = prefix,
+ mac_address = mac_addr,
+ interface = next_hop_interface)
+ else:
+ # We need to be sure we have a route to the gw from the node
+ if not next_hop_ingress.ip_address in ip_routes[src_node]:
+ pre_route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = next_hop_ingress.ip_address,
+ mac_address = mac_addr,
+ interface = next_hop_interface)
+ ip_routes[src_node].append(next_hop_ingress.ip_address)
+ pre_routes.append(pre_route)
+
+ # Route on src_node.name:
+ # route add [prefix] dev [next_hop_interface_.device_name]
+ # via [next_hop_ingress.ip_address]
+ route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = prefix,
+ interface = next_hop_interface,
+ mac_address = mac_addr,
+ gateway = next_hop_ingress.ip_address)
+ ip_routes[src_node].append(prefix)
+ routes.append(route)
+ return pre_routes, routes
+
+ def _get_pair_ip_routes(self):
+ """
+ IP routing strategy : direct routes only
+ """
+ routes = list()
+ G = _get_l2_graph(self._state.manager)
+ for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True):
+ src_node = self._state.manager.by_uuid(src_node_uuid)
+ dst_node = self._state.manager.by_uuid(dst_node_uuid)
+
+ map_ = data['map_node_interface']
+ src = self._state.manager.by_uuid(map_[src_node_uuid])
+ dst = self._state.manager.by_uuid(map_[dst_node_uuid])
+
+ log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format(
+ src_node.name, src.device_name, src.ip_address,
+ dst_node.name, dst.device_name, dst.ip_address))
+ log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format(
+ dst_node.name, dst.device_name, dst.ip_address,
+ src_node.name, src.device_name, src.ip_address))
+
+ route = IPRoute(node = src_node,
+ managed = False,
+ owner = self,
+ ip_address = dst.ip_address,
+ mac_address = dst.mac_address,
+ interface = src)
+ routes.append(route)
+
+ route = IPRoute(node = dst_node,
+ managed = False,
+ owner = self,
+ ip_address = src.ip_address,
+ mac_address = src.mac_address,
+ interface = dst)
+ routes.append(route)
+
+ return routes
+
+#-------------------------------------------------------------------------------
+
+class ICNFaces(Resource):
+ """
+ Resource: ICNFaces
+
+ Centralized ICN face creation.
+ """
+ protocol_name = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound # always create faces
+
+ @inline_task
+ def __create__(self):
+ icn_faces = self._get_faces()
+ for face in icn_faces:
+ face.node.forwarder.faces << face
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_faces(self):
+ """
+ Face creation (heuristic: facemgr)
+
+ Requires: at least direct IP links
+ """
+ protocol = FaceProtocol.from_string(self.protocol_name)
+
+ faces = list()
+ G = _get_l2_graph(self._state.manager)
+ for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True):
+ src_node = self._state.manager.by_uuid(src_node_uuid)
+ dst_node = self._state.manager.by_uuid(dst_node_uuid)
+
+ map_ = data['map_node_interface']
+ src = self._state.manager.by_uuid(map_[src_node_uuid])
+ dst = self._state.manager.by_uuid(map_[dst_node_uuid])
+
+ log.debug('{} -> {} ({} -> {})'.format(src_node_uuid,
+ dst_node_uuid, src.device_name, dst.device_name))
+
+ if protocol == FaceProtocol.ether:
+ src_face = L2Face(node = src_node,
+ owner = self,
+ protocol = protocol,
+ src_nic = src,
+ dst_mac = dst.mac_address)
+ dst_face = L2Face(node = dst_node,
+ owner = self,
+ protocol = protocol,
+ src_nic = dst,
+ dst_mac = src.mac_address)
+
+ elif protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6,
+ FaceProtocol.udp4, FaceProtocol.udp6):
+ src_face = L4Face(node = src_node,
+ owner = self,
+ protocol = protocol,
+ src_ip = src.ip_address,
+ dst_ip = dst.ip_address,
+ src_port = TMP_DEFAULT_PORT,
+ dst_port = TMP_DEFAULT_PORT)
+ dst_face = L4Face(node = dst_node,
+ owner = self,
+ protocol = protocol,
+ src_ip = dst.ip_address,
+ dst_ip = src.ip_address,
+ src_port = TMP_DEFAULT_PORT,
+ dst_port = TMP_DEFAULT_PORT)
+ else:
+ raise NotImplementedError
+
+ # We key the sibling face for easier building of the ICN graph
+ src_face._internal_data['sibling_face'] = dst_face._state.uuid
+ dst_face._internal_data['sibling_face'] = src_face._state.uuid
+
+ faces.append(src_face)
+ faces.append(dst_face)
+
+ return faces
+
+#------------------------------------------------------------------------------
+
+class ICNRoutes(Resource):
+ """
+ Resource: ICNRoutes
+
+ Centralized ICN route computation.
+ """
+
+ routing_strategy = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound # always create routes
+
+ @inline_task
+ def __create__(self):
+ icn_routes = self._get_icn_routes()
+ for route in icn_routes:
+ route.node.forwarder.routes << route
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_prefix_origins(self):
+ origins = dict()
+ for producer in self._state.manager.by_type(Producer):
+ node_uuid = producer.node._state.uuid
+ if not node_uuid in origins:
+ origins[node_uuid] = list()
+ origins[node_uuid].extend(producer.prefixes)
+ return origins
+
+ def _get_icn_routes(self):
+ strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy)
+
+ G = _get_icn_graph(self._state.manager)
+ origins = self._get_prefix_origins()
+
+ routes = list()
+ for src, prefix, dst in strategy(G, origins):
+ data = G.get_edge_data(src, dst)
+
+ map_ = data['map_node_face']
+ next_hop_face = map_[src]
+
+ route = ICNRoute(node = src,
+ owner = self,
+ prefix = prefix,
+ face = next_hop_face)
+ routes.append(route)
+ return routes
+
+#------------------------------------------------------------------------------
+
+class DnsServerEntry(Resource):
+ """
+ Resource: DnsServerEntry
+
+ Setup of DNS resolver for LxcContainers
+
+ Todo:
+ - This should be merged into the LxcContainer resource
+ """
+
+ node = Attribute(String)
+ ip_address = Attribute(String)
+ interface_name = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CONTAINER_SET_DNS,
+ {'ip_dns': self.ip_address,
+ 'interface_name': self.interface_name})
+
+ def __delete__(self):
+ raise NotImplementedError
+
+#------------------------------------------------------------------------------
+
+class ContainerSetup(Resource):
+ """
+ Resource: ContainerSetup
+
+ Setup of container networking
+
+ Todo:
+ - This should be merged into the LxcContainer resource
+ """
+
+ container = Attribute(LxcContainer)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+
+ # a) routes: host -> container
+ # . container interfaces
+ # . container host (main) interface
+ # route add -host {ip_address} dev {bridge_name}
+ route = IPRoute(node = self.container.node,
+ managed = False,
+ owner = self,
+ ip_address = self.container.host_interface.ip_address,
+ interface = self.container.node.bridge)
+ route.node.routing_table.routes << route
+
+ # b) route: container -> host
+ # route add {ip_gateway} dev {interface_name}
+ # route add default gw {ip_gateway} dev {interface_name}
+ route = IPRoute(node = self.container,
+ owner = self,
+ managed = False,
+ ip_address = self.container.node.bridge.ip_address,
+ interface = self.container.host_interface)
+ route.node.routing_table.routes << route
+ route_gw = IPRoute(node = self.container,
+ managed = False,
+ owner = self,
+ ip_address = 'default',
+ interface = self.container.host_interface,
+ gateway = self.container.node.bridge.ip_address)
+ route_gw.node.routing_table.routes << route_gw
+
+ # c) dns
+ dns_server_entry = DnsServerEntry(node = self.container,
+ owner = self,
+ ip_address = self.container.node.bridge.ip_address,
+ interface_name = self.container.host_interface.device_name)
+
+ return dns_server_entry
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ return BashTask(self.container.node, CMD_IP_FORWARD)
+
+#------------------------------------------------------------------------------
+
+class ContainersSetup(Resource):
+ """
+ Resource: ContainersSetup
+
+ Setup of LxcContainers (main resource)
+
+ Todo:
+ - This should be merged into the LxcContainer resource
+ """
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ containers = self._state.manager.by_type(LxcContainer)
+ if len(containers) == 0:
+ return None
+
+ container_resources = [ContainerSetup(owner = self, container = c)
+ for c in containers]
+
+ return Resource.__concurrent__(*container_resources)
+
+#------------------------------------------------------------------------------
+
+class CentralIP(Resource):
+ """
+ Resource: CentralIP
+
+ Central IP management (main resource)
+ """
+
+ ip_routing_strategy = Attribute(String, description = 'IP routing strategy',
+ default = 'pair') # spt, pair
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after_init__(self):
+ return ('Node', 'Channel', 'Interface')
+
+ def __subresources__(self):
+ ip_assign = IPAssignment(owner=self)
+ containers_setup = ContainersSetup(owner=self)
+ ip_routes = IPRoutes(owner = self,
+ routing_strategy = self.ip_routing_strategy)
+
+ return ip_assign > (ip_routes | containers_setup)
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ __delete__ = None
+
+#------------------------------------------------------------------------------
+
+class CentralICN(Resource):
+ """
+ Resource: CentralICN
+
+ Central ICN management (main resource)
+ """
+
+ # Choices: spt, max_flow
+ icn_routing_strategy = Attribute(String,
+ description = 'ICN routing strategy',
+ default = 'spt')
+ face_protocol = Attribute(String,
+ description = 'Protocol used to create faces',
+ default = 'ether')
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ """
+ We need to wait for IP configuration in order to be able to build
+ overload ICN faces, and producers for prefix origins.
+ """
+ return ('CentralIP',)
+
+ def __subresources__(self):
+ icn_faces = ICNFaces(owner = self, protocol_name = self.face_protocol)
+ icn_routes = ICNRoutes(owner = self,
+ routing_strategy = self.icn_routing_strategy)
+ return icn_faces > icn_routes
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ __delete__ = None
diff --git a/vicn/resource/channel.py b/vicn/resource/channel.py
new file mode 100644
index 00000000..cd64b641
--- /dev/null
+++ b/vicn/resource/channel.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.resource import Resource
+from vicn.core.attribute import Attribute
+
+class Channel(Resource):
+ """
+ Resource: Channel
+ """
+
+ #--------------------------------------------------------------------------
+ # Public API
+ #--------------------------------------------------------------------------
+
+ def get_remote_name(self, name):
+ if len(self._interfaces) != 2:
+ return None
+ return next(x for x in self._interfaces if x.get_name() != name)
+
+ def get_sortable_name(self):
+ """
+ This method is used to sort channel during IP assignment. This is
+ necessary to get the same IP configuration on the same experiment.
+ """
+ ret = "{:03}".format(len(self.interfaces))
+ ret = ret + ''.join(sorted(map(lambda x : x.node.name, self.interfaces)))
+ return ret
diff --git a/vicn/resource/dns_server.py b/vicn/resource/dns_server.py
new file mode 100644
index 00000000..3fbe89f9
--- /dev/null
+++ b/vicn/resource/dns_server.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import FactoryResource
+from vicn.resource.application import Application
+
+class DnsServer(Application):
+ """
+ Resource: DnsServer
+ """
+
+ __type__ = FactoryResource
+
+ node = Attribute(
+ reverse_name = 'dns_server',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
diff --git a/vicn/resource/gui.py b/vicn/resource/gui.py
new file mode 100644
index 00000000..3ded7a5a
--- /dev/null
+++ b/vicn/resource/gui.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.helpers.resource_definition import *
+
+class GUI(Resource):
+ """
+ Resource: GUI
+
+ This resource is empty on purpose. It is a temporary resource used as a
+ placeholder for controlling the GUI and should be deprecated in future
+ releases.
+ """
+ pass
diff --git a/vicn/resource/icn/__init__.py b/vicn/resource/icn/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/icn/__init__.py
diff --git a/vicn/resource/icn/ccnx_consumer_producer_test.py b/vicn/resource/icn/ccnx_consumer_producer_test.py
new file mode 100644
index 00000000..f682657d
--- /dev/null
+++ b/vicn/resource/icn/ccnx_consumer_producer_test.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask
+from vicn.resource.icn.icn_application import ICN_SUITE_CCNX_1_0
+from vicn.resource.icn.consumer import Consumer
+from vicn.resource.icn.producer import Producer
+from vicn.resource.node import Node
+
+class CcnxConsumerTest(Consumer):
+ """
+ Resource: CcnxConsumerTest
+
+ Test consumer exchanging dummy data.
+ """
+
+ __package_names__ = ["libconsumer-producer-ccnx"]
+
+ prefixes = Attribute(String,
+ description = "Name served by the producer server test",
+ default = lambda self: self.default_name(),
+ mandatory = False,
+ multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ requirements=[
+ Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0']),
+ properties = {"protocol_suites" : ICN_SUITE_CCNX_1_0})
+ ])
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/ccnxtest']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ template = ["consumer-test", " ccnx:{prefix}"]
+ params = {'prefix' : self.prefixes[0]}
+ return BashTask(self.node, ' '.join(template), parameters = params)
+
+ def __method_stop__(self):
+ raise NotImplementedError
+
+#------------------------------------------------------------------------------
+
+class CcnxProducerTest(Producer):
+ """
+ Resource: CcnxConsumerTest
+
+ Test producer exchanging dummy data.
+ """
+
+ __package_names__ = ["libconsumer-producer-ccnx"]
+
+ node = Attribute(Node,
+ requirements = [Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0']),
+ properties = {"protocol_suites" : ICN_SUITE_CCNX_1_0})])
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/ccnxtest']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ template = ["producer-test", " ccnx:{prefix}"]
+ params = {'prefix' : self.prefixes[0]}
+
+ return BashTask(self.node, ' '.join(template), parameters = params)
+
+ def __method_stop__(self):
+ raise NotImplementedError
+
diff --git a/vicn/resource/icn/ccnx_keystore.py b/vicn/resource/icn/ccnx_keystore.py
new file mode 100644
index 00000000..ddd87019
--- /dev/null
+++ b/vicn/resource/icn/ccnx_keystore.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String, Integer
+from vicn.core.attribute import Attribute, Reference
+from vicn.core.task import BashTask
+from vicn.resource.linux.file import File
+from vicn.resource.linux.package_manager import Packages
+
+METIS_KEYSTORE_CREATE = ('parc-publickey -c {filename} {password} '
+ '{subject_name} {size} {validity}')
+
+# FIXME default passwords, not very sensitive
+DEFAULT_KEYSTORE_FILE = "keystore.pkcs12"
+DEFAULT_KEYSTORE_PASSWD = "password"
+DEFAULT_KEYSTORE_VALIDITY = 365
+DEFAULT_KEYSTORE_SUBJ = "password"
+DEFAULT_KEYSTORE_KEYLENGTH = 2048
+
+class MetisKeystore(File):
+ """
+ Resource: MetisKeystore
+ """
+
+ filename = Attribute(String, description = "File containing the keystore",
+ default = DEFAULT_KEYSTORE_FILE, mandatory=False)
+ password = Attribute(String,
+ description = "Password for the keystore file",
+ default = DEFAULT_KEYSTORE_PASSWD)
+ subject_name = Attribute(String,
+ description = "Subject name for the keystore",
+ default = DEFAULT_KEYSTORE_SUBJ)
+ validity = Attribute(String,
+ description = "Validity period of the keystore",
+ default = DEFAULT_KEYSTORE_VALIDITY)
+ size = Attribute(Integer, description = 'Length of the keys',
+ default = DEFAULT_KEYSTORE_KEYLENGTH)
+
+ __package_names__ = ['libparc']
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ packages = Packages(node=Reference(self, 'node'),
+ names=self._get_package_names(), owner=self)
+ return packages
+
+ def __create__(self):
+ args = {'filename' : self.filename, 'password' : self.password,
+ 'subject_name' : self.subject_name, 'validity' : self.validity,
+ 'size' : self.size}
+ return BashTask(self.node, METIS_KEYSTORE_CREATE, args)
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_package_names(self):
+ package_names = list()
+ for base in self.__class__.mro():
+ if not '__package_names__' in vars(base):
+ continue
+ package_names.extend(getattr(base, '__package_names__'))
+ return package_names
+
+
+ def format_baseline(self, baseline):
+ return baseline.format(keystore_file=self.filename, password=self.password)
+
+
diff --git a/vicn/resource/icn/ccnx_metis.py b/vicn/resource/icn/ccnx_metis.py
new file mode 100644
index 00000000..ead9b9bf
--- /dev/null
+++ b/vicn/resource/icn/ccnx_metis.py
@@ -0,0 +1,368 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from functools import wraps
+
+from netmodel.model.type import String, Integer, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import BashTask, EmptyTask, task
+from vicn.resource.icn.ccnx_keystore import MetisKeystore
+from vicn.resource.icn.face import L2Face, L4Face, FaceProtocol
+from vicn.resource.icn.face import DEFAULT_ETHER_PROTO
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.linux.file import TextFile
+from vicn.resource.linux.service import Service
+
+METIS_CONTROL_BASELINE = (
+ 'metis_control --keystore {keystore_file} --password {password}')
+
+CMD_ADD_LISTENER_ETHER = (
+ 'add listener ether ether{conn_id} {listener.src_nic.device_name} '
+ '{listener.ether_proto}')
+CMD_ADD_LISTENER_L4 = 'add listener {protocol} transport{conn_id} {infos}'
+CMD_ADD_CONNECTION_ETHER = ('add connection ether {face.id} {face.dst_mac} '
+ '{face.src_nic.device_name}')
+CMD_ADD_CONNECTION_L4 = ('add connection {protocol} {face.id} {face.dst_ip} '
+ '{face.dst_port} {face.src_ip} {face.src_port}')
+CMD_ADD_ROUTE = 'add route {route.face.id} ccnx:{route.prefix} {route.cost}'
+METIS_DAEMON_BOOTSTRAP = (
+ 'metis_daemon --port {port} --daemon --log-file {log_file} '
+ '--capacity {cs_size} --config {config}')
+METIS_DAEMON_STOP = "killall -9 metis_daemon"
+
+BASE_CONN_NAME = "conn"
+
+METIS_DEFAULT_PORT = 9596
+
+METIS_ETC_DEFAULT = "/etc/default/metis-forwarder"
+
+#------------------------------------------------------------------------------
+# Listeners
+#------------------------------------------------------------------------------
+
+class MetisListener:
+
+ def __init__(self, protocol):
+ self.protocol = protocol
+
+ @staticmethod
+ def listener_from_face(face):
+ if face.protocol is FaceProtocol.ether:
+ return MetisEtherListener(face.protocol, face.src_nic,
+ face.ether_proto)
+ elif face.protocol in [FaceProtocol.tcp4, FaceProtocol.tcp6,
+ FaceProtocol.udp4, FaceProtocol.udp6]:
+ return MetisL4Listener(face.protocol, face.src_ip, face.src_port)
+ else:
+ raise ValueError("Metis only supports Ethernet and TCP/UDP faces")
+
+class MetisEtherListener(MetisListener):
+
+ def __init__(self, protocol, src_nic, ether_proto=DEFAULT_ETHER_PROTO):
+ super().__init__(protocol)
+ self.src_nic = src_nic
+ self.ether_proto = ether_proto
+
+ def get_setup_command(self, conn_id):
+ return CMD_ADD_LISTENER_ETHER.format(listener = self,
+ conn_id = conn_id)
+
+ def __eq__(self, other):
+ return (isinstance(other, MetisEtherListener)
+ and (other.src_nic == self.src_nic)
+ and (other.ether_proto == self.ether_proto))
+
+ def __ne__(self, other):
+ return ((not isinstance(other, MetisEtherListener))
+ or (other.src_nic != self.src_nic)
+ or (other.ether_proto != self.ether_proto))
+
+class MetisL4Listener(MetisListener):
+
+ def __init__(self, protocol, src_ip, src_port):
+ super().__init__(protocol)
+ self.src_ip = src_ip
+ self.src_port = src_port
+
+ def _get_proto_as_str(self):
+ if self.protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6):
+ return "tcp"
+ elif self.protocol in (FaceProtocol.udp4, FaceProtocol.udp6):
+ return "udp"
+
+ def get_setup_command(self, conn_id):
+ infos = '{} {}'.format(self.src_ip, self.src_port)
+
+ return CMD_ADD_LISTENER_L4.format(protocol = self._get_proto_as_str(),
+ conn_id = conn_id, infos = infos)
+
+ def __eq__(self, other):
+ return (isinstance(other, MetisL4Listener) and
+ self.protocol == other.protocol and
+ self.src_ip == other.src_ip and
+ self.src_port == other.src_port)
+
+#------------------------------------------------------------------------------
+
+class MetisForwarder(Forwarder, Service):
+ __capabilities__ = set(['ICN_SUITE_CCNX_1_0'])
+ __package_names__ = ['metis-forwarder']
+ __service_name__ = "metis-forwarder"
+
+ log_file = Attribute(String, description = 'File for metis logging',
+ default = '/tmp/ccnx-metis.log') # '/dev/null')
+ port = Attribute(Integer, description = 'TCP port for metis',
+ default = 9695)
+ gen_config = Attribute(Bool,
+ description = 'Set to record all metis commands in a config file',
+ default = True)
+ config_file = Attribute(String, default = '/root/.ccnx_metis.conf')
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._nb_conn = 0
+ self._listeners = []
+ self._listeners_idx = 0
+ self.keystore = None
+
+ # Cache
+ self._faces = set()
+ self._routes = set()
+
+ # Internal subresources
+ self._config = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ('CentralICN',)
+
+ def __subresources__(self):
+ self.keystore = MetisKeystore(node = self.node, owner = self)
+ self.env_file = self._write_environment_file()
+ return self.keystore | self.env_file
+
+ @task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+
+ # Alternatively, we might put all commands in a configuration file
+ # before starting the forwarder. In that case, we need to restart it if
+ # it is already started.
+ # XXX Need to schedule subresource before and after some other tasks
+
+ _, faces = self._cmd_create_faces()
+ _, routes = self._cmd_create_routes()
+
+ cfg = list()
+ cfg.append('add listener tcp local0 127.0.0.1 9695')
+ cfg.extend(faces)
+ cfg.extend(routes)
+
+ self._config = TextFile(filename = self.config_file,
+ node = self.node,
+ owner = self,
+ content = '\n'.join(cfg),
+ overwrite = True)
+
+ self._state.manager.commit_resource(self._config)
+
+ start_or_restart = self.__method_restart__()
+
+ return wait_resource_task(self._config) > start_or_restart
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ # Force local management of faces and routes
+
+ _add_faces = None
+ _remove_faces = None
+ _get_faces = None
+ _set_faces = None
+
+ _add_routes = None
+ _remove_routes = None
+ _get_routes = None
+ _set_routes = None
+
+ #--------------------------------------------------------------------------
+ # Method helpers
+ #--------------------------------------------------------------------------
+
+ def _start_as_daemon(self):
+ """
+ Start the metis forwarder as normal daemon
+ """
+
+ command = METIS_DAEMON_BOOTSTRAP
+ args = {'port' : self.port, 'log_file' : self.log_file,
+ 'cs_size' : self.cache_size, 'config' : self.config_file}
+ return BashTask(self.node, command, parameters = args)
+
+ def _restart_as_daemon(self):
+ """
+ Restart the metis forwarder as normal daemon
+ """
+
+ command = METIS_DAEMON_STOP + '; ' + METIS_DAEMON_BOOTSTRAP
+ args = {'port' : self.port, 'log_file' : self.log_file,
+ 'cs_size' : self.cache_size, 'config' : self.config_file}
+ return BashTask(self.node, command, parameters = args)
+
+ def _start_as_service(self):
+ """
+ Start the metis forwarder as service managed by systemd
+ """
+ return super().__method_start__()
+
+ def _restart_as_service(self):
+ """
+ Restart the metis forwarder as service managed by systemd
+ """
+ return super().__method_restart__()
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ return self._start_as_service()
+
+ def __method_restart__(self):
+ return self._restart_as_service()
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _cmd_create_faces(self):
+ """Returns the list of commands used to update faces (delete and
+ create).
+
+ We need two lists because some delete might need to occur before create.
+ This function is used to populate the config file and also alter the
+ configuration of the forwarder live. It might be possible to further
+ optimize but keeping the two separate seems important, since delete is
+ only used for an already running metis.
+ """
+ create_cmds = list()
+ delete_cmds = list()
+
+ for face in self.faces:
+ listener = MetisListener.listener_from_face(face)
+ if listener not in self._listeners:
+ self._listeners.append(listener)
+ conn_id = self._listeners_idx
+ self._listeners_idx += 1
+ cmd = listener.get_setup_command(conn_id)
+ create_cmds.append(cmd)
+
+ face.id = 'conn{}'.format(self._nb_conn)
+ self._nb_conn += 1
+
+ if face.protocol is FaceProtocol.ether:
+ assert isinstance(face, L2Face), \
+ 'Ethernet face should be instance of L2Face'
+ cmd = CMD_ADD_CONNECTION_ETHER.format(face = face)
+
+ elif face.protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6):
+ assert isinstance(face, L4Face), \
+ "TCP/UDP face should be instance of L4Face"
+ cmd = CMD_ADD_CONNECTION_L4.format(face = face,
+ protocol = 'tcp')
+
+ elif face.protocol in (FaceProtocol.udp4, FaceProtocol.udp6):
+ assert isinstance(face, L4Face), \
+ 'TCP/UDP face should be instance of L4Face'
+ cmd = CMD_ADD_CONNECTION_L4.format(face = face,
+ protocol = 'udp')
+
+ else:
+ raise ValueError('Unsupported face type for Metis')
+
+ create_cmds.append(cmd)
+
+ return (delete_cmds, create_cmds)
+
+ def _cmd_create_routes(self):
+ create_cmds = list()
+ delete_cmds = list()
+ for route in self.routes:
+ cmd = CMD_ADD_ROUTE.format(route = route)
+ create_cmds.append(cmd)
+ return (delete_cmds, create_cmds)
+
+ def _task_create_faces(self):
+ delete_cmds, create_cmds = self._cmd_create_faces()
+
+ delete_task = EmptyTask()
+ if len(delete_cmds) > 0:
+ cmds = '\n'.join('{} {}'.format(self._baseline, command)
+ for command in delete_cmds)
+ delete_task = BashTask(self.node, cmds)
+
+ create_task = EmptyTask()
+ if len(create_cmds) > 0:
+ cmds = '\n'.join('{} {}'.format(self._baseline, command)
+ for command in create_cmds)
+ create_task = BashTask(self.node, cmds)
+
+ return delete_task > create_task
+
+ def _task_create_routes(self):
+ delete_cmds, create_cmds = self._cmd_create_routes()
+
+ delete_task = EmptyTask()
+ if len(delete_cmds) > 0:
+ delete_task = BashTask(self.node, "\n".join(delete_cmds))
+
+ create_task = EmptyTask()
+ if len(create_cmds) > 0:
+ create_task = BashTask(self.node, '\n'.join(create_cmds))
+
+ return delete_task > create_task
+
+ def _write_environment_file(self):
+ param_port = "PORT={port}"
+ param_log_file = "LOG_FILE={log_file}"
+ param_cs_capacity = "CS_SIZE={cs_size}"
+ param_config = "CONFIG={config}"
+
+ env = [param_port.format(port = self.port),
+ param_log_file.format(log_file = self.log_file),
+ param_cs_capacity.format(cs_size = self.cache_size),
+ param_config.format(config = self.config_file)]
+
+ environment_file = TextFile(filename = METIS_ETC_DEFAULT,
+ node = self.node,
+ owner = self,
+ overwrite = True,
+ content = '\n'.join(env))
+ return environment_file
diff --git a/vicn/resource/icn/ccnx_simpleTrafficGenerator.py b/vicn/resource/icn/ccnx_simpleTrafficGenerator.py
new file mode 100644
index 00000000..221298fc
--- /dev/null
+++ b/vicn/resource/icn/ccnx_simpleTrafficGenerator.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import Resource, EmptyResource
+from vicn.core.task import EmptyTask
+from vicn.resource.icn.icn_application import ICN_SUITE_CCNX_1_0
+from vicn.resource.node import Node
+
+from vicn.resource.icn.ccnx_consumer_producer_test import CcnxConsumerTest
+from vicn.resource.icn.ccnx_consumer_producer_test import CcnxProducerTest
+
+class CcnxSimpleTrafficGenerator(Resource):
+
+ prefix = Attribute(String,
+ description = "Routable prefix for the applications",
+ default = lambda self: self.default_name(),
+ mandatory = False)
+ consumers = Attribute(Node,
+ multiplicity = Multiplicity.OneToMany)
+ producers = Attribute(Node,
+ multiplicity = Multiplicity.OneToMany)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._sr = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ """
+ Create the list of consumers and producers.
+ For each of them, assign a different namespace under the same prefix.
+ """
+
+ sr = EmptyResource()
+ for producer in self.producers:
+ producer = CcnxProducerTest(node = producer,
+ owner = self,
+ prefixes = [self.prefix])
+ sr = sr | producer
+ for consumer in self.consumers:
+ full_prefix = self.prefix
+ consumer = CcnxConsumerTest(node = consumer,
+ owner = self,
+ prefixes = [full_prefix])
+ sr = sr | consumer
+ self._sr = sr
+ return sr
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ if self._sr is None:
+ return
+
+ tasks = EmptyTask()
+ for sr in self._sr:
+ sr_task = sr.__method_start__()
+ tasks = tasks | sr_task
+ return tasks
+
+ def __method_stop__(self):
+ if self._sr is None:
+ return
+
+ tasks = EmptyTask()
+ for sr in self._sr:
+ sr_task = sr.__method_stop__()
+ tasks = tasks | sr_task
+ return tasks
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/ccnxtest']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
diff --git a/vicn/resource/icn/consumer.py b/vicn/resource/icn/consumer.py
new file mode 100644
index 00000000..8c4c5e76
--- /dev/null
+++ b/vicn/resource/icn/consumer.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.icn.icn_application import ICNApplication
+
+class Consumer(ICNApplication):
+ """
+ Resource: Consumer
+ """
+ pass
diff --git a/vicn/resource/icn/face.py b/vicn/resource/icn/face.py
new file mode 100644
index 00000000..db72730d
--- /dev/null
+++ b/vicn/resource/icn/face.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.requirement import Requirement
+from vicn.core.resource import Resource
+from vicn.resource.node import Node
+from vicn.resource.interface import Interface
+
+DEFAULT_ETHER_PROTO = 0x0801
+FMT_L4FACE = '{protocol.name}://{dst_ip}:{dst_port}/'
+FMT_L2FACE = '{protocol.name}://[{dst_mac}]/{src_nic.device_name}'
+
+class FaceProtocol(Enum):
+ ether = 0
+ ip4 = 1
+ ip6 = 2
+ tcp4 = 3
+ tcp6 = 4
+ udp4 = 5
+ udp6 = 7
+ app = 8
+
+ @staticmethod
+ def from_string(protocol):
+ return getattr(FaceProtocol, protocol)
+
+#------------------------------------------------------------------------------
+
+class Face(Resource):
+ """
+ Resource: Face
+ """
+
+ node = Attribute(Node, mandatory = True,
+ requirements = [
+ Requirement('forwarder')
+ ])
+ protocol = Attribute(String,
+ description = 'Face underlying protocol',
+ mandatory = True)
+ id = Attribute(String, description = 'Local face ID',
+ ro = True)
+
+ # Cisco's extensions
+ wldr = Attribute(Bool, description = 'flag: WLDR enabled',
+ default = False)
+ x2 = Attribute(Bool, description = 'flag: X2 face',
+ default = False)
+
+ # NFD extensions
+ permanent = Attribute(Bool, description = 'flag: permanent face',
+ default = True)
+ nfd_uri = Attribute(String, description = 'Face uri',
+ func = lambda self : self._lambda_nfd_uri())
+ nfdc_flags = Attribute(String,
+ description = 'Flags for face creation with NFDC',
+ func = lambda self : self._lambda_nfdc_flags())
+
+ def __repr__(self):
+ flags = ''
+ if self.permanent:
+ flags += 'permanent '
+ if self.wldr:
+ flags += 'wldr '
+ if self.x2:
+ flags += 'x2 '
+ sibling_face_name = self.data.get('sibling_face', None)
+ sibling_face = self._state.manager.by_name(sibling_face_name) \
+ if sibling_face_name else None
+ dst_node = sibling_face.node.name if sibling_face else None
+ return '<Face {} {} on node {} -- to node {}>'.format(
+ self.nfd_uri, flags, self.node.name, dst_node)
+
+ __str__ = __repr__
+
+ # NFD specifics
+
+ def _lambda_nfd_uri(self):
+ raise NotImplementedError
+
+ def _lambda_nfdc_flags(self):
+ flags = ''
+ if self.permanent:
+ flags += '-P '
+ if self.wldr:
+ flags += '-W '
+ if self.x2:
+ flags += '-X '
+ return flags
+
+#------------------------------------------------------------------------------
+
+class L2Face(Face):
+
+ src_nic = Attribute(Interface,
+ description = "Name of the network interface linked to the face",
+ mandatory=True)
+ dst_mac = Attribute(String, description = "destination MAC address",
+ mandatory=True)
+ ether_proto = Attribute(String,
+ description = "Ethernet protocol number used by the face",
+ default=DEFAULT_ETHER_PROTO)
+
+ def _lambda_nfd_uri(self):
+ return self.format(FMT_L2FACE)
+
+#------------------------------------------------------------------------------
+
+class L4Face(Face):
+
+ ip_version = Attribute(Integer, description = "IPv4 or IPv6", default = 4)
+ src_ip = Attribute(String, description = "local IP address",
+ mandatory = True)
+ src_port = Attribute(Integer, description = "local TCP/UDP port")
+ dst_ip = Attribute(String, descrition = "remote IP address",
+ mandatory=True)
+ dst_port = Attribute(Integer, description = "remote TCP/UDP port",
+ mandatory=True)
+
+ def _lambda_nfd_uri(self):
+ return self.format(FMT_L4FACE)
diff --git a/vicn/resource/icn/forwarder.py b/vicn/resource/icn/forwarder.py
new file mode 100644
index 00000000..a719caf7
--- /dev/null
+++ b/vicn/resource/icn/forwarder.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from abc import ABC
+from enum import Enum
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.resource import FactoryResource
+from vicn.resource.icn.icn_application import ICNApplication
+from vicn.resource.icn.face import Face
+from vicn.resource.icn.route import Route
+
+DEFAULT_CACHE_SIZE = 1000 # pk
+DEFAULT_CACHE_POLICY = 'LRU'
+DEFAULT_STRATEGY = 'best-route'
+
+class Forwarder(ICNApplication, ABC):
+ """
+ Resource: Forwarder
+ """
+
+ __type__ = FactoryResource
+
+ faces = Attribute(Face, description = 'ICN ffaces of the forwarder',
+ multiplicity = Multiplicity.OneToMany,
+ reverse_name = 'forwarder')
+ routes = Attribute(Route, description = 'Routes in the ICN FIB',
+ multiplicity = Multiplicity.OneToMany,
+ reverse_name = 'forwarder')
+ cache_size = Attribute(Integer,
+ description = 'Size of the cache (in chunks)',
+ default = DEFAULT_CACHE_SIZE)
+ cache_policy = Attribute(String, description = 'Cache policy',
+ default = DEFAULT_CACHE_POLICY)
+ strategy = Attribute(String, description = 'Forwarding Strategy',
+ default = DEFAULT_STRATEGY)
+ config_file = Attribute(String, description = 'Configuration file')
+ port = Attribute(Integer, description = 'Default listening port',
+ default = lambda self: self._get_default_port())
+ log_file = Attribute(String, description = 'Log file')
+
+ # Overloaded attributes
+
+ node = Attribute(
+ reverse_name = 'forwarder',
+ reverse_description = 'ICN forwarder attached to the node',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
diff --git a/vicn/resource/icn/icn_application.py b/vicn/resource/icn/icn_application.py
new file mode 100644
index 00000000..5abee3c5
--- /dev/null
+++ b/vicn/resource/icn/icn_application.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.application import LinuxApplication
+from vicn.core.attribute import Attribute
+from netmodel.model.type import Integer
+
+ICN_SUITE_CCNX_1_0=0
+ICN_SUITE_NDN=1
+
+class ICNApplication(LinuxApplication):
+ """
+ Resource: ICNApplication
+ """
+
+ protocol_suites = Attribute(Integer,
+ description = 'Protocol suites supported by the application',
+ default = lambda self: self._def_protocol_suite())
+
+ def _def_protocol_suite(self):
+ return -1
+
diff --git a/vicn/resource/icn/icn_tools.py b/vicn/resource/icn/icn_tools.py
new file mode 100644
index 00000000..54823719
--- /dev/null
+++ b/vicn/resource/icn/icn_tools.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.icn.icn_application import ICNApplication
+
+class ICNTools(ICNApplication):
+ """
+ Resource: ICNTools
+ """
+
+ __package_names__ = ['libconsumer-producer-ccnx']
diff --git a/vicn/resource/icn/iping.py b/vicn/resource/icn/iping.py
new file mode 100644
index 00000000..0e04eadc
--- /dev/null
+++ b/vicn/resource/icn/iping.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask
+from vicn.resource.icn.icn_application import ICNApplication
+from vicn.resource.icn.icn_application import ICN_SUITE_CCNX_1_0
+from vicn.resource.icn.producer import Producer
+from vicn.resource.icn.consumer import Consumer
+from vicn.resource.node import Node
+
+DEFAULT_PING_PAYLOAD_SIZE = 64
+DEFAULT_PING_COUNT = 100
+
+class IPing(ICNApplication):
+ """
+ Resource: IPingClient
+ """
+
+ __package_names__ = ["libicnet"]
+
+ prefixes = Attribute(String,
+ description = "name served by the ping server",
+ default = lambda self: self.default_name(),
+ mandatory = False,
+ multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ requirements=[
+ Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0']),
+ properties = {"protocol_suites" : ICN_SUITE_CCNX_1_0})
+ ])
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_start__(self):
+ return self._build_command()
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def default_name(self):
+ return ['/iping']
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_CCNX_1_0
+
+#------------------------------------------------------------------------------
+
+class IPingClient(IPing, Producer):
+ """
+ Resource: IPingClient
+ """
+
+ flood = Attribute(Bool, description = 'enable flood mode',
+ default = False)
+ count = Attribute(Integer, description = 'number of ping to send')
+ interval = Attribute(Integer,
+ description = 'interval between interests in ping mode')
+ size = Attribute(Integer, description = 'size of the interests')
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _build_command(self):
+ template = ["iPing_Client", "-l ccnx:{prefix}"]
+ params={'prefix' : self.prefixes[0]}
+
+ if self.flood:
+ template.append("-f")
+ else:
+ template.append("-p") #Ping mode
+
+ if self.count:
+ template.append("-c {count}")
+ params["count"] = self.count
+ if self.size:
+ template.append("-s {size}")
+ params['size'] = self.size
+ if self.interval:
+ template.append("-i {interval}")
+ params['interval'] = self.interval
+
+ return BashTask(self.node, ' '.join(template), parameters=params)
+
+#------------------------------------------------------------------------------
+
+class IPingServer(IPing, Consumer):
+
+ size = Attribute(Integer, description = "size of the payload")
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _build_command(self):
+ template = ["iPing_Server", "-l ccnx:{prefix}"]
+ params={'prefix' : self.prefixes[0]}
+
+ if self.size:
+ template.append("-s {size}")
+ params['size'] = self.size
+
+ return BashTask(self.node, ' '.join(template), parameters=params)
diff --git a/vicn/resource/icn/ndnpingserver.py b/vicn/resource/icn/ndnpingserver.py
new file mode 100644
index 00000000..da13f59b
--- /dev/null
+++ b/vicn/resource/icn/ndnpingserver.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask
+from vicn.resource.icn.producer import Producer
+from vicn.resource.linux.service import Service
+
+TPL_DEFAULT_PREFIX='/ndn/{node.name}'
+
+FN_ETC_DEFAULT='/etc/default/ndnping'
+
+TPL_ETC_DEFAULT='''
+# defaults for ndnping server
+
+# Prefix should be set to a valid value
+PREFIX="/ndn/server"
+
+FLAGS=""
+'''
+
+CMD_START = 'ndnpingserver {prefix} &'
+
+class NDNPingServerBase(Producer):
+ """NDNPingServer Resource
+
+ This NDNPingServer resource wraps a NDN ping server
+
+ Attributes:
+ prefixes (List[str]) : (overloaded) One-element list containing the
+ prefix on which the ping server is listening.
+
+ TODO:
+ - ndnpingserver only supports a single prefix.
+ """
+ prefixes = Attribute(String,
+ default = lambda self: self._default_prefixes())
+
+ node = Attribute(requirements = [
+ Requirement("forwarder",
+ capabilities = set(['ICN_SUITE_CCNX_1_0'])) ])
+
+ __package_names__ = ['ndnping']
+
+ def _default_prefixes(self):
+ return [self.format(TPL_DEFAULT_PREFIX)]
+
+#------------------------------------------------------------------------------
+
+class NDNPingServer(NDNPingServerBase):
+
+ def __method_start__(self):
+ return BashTask(self.node, CMD_START)
+
+#------------------------------------------------------------------------------
+
+class NDNPingService(NDNPingServerBase, Service):
+ __package_names__ = ['ndnping']
+ __service_name__ = 'ndnping'
diff --git a/vicn/resource/icn/nfd.py b/vicn/resource/icn/nfd.py
new file mode 100644
index 00000000..c65fdeb8
--- /dev/null
+++ b/vicn/resource/icn/nfd.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import re
+
+from vicn.core.exception import ResourceNotFound
+from vicn.core.task import inline_task, BashTask
+from vicn.core.task import ParseRegexTask
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.icn.icn_application import ICN_SUITE_NDN
+
+log = logging.getLogger(__name__)
+
+NFD_CONF_FILE = "/etc/ndn/nfd.conf"
+
+CMD_SET_STRATEGY_CACHE = '\n'.join([
+ 'sed -i "s/^.*cs_max_packets .*$/ cs_max_packets {nfd.cache_size}/" ' \
+ '{conf_file}',
+ 'sed -i "0,/\/ / s/\/localhost\/nfd\/strategy\/.*/' \
+ '\/localhost\/nfd\/strategy\/{nfd.fw_strategy}/" {conf_file}',
+ 'service nfd restart'])
+CMD_RESET_CACHE = '''
+sed -i "s/^.*cs_max_packets .*$/ cs_max_packets 65536/" {conf_file}
+service nfd restart
+'''
+
+CMD_ADD_ROUTE = 'nfdc register {route.prefix} {route.face.nfd_uri}'
+# or: nfdc register {route.prefix} {route.face.id}
+
+CMD_REMOVE_ROUTE = 'nfdc unregister {route.prefix} {route.face.nfd_uri}'
+# or: nfdc unregister {route.prefix} {route.face.id}
+
+CMD_ADD_FACE = 'nfdc create {face.nfdc_flags} {face.nfd_uri}'
+
+CMD_REMOVE_FACE = 'nfdc destroy {face.id}'
+# or: nfdc destroy {face.nfd_uri}
+
+# FIXME redundant with Forwarder.FaceType
+layer_2_protocols = ["udp", "udp4", "tcp", "tcp4", "ether"]
+
+NFD_DEFAULT_PORT = 6363
+
+# Regular expressions used for parsing nfdc results
+STR_ADD_FACE = ('Face creation succeeded: ControlParameters\(FaceId: '
+ '(?P<id>.*?), Uri: (?P<face_uri>.*?), \)')
+RX_ADD_FACE = re.compile(STR_ADD_FACE)
+
+class NFD(Forwarder):
+ """
+ Resource: NFD
+ """
+
+ __capabilities__ = set(['ICN_SUITE_NDN'])
+ __service_name__ = 'nfd'
+ __package_names__ = ['nfd']
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ # NFD is assumed not to exist
+ raise ResourceNotFound
+
+ def __create__(self):
+ # Modify the configuration file before running the forwarder service
+ conf = BashTask(self.node, CMD_SET_STRATEGY_CACHE, {'nfd': self})
+ forwarder = Forwarder.__create__(self)
+ return conf.then(forwarder)
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def _get_routes(self):
+ return {'routes': list()}
+
+ @inline_task
+ def _add_routes(self, route):
+ return BashTask(self.node, CMD_ADD_ROUTE, {'route': route})
+
+ @inline_task
+ def _remove_routes(self, route):
+ return BashTask(self.node, CMD_REMOVE_ROUTE, {'route': route})
+
+ @inline_task
+ def _get_faces(self):
+ return {'faces': list()}
+
+ @inline_task
+ def _add_faces(self, face):
+ add_face = BashTask(self.node, CMD_ADD_FACE, {'face': face})
+ set_face_id = ParseRegexTask(RX_ADD_FACE)
+ return add_face.compose(set_face_id)
+
+ @inline_task
+ def _remove_faces(self, face):
+ return BashTask(self.node, CMD_REMOVE_FACE, {'face': face})
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ def __method_reset_cache__(self, conf_file):
+ return BashTask(self.node, CMD_RESET_CACHE, {'conf_file': conf_file})
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _get_default_port(self):
+ return NFD_DEFAULT_PORT
+
+ def _def_protocol_suite(self):
+ return ICN_SUITE_NDN
diff --git a/vicn/resource/icn/producer.py b/vicn/resource/icn/producer.py
new file mode 100644
index 00000000..23434ebd
--- /dev/null
+++ b/vicn/resource/icn/producer.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.resource.icn.icn_application import ICNApplication
+from vicn.core.attribute import Attribute, Multiplicity
+
+class Producer(ICNApplication):
+ """
+ Resource: Producer
+ """
+
+ prefixes = Attribute(String, description = 'List of served prefixes',
+ multiplicity = Multiplicity.OneToMany)
diff --git a/vicn/resource/icn/repo-ng.py b/vicn/resource/icn/repo-ng.py
new file mode 100644
index 00000000..7b654a6a
--- /dev/null
+++ b/vicn/resource/icn/repo-ng.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.service import Service
+
+class RepoNG(Service):
+ """
+ Resource: RepoNG
+ """
+ __service_name__ = 'repo-ng'
diff --git a/vicn/resource/icn/route.py b/vicn/resource/icn/route.py
new file mode 100644
index 00000000..0dc2ed2f
--- /dev/null
+++ b/vicn/resource/icn/route.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute
+from vicn.core.resource import Resource
+from vicn.resource.icn.face import Face
+from vicn.resource.node import Node
+
+class Route(Resource):
+ node = Attribute(Node, mandatory = True)
+ prefix = Attribute(String, mandatory = True)
+ face = Attribute(Face, description = "face used to forward interests",
+ mandatory=True)
+ cost = Attribute(Integer, default=1)
+
+ def __repr__(self):
+ return '<Route {} {} on node {}>'.format(self.prefix, self.face,
+ self.node.name)
+
+ __str__ = __repr__
diff --git a/vicn/resource/icn/virtual-repo.py b/vicn/resource/icn/virtual-repo.py
new file mode 100644
index 00000000..8cb306d9
--- /dev/null
+++ b/vicn/resource/icn/virtual-repo.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String, Integer
+from vicn.core.attribute import Attribute
+from vicn.resource.icn.producer import Producer
+
+DEFAULT_CHUNK_SIZE = 1300
+
+class VirtualRepo(Producer):
+ """
+ Resource: VirtualRepo
+
+ Note:
+ ndn-virtual-repo {self.folder} -s {self.chunk_size}
+ """
+
+ __package_names__ = ['ndn-virtual-repo']
+
+ folder = Attribute(String, description = "Folder")
+ chunk_size = Attribute(Integer, description = "Chunk size",
+ default = DEFAULT_CHUNK_SIZE)
diff --git a/vicn/resource/icn/webserver.py b/vicn/resource/icn/webserver.py
new file mode 100644
index 00000000..8b8e2ef3
--- /dev/null
+++ b/vicn/resource/icn/webserver.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.icn.producer import Producer
+
+class WebServer(Producer):
+ """
+ Resource: WebServer
+
+ CCNX Webserver
+ """
+
+ __package_names__ = ['webserver-ccnx']
+ __service_name__ = 'webserver-ccnx'
diff --git a/vicn/resource/interface.py b/vicn/resource/interface.py
new file mode 100644
index 00000000..db5f5427
--- /dev/null
+++ b/vicn/resource/interface.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from netmodel.model.type import Bool
+from vicn.core.resource import Resource
+from vicn.resource.node import Node
+from vicn.resource.channel import Channel
+
+class Interface(Resource):
+ """
+ Resource: Interface
+ """
+
+ node = Attribute(Node, description = 'Node to which the interface belongs',
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'interfaces',
+ mandatory = True)
+ channel = Attribute(Channel, description = 'Channel to which the interface is attached',
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'interfaces')
+ promiscuous = Attribute(Bool, description = 'Promiscuous mode',
+ default = False)
+ up = Attribute(Bool, description = 'Interface up/down status',
+ default = True)
+ monitored = Attribute(Bool, default = True)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Flag to check whether vpp uses that interface
+ self.has_vpp_child = False
diff --git a/vicn/resource/ip/__init__.py b/vicn/resource/ip/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/ip/__init__.py
diff --git a/vicn/resource/ip/route.py b/vicn/resource/ip/route.py
new file mode 100644
index 00000000..f073e426
--- /dev/null
+++ b/vicn/resource/ip/route.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.resource.node import Node
+from vicn.core.attribute import Attribute
+from vicn.core.resource import Resource
+from vicn.resource.interface import Interface
+
+class IPRoute(Resource):
+ node = Attribute(Node, mandatory = True)
+ ip_address = Attribute(String, mandatory = True)
+ interface = Attribute(Interface, mandatory = True)
+ gateway = Attribute(String)
+
+ # FIXME Temp hack for VPP, migrate this to an ARP table resource
+ mac_address = Attribute(String)
diff --git a/vicn/resource/ip/routing_table.py b/vicn/resource/ip/routing_table.py
new file mode 100644
index 00000000..52b81794
--- /dev/null
+++ b/vicn/resource/ip/routing_table.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import EmptyTask, BashTask
+from vicn.resource.ip.route import IPRoute
+from vicn.resource.node import Node
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ROUTE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ROUTE_GW
+
+CMD_ADD_ROUTE = ('ip route add {route.ip_address} '
+ 'dev {route.interface.device_name} || true')
+CMD_ADD_ROUTE_GW = ('ip route add {route.ip_address} '
+ 'dev {route.interface.device_name} via {route.gateway} || true')
+CMD_DEL_ROUTE = ('ip route del {route.ip_address} '
+ 'dev {route.interface.device_name}')
+CMD_SHOW_ROUTES = 'ip route show'
+
+CMD_ADD_ARP_ENTRY = 'arp -s {route.ip_address} {route.mac_address}'
+
+# Populate arp table too. The current configuration with one single bridge
+# connecting every container and vpp nodes seem to create loops that prevent
+# vpp from netmodel.network.interface for routing ip packets.
+
+VPP_ARP_FIX = True
+
+def _iter_routes(out):
+ for line in out.splitlines():
+ toks = line.strip().split()
+ route = {'ip_address': toks[0]}
+ for pos in range(1, len(toks)):
+ if toks[pos] == '':
+ pos+=1
+ elif toks[pos] == 'dev':
+ route['interface_name'] = toks[pos+1]
+ pos+=2
+ elif toks[pos] in ['src', 'proto', 'scope', 'metric']:
+ pos+=2
+ elif toks[pos] == 'via':
+ route['gateway'] = toks[pos+1]
+ pos+=2
+ elif toks[pos] in ['linkdown', 'onlink']:
+ pos+=1
+ yield route
+
+#------------------------------------------------------------------------------
+
+class RoutingTable(Resource):
+ """
+ Resource: RoutingTable
+
+ IP Routing Table management
+
+ """
+
+ node = Attribute(Node,
+ mandatory = True,
+ reverse_name = 'routing_table',
+ reverse_description = 'Routing table of the node',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
+
+ routes = Attribute(IPRoute,
+ multiplicity = Multiplicity.OneToMany)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self._routes = dict()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ('CentralIP', 'VPPInterface')
+
+ def __get__(self):
+ def cache(rv):
+ for route in _iter_routes(rv.stdout):
+ self._routes[route['ip_address']] = route
+
+ # Force routing update
+ raise ResourceNotFound
+ return BashTask(self.node, CMD_SHOW_ROUTES, parse=cache)
+
+ def __create__(self):
+ """
+ Create a single BashTask for all routes
+ """
+
+ done = set()
+ routes_cmd = list()
+ routes_via_cmd = list()
+ arp_cmd = list()
+
+ # vppctl lock
+ # NOTE: we currently lock vppctl during the whole route update
+ routes_lock = None
+ routes_via_lock = None
+
+ for route in self.routes:
+ if route.ip_address in self._routes:
+ continue
+ if route.ip_address in done:
+ continue
+ done.add(route.ip_address)
+
+ # TODO VPP should provide its own implementation of routing table
+ # on the node
+ if not route.interface.has_vpp_child:
+ if route.gateway is None:
+ cmd = CMD_ADD_ROUTE.format(route = route)
+ routes_cmd.append(cmd)
+ else:
+ cmd = CMD_ADD_ROUTE_GW.format(route = route)
+ routes_via_cmd.append(cmd)
+ if VPP_ARP_FIX and route.mac_address:
+ if route.ip_address != "default":
+ cmd = CMD_ADD_ARP_ENTRY.format(route = route)
+ arp_cmd.append(cmd)
+ else:
+ if route.gateway is None:
+ cmd = CMD_VPP_ADD_ROUTE.format(route = route)
+ routes_cmd.append(cmd)
+ routes_lock = route.node.vpp.vppctl_lock
+ else:
+ cmd = CMD_VPP_ADD_ROUTE_GW.format(route = route)
+ routes_via_cmd.append(cmd)
+ routes_via_lock = route.node.vpp.vppctl_lock
+
+ # TODO: checks
+ clean_routes_task = EmptyTask()
+
+ if len(routes_cmd) > 0:
+ routes_task = BashTask(self.node, '\n'.join(routes_cmd),
+ lock = routes_lock)
+ else:
+ routes_task = EmptyTask()
+
+ if len(routes_via_cmd) > 0:
+ routes_via_task = BashTask(self.node, '\n'.join(routes_via_cmd),
+ lock = routes_via_lock)
+ else:
+ routes_via_task = EmptyTask()
+
+ if len(arp_cmd) > 0:
+ arp_task = BashTask(self.node, '\n'.join(arp_cmd))
+ else:
+ arp_task = EmptyTask()
+
+ return ((clean_routes_task > routes_task) > routes_via_task) > arp_task
+
+ def __delete__(self):
+ raise NotImplementedError
diff --git a/vicn/resource/linux/__init__.py b/vicn/resource/linux/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/linux/__init__.py
diff --git a/vicn/resource/linux/application.py b/vicn/resource/linux/application.py
new file mode 100644
index 00000000..d2b5139e
--- /dev/null
+++ b/vicn/resource/linux/application.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Reference
+from vicn.core.resource import Resource, EmptyResource
+from vicn.resource.application import Application
+from vicn.resource.linux.package_manager import Packages
+
+
+class LinuxApplication(Application):
+ """
+ Resource: Linux Application
+
+ This resource ensures that the application is present on the system, and
+ installs it during setup if necessary.
+ """
+
+ def __subresources__(self):
+ package_names = self._get_package_names()
+ if package_names:
+ packages = Packages(node=Reference(self, 'node'),
+ names=package_names,
+ owner=self)
+ else:
+ packages = EmptyResource()
+
+ process = None
+
+ return packages > process
+
+ #--------------------------------------------------------------------------
+ # Private methods
+ #--------------------------------------------------------------------------
+
+ def _get_package_names(self):
+ package_names = list()
+ for base in self.__class__.mro():
+ if not '__package_names__' in vars(base):
+ continue
+ package_names.extend(getattr(base, '__package_names__'))
+ return package_names
diff --git a/vicn/resource/linux/bridge.py b/vicn/resource/linux/bridge.py
new file mode 100644
index 00000000..882f0226
--- /dev/null
+++ b/vicn/resource/linux/bridge.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.task import inline_task
+from vicn.resource.channel import Channel
+from vicn.resource.linux.bridge_mgr import BridgeManager
+from vicn.resource.linux.net_device import BaseNetDevice
+
+log = logging.getLogger(__name__)
+
+# FIXME This should use the AddressManager to get allocated a name that does
+# not exist
+DEFAULT_BRIDGE_NAME = 'br0'
+
+class Bridge(Channel, BaseNetDevice):
+ """
+ Resource: Bridge
+ """
+ node = Attribute(
+ reverse_name = 'bridge',
+ reverse_description = 'Main bridge',
+ reverse_auto = 'true',
+ multiplicity = Multiplicity.OneToOne,
+ requirements = [
+ Requirement('bridge_manager')
+ ])
+ device_name = Attribute(
+ default = DEFAULT_BRIDGE_NAME,
+ mandatory = False)
+
+ #--------------------------------------------------------------------------
+ # Constructor / Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'br'
+ self.netdevice_type = 'bridge'
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ # FIXME we currently force the recreation of the bridge, delegating the
+ # check to the creation function
+ raise ResourceNotFound
+
+ def __create__(self):
+ # FIXME : reserves .1 IP address for the bridge, provided no other
+ # class uses this trick
+ AddressManager().get_ip(self)
+ return self.node.bridge_manager.add_bridge(self.device_name)
+
+ # Everything should be handled by BaseNetDevice
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def _add_interface(self, interface, vlan=None):
+ """
+ Returns:
+ Task
+ """
+ return self.node.bridge_manager.add_interface(self.device_name,
+ interface.device_name, vlan)
+
+ def __method_add_interface__(self, interface, vlan=None):
+ return self._add_interface(interface, vlan)
+
+ def _remove_interface(self, interface):
+ """
+ Returns:
+ Task
+ """
+ log.info('Removing interface {} from bridge {}'.format(
+ interface.device_name, self.name))
+ return self.node.bridge_manager.del_interface(self.device_name,
+ interface.device_name)
+
diff --git a/vicn/resource/linux/bridge_mgr.py b/vicn/resource/linux/bridge_mgr.py
new file mode 100644
index 00000000..b7035221
--- /dev/null
+++ b/vicn/resource/linux/bridge_mgr.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.application import LinuxApplication
+from vicn.core.resource import FactoryResource
+from vicn.core.attribute import Attribute, Multiplicity
+
+class BridgeManager(LinuxApplication):
+ """
+ Resource: Bridge Manager
+
+ A bridge manager is responsible to manage bridges on a node.
+ """
+ __type__ = FactoryResource
+
+ # Overloaded reverse attribute
+ node = Attribute(reverse_name = 'bridge_manager',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
diff --git a/vicn/resource/linux/certificate.py b/vicn/resource/linux/certificate.py
new file mode 100644
index 00000000..e8750dff
--- /dev/null
+++ b/vicn/resource/linux/certificate.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os.path
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity, Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import task, inline_task, BashTask
+from vicn.resource.linux.file import File
+from vicn.resource.node import Node
+
+DEFAULT_RSA_LENGTH = '4096'
+DEFAULT_SUBJECT = '/CN=www.cisco.com/L=Paris/O=Cisco/C=FR'
+
+CMD_CREATE='\n'.join([
+ '# Generate a new certificate',
+ 'openssl req -x509 -newkey rsa:' + DEFAULT_RSA_LENGTH +
+ ' -keyout {self.key} -out {self.cert} -subj ' + DEFAULT_SUBJECT + ' -nodes'
+])
+
+class Certificate(Resource):
+ """
+ Resource: Certificate
+
+ Implements a SSL certificate.
+
+ Todo:
+ - ideally, this should be implemented as a pair of tightly coupled files.
+ """
+ node = Attribute(Node,
+ description = 'Node on which the certificate is created',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne)
+ cert = Attribute(String, description = 'Certificate path',
+ mandatory = True)
+ key = Attribute(String, description = 'Key path',
+ mandatory = True)
+
+ @inline_task
+ def __initialize__(self):
+ self._cert_file = File(node = Reference(self, 'node'),
+ filename = Reference(self, 'cert'),
+ managed = False)
+ self._key_file = File(node = Reference(self, 'node'),
+ filename = Reference(self, 'key'),
+ managed = False)
+
+ def __get__(self):
+ return self._cert_file.__get__() | self._key_file.__get__()
+
+ def __create__(self):
+ return BashTask(None, CMD_CREATE, {'self': self})
+
+ def __delete__(self):
+ return self._cert_file.__delete__() | self._key_file.__delete__()
+
+
diff --git a/vicn/resource/linux/dnsmasq.py b/vicn/resource/linux/dnsmasq.py
new file mode 100644
index 00000000..e18f750f
--- /dev/null
+++ b/vicn/resource/linux/dnsmasq.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import ipaddress
+import logging
+
+from string import Template
+
+from netmodel.model.type import String, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.resource import EmptyResource
+from vicn.resource.dns_server import DnsServer
+from vicn.resource.interface import Interface
+from vicn.resource.linux.file import TextFile
+from vicn.resource.linux.service import Service
+
+log = logging.getLogger(__name__)
+
+FN_CONF='/etc/dnsmasq.conf'
+
+TPL_CONF='''
+# Configuration file for dnsmasq.
+#
+# Format is one option per line, legal options are the same
+# as the long options legal on the command line. See
+# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details.
+
+interface=$interface
+dhcp-range=$dhcp_range
+dhcp-host=00:0e:c6:81:79:01,192.168.128.200,12h
+
+#server=$server
+$flags
+'''
+
+DHCP_OFFSET = 195
+
+class DnsMasq(Service, DnsServer):
+ """
+
+ Todo:
+ - Currently, a single interface is supported.
+ - DHCP range is hardcoded
+ """
+ __package_names__ = ['dnsmasq']
+ __service_name__ = 'dnsmasq'
+
+ interface = Attribute(Interface,
+ description = 'Interface on which to listen')
+ lease_interval = Attribute(String,
+ default = '12h')
+ server = Attribute(String)
+ dhcp_authoritative = Attribute(Bool,
+ description = 'Flag: DHCP authoritative',
+ default = True)
+ log_queries = Attribute(Bool, description = 'Flag: log DNS queries',
+ default = True)
+ log_dhcp = Attribute(Bool, description = 'Flag: log DHCP queries',
+ default = True)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if not self.interface:
+ if self.node.bridge:
+ self.interface = self.node.bridge
+ else:
+ self.interface = self.node.host_interface
+
+ def __subresources__(self):
+ # Overwrite configuration file
+ flags = list()
+ if self.dhcp_authoritative:
+ flags.append('dhcp-authoritative')
+ if self.log_queries:
+ flags.append('log-queries')
+ if self.log_dhcp:
+ flags.append('log-dhcp')
+ network = self._state.manager.get('network')
+ network = ipaddress.ip_network(network, strict=False)
+
+ dhcp_range = '{},{},{},{},{}'.format(
+ self.interface.device_name,
+ str(network[DHCP_OFFSET]),
+ str(network[DHCP_OFFSET + 5]), # eg. .253
+ "255.255.255.0",
+ self.lease_interval)
+
+ t_dict = {
+ 'interface' : self.interface.device_name,
+ 'dhcp_range': dhcp_range,
+ 'server' : str(network[-2]), # unused so far
+ 'flags' : '\n'.join(flags)
+ }
+
+ t = Template(TPL_CONF)
+ conf = t.substitute(t_dict)
+
+ return TextFile(node = self.node, owner = self, filename = FN_CONF,
+ content = conf, overwrite = True)
diff --git a/vicn/resource/linux/file.py b/vicn/resource/linux/file.py
new file mode 100644
index 00000000..cddda8ed
--- /dev/null
+++ b/vicn/resource/linux/file.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String, Bool
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, inline_task
+from vicn.resource.node import Node
+
+CREATE_DIR_CMD = "mkdir -p {dir}"
+CREATE_FILE_CMD = "mkdir -p $(dirname {file.filename}) && touch {file.filename}"
+DELETE_FILE_CMD = "rm -f {file.filename}"
+
+GET_FILE_CMD = 'test -f {file.filename} && readlink -e {file.filename}'
+
+GREP_FILE_CMD = "cat {file.filename}"
+
+CMD_PRINT_TO_FILE = 'echo -n "{file.content}" > {file.filename}'
+
+class File(Resource):
+ """
+ Resource: File
+ """
+ filename = Attribute(String, description = 'Path to the file',
+ mandatory = True)
+ node = Attribute(Node, description = 'Node on which the file is created',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne,
+ reverse_name = 'files',
+ reverse_description = 'Files created on the node')
+ overwrite = Attribute(Bool,
+ description = 'Determines whether an existing file is overwritten',
+ default = False)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __get__(self):
+
+ # UGLY
+ @inline_task
+ def not_found():
+ raise ResourceNotFound
+
+ if self.overwrite:
+ return not_found()
+
+ def is_path (rv):
+ if rv is None or rv.stdout is None or len(rv.stdout) == 0 or \
+ rv.return_value != 0:
+ raise ResourceNotFound
+ return {} # 'filename': rv.stdout}
+
+ test = BashTask(self.node, GET_FILE_CMD, {"file": self}, parse=is_path)
+ return test
+
+ def __create__(self):
+ ctask = BashTask(self.node, CREATE_FILE_CMD, {"file": self})
+ if self.overwrite:
+ ctask = BashTask(self.node, DELETE_FILE_CMD, {'file': self}) > ctask
+ return ctask
+
+ def __delete__(self):
+ return BashTask(self.node, DELETE_FILE_CMD, { "file" : self})
+
+#------------------------------------------------------------------------------
+
+class TextFile(File):
+ """
+ Resource: TextFile
+
+ A file with text content.
+ """
+
+ content = Attribute(String, default='')
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_PRINT_TO_FILE, {'file': self})
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _set_content(self):
+ return self.__create__()
+
+ def _get_content(self):
+ return BashTask(self.node, GREP_FILE_CMD, {'file': self},
+ parse =( lambda x : x.stdout))
diff --git a/vicn/resource/linux/iperf.py b/vicn/resource/linux/iperf.py
new file mode 100644
index 00000000..a0780a1c
--- /dev/null
+++ b/vicn/resource/linux/iperf.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from abc import ABC
+
+from netmodel.model.type import Integer
+from vicn.core.attribute import Attribute
+from vicn.resource.linux.application import LinuxApplication
+
+class Iperf3(LinuxApplication, ABC):
+ __package_names__ = ['iperf3']
+
diff --git a/vicn/resource/linux/link.py b/vicn/resource/linux/link.py
new file mode 100644
index 00000000..4304a948
--- /dev/null
+++ b/vicn/resource/linux/link.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import random
+import string
+import logging
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute, Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.state import ResourceState, AttributeState
+from vicn.core.task import inline_task, async_task, run_task
+from vicn.core.task import get_attributes_task, BashTask
+from vicn.resource.channel import Channel
+from vicn.resource.interface import Interface
+from vicn.resource.linux.net_device import NonTapBaseNetDevice
+from vicn.resource.node import Node
+
+# FIXME remove VPP specific code
+from vicn.resource.vpp.interface import VPPInterface
+
+log = logging.getLogger(__name__)
+
+CMD_DELETE_IF_EXISTS='ip link show {interface.device_name} && ' \
+ 'ip link delete {interface.device_name} || true'
+
+CMD_CREATE='''
+# Create veth pair in the host node
+ip link add name {tmp_src} type veth peer name {tmp_dst}
+ip link set dev {tmp_src} netns {pid[0]} name {interface.src.device_name}
+ip link set dev {tmp_dst} netns {pid[1]} name {interface.dst.device_name}
+'''
+CMD_UP='''
+ip link set dev {interface.device_name} up
+'''
+
+class Link(Channel):
+ """
+ Resource: Link
+
+ Implements a virtual wired link between containers. It is a VethPair, both
+ sides of which sit inside a different container.
+
+ Because of this, the resource only supports passing source and destination
+ containers, and not interfaces. It also explains the relative complexity of
+ the current implementation.
+ """
+
+ src = Attribute(Interface, description = 'Source interface')
+ dst = Attribute(Interface, description = 'Destination interface')
+
+ capacity = Attribute(Integer, description = 'Link capacity (Mb/s)')
+ delay = Attribute(String, description = 'Link propagation delay')
+
+ src_node = Attribute(Node, description = 'Source node',
+ mandatory = True)
+ dst_node = Attribute(Node, description = 'Destination node',
+ mandatory = True)
+
+ def __init__(self, *args, **kwargs):
+ assert 'src' not in kwargs and 'dst' not in kwargs
+ assert 'src_node' in kwargs and 'dst_node' in kwargs
+ super().__init__(*args, **kwargs)
+
+ @inline_task
+ def __initialize__(self):
+
+ # We create two managed net devices that are pre-setup
+ # but the resource manager has to take over for IP addresses etc.
+ # Being done in initialize, those attributes won't be considered as
+ # dependencies and will thus not block the resource state machine.
+ self.src = NonTapBaseNetDevice(node = self.src_node,
+ device_name = self.dst_node.name,
+ channel = self,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.dst = NonTapBaseNetDevice(node = self.dst_node,
+ device_name = self.src_node.name,
+ channel = self,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.dst.remote = self.src
+ self.src.remote = self.dst
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ async def _commit(self):
+ manager = self._state.manager
+
+ # We mark the src and dst interfaces created because we are pre-setting
+ # them up in __create__ using a VethPair
+ # We go through both INITIALIZED and CREATED stats to raise the proper
+ # events and satisfy any eventual wait_* command.
+ await manager._set_resource_state(self.src, ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.dst, ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.src, ResourceState.CREATED)
+ await manager._set_resource_state(self.dst, ResourceState.CREATED)
+
+ # We mark the attribute clean so that it is not updated
+ await manager._set_attribute_state(self, 'src', AttributeState.CLEAN)
+ await manager._set_attribute_state(self, 'dst', AttributeState.CLEAN)
+
+ manager.commit_resource(self.src)
+ manager.commit_resource(self.dst)
+
+ # Disable rp_filtering
+ # self.src.rp_filter = False
+ # self.dst.rp_filter = False
+
+ #XXX VPP
+ if hasattr(self.src_node, 'vpp') and not self.src_node.vpp is None:
+ vpp_src = VPPInterface(parent = self.src,
+ vpp = self.src_node.vpp,
+ ip_address = Reference(self.src, 'ip_address'),
+ device_name = 'vpp' + self.src.device_name)
+ manager.commit_resource(vpp_src)
+
+ if hasattr(self.dst_node, 'vpp') and not self.dst_node.vpp is None:
+ vpp_dst = VPPInterface(parent = self.dst,
+ vpp = self.dst_node.vpp,
+ ip_address = Reference(self.dst, 'ip_address'),
+ device_name = 'vpp' + self.dst.device_name)
+ manager.commit_resource(vpp_dst)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @async_task
+ async def __get__(self):
+ manager = self._state.manager
+
+ try:
+ await run_task(self.src.__get__(), manager)
+ await run_task(self.dst.__get__(), manager)
+ except ResourceNotFound:
+ # This is raised if any of the two side of the VethPair is missing
+ raise ResourceNotFound
+
+ # We always need to commit the two endpoints so that their attributes
+ # are correctly updated
+ await self._commit()
+
+ def __create__(self):
+ assert self.src_node.get_type() == 'lxccontainer'
+ assert self.dst_node.get_type() == 'lxccontainer'
+
+ src_host = self.src_node.node
+ dst_host = self.dst_node.node
+
+ assert src_host == dst_host
+ host = src_host
+
+ # Sometimes a down interface persists on one side
+ delif_src = BashTask(self.src_node, CMD_DELETE_IF_EXISTS,
+ {'interface': self.src})
+ delif_dst = BashTask(self.dst_node, CMD_DELETE_IF_EXISTS,
+ {'interface': self.dst})
+
+ pid_src = get_attributes_task(self.src_node, ['pid'])
+ pid_dst = get_attributes_task(self.dst_node, ['pid'])
+
+ tmp_src = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase +
+ string.digits) for _ in range(5))
+ tmp_dst = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase +
+ string.digits) for _ in range(5))
+
+ create = BashTask(host, CMD_CREATE, {'interface': self,
+ 'tmp_src': tmp_src, 'tmp_dst': tmp_dst})
+
+ up_src = BashTask(self.src_node, CMD_UP, {'interface': self.src})
+ up_dst = BashTask(self.dst_node, CMD_UP, {'interface': self.dst})
+
+ @async_task
+ async def set_state():
+ # We always need to commit the two endpoints so that their attributes
+ # are correctly updated
+ await self._commit()
+
+ delif = delif_src | delif_dst
+ up = up_src | up_dst
+ pid = pid_src | pid_dst
+ return ((delif > (pid @ create)) > up) > set_state()
+
diff --git a/vicn/resource/linux/macvlan.py b/vicn/resource/linux/macvlan.py
new file mode 100644
index 00000000..ea9c37c1
--- /dev/null
+++ b/vicn/resource/linux/macvlan.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.task import BashTask
+from vicn.resource.linux.net_device import SlaveBaseNetDevice
+
+CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \
+ 'link {netdevice.parent.device_name} ' \
+ 'type {netdevice.netdevice_type} mode {netdevice.mode}'
+
+class MacVlan(SlaveBaseNetDevice):
+ """
+ Resource: MacVlan
+
+ Implements a MacVlan interface.
+ """
+
+ mode = Attribute(String, description = 'MACVLAN mode',
+ default = 'bridge')
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'macvlan'
+ self.netdevice_type = 'macvlan'
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self})
diff --git a/vicn/resource/linux/macvtap.py b/vicn/resource/linux/macvtap.py
new file mode 100644
index 00000000..82002e02
--- /dev/null
+++ b/vicn/resource/linux/macvtap.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.task import BashTask
+from vicn.resource.linux.net_device import SlaveBaseNetDevice
+
+CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \
+ 'link {netdevice.parent.device_name} ' \
+ 'type {netdevice.netdevice_type} mode {netdevice.mode}'
+
+class MacVtap(SlaveBaseNetDevice):
+ """
+ Resource: MacVtap
+
+ Implements a MacVtap interface.
+ """
+
+ mode = Attribute(String, description = 'MACVTAP mode',
+ default = 'bridge'),
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'macvtap'
+ self.netdevice_type = 'macvtap'
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self})
diff --git a/vicn/resource/linux/net_device.py b/vicn/resource/linux/net_device.py
new file mode 100644
index 00000000..f0a08991
--- /dev/null
+++ b/vicn/resource/linux/net_device.py
@@ -0,0 +1,519 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import re
+import math
+import random
+import string
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import BaseResource
+from vicn.core.task import BashTask, task, EmptyTask
+from vicn.resource.application import Application
+from vicn.resource.interface import Interface
+
+# parse_ip_addr inspired from:
+# From: https://github.com/ohmu/poni/blob/master/poni/cloud_libvirt.py
+
+LXD_FIX = lambda cmd: 'sleep 1 && {}'.format(cmd)
+
+MAX_DEVICE_NAME_SIZE = 15
+
+CMD_FLUSH_IP = 'ip addr flush dev {device_name}'
+
+CMD_INTERFACE_LIST = 'ip link show | grep -A 1 @{}'
+RX_INTERFACE_LIST = '.*?(?P<ifname>[^ ]*)@{}:'
+CMD_INTERFACE_GET = 'ip link show | grep -A 1 {}@{}'
+RX_INTERFACE_GET = '.*?(?P<ifname>{})@{}:'
+
+log = logging.getLogger(__name__)
+
+CMD_GET = LXD_FIX('ip link show {netdevice.device_name}')
+CMD_CREATE = 'ip link add name {netdevice.device_name} ' \
+ 'type {netdevice.netdevice_type}'
+CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \
+ 'link {netdevice.parent.device_name} ' \
+ 'type {netdevice.netdevice_type}'
+CMD_DELETE = 'ip link delete {netdevice.device_name}'
+CMD_SET_MAC_ADDRESS = 'ip link set dev {netdevice.device_name} ' \
+ 'address {netdevice.mac_address}'
+CMD_GET_IP_ADDRESS = 'ip addr show {netdevice.device_name}'
+CMD_SET_IP_ADDRESS = 'ip addr add dev {netdevice.device_name} ' \
+ '{netdevice.ip_address} brd + || true'
+CMD_SET_PROMISC = 'ip link set dev {netdevice.device_name} promisc {on_off}'
+CMD_SET_UP = 'ip link set {netdevice.device_name} {up_down}'
+CMD_SET_CAPACITY='\n'.join([
+ 'tc qdisc del dev {netdevice.device_name} root || true',
+ 'tc qdisc add dev {netdevice.device_name} root handle 1: tbf rate '
+ '{netdevice.capacity}Mbit burst {burst}kb latency 70ms'
+ 'tc qdisc add dev {netdevice.device_name} parent 1:1 codel',
+])
+CMD_GET_PCI_ADDRESS='ethtool -i {netdevice.device_name} | ' \
+ "sed -n '/bus-info/{{s/.*: [^:]*:\(.*\)/\\1/p}}'"
+CMD_GET_OFFLOAD='ethtool -k {netdevice.device_name} | ' \
+ 'grep rx-checksumming | cut -d " " -f 2'
+CMD_SET_OFFLOAD='ethtool -K {netdevice.device_name} rx on tx on'
+CMD_UNSET_OFFLOAD='ethtool -K {netdevice.device_name} rx off tx off'
+
+CMD_UNSET_RP_FILTER = '''
+sysctl -w net.ipv4.conf.all.rp_filter=0
+sysctl -w net.ipv4.conf.{netdevice.device_name}.rp_filter=0
+'''
+CMD_SET_RP_FILTER = 'sysctl -w ' \
+ 'net.ipv4.conf.{netdevice.device_name}.rp_filter=1'
+CMD_GET_RP_FILTER = '''
+sysctl net.ipv4.conf.all.rp_filter
+sysctl net.ipv4.conf.{netdevice.device_name}.rp_filter
+'''
+
+#-------------------------------------------------------------------------------
+
+# FIXME GPL code
+
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Utility to parse 'ip link [show]'.
+
+Example dictionary returned by parse_ip_link():
+
+{u'eth0': {u'flags': set([u'BROADCAST', u'LOWER_UP', u'MULTICAST', u'UP']),
+ u'index': 2,
+ u'mac': u'80:fa:5c:0d:43:5e',
+ u'name': u'eth0',
+ u'settings': {u'group': u'default',
+ u'mode': u'DEFAULT',
+ u'mtu': u'1500',
+ u'qdisc': u'pfifo_fast',
+ u'qlen': u'1000',
+ u'state': u'UP'}},
+ u'lo': {u'flags': set([u'LOOPBACK', u'LOWER_UP', u'UP']),
+ u'index': 1,
+ u'name': u'lo',
+ u'settings': {u'group': u'default',
+ u'mode': u'DEFAULT',
+ u'mtu': u'65536',
+ u'qdisc': u'noqueue',
+ u'state': u'UNKNOWN'}}}
+
+The dictionary above is generated given the following input:
+
+ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN \
+mode DEFAULT group default
+ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
+ 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast \
+state UP mode DEFAULT group default qlen 1000
+ link/ether 80:fa:5c:0d:43:5e brd ff:ff:ff:ff:ff:ff
+"""
+
+def _get_settings_dict(settings_line):
+ """
+ Given a string of the format:
+ "[[<key1> <value1>] <key2> <value2>][...]"
+ Returns a dictionary mapping each key to its corresponding value.
+ :param settings_line: unicode
+ :return: dict
+ """
+ settings = settings_line.strip().split()
+ num_tokens = len(settings)
+ assert num_tokens % 2 == 0
+ return {
+ settings[2 * i]: settings[2 * i + 1] for i in range(num_tokens // 2)
+ }
+
+
+def _parse_interface_definition(line):
+ """Given a string of the format:
+ <interface_index>: <interface_name>: <flags> <settings>
+ Returns a dictionary containing the component parts.
+ :param line: unicode
+ :return: dict
+ :raises: ValueError if a malformed interface definition line is presented
+ """
+ interface = {}
+
+ # This line is in the format:
+ # <interface_index>: <interface_name>: <properties>
+ [index, name, properties] = map(
+ lambda s: s.strip(), line.split(':'))
+
+ interface['index'] = int(index)
+ if '@' in name:
+ name, parent = name.split('@')
+ interface['name'] = name
+ interface['parent'] = parent
+ else:
+ interface['name'] = name
+ interface['parent'] = None
+
+ # Now parse the <properties> part from above.
+ # This will be in the form "<FLAG1,FLAG2> key1 value1 key2 value2 ..."
+ matches = re.match(r"^<(.*)>(.*)", properties)
+ if matches:
+ flags = matches.group(1)
+ if len(flags) > 0:
+ flags = flags.split(',')
+ else:
+ flags = []
+ interface['flags'] = set(flags)
+ interface['settings'] = _get_settings_dict(matches.group(2))
+ else:
+ raise ValueError("Malformed 'ip link' line (%s)" % line)
+ return interface
+
+
+def _add_additional_interface_properties(interface, line):
+ """
+ Given the specified interface and a specified follow-on line containing
+ more interface settings, adds any additional settings to the interface
+ dictionary. (currently, the only relevant setting is the interface MAC.)
+ :param interface: dict
+ :param line: unicode
+ """
+ settings = _get_settings_dict(line)
+ # We only care about the MAC address for Ethernet interfaces.
+ mac = settings.get('link/ether')
+ if mac is not None:
+ interface['mac'] = mac
+
+
+def parse_ip_link(output):
+ """
+ Given the full output from 'ip link [show]', parses it and returns a
+ dictionary mapping each interface name to its settings.
+ :param output: string or unicode
+ :return: dict
+ """
+ interfaces = {}
+ interface = None
+ for line in output.splitlines():
+ if re.match(r'^[0-9]', line):
+ interface = _parse_interface_definition(line)
+ if interface is not None:
+ interfaces[interface['name']] = interface
+ else:
+ if interface is not None:
+ _add_additional_interface_properties(interface, line)
+ return interfaces
+
+#------------------------------------------------------------------------------
+
+_IP_ADDR_SPLIT_RE = re.compile("^[0-9]+: ", flags=re.MULTILINE)
+
+def parse_ip_addr(data):
+ """
+ Parse addresses from 'ip addr' output
+ """
+
+ for iface in _IP_ADDR_SPLIT_RE.split(data.strip()):
+ if not iface:
+ continue
+ lines = [l.strip() for l in iface.splitlines()]
+ # XXX @ in name not supported
+ name = lines.pop(0).partition(":")[0]
+ info = {
+ "ip-addresses": [],
+ "hardware-address": None,
+ }
+ if '@' in name:
+ name, parent = name.split('@')
+ info['name'] = name
+ info['parent'] = parent
+ else:
+ info['name'] = name
+ info['parent'] = None
+
+ for line in lines:
+ words = line.split()
+ if words[0].startswith("link/") and len(words) >= 2:
+ info["hardware-address"] = words[1]
+ elif words[0] in ("inet", "inet6"):
+ addrtype = "ipv6" if words[0] == "inet6" else "ipv4"
+ addr, _, prefix = words[1].partition("/")
+ if prefix == '':
+ prefix = 128 if addrtype == "ipv6" else 32
+ info["ip-addresses"].append({"ip-address-type": addrtype,
+ "ip-address": addr, "prefix": int(prefix)})
+ yield info
+
+#------------------------------------------------------------------------------
+
+class BaseNetDevice(Interface, Application):
+ __type__ = BaseResource
+
+ # XXX note: ethtool only required if we need to get the pci address
+ __package_names__ = ['ethtool']
+
+ device_name = Attribute(String, description = 'Name of the NetDevice',
+ default = lambda x : x._default_device_name(),
+ max_size = MAX_DEVICE_NAME_SIZE)
+ capacity = Attribute(Integer,
+ description = 'Capacity for interface shaping (Mb/s)')
+ mac_address = Attribute(String, description = 'Mac address of the device')
+ ip_address = Attribute(String, description = 'IP address of the device')
+ pci_address = Attribute(String,
+ description = 'PCI bus address of the device',
+ ro = True)
+ promiscuous = Attribute(Bool, description = 'Promiscuous', default = False)
+ up = Attribute(Bool, description = 'Promiscuous', default = True)
+ netdevice_type = Attribute(String, description = 'Type of the netdevice',
+ ro = True)
+ prefix = Attribute(String, default = 'dev')
+ rp_filter = Attribute(Bool, description = 'Reverse-path filtering enabled',
+ default = True)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Dummy member to store the other side of a VethPair
+ # We use it to disable offloading on interfaces connected to VPP
+ self.remote = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __get__(self):
+ def check(rv):
+ if not bool(rv):
+ raise ResourceNotFound
+ return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True,
+ parse=check)
+
+ __create__ = None
+
+ def __delete__(self):
+ return BashTask(self.node, CMD_DELETE, {'netdevice': self})
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_mac_address(self):
+ # Merge into parse_ip_link
+ def parse(rv):
+ assert rv is not None
+
+ nds = parse_ip_link(rv.stdout)
+ # This will raise an exception is the interface does not exist
+ nd = nds[self.device_name]
+ attrs = { 'mac_address': nd['mac'], }
+ return attrs
+
+ return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True,
+ parse=parse)
+
+ def _set_mac_address(self):
+ return BashTask(self.node, CMD_SET_MAC_ADDRESS, {'netdevice': self})
+
+ def _get_ip_address(self):
+ """
+ NOTE: Incidently, this will also give the MAC address, as well as other
+ attributes...
+ """
+ def parse(rv):
+ attrs = dict()
+
+ assert rv is not None
+
+ nds = list(parse_ip_addr(rv.stdout))
+
+ assert nds
+ assert len(nds) <= 1
+
+ nd = nds[0]
+
+ assert nd['name'] == self.device_name
+
+ attrs['mac_address'] = nd['hardware-address']
+
+ # We assume a single IPv4 address for now...
+ ips = [ip for ip in nd['ip-addresses']
+ if ip['ip-address-type'] == 'ipv4']
+ if len(ips) >= 1:
+ if len(ips) > 1:
+ log.warning('Keeping only first of many IP addresses...')
+ ip = ips[0]
+ attrs['ip_address'] = ip['ip-address']
+ else:
+ attrs['ip_address'] = None
+ return attrs
+
+ return BashTask(self.node, CMD_GET_IP_ADDRESS,
+ {'netdevice': self}, parse=parse)
+
+ def _set_ip_address(self):
+ if self.ip_address is None:
+ # Unset IP
+ return BashTask(self.node, CMD_FLUSH_IP,
+ {'device_name': self.device_name})
+ return BashTask(self.node, CMD_SET_IP_ADDRESS,
+ {'netdevice': self})
+
+ @task
+ def _get_promiscuous(self):
+ return {'promiscuous': False}
+
+ def _set_promiscuous(self):
+ on_off = 'on' if self.promiscuous else 'off'
+ return BashTask(self.node, CMD_SET_PROMISC,
+ {'netdevice': self, 'on_off' : on_off})
+
+ @task
+ def _get_up(self):
+ return {'up': False}
+
+ def _set_up(self):
+ up_down = 'up' if self.up else 'down'
+ return BashTask(self.node, CMD_SET_UP,
+ {'netdevice': self, 'up_down': up_down})
+
+ @task
+ def _get_capacity(self):
+ return {'capacity': None}
+
+ def _set_capacity(self):
+ if self.capacity is None:
+ log.warning('set_capacity(None) not implemented')
+ return EmptyTask()
+
+ # http://unix.stackexchange.com/questions/100785/bucket-size-in-tbf
+ MBPS = 1000000
+ KBPS = 1024
+ BYTES = 8
+ HZ = 250
+
+ # Round to power of two... see manpage
+ burst = math.ceil((((self.capacity * MBPS) / HZ) / BYTES) / KBPS)
+ burst = 1 << (burst - 1).bit_length()
+
+ return BashTask(self.node, CMD_SET_CAPACITY,
+ {'netdevice': self, 'burst': burst})
+
+ def _get_rp_filter(self):
+ def parse(rv):
+ lines = rv.stdout.splitlines()
+ return (int(lines[0][-1]) + int(lines[1][-1]) > 0)
+ return BashTask(self.node, CMD_GET_RP_FILTER, {'netdevice' :self},
+ parse = parse)
+
+ def _set_rp_filter(self):
+ cmd = CMD_SET_RP_FILTER if self.rp_filter else CMD_UNSET_RP_FILTER
+ return BashTask(self.node, cmd, {'netdevice' : self})
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _remote_node_name(self):
+ remote_interface = self._remote_interface()
+ if remote_interface:
+ return remote_interface.node.name
+ else:
+ rnd = ''.join(random.choice(string.ascii_uppercase + string.digits)
+ for _ in range(3))
+ return 'unk{}'.format(rnd)
+
+ def _remote_interface(self):
+ if not self.channel:
+ return None
+ interfaces = self.channel.interfaces
+ for interface in interfaces:
+ if interface == self:
+ continue
+ return interface
+
+ def _default_device_name(self):
+ remote_node_name = self._remote_node_name()
+ if remote_node_name:
+ return remote_node_name
+ else:
+ return AddressManager().get('device_name', self,
+ prefix = self.prefix, scope = self.prefix)
+
+#------------------------------------------------------------------------------
+
+class NonTapBaseNetDevice(BaseNetDevice):
+ # Tap devices for instance don't have offload
+ offload = Attribute(Bool, description = 'Offload', default=True)
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_offload(self):
+ return BashTask(self.node, CMD_GET_OFFLOAD, {'netdevice': self},
+ parse = lambda rv : rv.stdout.strip() == 'on')
+
+ def _set_offload(self):
+ cmd = None
+ if self.offload:
+ cmd = CMD_SET_OFFLOAD
+ else:
+ cmd = CMD_UNSET_OFFLOAD
+ return BashTask(self.node, cmd, {'netdevice' : self})
+
+#------------------------------------------------------------------------------
+
+class NetDevice(NonTapBaseNetDevice):
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE, {'netdevice': self})
+
+#------------------------------------------------------------------------------
+
+class SlaveBaseNetDevice(BaseNetDevice):
+ parent = Attribute(NetDevice, description = 'Parent NetDevice')
+
+ host = Attribute(NetDevice, description = 'Host interface',
+ default = lambda x : x._default_host())
+
+ def _default_host(self):
+ if self.node.__class__.__name__ == 'LxcContainer':
+ host = self.node.node
+ else:
+ host = self.node
+ max_len = MAX_DEVICE_NAME_SIZE - len(self.node.name) - 1
+ device_name = self.device_name[:max_len]
+
+ return NetDevice(node = host,
+ device_name = '{}-{}'.format(self.node.name, device_name),
+ managed = False)
+
+#------------------------------------------------------------------------------
+
+class SlaveNetDevice(SlaveBaseNetDevice):
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self})
diff --git a/vicn/resource/linux/netmon.py b/vicn/resource/linux/netmon.py
new file mode 100644
index 00000000..8472f308
--- /dev/null
+++ b/vicn/resource/linux/netmon.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.service import Service
+
+class NetMon(Service):
+ """
+ Resource: NetMon
+
+ Generic network monitoring daemon, used internally by VICN for resource
+ monitoring.
+ """
+ __package_names__ = ['netmon']
+ __service_name__ = 'netmon'
diff --git a/vicn/resource/linux/numa_mgr.py b/vicn/resource/linux/numa_mgr.py
new file mode 100644
index 00000000..632264ce
--- /dev/null
+++ b/vicn/resource/linux/numa_mgr.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import re
+from itertools import cycle
+
+from netmodel.model.type import BaseType
+from vicn.core.resource import Resource
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.task import BashTask
+from vicn.resource.node import Node
+
+PATTERN_LSCPU_NUMA = 'NUMA node[0-9]+ CPU\(s\)'
+CMD_LSCPU = 'lscpu'
+
+class CycleType(BaseType, cycle):
+ """
+ Type: CycleType
+ """
+ pass
+
+#------------------------------------------------------------------------------
+
+def parse_lscpu_line(line):
+ #Format: NUMA node0 CPU(s): 0-17,36-53
+ line = line.split(':')[1]
+ #line = 0-17,36,53
+
+ def limits_to_list(string):
+ limits = string.split('-')
+ lower_limit = int(limits[0])
+ #Removes core 0 as it is used the most often by the kernl
+ if lower_limit is 0 : lower_limit = 1
+ return cycle(range(lower_limit, int(limits[1])))
+ return cycle(map(limits_to_list, line.split(',')))
+
+def parse_lscpu_rv(rv):
+ ret = []
+ for line in rv.stdout.splitlines():
+ if re.search(PATTERN_LSCPU_NUMA, line):
+ ret.append(parse_lscpu_line(line))
+ return ret
+
+#------------------------------------------------------------------------------
+
+class NumaManager(Resource):
+ """
+ Resource: NumaManager
+ """
+
+ node = Attribute(Node,
+ mandatory = True,
+ multiplicity = Multiplicity.OneToOne,
+ reverse_auto = True,
+ reverse_name = 'numa_mgr')
+ numa_repartitor = Attribute(CycleType,
+ description = 'Tool to separate cores/CPUs/sockets',
+ multiplicity = Multiplicity.OneToMany,
+ ro = True)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ __create__ = None
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.current_numa_node = 0
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_numa_repartitor(self):
+ return BashTask(self.node, CMD_LSCPU, parse=parse_lscpu_rv)
+
+ #--------------------------------------------------------------------------
+ # Public API
+ #--------------------------------------------------------------------------
+
+ def get_numa_core(self, numa_node=None):
+ if numa_node is None:
+ numa_node = self.current_numa_node
+ self.current_numa_node = (self.current_numa_node+1) % \
+ len(self.numa_repartitor)
+ numa_list = self.numa_repartitor[numa_node]
+
+ socket = next(numa_list)
+ return numa_node, next(socket)
+
+ def get_number_of_numa(self):
+ return len(self.numa_repartitor)
diff --git a/vicn/resource/linux/ovs.py b/vicn/resource/linux/ovs.py
new file mode 100644
index 00000000..d67e4bca
--- /dev/null
+++ b/vicn/resource/linux/ovs.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.task import BashTask
+from vicn.resource.linux.bridge_mgr import BridgeManager
+
+CMD_ADD_BRIDGE = '''
+ovs-vsctl --may-exist add-br {bridge_name}
+ip link set dev {bridge_name} up
+'''
+
+CMD_DEL_BRIDGE = 'ovs-vsctl --if-exists del-br {bridge_name}'
+
+CMD_ADD_INTERFACE = 'ovs-vsctl --may-exist add-port {bridge_name} ' \
+ '{interface_name}'
+CMD_ADD_INTERFACE_VLAN = CMD_ADD_INTERFACE + ' tag={vlan}'
+CMD_DEL_INTERFACE = 'ovs-vsctl --if-exists del-port {bridge_name} ' \
+ '{interface_name}'
+
+class OVS(BridgeManager):
+ """
+ Resource: OVS
+
+ OpenVSwitch bridge manager
+ """
+
+ __package_names__ = ['openvswitch-switch']
+
+ #---------------------------------------------------------------------------
+ # BridgeManager API
+ #---------------------------------------------------------------------------
+
+ def add_bridge(self, bridge_name):
+ return BashTask(self.node, CMD_ADD_BRIDGE,
+ {'bridge_name': bridge_name},
+ output = False, as_root = True)
+
+ def del_bridge(self, bridge_name):
+ return BashTask(self.node, CMD_DEL_BRIDGE,
+ {'bridge_name': bridge_name},
+ output = False, as_root = True)
+
+ def add_interface(self, bridge_name, interface_name, vlan=None):
+ cmd = CMD_ADD_INTERFACE_VLAN if vlan is not None else CMD_ADD_INTERFACE
+ return BashTask(self.node, cmd, {'bridge_name': bridge_name,
+ 'interface_name': interface_name, 'vlan': vlan},
+ output = False, as_root = True)
+
+ def del_interface(self, bridge_name, interface_name, vlan=None):
+ return BashTask(self.node, CMD_DEL_INTERFACE,
+ {'bridge_name': bridge_name, 'interface_name': interface_name,
+ 'vlan': vlan},
+ output = False, as_root = True)
diff --git a/vicn/resource/linux/package_manager.py b/vicn/resource/linux/package_manager.py
new file mode 100644
index 00000000..cb149ac6
--- /dev/null
+++ b/vicn/resource/linux/package_manager.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+import logging
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, EmptyTask, async_task
+from vicn.core.task import inline_task, run_task
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+CMD_APT_GET_KILL = 'kill -9 $(pidof apt-get) || true'
+CMD_DPKG_CONFIGURE_A = 'dpkg --configure -a'
+
+CMD_APT_GET_UPDATE = '''
+# Force IPv4
+echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4
+# Update package repository on node {node}
+apt-get update
+'''
+
+# We need to double { } we want to preserve
+CMD_PKG_TEST='dpkg -s {self.package_name}'
+
+CMD_PKG_INSTALL='''
+# Installing package {package_name}
+apt-get -y --allow-unauthenticated install {package_name}
+'''
+
+CMD_PKG_UNINSTALL='''
+# Uninstalling package {self.package_name}
+apt-get remove {self.package_name}
+'''
+
+CMD_SETUP_REPO = '''
+# Initialize package repository {repository.repo_name} on node {self.node.name}
+echo "{deb_source}" > {path}
+'''
+
+class PackageManager(Resource):
+ """
+ Resource: PackageManager
+
+ APT package management wrapper.
+
+ Todo:
+ - We assume a package manager is always installed on every machine.
+ - Currently, we limit ourselves to debian/ubuntu, and voluntarily don't
+ subclass this as we have (so far) no code for selecting the right
+ subclass, eg choising dynamically between DebRepositoryManager and
+ RpmRepositoryManager.
+ - We currently don't use package version numbers, which means a package
+ can be installed but not be up to date.
+ """
+
+ node = Attribute(Node,
+ reverse_name = 'package_manager',
+ reverse_auto = True,
+ mandatory = True,
+ multiplicity = Multiplicity.OneToOne)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._up_to_date = False
+ self.apt_lock = asyncio.Lock()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ if self.node.__class__.__name__ == 'Physical':
+ # UGLY : This blocking code is currently needed
+ task = self.node.host_interface._get_ip_address()
+ ip_dict = task.execute_blocking()
+ self.node.host_interface.ip_address = ip_dict['ip_address']
+ return ('Repository',)
+ else:
+ return ('Repository', 'CentralIP', 'RoutingTable')
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ repos = EmptyTask()
+ for repository in self._state.manager.by_type_str('Repository'):
+ deb_source = self._get_deb_source(repository)
+ path = self._get_path(repository)
+ repo = BashTask(self.node, CMD_SETUP_REPO,
+ {'deb_source': deb_source, 'path': path})
+ repos = repos | repo
+
+ return repos
+
+ #---------------------------------------------------------------------------
+ # Methods
+ #---------------------------------------------------------------------------
+
+ def __method_update__(self):
+ kill = BashTask(self.node, CMD_APT_GET_KILL, {'node': self.node.name},
+ lock = self.apt_lock)
+
+ # Setup during a reattempt
+ if hasattr(self, '_dpkg_configure_a'):
+ dpkg_configure_a = BashTask(self.node, CMD_DPKG_CONFIGURE_A,
+ lock = self.apt_lock)
+ else:
+ dpkg_configure_a = EmptyTask()
+
+ if not self.node.package_manager._up_to_date:
+ update = BashTask(self.node, CMD_APT_GET_UPDATE, {'node': self.node.name},
+ lock = self.apt_lock, post = self._mark_updated)
+ else:
+ update = EmptyTask()
+
+ return (kill > dpkg_configure_a) > update
+
+ def __method_install__(self, package_name):
+ update = self.__method_update__()
+ install = BashTask(self.node, CMD_PKG_INSTALL, {'package_name':
+ package_name}, lock = self.apt_lock)
+ return update > install
+
+ #---------------------------------------------------------------------------
+ # Internal methods
+ #---------------------------------------------------------------------------
+
+ def _mark_updated(self):
+ self._up_to_date = True
+
+ def _get_path(self, repository):
+ return '/etc/apt/sources.list.d/{}.list'.format(repository.repo_name)
+
+ def _get_deb_source(self, repository):
+ path = repository.node.host_interface.ip_address + '/'
+ if repository.directory:
+ path += repository.directory + '/'
+ return 'deb http://{} {}/'.format(path, self.node.dist)
+
+#------------------------------------------------------------------------------
+
+class Package(Resource):
+ """
+ Resource: Package
+
+ deb package support
+ """
+
+ package_name = Attribute(String, mandatory = True)
+ node = Attribute(Node,
+ mandatory = True,
+ requirements=[
+ Requirement('package_manager')
+ ])
+
+ #---------------------------------------------------------------------------
+ # Resource lifecycle
+ #---------------------------------------------------------------------------
+
+ def __get__(self):
+ return BashTask(self.node, CMD_PKG_TEST, {'self': self})
+
+ def __create__(self):
+ return self.node.package_manager.__method_install__(self.package_name)
+
+ @async_task
+ async def __delete__(self):
+ with await self.node.package_manager._lock:
+ task = BashTask(self.node, CMD_PKG_UNINSTALL, {'self': self})
+ ret = await run_task(task, self._state.manager)
+ return ret
+
+#------------------------------------------------------------------------------
+
+class Packages(Resource):
+ """
+ Resource: Packages
+
+ Todo:
+ - The number of concurrent subresources is not dynamically linked to the
+ nodes. We may need to link subresources to the attribute in general, but
+ since package_names are static for a resource, this is not a problem here.
+ """
+ names = Attribute(String, multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ mandatory = True,
+ requirements=[
+ Requirement('package_manager')
+ ])
+
+ #---------------------------------------------------------------------------
+ # Resource lifecycle
+ #---------------------------------------------------------------------------
+
+ def __subresources__(self):
+ """
+ Note: Although packages are (rightfully) specified concurrent, apt tasks
+ will be exlusive thanks to the use of a lock in the package manager.
+ """
+ if self.names:
+ packages = [Package(node=self.node, package_name=name, owner=self)
+ for name in self.names]
+ return Resource.__concurrent__(*packages)
+ else:
+ return None
+
diff --git a/vicn/resource/linux/phy_interface.py b/vicn/resource/linux/phy_interface.py
new file mode 100644
index 00000000..c1aef27e
--- /dev/null
+++ b/vicn/resource/linux/phy_interface.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.resource import BaseResource
+from vicn.resource.interface import Interface
+
+class PhyInterface(Interface):
+ """
+ Resource: PhyInterface
+
+ Physical network interface.
+ """
+
+ __type__ = BaseResource
+
+ device_name = Attribute(String, description = 'Name of the DpdkDevice',
+ mandatory = True)
+ pci_address = Attribute(String, description = "Device's PCI bus address",
+ mandatory = True)
+ mac_address = Attribute(String, description = "Device's MAC address",
+ mandatory=True)
+ ip_address = Attribute(String, description = "Device's IP address")
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ if not self.name:
+ self.name = self.node.name + '-' + self.device_name
diff --git a/vicn/resource/linux/phy_link.py b/vicn/resource/linux/phy_link.py
new file mode 100644
index 00000000..878cf7c6
--- /dev/null
+++ b/vicn/resource/linux/phy_link.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.attribute import Attribute
+from vicn.core.task import inline_task
+from vicn.resource.channel import Channel
+from vicn.resource.linux.phy_interface import PhyInterface
+
+class PhyLink(Channel):
+ """
+ Resource: PhyLink
+
+ Physical Link to inform the orchestrator about Layer2 connectivity.
+ """
+
+ src = Attribute(PhyInterface, description = 'Source interface',
+ mandatory = True)
+ dst = Attribute(PhyInterface, description = 'Destination interface',
+ mandatory = True)
+
+ @inline_task
+ def __initialize__(self):
+ self.src.set('channel', self)
+ self.dst.set('channel', self)
diff --git a/vicn/resource/linux/physical.py b/vicn/resource/linux/physical.py
new file mode 100644
index 00000000..e5eba2d3
--- /dev/null
+++ b/vicn/resource/linux/physical.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import stat
+import logging
+import subprocess
+import shlex
+
+from netmodel.model.type import String, Integer
+from netmodel.util.misc import is_local_host
+from netmodel.util.socket import check_port
+from vicn.core.attribute import Attribute
+from vicn.core.commands import Command, ReturnValue
+from vicn.core.exception import ResourceNotFound
+from vicn.core.task import Task, task
+from vicn.resource.node import Node, DEFAULT_USERNAME
+from vicn.resource.node import DEFAULT_SSH_PUBLIC_KEY
+from vicn.resource.node import DEFAULT_SSH_PRIVATE_KEY
+
+log = logging.getLogger(__name__)
+
+CMD_SSH_COPY_ID = 'ssh-copy-id {ssh_options} -i {public_key} -p {port} ' \
+ '{user}@{host}'
+CMD_SSH = 'ssh {ssh_options} -i {private_key} -p {port} ' \
+ '{user}@{host} {command}'
+CMD_SSH_NF = 'ssh -n -f {ssh_options} -i {private_key} -p {port} ' \
+ '{user}@{host} {command}'
+
+class Physical(Node):
+ """
+ Resource: Physical
+
+ Physical node
+ """
+
+ hostname = Attribute(String, description = 'Hostname or IP address',
+ mandatory = True)
+ ssh_port = Attribute(Integer, description = 'SSH port number',
+ default = 22)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.node_with_kernel = self
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @task
+ def __get__(self, attributes=None):
+ if not check_port(self.hostname, self.ssh_port):
+ raise ResourceNotFound
+
+ def __create__(self):
+ tasks = list()
+ modes = (True, False) if DEFAULT_USERNAME != 'root' else (True,)
+ for as_root in modes:
+ tasks.append(self._setup_ssh_key(as_root))
+ return Task.__concurrent__(*tasks)
+
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ @task
+ def _setup_ssh_key(self, as_root):
+ os.chmod(DEFAULT_SSH_PUBLIC_KEY, stat.S_IRUSR | stat.S_IWUSR)
+ os.chmod(DEFAULT_SSH_PRIVATE_KEY, stat.S_IRUSR | stat.S_IWUSR)
+ cmd_params = {
+ 'public_key' : DEFAULT_SSH_PUBLIC_KEY,
+ 'ssh_options' : '',
+ 'port' : self.ssh_port,
+ 'user' : 'root' if as_root else DEFAULT_USERNAME,
+ 'host' : self.hostname,
+ }
+
+ c = Command(CMD_SSH_COPY_ID, parameters = cmd_params)
+
+ return self._do_execute_process(c.full_commandline, output=False)
+
+ #--------------------------------------------------------------------------
+ # Public API
+ #--------------------------------------------------------------------------
+
+ def _do_execute_process(self, command, output = False):
+ p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
+ if output:
+ out, err = p.communicate()
+ return ReturnValue(p.returncode, stdout=out, stderr=err)
+
+ p.wait()
+ return ReturnValue(p.returncode)
+
+ def _do_execute_ssh(self, command, output=False, as_root=False,
+ ssh_options=None):
+ if not ssh_options:
+ ssh_options = dict()
+ cmd_params = {
+ 'private_key' : DEFAULT_SSH_PRIVATE_KEY,
+ 'ssh_options' : ' '.join(['-o {}={}'.format(k, v)
+ for k, v in ssh_options.items()]),
+ 'port' : self.ssh_port,
+ 'user' : 'root' if as_root else DEFAULT_USERNAME,
+ 'host' : self.hostname,
+ 'command' : shlex.quote(command),
+ }
+
+ if (command[-1] != '&'):
+ c = Command(CMD_SSH, parameters = cmd_params)
+ else:
+ c = Command(CMD_SSH_NF, parameters = cmd_params)
+
+ return self._do_execute_process(c.full_commandline_nobashize, output)
+
+ def execute(self, command, output=False, as_root=False):
+ if is_local_host(self.hostname):
+ rv = self._do_execute_process(command, output = output)
+ else:
+ rv = self._do_execute_ssh(command, output = output,
+ as_root = as_root)
+ return rv
diff --git a/vicn/resource/linux/repository.py b/vicn/resource/linux/repository.py
new file mode 100644
index 00000000..f3e70565
--- /dev/null
+++ b/vicn/resource/linux/repository.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.resource.application import Application
+
+class Repository(Application):
+ """
+ Resource: Repository
+
+ deb package repository
+
+ Note: As PackageManager uses a Repository, this resource cannot be a
+ LinuxApplication resource. We have no package to install since they are
+ part of any basic distribution install.
+ """
+
+ repo_name = Attribute(String, description = 'Name of the repository',
+ default = 'vicn')
+ directory = Attribute(String, description = 'Directory holding packages',
+ default = '')
+ distributions = Attribute(String,
+ description = 'List of distributions served by this repository',
+ multiplicity = Multiplicity.ManyToMany,
+ default = ['sid', 'trusty', 'xenial'])
diff --git a/vicn/resource/linux/service.py b/vicn/resource/linux/service.py
new file mode 100644
index 00000000..3eb753fc
--- /dev/null
+++ b/vicn/resource/linux/service.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import CategoryResource
+from vicn.core.task import inline_task, BashTask, EmptyTask
+from vicn.resource.linux.application import LinuxApplication
+
+log = logging.getLogger(__name__)
+
+CMD_START = 'service {service_name} start'
+CMD_STOP = 'service {service_name} stop'
+CMD_RESTART = 'service {service_name} restart'
+CMD_STOP_START = 'service {service_name} stop && sleep 1 && ' \
+ 'service {service_name} start'
+
+class Service(LinuxApplication):
+ """Service resource
+
+ This resource wraps a Linux Service, and ensure the service is started
+ (resp. stopped) during setup (resp. teardown).
+
+ Required tags:
+ __service_name__ (str): all classes that inherit from Service should
+ inform this tag which gives the name of the service known to the
+ system.
+
+ TODO:
+ * Support for upstart, sysvinit and systemd services.
+ * Start and Stop method
+ * Status attribute
+ """
+
+ __type__ = CategoryResource
+
+
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __method_restart__(self):
+ return BashTask(self.node, CMD_RESTART,
+ {'service_name': self.__service_name__})
+
+ def __method_start__(self):
+ return BashTask(self.node, CMD_START,
+ {'service_name': self.__service_name__})
+
+
+ def __create__(self):
+ if self.__service_name__ == 'lxd':
+ log.warning('Not restarting LXD')
+ return EmptyTask()
+
+ if self.__service_name__ == 'dnsmasq':
+ return BashTask(self.node, CMD_STOP_START,
+ {'service_name': self.__service_name__})
+
+ return self.__method_restart__()
+
+
+ def __delete__(self):
+ return BashTask(self.node, CMD_STOP,
+ {'service_name': self.__service_name__})
+
diff --git a/vicn/resource/linux/sym_veth_pair.py b/vicn/resource/linux/sym_veth_pair.py
new file mode 100644
index 00000000..bf79a69b
--- /dev/null
+++ b/vicn/resource/linux/sym_veth_pair.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import random
+import string
+
+from netmodel.model.type import Integer
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.state import ResourceState, AttributeState
+from vicn.core.task import BashTask, get_attributes_task
+from vicn.core.task import async_task, task, inline_task
+from vicn.core.task import run_task
+from vicn.resource.interface import Interface
+from vicn.resource.node import Node
+from vicn.resource.linux.net_device import NonTapBaseNetDevice
+from vicn.resource.linux.link import CMD_DELETE_IF_EXISTS
+from vicn.resource.linux.link import CMD_UP
+
+CMD_CREATE='''
+# Create veth pair in the host node
+ip link add name {tmp_side1} type veth peer name {tmp_side2}
+ip link set dev {tmp_side1} netns {pid[0]} name {side1_device_name}
+ip link set dev {tmp_side2} netns {pid[1]} name {side2_device_name}
+'''
+
+class SymVethPair(Resource):
+ """
+ Resource: SymVethPair
+
+ This resource is used in VPPBridge. The main difference with the Link
+ resource is that is it not a channel.
+ """
+
+ node1 = Attribute(Node,
+ description = 'Node on which one side of the veth will sit',
+ mandatory = True)
+ node2 = Attribute(Node,
+ description = 'Node on which the other side of the veth will sit',
+ mandatory = True)
+ capacity = Attribute(Integer,
+ description = 'Capacity of the veth pair (Mb/s)')
+ side1 = Attribute(Interface, description = 'Source interface')
+ side2 = Attribute(Interface, description = 'Destination interface')
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ async def _commit(self):
+ # see link.py for explanations
+ manager = self._state.manager
+ await manager._set_resource_state(self.side1,
+ ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.side2,
+ ResourceState.INITIALIZED)
+ await manager._set_resource_state(self.side1, ResourceState.CREATED)
+ await manager._set_resource_state(self.side2, ResourceState.CREATED)
+ await manager._set_attribute_state(self, 'side1', AttributeState.CLEAN)
+ await manager._set_attribute_state(self, 'side2', AttributeState.CLEAN)
+ manager.commit_resource(self.side1)
+ manager.commit_resource(self.side2)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __initialize__(self):
+ self.side1 = NonTapBaseNetDevice(node = self.node1,
+ device_name = self.node2.name,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.side2 = NonTapBaseNetDevice(node = self.node2,
+ device_name = self.node1.name,
+ capacity = self.capacity,
+ owner = self.owner)
+ self.side1.remote = self.side2
+ self.side2.remote = self.side1
+
+ @async_task
+ async def __get__(self):
+ manager = self._state.manager
+
+ try:
+ await run_task(self.side1.__get__(), manager)
+ await run_task(self.side2.__get__(), manager)
+ except ResourceNotFound:
+ raise ResourceNotFound
+
+ await self._commit()
+
+ def __create__(self):
+ assert self.node1.get_type() == 'lxccontainer'
+ assert self.node2.get_type() == 'lxccontainer'
+
+ node1_host = self.node1.node
+ node2_host = self.node2.node
+
+ assert node1_host == node2_host
+ host = node1_host
+
+ # Sometimes a down interface persists on one side
+ delif_side1 = BashTask(self.node1, CMD_DELETE_IF_EXISTS,
+ {'interface': self.side1})
+ delif_side2 = BashTask(self.node2, CMD_DELETE_IF_EXISTS,
+ {'interface': self.side2})
+
+ pid_node1 = get_attributes_task(self.node1, ['pid'])
+ pid_node2 = get_attributes_task(self.node2, ['pid'])
+
+ tmp_side1 = 'tmp-veth-' + ''.join(random.choice(
+ string.ascii_uppercase + string.digits) for _ in range(5))
+ tmp_side2 = 'tmp-veth-' + ''.join(random.choice(
+ string.ascii_uppercase + string.digits) for _ in range(5))
+
+ create = BashTask(host, CMD_CREATE,
+ {'side1_device_name': self.side1.device_name,
+ 'side2_device_name': self.side2.device_name,
+ 'tmp_side1': tmp_side1, 'tmp_side2': tmp_side2})
+
+ up_side1 = BashTask(self.node1, CMD_UP, {'interface': self.side1})
+ up_side2 = BashTask(self.node2, CMD_UP, {'interface': self.side2})
+
+ @async_task
+ async def set_state():
+ await self._commit()
+
+ delif = delif_side1 | delif_side2
+ up = up_side1 | up_side2
+ pid = pid_node1 | pid_node2
+ return ((delif > (pid @ create)) > up) > set_state()
+
+ def __delete__(self):
+ raise NotImplementedError
diff --git a/vicn/resource/linux/tap_device.py b/vicn/resource/linux/tap_device.py
new file mode 100644
index 00000000..68c0b9c7
--- /dev/null
+++ b/vicn/resource/linux/tap_device.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute
+from vicn.core.task import BashTask
+from vicn.resource.linux.net_device import BaseNetDevice
+
+CMD_CREATE='ip tuntap add name {netdevice.device_name} mode tap'
+CMD_FLUSH_IP='ip addr flush dev {device_name}'
+CMD_SET_IP_ADDRESS='ip addr add dev {netdevice.device_name} 0.0.0.0'
+
+class TapDevice(BaseNetDevice):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'tap'
+ self.netdevice_type = 'tap'
+
+ def __create__(self):
+ return BashTask(self.node, CMD_CREATE, {'netdevice': self})
+
+ def _set_ip_address(self):
+ if self.ip_address is None:
+ # Unset IP
+ return BashTask(self.node, CMD_FLUSH_IP,
+ {'device_name': self.device_name})
+ return BashTask(self.node, CMD_SET_IP_ADDRESS,
+ {'netdevice': self})
+
+class TapChannel(TapDevice):
+ station_name = Attribute(String)
+ channel_name = Attribute(String)
diff --git a/vicn/resource/linux/traceroute.py b/vicn/resource/linux/traceroute.py
new file mode 100644
index 00000000..2f8cb2c9
--- /dev/null
+++ b/vicn/resource/linux/traceroute.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.linux.application import LinuxApplication
+
+class Traceroute(LinuxApplication):
+ __package_names__ = ['traceroute']
+
diff --git a/vicn/resource/linux/veth_pair.py b/vicn/resource/linux/veth_pair.py
new file mode 100644
index 00000000..53fa9bf8
--- /dev/null
+++ b/vicn/resource/linux/veth_pair.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import random
+import string
+
+from vicn.resource.linux.net_device import SlaveBaseNetDevice
+from vicn.core.task import BashTask, get_attributes_task
+
+# ip link add veth0 type veth peer name veth1
+
+CMD_CREATE='''
+# Create veth pair in the host node
+ip link add name {interface.host.device_name} type veth peer name {tmp_name}
+# The host interface will always be up...
+ip link set dev {interface.host.device_name} up
+# Move interface into container and rename it
+ip link set dev {tmp_name} netns {pid} name {interface.device_name}
+'''
+CMD_UP='''
+ip link set dev {interface.device_name} up
+'''
+
+# see:
+# http://stackoverflow.com/questions/22780927/lxc-linux-containers-add-new-network-interface-without-restarting
+
+class VethPair(SlaveBaseNetDevice):
+ # Do not need the parent attribute...
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self, *args, **kwargs)
+ self.prefix = 'veth'
+ self.netdevice_type = 'veth'
+
+ def __create__(self):
+ assert self.node.__class__.__name__ == 'LxcContainer'
+ host = self.node.node
+ pid = get_attributes_task(self.node, ['pid'])
+ tmp_name = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase \
+ + string.digits) for _ in range(5))
+ create = BashTask(host, CMD_CREATE, {'tmp_name': tmp_name,
+ 'interface': self})
+ up = BashTask(self.node, CMD_UP, {'interface': self})
+ bridge = host.bridge_manager.add_interface(host.bridge.device_name,
+ self.host.device_name)
+ return ((pid @ create) > up) > bridge
+
+ # ... IP and UP missing...
diff --git a/vicn/resource/lxd/__init__.py b/vicn/resource/lxd/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/lxd/__init__.py
diff --git a/vicn/resource/lxd/lxc_container.py b/vicn/resource/lxd/lxc_container.py
new file mode 100644
index 00000000..afa64aba
--- /dev/null
+++ b/vicn/resource/lxd/lxc_container.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import shlex
+import time
+
+# Suppress logging from pylxd dependency on ws4py
+# (this needs to be included before pylxd)
+from ws4py import configure_logger
+configure_logger(level=logging.ERROR)
+import pylxd
+
+from netmodel.model.type import String, Integer, Bool, Self
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute, Reference, Multiplicity
+from vicn.core.commands import ReturnValue
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import task, inline_task, BashTask
+from vicn.resource.linux.net_device import NetDevice
+from vicn.resource.node import Node
+from vicn.resource.vpp.scripts import APPARMOR_VPP_PROFILE
+
+log = logging.getLogger(__name__)
+
+# Default name of VICN management/monitoring interface
+DEFAULT_LXC_NETDEVICE = 'eth0'
+
+# Default remote server (pull mode only)
+DEFAULT_SOURCE_URL = 'https://cloud-images.ubuntu.com/releases/'
+
+# Default protocol used to download images (lxd or simplestreams)
+DEFAULT_SOURCE_PROTOCOL = 'simplestreams'
+
+# Commands used to interact with LXD (in addition to pylxd bindings)
+CMD_GET_PID='lxc info {container.name} | grep Pid | cut -d " " -f 2'
+
+# Type: ContainerName
+ContainerName = String(max_size = 64, ascii = True,
+ forbidden = ('/', ',', ':'))
+
+class LxcContainer(Node):
+ """
+ Resource: LxcContainer
+
+ Todo:
+ - Remove VPP dependency
+ - The bridge is not strictly needed, but we currently have no automated
+ way to determine whether we need it or not
+ - The management interface should be added by VICN, not part of the
+ resource, and its name should be determined automatically.
+ """
+
+ architecture = Attribute(String, description = 'Architecture',
+ default = 'x86_64')
+ container_name = Attribute(ContainerName,
+ description = 'Name of the container',
+ default = Reference(Self, 'name'))
+ ephemeral = Attribute(Bool, description = 'Ephemeral container flag',
+ default = False)
+ node = Attribute(Node,
+ description = 'Node on which the container is running',
+ mandatory = True,
+ requirements = [
+ # We need the hypervisor setup to be able to check for the
+ # container; more generally, all dependencies
+ Requirement('lxd_hypervisor'), # not null
+ # The bridge is not strictly needed, but we currently have
+ # no automated way to determine whether we need it or not
+ Requirement('bridge'),
+ # A DNS server is required to provide internet connectivity to
+ # the containers
+ Requirement('dns_server'),
+ ])
+ profiles = Attribute(String, multiplicity = Multiplicity.OneToMany,
+ default = ['default'])
+ image = Attribute(String, description = 'image', default = None)
+ is_image = Attribute(Bool, defaut = False)
+ pid = Attribute(Integer, description = 'PID of the container')
+
+ #--------------------------------------------------------------------------
+ # Constructor / Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._container = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __initialize__(self):
+ """
+ We need to intanciate VPPHost before container creation.
+ """
+ self.node_with_kernel = Reference(self, 'node')
+
+ # We automatically add the management/monitoring interface
+ self._host_interface = NetDevice(node = self,
+ owner = self,
+ monitored = False,
+ device_name = DEFAULT_LXC_NETDEVICE)
+ self._state.manager.commit_resource(self._host_interface)
+
+ for iface in self.interfaces:
+ if iface.get_type() == "dpdkdevice":
+ self.node.vpp_host.dpdk_devices.append(iface.pci_address)
+
+ if 'vpp' in self.profiles:
+ dummy = self.node.vpp_host.uio_devices
+
+ @task
+ def __get__(self):
+ client = self.node.lxd_hypervisor.client
+ try:
+ self._container = client.containers.get(self.name)
+ except pylxd.exceptions.NotFound:
+ raise ResourceNotFound
+
+ def __create__(self):
+ """
+ Make sure vpp_host is instanciated before starting the container.
+ """
+ wait_vpp_host = wait_resource_task(self.node.vpp_host)
+ create = self._create_container()
+ start = self.__method_start__()
+ return wait_vpp_host > (create > start)
+
+ @task
+ def _create_container(self):
+ container = self._get_container_description()
+ log.debug('Container description: {}'.format(container))
+ client = self.node.lxd_hypervisor.client
+ self._container = client.containers.create(container, wait=True)
+ self._container.start(wait = True)
+
+ def _get_container_description(self):
+ # Base configuration
+ container = {
+ 'name' : self.container_name,
+ 'architecture' : self.architecture,
+ 'ephemeral' : self.ephemeral,
+ 'profiles' : ['default'],
+ 'config' : {},
+ 'devices' : {},
+ }
+
+ # DEVICES
+
+ devices = {}
+ # FIXME Container profile support is provided by setting changes into
+ # configuration (currently only vpp profile is supported)
+ for profile in self.profiles:
+ if profile == 'vpp':
+ # Set the new apparmor profile. This will be created in VPP
+ # application
+ # Mount hugetlbfs in the container.
+ container['config']['raw.lxc'] = APPARMOR_VPP_PROFILE
+ container['config']['security.privileged'] = 'true'
+
+ for device in self.node.vpp_host.uio_devices:
+ container['devices'][device] = {
+ 'path' : '/dev/{}'.format(device),
+ 'type' : 'unix-char' }
+
+ # NETWORK (not for images)
+
+ if not self.is_image:
+ container['config']['user.network_mode'] = 'link-local'
+ device = {
+ 'type' : 'nic',
+ 'name' : self.host_interface.device_name,
+ 'nictype' : 'bridged',
+ 'parent' : self.node.bridge.device_name,
+ }
+ device['hwaddr'] = AddressManager().get_mac(self)
+ prefix = 'veth-{}'.format(self.container_name)
+ device['host_name'] = AddressManager().get('device_name', self,
+ prefix = prefix, scope = prefix)
+
+ container['devices'][device['name']] = device
+
+
+ # SOURCE
+
+ image_names = [alias['name'] for alias in self.node.lxd_hypervisor.aliases]
+ image_exists = self.image is not None and self.image in image_names
+
+ if image_exists:
+ container['source'] = {
+ 'type' : 'image',
+ 'mode' : 'local',
+ 'alias' : self.image,
+ }
+ else:
+ container['source'] = {
+ 'type' : 'image',
+ 'mode' : 'pull',
+ 'server' : DEFAULT_SOURCE_URL,
+ 'protocol' : DEFAULT_SOURCE_PROTOCOL,
+ 'alias' : self.dist,
+ }
+
+ log.info('Creating container: {}'.format(container))
+ return container
+
+ @task
+ def __delete__(self):
+ log.info("Delete container {}".format(self.container_name))
+ self.node.lxd_hypervisor.client.containers.remove(self.name)
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_pid(self):
+ """
+ Attribute: pid (getter)
+ """
+ return BashTask(self.node, CMD_GET_PID, {'container': self},
+ parse = lambda rv: {'pid': rv.stdout.strip()})
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ @task
+ def __method_start__(self):
+ """
+ Method: Start the container
+ """
+ self._container.start(wait = True)
+
+ @task
+ def __method_stop__(self):
+ """
+ Method: Stop the container
+ """
+ self._container.stop(wait = True)
+
+ @task
+ def __method_to_image__(self):
+ """
+ Returns:
+ Image metadata as returned by LXD REST API.
+ """
+ publish_description = {
+ "public": True,
+ "properties": {
+ "os": "Ubuntu",
+ "architecture": "x86_64",
+ "description": "Image generated from container {}".format(
+ self.container_name),
+ },
+ "source": {
+ "type": "container", # One of "container" or "snapshot"
+ "name": 'image-{}'.format(self.container_name),
+ }
+ }
+ return self.node.lxd_hypervisor.publish_image(publish_description)
+
+ #--------------------------------------------------------------------------
+ # Node API
+ #--------------------------------------------------------------------------
+
+ def execute(self, command, output = False, as_root = False):
+ """
+ Executes a command on the node
+
+ Params:
+ output (bool) : Flag determining whether the method should return
+ the output value.
+ as_root (bool) : Flag telling whether the command should be
+ executed as root.
+
+ Returns:
+ ReturnValue containing exit code, and eventually stdout and stderr.
+
+ Raises
+ Exception in case of error
+
+ The node exposes an interface allowing command execution through LXD.
+ We don't currently use an eventually available SSH connection.
+ """
+
+ ret = self._container.execute(shlex.split(command))
+
+ # NOTE: pylxd documents the return value as a tuple, while it is in
+ # fact a ContainerExecuteResult object
+ if not hasattr(ret, "exit_code"):
+ log.error("LXD return value does not have an exit code. "
+ "Try installing pylxd>=2.2.2 with pip3")
+ import sys; sys.exit(1)
+
+ args = (ret.exit_code,)
+ if output:
+ args += (ret.stdout, ret.stderr)
+ return ReturnValue(*args)
diff --git a/vicn/resource/lxd/lxc_image.py b/vicn/resource/lxd/lxc_image.py
new file mode 100644
index 00000000..2cc7220d
--- /dev/null
+++ b/vicn/resource/lxd/lxc_image.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import time
+
+from netmodel.model.type import Self
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource import Resource
+from vicn.core.task import task, inline_task
+from vicn.resource.linux.application import LinuxApplication as Application
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+class LxcImage(Resource):
+ """
+ Resource: LxcImage
+ """
+
+ node = Attribute(Node, description = 'Node on which the image is stored',
+ mandatory = True,
+ requirements = [
+ Requirement('lxd_hypervisor')
+ ])
+ image = Attribute(Self, description = 'image', default = None)
+ applications = Attribute(Application, multiplicity = Multiplicity.OneToMany)
+
+ #---------------------------------------------------------------------------
+ # Constructor / Accessors
+ #---------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ self.fingerprint = None
+ self._tmp_container = None
+ super().__init__(*args, **kwargs)
+
+ #---------------------------------------------------------------------------
+ # Resource lifecycle
+ #---------------------------------------------------------------------------
+
+ @task
+ def __get__(self):
+ aliases = [alias['name'] for images in self.node.lxd_hypervisor.client.images.all()
+ for alias in images.aliases]
+ if not self.image in aliases:
+ raise ResourceNotFound
+
+ @inline_task
+ def __create__(self):
+ log.warning('Image creation is currently disabled')
+ return
+
+
+ @task
+ def __create_DISABLED__(self):
+ """
+ Image creation consists in setting up a temporary container, stopping
+ it, publishing an image of it, setting an alias, and deleting it.
+ """
+
+
+ tmp_container.setup()
+
+ print("TODO: Installing applications...")
+ for application in self.applications:
+ print('Installing application on image')
+ application.setup()
+
+ # XXX stop() hangs if run to early wrt container start
+ # - is it related to ZFS ? is it a more general problem ?
+ time.sleep(5)
+
+ print("I: Stopping container")
+ tmp_container.stop()
+
+ print("I: Publishing image")
+ image_metadata = tmp_container.publish_image() # METHOD !
+ print("MD=", image_metadata)
+ self.fingerprint = image_metadata['fingerprint']
+ self.set_alias()
+
+ tmp_container.delete()
+
+ @task
+ def __delete__(self):
+ self.node.lxd_hypervisor.client.images.delete(self.name)
+
+ #---------------------------------------------------------------------------
+ # Public methods
+ #---------------------------------------------------------------------------
+
+ def set_alias(self):
+ alias_dict = {
+ "description": "Ubuntu 14.04 image with ICN software already installed",
+ "target": self.fingerprint,
+ "name": self.name
+ }
+ self.node.lxd_hypervisor.set_alias(alias_dict)
diff --git a/vicn/resource/lxd/lxd_hypervisor.py b/vicn/resource/lxd/lxd_hypervisor.py
new file mode 100644
index 00000000..328f3fdf
--- /dev/null
+++ b/vicn/resource/lxd/lxd_hypervisor.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#-------------------------------------------------------------------------------
+# NOTES
+#-------------------------------------------------------------------------------
+# - lxd >= 2.0.4 is required
+# daemon/container: Remember the return code in the non wait-for-websocket
+# case (Issue #2243)
+# - Reference: https://github.com/lxc/lxd/tree/master/doc
+#-------------------------------------------------------------------------------
+
+import logging
+import os
+from pylxd import Client
+from pylxd.exceptions import LXDAPIException
+
+from netmodel.model.type import String, Integer
+from vicn.core.attribute import Attribute, Multiplicity, Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, task
+from vicn.resource.linux.application import LinuxApplication as Application
+from vicn.resource.linux.service import Service
+from vicn.resource.linux.certificate import Certificate
+
+# Suppress non-important logging messages from requests and urllib3
+logging.getLogger("requests").setLevel(logging.WARNING)
+logging.getLogger("urllib3").setLevel(logging.WARNING)
+log = logging.getLogger(__name__)
+
+# FIXME use system-wide files
+DEFAULT_CERT_PATH = os.path.join(os.path.dirname(__file__),
+ '..', '..', '..', 'config', 'lxd_client_cert', 'client_cert.pem')
+DEFAULT_KEY_PATH = os.path.join(os.path.dirname(__file__),
+ '..', '..', '..', 'config', 'lxd_client_cert', 'client_key.pem')
+
+# FIXME hardcoded password for LXD server
+DEFAULT_TRUST_PASSWORD = 'vicn'
+
+DEFAULT_LXD_STORAGE = 100 # GB
+
+# Commands used to interact with the LXD hypervisor
+CMD_LXD_CHECK_INIT = 'lsof -i:{lxd.lxd_port}'
+
+CMD_LXD_INIT_BASE = 'lxd init --auto '
+CMD_LXD_INIT='''
+{base}
+lxc profile unset default environment.http_proxy
+lxc profile unset default user.network_mode
+'''
+
+#------------------------------------------------------------------------------
+# Subresources
+#------------------------------------------------------------------------------
+
+class LxdInit(Application):
+ __package_names__ = ['lxd', 'zfsutils-linux', 'lsof']
+
+ def __get__(self):
+ return BashTask(self.owner.node, CMD_LXD_CHECK_INIT,
+ {'lxd': self.owner})
+
+ def __create__(self):
+ cmd_params = {
+ 'storage-backend' : self.owner.storage_backend,
+ 'network-port' : self.owner.lxd_port,
+ 'network-address' : '0.0.0.0',
+ 'trust-password' : DEFAULT_TRUST_PASSWORD,
+ }
+
+ if self.owner.storage_backend == 'zfs':
+ cmd_params['storage-pool'] = self.owner.zfs_pool
+
+ # zpool list -H -o name,cap
+ # don't create it if it exists
+ zfs_pool_exists = True
+
+ if zfs_pool_exists:
+ cmd_params['storage-create-loop'] = self.owner.storage_size
+ elif self.owner.storage_backend == 'dir':
+ raise NotImplementedError
+ else:
+ raise NotImplementedError
+ cmd = CMD_LXD_INIT_BASE + ' '.join('--{}={}'.format(k, v)
+ for k, v in cmd_params.items())
+
+ # error: Failed to create the ZFS pool: The ZFS modules are not loaded.
+ # Try running '/sbin/modprobe zfs' as root to load them.
+ # zfs-dkms in the host
+ return BashTask(self.owner.node, CMD_LXD_INIT, {'base': cmd},
+ as_root = True)
+
+ def __delete__(self):
+ raise NotImplementedError
+
+class LxdInstallCert(Resource):
+ certificate = Attribute(Certificate, mandatory = True)
+
+ @task
+ def __get__(self):
+ try:
+ self.owner.client.certificates.all()
+ except LXDAPIException as e:
+ if e.response.raw.status == 403:
+ raise ResourceNotFound
+ raise
+ except Exception:
+ # Missing certificates raises an exception
+ raise ResourceNotFound
+
+
+ @task
+ def __create__(self):
+ """
+ Some operations with containers requires the client to be trusted by
+ the server. So at the beginning we have to upload a (self signed)
+ client certificate for the LXD daemon.
+ """
+ log.info('Adding certificate on LXD')
+ self.owner.client.authenticate(DEFAULT_TRUST_PASSWORD)
+ if not self.owner.client.trusted:
+ raise Exception
+
+#------------------------------------------------------------------------------
+
+class LxdHypervisor(Service):
+ """
+ Resource: LxdHypervisor
+
+ Manages a LXD hypervisor, accessible through a REST API.
+ """
+ __service_name__ = 'lxd'
+
+ lxd_port = Attribute(Integer, description = 'LXD REST API port',
+ default = 8443)
+ storage_backend = Attribute(String, description = 'Storage backend',
+ default = 'zfs',
+ choices = ['zfs'])
+ storage_size = Attribute(Integer, description = 'Storage size',
+ default = DEFAULT_LXD_STORAGE) # GB
+ zfs_pool = Attribute(String, description = 'ZFS pool',
+ default='vicn')
+
+ # Just overload attribute with a new reverse
+ node = Attribute(
+ reverse_name = 'lxd_hypervisor',
+ reverse_description = 'LXD hypervisor',
+ reverse_auto = True,
+ multiplicity = Multiplicity.OneToOne)
+
+ #--------------------------------------------------------------------------
+ # Constructor / Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._client = None
+ self._images = None
+
+ @property
+ def client(self):
+ if not self._client:
+ self._client = Client(endpoint = self._get_server_url(),
+ cert=(DEFAULT_CERT_PATH, DEFAULT_KEY_PATH),
+ verify=False)
+ return self._client
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ lxd_init = LxdInit(owner=self, node = self.node)
+ lxd_local_cert = Certificate(node = Reference(self, 'node'),
+ cert = DEFAULT_CERT_PATH,
+ key = DEFAULT_KEY_PATH,
+ owner = self)
+ lxd_cert_install = LxdInstallCert(node = Reference(self, 'node'),
+ certificate = lxd_local_cert,
+ owner = self)
+
+ return (lxd_init | lxd_local_cert) > lxd_cert_install
+
+ #--------------------------------------------------------------------------
+ # Private methods
+ #--------------------------------------------------------------------------
+
+ def _get_server_url(self):
+ return 'https://{0}:{1}'.format(self.node.hostname, self.lxd_port)
+
+ #--------------------------------------------------------------------------
+ # Public interface
+ #--------------------------------------------------------------------------
+
+ @property
+ def images(self):
+ """
+ This method caches available images to minimize the number of queries
+ done when creating multiple containers.
+ """
+ if not self._images:
+ self._images = self.node.lxd_hypervisor.client.images.all()
+ return self._images
+
+ @property
+ def aliases(self):
+ return [alias for image in self.images for alias in image.aliases]
diff --git a/vicn/resource/node.py b/vicn/resource/node.py
new file mode 100644
index 00000000..bfb2f9ec
--- /dev/null
+++ b/vicn/resource/node.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import os
+
+from netmodel.model.type import Double, String, Self
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute
+from vicn.core.resource import Resource
+
+log = logging.getLogger(__name__)
+
+DEFAULT_USERNAME = 'root'
+DEFAULT_SSH_PRIVATE_KEY = os.path.join(os.path.dirname(__file__),
+ '..', '..', 'config', 'ssh_client_cert', 'ssh_client_key')
+DEFAULT_SSH_PUBLIC_KEY = os.path.join(os.path.dirname(__file__),
+ '..', '..', 'config', 'ssh_client_cert', 'ssh_client_key.pub')
+
+class Node(Resource):
+ """
+ Resource: Node
+ """
+
+ x = Attribute(Double, description = 'X coordinate',
+ default = 0.0)
+ y = Attribute(Double, description = 'Y coordinate',
+ default = 0.0)
+ category = Attribute(String)
+ os = Attribute(String, description = 'OS',
+ default = 'ubuntu',
+ choices = ['debian', 'ubuntu'])
+ dist = Attribute(String, description = 'Distribution name',
+ default = 'xenial',
+ choices = ['trusty', 'xenial', 'sid'])
+ arch = Attribute(String, description = 'Architecture',
+ default = 'amd64',
+ choices = ['amd64'])
+ node_with_kernel = Attribute(Self,
+ description = 'Node on which the kernel sits',
+ ro = True)
+
+ #---------------------------------------------------------------------------
+ # Constructor and Accessors
+ #---------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._host_interface = None
+
+ #---------------------------------------------------------------------------
+ # Public API
+ #---------------------------------------------------------------------------
+
+ @property
+ def host_interface(self):
+ """
+ We assume that any unmanaged interface associated to the host is the
+ main host interface. It should thus be declared in the JSON topology.
+ We might later perform some kind of auto discovery.
+
+ This unmanaged interface is only required to get the device_name:
+ - to create Veth (need a parent)
+ - to ssh a node, get its ip address (eg for the repo)
+ - to avoid loops in type specification
+
+ It is used for all nodes to provide network connectivity.
+ """
+
+ for interface in self.interfaces:
+ if not interface.managed or interface.owner is not None:
+ return interface
+
+ raise Exception('Cannot find host interface for node {}: {}'.format(
+ self, self.interfaces))
+
+ def execute(self, command, output = False, as_root = False):
+ raise NotImplementedError
diff --git a/vicn/resource/ns3/__init__.py b/vicn/resource/ns3/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/ns3/__init__.py
diff --git a/vicn/resource/ns3/emulated_channel.py b/vicn/resource/ns3/emulated_channel.py
new file mode 100644
index 00000000..08d7a14b
--- /dev/null
+++ b/vicn/resource/ns3/emulated_channel.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import random
+
+from netmodel.model.type import Integer
+from netmodel.util.socket import check_port
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource import BaseResource
+from vicn.core.resource_mgr import wait_resources
+from vicn.core.task import inline_task, async_task, task
+from vicn.core.task import BashTask, run_task
+from vicn.resource.channel import Channel
+from vicn.resource.linux.application import LinuxApplication as Application
+from vicn.resource.linux.net_device import NetDevice
+from vicn.resource.linux.tap_device import TapDevice
+from vicn.resource.linux.veth_pair import VethPair
+from vicn.resource.lxd.lxc_container import LxcContainer
+from vicn.resource.node import Node
+
+log = logging.getLogger(__name__)
+
+class EmulatedChannel(Channel, Application):
+ """EmulatedChannel resource
+
+ This resources serves as a base class for wireless channels emulated by
+ means of ns3 simulation.
+
+ Attributes:
+ ap (Reference[node]): Reference to the AP node
+ stations (Reference[node]): Reference to the list of stations.
+ control_port (int): Port used to communicate with the management
+ interface of the simulation.
+
+ Implementation notes:
+ - Both AP and stations are allocated a separate VLAN to isolate broadcast
+ traffic and prevent loops on the bridge.
+ - We also need that all interfaces related to ap and stations are created
+ before we run the commandline (currently, dynamically adding/removing
+ AP and stations is not supported by the emulator). This is made
+ possible thanks to the key=True parameter, which makes sure the
+ attributes are processed before the __create__ is called.
+
+ Todo:
+ - Retrieve the process PID to kill it during __delete__
+ """
+
+ __resource_type__ = BaseResource
+
+ ap = Attribute(Node, description = 'AP', key = True)
+ stations = Attribute(Node, description = 'List of stations',
+ multiplicity = Multiplicity.OneToMany, key = True)
+ control_port = Attribute(Integer,
+ description = 'Control port for the simulation')
+
+ # Overloaded attributes
+ node = Attribute(requirements = [
+ Requirement('bridge')
+ ])
+
+ #--------------------------------------------------------------------------
+ # Constructor
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # AP (resp. stations) interfaces (for external connectivity) and
+ # tap_devices (for connection to emulator)
+ self._ap_if = None
+ self._ap_tap = None
+ self._sta_ifs = dict()
+ self._sta_taps = dict()
+
+ # Device names to be attached to the bridge (differs according to the
+ # node type, Physical or LxcContainer, and eventually None for an
+ # unmanaged stations)
+ self._ap_bridged = None
+ self._sta_bridged = dict()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ # NOTE: http://stackoverflow.com/questions/21141352/python-subprocess-
+ # calling-a-script-which-runs-a-background-process-hanging
+ # The output of the background scripts is still going to the same file
+ # descriptor as the child script, thus the parent script waits for it
+ # to finish.
+ cmd = '(' + self.__app_name__ + ' ' + self._get_cmdline_params() + \
+ '>/dev/null 2>&1) &'
+ return BashTask(self.node, cmd)
+
+ def __delete__(self):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Attribute handlers
+ #--------------------------------------------------------------------------
+
+ @async_task
+ async def _set_ap(self, ap=None):
+ if ap is None:
+ ap = self.ap
+ if ap is None:
+ log.info('Ignored setting ap to None...')
+ return
+
+ # Add a WiFi interface for the AP...
+ interfaces = list()
+ if isinstance(ap, LxcContainer):
+ # Ideally, We need to create a VethPair for each station
+ # This should be monitored for the total channel bw
+ host = NetDevice(node = ap.node,
+ device_name='vhh-' + ap.name + '-' + self.name,
+ monitored = False,
+ managed = False)
+ self._ap_if = VethPair(node = self.ap,
+ name = 'vh-' + ap.name + '-' + self.name,
+ device_name = 'vh-' + ap.name + '-' + self.name,
+ host = host,
+ owner = self)
+ self._ap_bridged = self._ap_if.host
+ else:
+ raise NotImplementedError
+ self._state.manager.commit_resource(self._ap_if)
+
+ interfaces.append(self._ap_if)
+
+ # Add a tap interface for the AP...
+ self._ap_tap = TapDevice(node = self.node,
+ owner = self,
+ device_name = 'tap-' + ap.name + '-' + self.name,
+ up = True,
+ promiscuous = True,
+ monitored = False)
+ self._state.manager.commit_resource(self._ap_tap)
+ interfaces.append(self._ap_tap)
+
+ # Wait for interfaces to be setup
+ await wait_resources(interfaces)
+
+ # NOTE: only set channel after the resource is created or it might
+ # create loops which, at this time, are not handled
+ self._ap_if.set('channel', self)
+
+ # Add interfaces to bridge
+ vlan = AddressManager().get('vlan', self, tag='ap')
+
+ # AS the container has created the VethPair already without Vlan, we
+ # need to delete and recreate it
+ task = self.node.bridge._remove_interface(self._ap_bridged)
+ await run_task(task, self._state.manager)
+ task = self.node.bridge._add_interface(self._ap_bridged, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(self._ap_tap, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ print('/!\ pass information to the running simulation')
+
+ @inline_task
+ def _get_ap(self):
+ return {'ap': None}
+
+ @inline_task
+ def _get_stations(self):
+ return {'stations': list()}
+
+ @async_task
+ async def _set_stations(self, stations=None):
+ print('adding stations...')
+ if stations is None:
+ stations = self.stations
+
+ for station in stations:
+ await self._add_station(station)
+
+ def _add_stations(self, stations):
+ raise NotImplementedError
+
+ @inline_task
+ def _remove_stations(self, station):
+ raise NotImplementedError
+
diff --git a/vicn/resource/ns3/emulated_lte_channel.py b/vicn/resource/ns3/emulated_lte_channel.py
new file mode 100644
index 00000000..8c7382cb
--- /dev/null
+++ b/vicn/resource/ns3/emulated_lte_channel.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.core.address_mgr import AddressManager
+from vicn.core.resource_mgr import wait_resources
+from vicn.core.task import run_task
+from vicn.resource.ns3.emulated_channel import EmulatedChannel
+from vicn.resource.linux.net_device import NetDevice
+
+DEFAULT_FADING_ENABLED = True
+DEFAULT_TW_BUFFER = 800000
+DEFAULT_NETMASK = 24
+
+class EmulatedLteChannel(EmulatedChannel):
+ """
+ Resource: EmulatedLteChannel
+
+ This resource uses ns3 based emulation to emulate a lte subnet with one pgw,
+ one enode B, and multiple UEs.
+
+ NOTE:
+ This model needs to be extended with LTE specific features like "network
+ resource" in the future. Currently it works in same way as wifi emulator
+ with no lte specific features.
+
+ Attributes:
+ ap (Reference[node]): Reference to the AP/pgw node
+ stations (Reference[node]): Reference to the list of UE.
+ control_port (int): Port used to communicate with the management
+ interface of the simulation.
+ """
+
+ __package_names__ = ['lte-emulator']
+ __app_name__ = 'lte_emulator'
+
+ #---------------------------------------------------------------------------
+ # Attribute handlers
+ #---------------------------------------------------------------------------
+
+ async def _add_station(self, station):
+ from vicn.resource.lxd.lxc_container import LxcContainer
+ from vicn.resource.linux.veth_pair import VethPair
+ from vicn.resource.linux.tap_device import TapChannel
+
+ interfaces = list()
+ # ... and each station
+ if not station.managed:
+ sta_if = None
+ else:
+ if isinstance(station, LxcContainer):
+ host = NetDevice(node = station.node,
+ device_name='vhh-' + station.name + '-' + self.name,
+ managed = False)
+ sta_if = VethPair(node = station,
+ name = 'vh-' + station.name + '-' + self.name,
+ device_name = 'vh-' + station.name + '-' + self.name,
+ host = host,
+ owner = self)
+ bridged_sta = sta_if.host
+ else:
+ raise NotImplementedError
+
+ if sta_if:
+ self._sta_ifs[station] = sta_if
+ self._sta_bridged[station] = bridged_sta
+ interfaces.append(sta_if)
+ self._state.manager.commit_resource(sta_if)
+
+ sta_tap = TapChannel(node = self.node,
+ owner = self,
+ device_name = 'tap-' + station.name + '-' + self.name,
+ up = True,
+ promiscuous = True,
+ station_name = station.name,
+ channel_name = self.name)
+ self._sta_taps[station] = sta_tap
+ interfaces.append(sta_tap)
+ self._state.manager.commit_resource(sta_tap)
+
+ # Wait for interfaces to be setup
+ await wait_resources(interfaces)
+
+ # Add interfaces to bridge
+ # One vlan per station is needed to avoid broadcast loops
+ vlan = AddressManager().get('vlan', sta_tap)
+ if sta_if:
+ sta_if.set('channel', self)
+
+ task = self.node.bridge._remove_interface(bridged_sta)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(bridged_sta,
+ vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(sta_tap, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ def _get_cmdline_params(self):
+
+ # IP have not been assign, use AddressManager for simplicity since it
+ # will remember the assignment
+ # NOTE: here the IP address passed to emulator program is hardcoded with
+ # a /24 mask(even if the associated IP with the station does not have a
+ # /24 mask). This is not a problem at all because the netmask passed to
+ # the emulator program has no impact on configuration in the emulator
+ # program. Indeed, the IP routing table in the emulator program are
+ # configured on a per address basis(one route per IP address) instead of
+ # on a per prefix basis(one route per prefix). This guarantees the IP
+ # routing will not change regardless of what netmask is. That is why we
+ # can always safely pass a hardcoded /24 mask to the emulator program.
+
+ sta_list = list() # list of identifiers
+ sta_macs = list() # list of macs
+ sta_taps = list()
+ sta_ips = list()
+ for station in self.stations:
+ if not station.managed:
+ interface = [i for i in station.interfaces if i.channel == self]
+ assert len(interface) == 1
+ interface = interface[0]
+
+ sta_list.append(interface.name)
+ sta_macs.append(interface.mac_address)
+ sta_ips.append(interface.ip_address + '/24')
+ else:
+ identifier = self._sta_ifs[station]._state.uuid._uuid
+ sta_list.append(identifier)
+
+ mac = self._sta_ifs[station].mac_address
+ sta_macs.append(mac)
+
+ # Preallocate IP address
+ ip = AddressManager().get_ip(self._sta_ifs[station]) + '/24'
+ sta_ips.append(ip)
+
+ tap = self._sta_taps[station].device_name
+ sta_taps.append(tap)
+
+ params = {
+ # Name of the tap between NS3 and the base station
+ 'bs-tap' : self._ap_tap.device_name,
+ # Number of stations
+ 'n-sta' : len(self._sta_taps),
+ # List of the stations of the simulation
+ 'sta-list' : ','.join(sta_list),
+ # List of the taps between NS3 and the mobile stations
+ 'sta-taps' : ','.join(sta_taps),
+ # List of the macs of the mobile stations
+ 'sta-macs' : ','.join(sta_macs),
+ # X position of the Base Station
+ 'bs-x' : 0, #self.ap.x,
+ # Y position of the Base Station
+ 'bs-y' : 0, #self.ap.y,
+ # Experiment ID
+ 'experiment-id' : 'vicn',
+ # Index of the base station
+ 'bs-name' : self._ap_tap.device_name,
+ # Base station IP address
+ 'bs-mac' : self._ap_if.mac_address,
+ # Control port for dynamically managing the stations movement
+ 'control-port' : self.control_port,
+ # Coma-separated list of stations' IP/netmask len
+ 'sta-ips' : ','.join(sta_ips),
+ # Base station IP/netmask len
+ 'bs-ip' : AddressManager().get_ip(self._ap_if) +
+ DEFAULT_NETMASK,
+ 'txBuffer' : '800000',
+ 'isFading' : 'true' if DEFAULT_FADING_ENABLED else 'false',
+ }
+
+ return ' '.join(['--{}={}'.format(k, v) for k, v in params.items()])
+
diff --git a/vicn/resource/ns3/emulated_wifi_channel.py b/vicn/resource/ns3/emulated_wifi_channel.py
new file mode 100644
index 00000000..088d4444
--- /dev/null
+++ b/vicn/resource/ns3/emulated_wifi_channel.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.resource.ns3.emulated_channel import EmulatedChannel
+from vicn.resource.linux.net_device import NetDevice
+from vicn.core.address_mgr import AddressManager
+from vicn.core.resource_mgr import wait_resources
+from vicn.core.task import async_task, run_task
+
+class EmulatedWiFiChannel(EmulatedChannel):
+ """EmulatedWiFiChannel Resource
+
+ This resource uses the ns3 simulator to emulate an 80211n wireless channel.
+ It assume no interference between base stations, and thus the emulated
+ channel corresponds to a single base station.
+ """
+ __package_names__ = ['wifi-emulator']
+ __app_name__ = 'wifi_emulator'
+
+ #---------------------------------------------------------------------------
+ # Attribute handlers
+ #---------------------------------------------------------------------------
+
+ async def _add_station(self, station):
+ from vicn.resource.lxd.lxc_container import LxcContainer
+ from vicn.resource.linux.veth_pair import VethPair
+ from vicn.resource.linux.tap_device import TapChannel
+ from vicn.resource.linux.macvlan import MacVlan
+
+ interfaces = list()
+ if not station.managed:
+ sta_if = None
+ else:
+ if isinstance(station, LxcContainer):
+ host = NetDevice(node = station.node,
+ device_name='vhh-' + station.name + '-' + self.name,
+ managed = False)
+ sta_if = VethPair(node = station,
+ name = 'vh-' + station.name + '-' + self.name,
+ device_name = 'vh-' + station.name + '-' + self.name,
+ host = host,
+ owner = self)
+ bridged_sta = sta_if.host
+ else:
+ raise NotImplementedError
+
+ if sta_if:
+ self._sta_ifs[station] = sta_if
+ self._sta_bridged[station] = bridged_sta
+ interfaces.append(sta_if)
+ self._state.manager.commit_resource(sta_if)
+
+ sta_tap = TapChannel(node = self.node,
+ owner = self,
+ device_name = 'tap-' + station.name + '-' + self.name,
+ up = True,
+ promiscuous = True,
+ station_name = station.name,
+ channel_name = self.name)
+ self._sta_taps[station] = sta_tap
+ interfaces.append(sta_tap)
+ self._state.manager.commit_resource(sta_tap)
+
+ # Wait for interfaces to be setup
+ await wait_resources(interfaces)
+
+ # Add interfaces to bridge
+ # One vlan per station is needed to avoid broadcast loops
+ vlan = AddressManager().get('vlan', sta_tap)
+ # sta_tap choosen because always there
+ if sta_if:
+ sta_if.set('channel', self)
+
+ task = self.node.bridge._remove_interface(bridged_sta)
+ await run_task(task, self._state.manager)
+ task = self.node.bridge._add_interface(bridged_sta,
+ vlan = vlan)
+ await run_task(task, self._state.manager)
+
+ task = self.node.bridge._add_interface(sta_tap, vlan = vlan)
+ await run_task(task, self._state.manager)
+
+
+ def _get_cmdline_params(self, ):
+
+ # sta-macs and sta-list for unmanaged stations
+ sta_list = list() # list of identifiers
+ sta_macs = list() # list of macs
+ sta_taps = list()
+ for station in self.stations:
+ if not station.managed:
+ interface = [i for i in station.interfaces if i.channel == self]
+ assert len(interface) == 1
+ interface = interface[0]
+
+ sta_list.append(interface.name)
+ sta_macs.append(interface.mac_address)
+ else:
+ identifier = self._sta_ifs[station]._state.uuid._uuid
+ sta_list.append(identifier)
+
+ mac = self._sta_ifs[station].mac_address
+ sta_macs.append(mac)
+
+ tap = self._sta_taps[station].device_name
+ sta_taps.append(tap)
+
+ params = {
+ # Name of the tap between NS3 and the base station
+ 'bs-tap' : self._ap_tap.device_name,
+ # Number of stations
+ 'n-sta' : len(self._sta_taps),
+ # List of the stations of the simulation # identifiers
+ 'sta-list' : ','.join(sta_list),
+ # List of the taps between NS3 and the mobile stations
+ 'sta-taps' : ','.join(sta_taps),
+ # List of the macs of the mobile stations
+ 'sta-macs' : ','.join(sta_macs),
+ # X position of the Base Station
+ 'bs-x' : 0, #self.ap.x,
+ # Y position of the Base Station
+ 'bs-y' : 0, #self.ap.y,
+ # Experiment ID
+ 'experiment-id' : 'vicn',
+ # Index of the base station
+ 'bs-name' : self._ap_tap.device_name,
+ # Base station MAC address
+ 'bs-mac' : self._ap_if.mac_address,
+ # Control port for dynamically managing the stations movement
+ 'control-port' : self.control_port,
+ }
+
+ return ' '.join(['--{}={}'.format(k, v) for k, v in params.items()])
diff --git a/vicn/resource/script.py b/vicn/resource/script.py
new file mode 100644
index 00000000..30196b21
--- /dev/null
+++ b/vicn/resource/script.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from vicn.helpers.resource_definition import *
+
+class Script(Resource):
+ """
+ Resource: Script
+
+ This resource is empty on purpose. It is a temporary resource used as a
+ placeholder for controlling the tool from GUI and should be deprecated in
+ future releases.
+ """
+
+ id = Attribute(String)
+ name = Attribute(String)
diff --git a/vicn/resource/vpp/__init__.py b/vicn/resource/vpp/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/vicn/resource/vpp/__init__.py
diff --git a/vicn/resource/vpp/cicn.py b/vicn/resource/vpp/cicn.py
new file mode 100644
index 00000000..be523a6c
--- /dev/null
+++ b/vicn/resource/vpp/cicn.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import re
+
+from netmodel.model.type import Integer, Bool
+from vicn.core.attribute import Attribute
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import async_task, task, BashTask, EmptyTask
+from vicn.resource.icn.forwarder import Forwarder
+from vicn.resource.node import Node
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ENABLE_PLUGIN
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_GET
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ICN_FACE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ICN_ROUTE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_GET_CACHE_SIZE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_SET_CACHE_SIZE
+
+_ADD_FACE_RETURN_FORMAT = "Face ID: [0-9]+"
+
+def check_face_id_return_format(data):
+ prog = re.compile(_ADD_FACE_RETURN_FORMAT)
+ return prog.match(data)
+
+def parse_face_id(data):
+ return data.partition(':')[2]
+
+class CICNForwarder(Forwarder):
+ """
+ NOTE: Based on the Vagrantfile, we recommend a node with mem=4096, cpu=4
+ """
+
+ node = Attribute(Node,
+ mandatory=True,
+ requirements = [Requirement('vpp')],
+ reverse_name='cicn')
+ numa_node = Attribute(Integer,
+ description = 'Numa node on which vpp will run',
+ default = None)
+ core = Attribute(Integer,
+ description = 'Core belonging the numa node on which vpp will run',
+ default = None)
+ enable_worker = Attribute(Bool,
+ description = 'Enable one worker for packet processing',
+ default = False)
+
+ #__packages__ = ['vpp-plugin-cicn']
+
+ def __after__(self):
+ return ['CentralICN']
+
+ def __get__(self):
+ def parse(rv):
+ if rv.return_value > 0 or 'cicn: not enabled' in rv.stdout:
+ raise ResourceNotFound
+ return BashTask(self.node, CMD_VPP_CICN_GET,
+ lock = self.node.vpp.vppctl_lock, parse=parse)
+
+ def __create__(self):
+
+ #self.node.vpp.plugins.append("cicn")
+ lock = self.node.vpp.vppctl_lock
+ create_task = BashTask(self.node, CMD_VPP_ENABLE_PLUGIN,
+ {'plugin' : 'cicn'}, lock = lock)
+
+ face_task = EmptyTask()
+ route_task = EmptyTask()
+
+ def parse_face(rv, face):
+ if check_face_id_return_format(rv.stdout):
+ face.id = parse_face_id(rv.stdout)
+ return {}
+
+ for face in self.faces:
+ face_task = face_task > BashTask(self.node, CMD_VPP_ADD_ICN_FACE,
+ {'face':face},
+ parse = (lambda x : parse_face(x, face)), lock = lock)
+
+ if not self.routes:
+ from vicn.resource.icn.route import Route
+ for route in self._state.manager.by_type(Route):
+ if route.node is self.node:
+ self.routes.append(route)
+ for route in self.routes:
+ route_task = route_task > BashTask(self.node,
+ CMD_VPP_ADD_ICN_ROUTE, {'route' : route}, lock = lock)
+
+ return (wait_resource_task(self.node.vpp) > create_task) > (face_task > route_task)
+
+ # Nothing to do
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ # Force local update
+
+ _add_faces = None
+ _remove_faces = None
+ _get_faces = None
+ _set_faces = None
+
+ _add_routes = None
+ _remove_routes = None
+ _get_routes = None
+ _set_routes = None
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _set_cache_size(self):
+ return BashTask(self.node, CMD_VPP_CICN_SET_CACHE_SIZE, {'self': self},
+ lock = self.node.vpp.vppctl_lock)
+
+ def _get_cache_size(self):
+ def parse(rv):
+ return int(rv.stdout)
+ return BashTask(self.node, CMD_VPP_CICN_GET_CACHE_SIZE, parse=parse,
+ lock = self.node.vpp.vppctl_lock)
diff --git a/vicn/resource/vpp/dpdk_device.py b/vicn/resource/vpp/dpdk_device.py
new file mode 100644
index 00000000..69449e48
--- /dev/null
+++ b/vicn/resource/vpp/dpdk_device.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String
+from vicn.core.attribute import Attribute
+from vicn.resource.linux.phy_interface import PhyInterface
+
+class DpdkDevice(PhyInterface):
+ """
+ Resource: DpdkDevice
+
+ A DpdkDevice is a physical net device supported by Dpdk and with parameters
+ specific to VPP.
+ """
+ numa_node = Attribute(Integer,
+ description = 'NUMA node on the same PCI bus as the DPDK card')
+ socket_mem = Attribute(Integer,
+ description = 'Memory used by the vpp forwarder',
+ default = 512)
+ mac_address = Attribute(String)
diff --git a/vicn/resource/vpp/interface.py b/vicn/resource/vpp/interface.py
new file mode 100644
index 00000000..efe4fe5a
--- /dev/null
+++ b/vicn/resource/vpp/interface.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer, String, Bool
+from vicn.core.resource import Resource
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.task import inline_task, BashTask, task
+from vicn.core.task import EmptyTask
+from vicn.resource.interface import Interface
+from vicn.resource.linux.net_device import NonTapBaseNetDevice
+from vicn.resource.vpp.vpp import VPP
+from vicn.resource.vpp.vpp_commands import CMD_VPP_CREATE_IFACE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_SET_IP, CMD_VPP_SET_UP
+
+class VPPInterface(Resource):
+ """
+ Resource: VPPInterface
+
+ An interface representation in VPP
+ """
+
+ vpp = Attribute(VPP,
+ description = 'Forwarder to which this interface belong to',
+ mandatory = True,
+ multiplicity = Multiplicity.ManyToOne,
+ key = True,
+ reverse_name = 'interfaces')
+ parent = Attribute(Interface, description = 'parent',
+ mandatory = True, reverse_name = 'vppinterface')
+ ip_address = Attribute(String)
+ prefix_len = Attribute(Integer, default = 31)
+ up = Attribute(Bool, description = 'Interface up/down status')
+ monitored = Attribute(Bool, default = True)
+
+ device_name = Attribute(String)
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ """
+ We need CentralIP to get the parent interface IP address
+ """
+ return ['CentralIP']
+
+ @inline_task
+ def __get__(self):
+ raise ResourceNotFound
+
+ def __create__(self):
+ # We must control what is the type of the parent netDevice (currently
+ # supported only veths, physical nics are coming)
+ create_task = EmptyTask()
+
+ # We must let the routing algorithm know that the parent interface
+ # belongs to vpp
+ self.parent.has_vpp_child = True
+
+ self.ip_address = self.parent.ip_address
+ self.up = True
+
+ if isinstance(self.parent,NonTapBaseNetDevice):
+ # Remove ip address in the parent device, it must only be set in
+ # the vpp interface otherwise vpp and the linux kernel will reply
+ # to non-icn request (e.g., ARP replies, port ureachable etc)
+
+ self.device_name = 'host-' + self.parent.device_name
+ create_task = BashTask(self.vpp.node, CMD_VPP_CREATE_IFACE,
+ {'vpp_interface': self},
+ lock = self.vpp.vppctl_lock)
+
+ self.parent.set('ip_address', None)
+ self.parent.set('offload', False)
+ self.parent.remote.set('offload', False)
+
+ elif self.parent.get_type() == 'dpdkdevice':
+ self.device_name = self.parent.device_name
+ else :
+ # Currently assume naively that everything else will be a physical
+ # NIC for VPP
+ #
+ # Before initialization, we need to make sure that the parent
+ # interface is down (vpp will control the nic)
+ self.device_name = 'host-' + self.parent.device_name
+ self.parent.set('up', False)
+
+ return create_task
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _set_ip_address(self):
+ if self.ip_address:
+ return BashTask(self.vpp.node, CMD_VPP_SET_IP, {'netdevice': self},
+ lock = self.vpp.vppctl_lock)
+
+ def _set_up(self):
+ return BashTask(self.vpp.node, CMD_VPP_SET_UP, {'netdevice': self},
+ lock = self.vpp.vppctl_lock)
+
+ @task
+ def _get_up(self):
+ return {'up' : False}
+
+ @task
+ def _get_ip_address(self):
+ return {'ip_address' : None}
diff --git a/vicn/resource/vpp/scripts.py b/vicn/resource/vpp/scripts.py
new file mode 100644
index 00000000..3a3d5e8f
--- /dev/null
+++ b/vicn/resource/vpp/scripts.py
@@ -0,0 +1,287 @@
+FN_APPARMOR_DPDK_SCRIPT='/etc/apparmor.d/lxc/lxc-dpdk'
+
+TPL_APPARMOR_DPDK_SCRIPT='''
+profile lxc-dpdk flags=(attach_disconnected,mediate_deleted) {
+ ### Base profile
+ capability,
+ dbus,
+ file,
+ network,
+ umount,
+
+ # Allow us to receive signals from anywhere.
+ signal (receive),
+
+ # Allow us to send signals to ourselves
+ signal peer=@{profile_name},
+
+ # Allow other processes to read our /proc entries, futexes, perf tracing and
+ # kcmp for now (they will need 'read' in the first place). Administrators can
+ # override with:
+ # deny ptrace (readby) ...
+ ptrace (readby),
+
+ # Allow other processes to trace us by default (they will need 'trace' in
+ # the first place). Administrators can override with:
+ # deny ptrace (tracedby) ...
+ ptrace (tracedby),
+
+ # Allow us to ptrace ourselves
+ ptrace peer=@{profile_name},
+
+ # ignore DENIED message on / remount
+ deny mount options=(ro, remount) -> /,
+ deny mount options=(ro, remount, silent) -> /,
+
+ # allow tmpfs mounts everywhere
+ mount fstype=tmpfs,
+
+ # allow hugetlbfs mounts everywhere
+ mount fstype=hugetlbfs,
+
+ # allow mqueue mounts everywhere
+ mount fstype=mqueue,
+
+ # allow fuse mounts everywhere
+ mount fstype=fuse,
+ mount fstype=fuse.*,
+
+ # deny access under /proc/bus to avoid e.g. messing with pci devices directly
+ deny @{PROC}/bus/** wklx,
+
+ # deny writes in /proc/sys/fs but allow binfmt_misc to be mounted
+ mount fstype=binfmt_misc -> /proc/sys/fs/binfmt_misc/,
+ deny @{PROC}/sys/fs/** wklx,
+
+ # allow efivars to be mounted, writing to it will be blocked though
+ mount fstype=efivarfs -> /sys/firmware/efi/efivars/,
+
+ # block some other dangerous paths
+ deny @{PROC}/kcore rwklx,
+ deny @{PROC}/kmem rwklx,
+ deny @{PROC}/mem rwklx,
+ deny @{PROC}/sysrq-trigger rwklx,
+
+ # deny writes in /sys except for /sys/fs/cgroup, also allow
+ # fusectl, securityfs and debugfs to be mounted there (read-only)
+ mount fstype=fusectl -> /sys/fs/fuse/connections/,
+ mount fstype=securityfs -> /sys/kernel/security/,
+ mount fstype=debugfs -> /sys/kernel/debug/,
+ deny mount fstype=debugfs -> /var/lib/ureadahead/debugfs/,
+ mount fstype=proc -> /proc/,
+ mount fstype=sysfs -> /sys/,
+ mount options=(rw, nosuid, nodev, noexec, remount) -> /sys/,
+ deny /sys/firmware/efi/efivars/** rwklx,
+ # note, /sys/kernel/security/** handled below
+ mount options=(move) /sys/fs/cgroup/cgmanager/ -> /sys/fs/cgroup/cgmanager.lower/,
+ mount options=(ro, nosuid, nodev, noexec, remount, strictatime) -> /sys/fs/cgroup/,
+
+ mount options=(ro, nosuid, nodev, noexec, remount, strictatime) -> /sys,
+
+ # deny reads from debugfs
+ deny /sys/kernel/debug/{,**} rwklx,
+
+ # allow paths to be made slave, shared, private or unbindable
+ # FIXME: This currently doesn't work due to the apparmor parser treating those as allowing all mounts.
+# mount options=(rw,make-slave) -> **,
+# mount options=(rw,make-rslave) -> **,
+# mount options=(rw,make-shared) -> **,
+# mount options=(rw,make-rshared) -> **,
+# mount options=(rw,make-private) -> **,
+# mount options=(rw,make-rprivate) -> **,
+# mount options=(rw,make-unbindable) -> **,
+# mount options=(rw,make-runbindable) -> **,
+
+ # allow bind-mounts of anything except /proc, /sys and /dev
+ mount options=(rw,bind) /[^spd]*{,/**},
+ mount options=(rw,bind) /d[^e]*{,/**},
+ mount options=(rw,bind) /de[^v]*{,/**},
+ mount options=(rw,bind) /dev/.[^l]*{,/**},
+ mount options=(rw,bind) /dev/.l[^x]*{,/**},
+ mount options=(rw,bind) /dev/.lx[^c]*{,/**},
+ mount options=(rw,bind) /dev/.lxc?*{,/**},
+ mount options=(rw,bind) /dev/[^.]*{,/**},
+ mount options=(rw,bind) /dev?*{,/**},
+ mount options=(rw,bind) /p[^r]*{,/**},
+ mount options=(rw,bind) /pr[^o]*{,/**},
+ mount options=(rw,bind) /pro[^c]*{,/**},
+ mount options=(rw,bind) /proc?*{,/**},
+ mount options=(rw,bind) /s[^y]*{,/**},
+ mount options=(rw,bind) /sy[^s]*{,/**},
+ mount options=(rw,bind) /sys?*{,/**},
+
+ # allow moving mounts except for /proc, /sys and /dev
+ mount options=(rw,move) /[^spd]*{,/**},
+ mount options=(rw,move) /d[^e]*{,/**},
+ mount options=(rw,move) /de[^v]*{,/**},
+ mount options=(rw,move) /dev/.[^l]*{,/**},
+ mount options=(rw,move) /dev/.l[^x]*{,/**},
+ mount options=(rw,move) /dev/.lx[^c]*{,/**},
+ mount options=(rw,move) /dev/.lxc?*{,/**},
+ mount options=(rw,move) /dev/[^.]*{,/**},
+ mount options=(rw,move) /dev?*{,/**},
+ mount options=(rw,move) /p[^r]*{,/**},
+ mount options=(rw,move) /pr[^o]*{,/**},
+ mount options=(rw,move) /pro[^c]*{,/**},
+ mount options=(rw,move) /proc?*{,/**},
+ mount options=(rw,move) /s[^y]*{,/**},
+ mount options=(rw,move) /sy[^s]*{,/**},
+ mount options=(rw,move) /sys?*{,/**},
+
+ # generated by: lxc-generate-aa-rules.py container-rules.base
+ deny /proc/sys/[^kn]*{,/**} wklx,
+ deny /proc/sys/k[^e]*{,/**} wklx,
+ deny /proc/sys/ke[^r]*{,/**} wklx,
+ deny /proc/sys/ker[^n]*{,/**} wklx,
+ deny /proc/sys/kern[^e]*{,/**} wklx,
+ deny /proc/sys/kerne[^l]*{,/**} wklx,
+ deny /proc/sys/kernel/[^smhd]*{,/**} wklx,
+ deny /proc/sys/kernel/d[^o]*{,/**} wklx,
+ deny /proc/sys/kernel/do[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/dom[^a]*{,/**} wklx,
+ deny /proc/sys/kernel/doma[^i]*{,/**} wklx,
+ deny /proc/sys/kernel/domai[^n]*{,/**} wklx,
+ deny /proc/sys/kernel/domain[^n]*{,/**} wklx,
+ deny /proc/sys/kernel/domainn[^a]*{,/**} wklx,
+ deny /proc/sys/kernel/domainna[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/domainnam[^e]*{,/**} wklx,
+ deny /proc/sys/kernel/domainname?*{,/**} wklx,
+ deny /proc/sys/kernel/h[^o]*{,/**} wklx,
+ deny /proc/sys/kernel/ho[^s]*{,/**} wklx,
+ deny /proc/sys/kernel/hos[^t]*{,/**} wklx,
+ deny /proc/sys/kernel/host[^n]*{,/**} wklx,
+ deny /proc/sys/kernel/hostn[^a]*{,/**} wklx,
+ deny /proc/sys/kernel/hostna[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/hostnam[^e]*{,/**} wklx,
+ deny /proc/sys/kernel/hostname?*{,/**} wklx,
+ deny /proc/sys/kernel/m[^s]*{,/**} wklx,
+ deny /proc/sys/kernel/ms[^g]*{,/**} wklx,
+ deny /proc/sys/kernel/msg*/** wklx,
+ deny /proc/sys/kernel/s[^he]*{,/**} wklx,
+ deny /proc/sys/kernel/se[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/sem*/** wklx,
+ deny /proc/sys/kernel/sh[^m]*{,/**} wklx,
+ deny /proc/sys/kernel/shm*/** wklx,
+ deny /proc/sys/kernel?*{,/**} wklx,
+ deny /proc/sys/n[^e]*{,/**} wklx,
+ deny /proc/sys/ne[^t]*{,/**} wklx,
+ deny /proc/sys/net?*{,/**} wklx,
+ deny /sys/[^fdck]*{,/**} wklx,
+ deny /sys/c[^l]*{,/**} wklx,
+ deny /sys/cl[^a]*{,/**} wklx,
+ deny /sys/cla[^s]*{,/**} wklx,
+ deny /sys/clas[^s]*{,/**} wklx,
+ deny /sys/class/[^nu]*{,/**} wklx,
+ deny /sys/class/n[^e]*{,/**} wklx,
+ deny /sys/class/ne[^t]*{,/**} wklx,
+ deny /sys/class/net?*{,/**} wklx,
+ deny /sys/class/u[^i]*{,/**} wklx,
+ deny /sys/class/ui[^o]*{,/**} wklx,
+ deny /sys/class?*{,/**} wklx,
+ deny /sys/d[^e]*{,/**} wklx,
+ deny /sys/de[^v]*{,/**} wklx,
+ deny /sys/dev[^i]*{,/**} wklx,
+ deny /sys/devi[^c]*{,/**} wklx,
+ deny /sys/devic[^e]*{,/**} wklx,
+ deny /sys/device[^s]*{,/**} wklx,
+# deny /sys/devices/[^vu]*{,/**} wklx,
+# deny /sys/devices/v[^i]*{,/**} wklx,
+# deny /sys/devices/vi[^r]*{,/**} wklx,
+# deny /sys/devices/vir[^t]*{,/**} wklx,
+# deny /sys/devices/virt[^u]*{,/**} wklx,
+# deny /sys/devices/virtu[^a]*{,/**} wklx,
+# deny /sys/devices/virtua[^l]*{,/**} wklx,
+# deny /sys/devices/virtual/[^n]*{,/**} wklx,
+# deny /sys/devices/virtual/n[^e]*{,/**} wklx,
+# deny /sys/devices/virtual/ne[^t]*{,/**} wklx,
+# deny /sys/devices/virtual/net?*{,/**} wklx,
+# deny /sys/devices/virtual?*{,/**} wklx,
+# deny /sys/devices?*{,/**} wklx,
+ deny /sys/f[^s]*{,/**} wklx,
+ deny /sys/fs/[^c]*{,/**} wklx,
+ deny /sys/fs/c[^g]*{,/**} wklx,
+ deny /sys/fs/cg[^r]*{,/**} wklx,
+ deny /sys/fs/cgr[^o]*{,/**} wklx,
+ deny /sys/fs/cgro[^u]*{,/**} wklx,
+ deny /sys/fs/cgrou[^p]*{,/**} wklx,
+ deny /sys/fs/cgroup?*{,/**} wklx,
+ deny /sys/fs?*{,/**} wklx,
+
+ ### Feature: unix
+ # Allow receive via unix sockets from anywhere
+ unix (receive),
+
+ # Allow all unix in the container
+ unix peer=(label=@{profile_name}),
+
+ ### Feature: cgroup namespace
+ mount fstype=cgroup -> /sys/fs/cgroup/**,
+
+ ### Feature: apparmor stacking
+
+ ### Configuration: apparmor loading disabled in privileged containers
+ deny /sys/k[^e]*{,/**} rwklx,
+ deny /sys/ke[^r]*{,/**} rwklx,
+ deny /sys/ker[^n]*{,/**} rwklx,
+ deny /sys/kern[^e]*{,/**} rwklx,
+ deny /sys/kerne[^l]*{,/**} rwklx,
+ deny /sys/kernel/[^sm]*{,/**} rwklx,
+ deny /sys/kernel/s[^e]*{,/**} rwklx,
+ deny /sys/kernel/se[^c]*{,/**} rwklx,
+ deny /sys/kernel/sec[^u]*{,/**} rwklx,
+ deny /sys/kernel/secu[^r]*{,/**} rwklx,
+ deny /sys/kernel/secur[^i]*{,/**} rwklx,
+ deny /sys/kernel/securi[^t]*{,/**} rwklx,
+ deny /sys/kernel/securit[^y]*{,/**} rwklx,
+ deny /sys/kernel/security/[^a]*{,/**} rwklx,
+ deny /sys/kernel/security/a[^p]*{,/**} rwklx,
+ deny /sys/kernel/security/ap[^p]*{,/**} rwklx,
+ deny /sys/kernel/security/app[^a]*{,/**} rwklx,
+ deny /sys/kernel/security/appa[^r]*{,/**} rwklx,
+ deny /sys/kernel/security/appar[^m]*{,/**} rwklx,
+ deny /sys/kernel/security/apparm[^o]*{,/**} rwklx,
+ deny /sys/kernel/security/apparmo[^r]*{,/**} rwklx,
+ deny /sys/kernel/security/apparmor?*{,/**} rwklx,
+ deny /sys/kernel/security?*{,/**} rwklx,
+ deny /sys/kernel?*{,/**} rwklx,
+}'''
+
+FN_VPP_DPDK_SCRIPT='/etc/vpp/startup.conf'
+
+TPL_VPP_DPDK_DAEMON_SCRIPT='''
+unix {
+ nodaemon
+ log /tmp/vpp.log
+ full-coredump
+}
+
+api-trace {
+ on
+}
+
+api-segment {
+ gid vpp
+}
+
+'''
+
+TPL_VPP_DPDK_SCRIPT='''
+unix {
+ log /tmp/vpp.log
+ full-coredump
+}
+
+api-trace {
+ on
+}
+
+api-segment {
+ gid vpp
+}
+
+'''
+
+APPARMOR_VPP_PROFILE = '''
+lxc.aa_profile = lxc-dpdk
+lxc.mount.entry = hugetlbfs dev/hugepages hugetlbfs rw,relatime,create=dir 0 0
+lxc.mount.auto = sys:rw'''
diff --git a/vicn/resource/vpp/vpp.py b/vicn/resource/vpp/vpp.py
new file mode 100644
index 00000000..f9d10703
--- /dev/null
+++ b/vicn/resource/vpp/vpp.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+
+from netmodel.model.type import String, Integer, Bool
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.resource import Resource
+from vicn.core.task import BashTask, task, inline_task
+from vicn.resource.lxd.lxc_container import LxcContainer
+from vicn.resource.node import Node
+from vicn.resource.linux.file import TextFile
+from vicn.resource.vpp.dpdk_device import DpdkDevice
+from vicn.resource.vpp.scripts import FN_VPP_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import TPL_VPP_DPDK_DAEMON_SCRIPT
+from vicn.resource.vpp.vpp_commands import CMD_VPP_DISABLE, CMD_VPP_STOP
+from vicn.resource.vpp.vpp_commands import CMD_VPP_START
+from vicn.resource.vpp.vpp_commands import CMD_VPP_ENABLE_PLUGIN
+
+#------------------------------------------------------------------------------
+# VPP forwarder
+#------------------------------------------------------------------------------
+
+CMD_GET = 'killall -0 vpp_main'
+CMD_DISABLE_IP_FORWARD = 'sysctl -w net.ipv4.ip_forward=0'
+
+class VPP(Resource):
+ """
+ Todo:
+ - make VPP an application with package install
+ - vpp should be a service (hence a singleton) for which we override the
+ start and stop commands
+ """
+
+ #__package_names__ = ['vpp', 'vpp-dbg', 'vpp-dpdk-dev']
+
+ plugins = Attribute(String,
+ multiplicity = Multiplicity.OneToMany)
+ node = Attribute(Node,
+ multiplicity = Multiplicity.OneToOne,
+ reverse_name = 'vpp')
+ numa_node = Attribute(Integer,
+ description = 'Numa node on which vpp will run')
+ core = Attribute(Integer,
+ description = 'Core belonging the numa node on which vpp will run')
+ enable_worker = Attribute(Bool,
+ description = 'Enable one worker for packet processing',
+ default = False)
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.vppctl_lock = asyncio.Lock()
+
+ self.dpdk_setup_file = None
+ if isinstance(self.node, LxcContainer):
+ if not 'vpp' in self.node.profiles:
+ self.node.profiles.append('vpp')
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __after__(self):
+ return ['BaseNetDevice']
+
+ def __get__(self):
+ return BashTask(self.node, CMD_GET)
+
+ def __subresources__(self):
+ self.dpdk_setup_file = TextFile(node = self.node,
+ filename = FN_VPP_DPDK_SCRIPT,
+ overwrite = True)
+ return self.dpdk_setup_file
+
+ def __create__(self):
+ socket_mem = dict()
+ numa_mgr = self.node.node_with_kernel.numa_mgr
+
+ for interface in self.node.interfaces:
+ if isinstance(interface, DpdkDevice):
+ # Assign as numa node the first numa node specified in a
+ # physical card (if any). If multiple nics connected to
+ # different numa nodes are assigned to this vpp memory access
+ # will be inefficient for the nics sitting in the other numa
+ # node.
+ socket_mem[interface.numa_node] = interface.socket_mem
+
+ for iface in self.interfaces:
+ if isinstance(iface.parent, DpdkDevice) and \
+ not iface.parent.numa_node is None:
+ self.numa_node = iface.parent.numa_node
+ break
+ if self.numa_node is None or self.core is None:
+ self.numa_node, self.core = \
+ numa_mgr.get_numa_core(numa_node = self.numa_node)
+
+ dpdk_list = list()
+
+ # On numa architecture socket-mem requires to set the amount of memory
+ # to be reserved on each numa node
+ socket_mem_str = 'socket-mem '
+ for numa in range (0,numa_mgr.get_number_of_numa()):
+ if numa in socket_mem:
+ socket_mem_str = socket_mem_str + str(socket_mem[numa])
+ else:
+ socket_mem_str = socket_mem_str + '0'
+
+ if numa < numa_mgr.get_number_of_numa()-1:
+ socket_mem_str = socket_mem_str + ','
+
+ dpdk_list.append(socket_mem_str)
+
+ for interface in self.node.interfaces:
+ if isinstance(interface, DpdkDevice):
+ dpdk_list.append('dev ' + interface.pci_address)
+
+ # Add the core on which running vpp and the dpdk parameters
+ setup = TPL_VPP_DPDK_DAEMON_SCRIPT + 'cpu {'
+
+ setup = setup + ''' \n main-core ''' + str(self.core)
+
+ if self.enable_worker:
+ self.numa_node, cpu_worker =numa_mgr.get_numa_core(self.numa_node)
+ setup = setup + '''\n corelist-workers ''' + str(cpu_worker)
+
+ setup = setup + '''\n}\n\n dpdk { '''
+
+ for dpdk_dev in dpdk_list:
+ setup = setup + ''' \n ''' + dpdk_dev
+
+ setup = setup + '\n}'
+
+
+ if any([isinstance(interface,DpdkDevice) for interface in self.node.interfaces]):
+ self.dpdk_setup_file.content = setup
+ else:
+ self.dpdk_setup_file.content = TPL_VPP_DPDK_DAEMON_SCRIPT
+
+ lock = self.node.node_with_kernel.vpp_host.vppstart_lock
+
+ vpp_disable = BashTask(self.node, CMD_VPP_DISABLE, lock = lock)
+ vpp_stop = BashTask(self.node, CMD_VPP_STOP, lock = lock)
+ enable_ip_forward = BashTask(self.node, CMD_DISABLE_IP_FORWARD)
+ start_vpp = BashTask(self.node, CMD_VPP_START, lock = lock)
+
+ return ((vpp_disable > vpp_stop) | enable_ip_forward) > start_vpp
+
+ def __delete__(self):
+ return BashTask(self.node, CMD_VPP_STOP)
+
+ def _add_plugins(self, plugin):
+ return BashTask(self.node, CMD_VPP_ENABLE_PLUGIN, {'plugin': plugin})
+
+ def _set_plugins(self):
+ cmd = None
+ for plugin in self.plugins:
+ cmd = cmd > BashTask(self.node, CMD_VPP_ENABLE_PLUGIN,
+ {'plugin' : plugin})
+ return cmd
+
+ def _remove_plugins(self, plugin):
+ raise NotImplementedError
+
+ @inline_task
+ def _get_plugins(self):
+ return {'plugins' : []}
diff --git a/vicn/resource/vpp/vpp_bridge.py b/vicn/resource/vpp/vpp_bridge.py
new file mode 100644
index 00000000..c7a70c02
--- /dev/null
+++ b/vicn/resource/vpp/vpp_bridge.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.type import Integer
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.attribute import Reference
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.resource import Resource
+from vicn.core.task import task, BashTask, EmptyTask
+from vicn.resource.channel import Channel
+from vicn.resource.linux.application import LinuxApplication
+from vicn.resource.linux.sym_veth_pair import SymVethPair
+from vicn.resource.linux.sym_veth_pair import SymVethPair
+from vicn.resource.node import Node
+from vicn.resource.vpp.dpdk_device import DpdkDevice
+from vicn.resource.vpp.interface import VPPInterface
+from vicn.resource.vpp.vpp import VPP
+
+CMD_ADD_INTERFACE_TO_BR = ('vppctl set interface l2 bridge '
+ '{interface.device_name} {br_domain}')
+
+class VPPBridge(Channel, LinuxApplication):
+ """
+ Resource: VPPBridge
+
+ VPPBridge instantiate a vpp resource and set it as a vpp bridge.
+
+ This resource requires to be run within a LxcContainer which will have VPP.
+ Every interface in the lxc_container (i.e., the ones contained in
+ self.node.interfaces) will be added to the vpp bridge. To connect other vpp
+ node to the bridge, the corresponding dpdkdevice must be added as an
+ interface to the channel.
+ """
+
+ # The vpp bridge _USES_ a VPP forwarder on the node
+ node = Attribute(Node, mandatory=True,
+ description = 'Node on which vpp is running',
+ requirements = [Requirement('vpp')])
+
+ connected_nodes = Attribute(Node, multiplicity = Multiplicity.OneToMany,
+ description = 'List of nodes to connect to the bridge')
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._vpp_interfaces = list()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__ (self):
+ # We don't need any reference to the list of SymVethPair because each
+ # side of a veth will be included in the node.interfaces list
+ self._veths = [SymVethPair(node1 = self.node, node2 = node,
+ owner = self) for node in self.connected_nodes]
+
+ return Resource.__concurrent__(*self._veths)
+
+ @task
+ def __initialize__ (self):
+ # Add the veth side on the connected_nodes to the set of interfaces of
+ # the channel
+ self.interfaces.extend([veth.side2 for veth in self._veths])
+
+ @task
+ def __get__(self):
+ # Forces creation
+ raise ResourceNotFound
+
+ # Nothing to do
+ __delete__ = None
+
+ def __create__(self):
+ manager = self._state.manager
+
+ # Create a VPPInterface for each interface in the node. These will be
+ # the interfaces we will connect to the vpp bridge process
+ vpp_interfaces = list()
+ for interface in self.node.interfaces:
+ # FIXME harcoded value
+ if interface.device_name == 'eth0':
+ continue
+
+ vpp_interface = VPPInterface(vpp = self.node.vpp,
+ parent = interface,
+ ip_address = Reference(interface, 'ip_address'),
+ device_name = 'host-' + interface.device_name)
+ vpp_interfaces.append(vpp_interface)
+ manager.commit_resource(vpp_interface)
+
+ tasks = EmptyTask()
+
+ for vpp_interface in vpp_interfaces:
+ tasks = tasks > (wait_resource_task(vpp_interface) >
+ self._add_interface(vpp_interface,0))
+
+ return wait_resource_task(self.node.vpp) > tasks
+
+ #--------------------------------------------------------------------------
+ # Internal methods
+ #--------------------------------------------------------------------------
+
+ def _add_interface(self, interface, br_domain):
+ return BashTask(self.node, CMD_ADD_INTERFACE_TO_BR,
+ {'interface': interface, 'br_domain': br_domain})
+
+ def _del_interface(self, interface, br_domain):
+ raise NotImplementedError('Interface removal not supported')
+
diff --git a/vicn/resource/vpp/vpp_commands.py b/vicn/resource/vpp/vpp_commands.py
new file mode 100644
index 00000000..8ee64bf6
--- /dev/null
+++ b/vicn/resource/vpp/vpp_commands.py
@@ -0,0 +1,41 @@
+##### VPP SETUP #####
+
+CMD_VPP_STOP_SERVICE = 'systemctl stop vpp.service'
+CMD_VPP_DISABLE = 'systemctl disable vpp.service'
+
+# 'sleep 1' ensures that VPP has enough time to start
+CMD_VPP_START = '''
+systemctl start vpp
+sleep 1
+'''
+CMD_VPP_STOP = '''
+systemctl stop vpp
+killall -9 vpp_main || true
+'''
+CMD_VPP_ENABLE_PLUGIN = 'vppctl {plugin} enable'
+
+##### VPP INTERFACES #####
+
+CMD_VPP_CREATE_IFACE = '''
+# Create vpp interface from netmodel.network.interface.device_name} with mac {self.parent.mac_address}
+vppctl create host-interface name {vpp_interface.parent.device_name} hw-addr {vpp_interface.parent.mac_address}
+vppctl set interface state {vpp_interface.device_name} up
+'''
+CMD_VPP_SET_IP = 'vppctl set int ip address {netdevice.device_name} {netdevice.ip_address}/{netdevice.prefix_len}'
+CMD_VPP_SET_UP = 'vppctl set int state {netdevice.device_name} up'
+
+##### VPP IP ROUTING #####
+
+CMD_VPP_ADD_ROUTE = 'vppctl set ip arp static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}'
+CMD_VPP_DEL_ROUTE = 'vppctl set ip arp del static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}'
+CMD_VPP_ADD_ROUTE_GW = 'vppctl ip route add {route.ip_address}/32 via {route.gateway} {route.interface.vppinterface.device_name}'
+CMD_VPP_DEL_ROUTE_GW = 'vppctl ip route del {route.ip_address}/32 via {route.gateway} {route.interface.vppinterface.device_name}'
+
+##### VPP CICN PLUGIN #####
+
+CMD_VPP_CICN_GET = "timeout 1 vppctl cicn show" #We timeout if vpp is not started
+CMD_VPP_ADD_ICN_ROUTE = 'vppctl cicn cfg fib add prefix {route.prefix} face {route.face.id}'
+CMD_VPP_ADD_ICN_FACE = 'vppctl cicn cfg face add local {face.src_ip}:{face.src_port} remote {face.dst_ip}:{face.dst_port}'
+
+CMD_VPP_CICN_GET_CACHE_SIZE = 'vppctl cicn show | grep "CS entries" | grep -E "[0-9]+"'
+CMD_VPP_CICN_SET_CACHE_SIZE = 'vppctl cicn control param cs size {self.cache_size}'
diff --git a/vicn/resource/vpp/vpp_host.py b/vicn/resource/vpp/vpp_host.py
new file mode 100644
index 00000000..134e65b0
--- /dev/null
+++ b/vicn/resource/vpp/vpp_host.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import asyncio
+
+from netmodel.model.type import String
+from vicn.core.attribute import Attribute, Multiplicity
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.task import BashTask, task, EmptyTask
+from vicn.resource.linux.application import LinuxApplication
+from vicn.resource.linux.file import TextFile
+from vicn.resource.node import Node
+from vicn.resource.vpp.scripts import FN_APPARMOR_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import TPL_APPARMOR_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import FN_VPP_DPDK_SCRIPT
+from vicn.resource.vpp.scripts import TPL_VPP_DPDK_DAEMON_SCRIPT
+from vicn.resource.vpp.vpp_commands import CMD_VPP_DISABLE
+from vicn.resource.vpp.vpp_commands import CMD_VPP_STOP_SERVICE
+
+CMD_INSERT_MODULES = 'modprobe uio && modprobe igb_uio'
+CMD_APP_ARMOR_RELOAD = '''
+# Force apparmor to reload profiles to include the new profile
+/etc/init.d/apparmor reload
+'''
+CMD_SYSCTL_HUGEPAGES = 'sysctl -w vm.nr_hugepages={nb_hp}'
+DEFAULT_NB_HUGEPAGES = 1024
+CMD_GREP_UIO_DEV = 'ls /dev | grep uio'
+CMD_CREATE_UIO_DEVICES = "dpdk_nic_bind --bind=igb_uio {pci_address}"
+
+class VPPHost(LinuxApplication):
+ """
+ Resource: VPPHost
+
+ Only used for container deployment
+
+ Packages required on the host
+ - vpp : sysctl configuration
+ - vpp-dpdk-dkms : kernel modules
+
+ Host must be configured to let vpp to work into container:
+ - install new apparmor profile (to let the container to read
+ hugepages info in /sys/kernel/mm/hugepages)
+ - set hugepages into the host
+ """
+
+ node = Attribute(Node,
+ description = 'Node on which the application is installed',
+ mandatory = True,
+ multiplicity = Multiplicity.OneToOne,
+ reverse_name = 'vpp_host',
+ reverse_description = 'Setup for hosting vpp containers',
+ reverse_auto = True,
+ requirements = [Requirement('numa_mgr')])
+ uio_devices = Attribute(String,
+ description = 'uio devices on the node',
+ multiplicity = Multiplicity.OneToMany,
+ ro = True)
+ dpdk_devices = Attribute(String,
+ description = 'Dpdk devices on the node',
+ multiplicity = Multiplicity.OneToMany)
+
+ __package_names__ = ['dpdk', 'vpp', 'vpp-dpdk-dkms']
+
+ #--------------------------------------------------------------------------
+ # Constructor and Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.vppstart_lock = asyncio.Lock()
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ def __subresources__(self):
+ app_armor_file = TextFile(node = self.node,
+ filename = FN_APPARMOR_DPDK_SCRIPT,
+ content = TPL_APPARMOR_DPDK_SCRIPT,
+ overwrite = True)
+ startup_conf = TextFile(node = self.node,
+ filename = FN_VPP_DPDK_SCRIPT,
+ content = TPL_VPP_DPDK_DAEMON_SCRIPT,
+ overwrite = True)
+ return app_armor_file | startup_conf
+
+ @task
+ def __get__(self):
+ """
+ This method always assumes the resource does not exist, since it is not
+ an issue to perform the modprobe call everytime.
+ """
+ raise ResourceNotFound
+
+ def __create__(self):
+ modules = BashTask(self.node, CMD_INSERT_MODULES)
+ app_armor_reload = BashTask(self.node, CMD_APP_ARMOR_RELOAD)
+ sysctl_hugepages = BashTask(self.node, CMD_SYSCTL_HUGEPAGES,
+ {'nb_hp': DEFAULT_NB_HUGEPAGES})
+
+ # Hook
+ # The following is needed to create uio devices in /dev. They are
+ # required to let vpp to use dpdk (or other compatibles) nics. From a
+ # container, vpp cannot create those devices, therefore we need to
+ # create them in the host and then mount them on each container running
+ # vpp (and using a physical nic)
+ stop_vpp = BashTask(self.node, CMD_VPP_STOP_SERVICE)
+ disable_vpp = BashTask(self.node, CMD_VPP_DISABLE)
+ disable_vpp = stop_vpp > disable_vpp
+
+ create_uio = EmptyTask()
+ for device in self.dpdk_devices:
+ create_uio = create_uio > BashTask(self.node,
+ CMD_CREATE_UIO_DEVICES, {'pci_address' : device})
+
+ return ((modules | app_armor_reload) | sysctl_hugepages) > \
+ (disable_vpp > create_uio)
+
+ __delete__ = None
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_uio_devices(self):
+ def parse(rv):
+ return rv.stdout.splitlines()
+ return BashTask(self.node, CMD_GREP_UIO_DEV, parse = parse)