diff options
author | Jordan Augé <jordan.auge+fdio@email.com> | 2017-02-24 14:58:01 +0100 |
---|---|---|
committer | Jordan Augé <jordan.auge+fdio@cisco.com> | 2017-02-24 18:36:29 +0000 |
commit | 85a341d645b57b7cd88a26ed2ea0a314704240ea (patch) | |
tree | bdda2b35003aae20103a796f86daced160b8a730 /vicn/resource | |
parent | 9b30fc10fb1cbebe651e5a107e8ca5b24de54675 (diff) |
Initial commit: vICN
Change-Id: I7ce66c4e84a6a1921c63442f858b49e083adc7a7
Signed-off-by: Jordan Augé <jordan.auge+fdio@cisco.com>
Diffstat (limited to 'vicn/resource')
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) |