From 85a341d645b57b7cd88a26ed2ea0a314704240ea Mon Sep 17 00:00:00 2001 From: Jordan Augé Date: Fri, 24 Feb 2017 14:58:01 +0100 Subject: Initial commit: vICN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I7ce66c4e84a6a1921c63442f858b49e083adc7a7 Signed-off-by: Jordan Augé --- vicn/resource/linux/__init__.py | 0 vicn/resource/linux/application.py | 56 ++++ vicn/resource/linux/bridge.py | 104 +++++++ vicn/resource/linux/bridge_mgr.py | 34 +++ vicn/resource/linux/certificate.py | 74 +++++ vicn/resource/linux/dnsmasq.py | 118 ++++++++ vicn/resource/linux/file.py | 110 +++++++ vicn/resource/linux/iperf.py | 27 ++ vicn/resource/linux/link.py | 201 +++++++++++++ vicn/resource/linux/macvlan.py | 52 ++++ vicn/resource/linux/macvtap.py | 52 ++++ vicn/resource/linux/net_device.py | 519 +++++++++++++++++++++++++++++++++ vicn/resource/linux/netmon.py | 29 ++ vicn/resource/linux/numa_mgr.py | 113 +++++++ vicn/resource/linux/ovs.py | 68 +++++ vicn/resource/linux/package_manager.py | 232 +++++++++++++++ vicn/resource/linux/phy_interface.py | 49 ++++ vicn/resource/linux/phy_link.py | 39 +++ vicn/resource/linux/physical.py | 144 +++++++++ vicn/resource/linux/repository.py | 41 +++ vicn/resource/linux/service.py | 83 ++++++ vicn/resource/linux/sym_veth_pair.py | 151 ++++++++++ vicn/resource/linux/tap_device.py | 48 +++ vicn/resource/linux/traceroute.py | 23 ++ vicn/resource/linux/veth_pair.py | 62 ++++ 25 files changed, 2429 insertions(+) create mode 100644 vicn/resource/linux/__init__.py create mode 100644 vicn/resource/linux/application.py create mode 100644 vicn/resource/linux/bridge.py create mode 100644 vicn/resource/linux/bridge_mgr.py create mode 100644 vicn/resource/linux/certificate.py create mode 100644 vicn/resource/linux/dnsmasq.py create mode 100644 vicn/resource/linux/file.py create mode 100644 vicn/resource/linux/iperf.py create mode 100644 vicn/resource/linux/link.py create mode 100644 vicn/resource/linux/macvlan.py create mode 100644 vicn/resource/linux/macvtap.py create mode 100644 vicn/resource/linux/net_device.py create mode 100644 vicn/resource/linux/netmon.py create mode 100644 vicn/resource/linux/numa_mgr.py create mode 100644 vicn/resource/linux/ovs.py create mode 100644 vicn/resource/linux/package_manager.py create mode 100644 vicn/resource/linux/phy_interface.py create mode 100644 vicn/resource/linux/phy_link.py create mode 100644 vicn/resource/linux/physical.py create mode 100644 vicn/resource/linux/repository.py create mode 100644 vicn/resource/linux/service.py create mode 100644 vicn/resource/linux/sym_veth_pair.py create mode 100644 vicn/resource/linux/tap_device.py create mode 100644 vicn/resource/linux/traceroute.py create mode 100644 vicn/resource/linux/veth_pair.py (limited to 'vicn/resource/linux') diff --git a/vicn/resource/linux/__init__.py b/vicn/resource/linux/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vicn/resource/linux/application.py b/vicn/resource/linux/application.py new file mode 100644 index 00000000..d2b5139e --- /dev/null +++ b/vicn/resource/linux/application.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from vicn.core.attribute import Reference +from vicn.core.resource import Resource, EmptyResource +from vicn.resource.application import Application +from vicn.resource.linux.package_manager import Packages + + +class LinuxApplication(Application): + """ + Resource: Linux Application + + This resource ensures that the application is present on the system, and + installs it during setup if necessary. + """ + + def __subresources__(self): + package_names = self._get_package_names() + if package_names: + packages = Packages(node=Reference(self, 'node'), + names=package_names, + owner=self) + else: + packages = EmptyResource() + + process = None + + return packages > process + + #-------------------------------------------------------------------------- + # Private methods + #-------------------------------------------------------------------------- + + def _get_package_names(self): + package_names = list() + for base in self.__class__.mro(): + if not '__package_names__' in vars(base): + continue + package_names.extend(getattr(base, '__package_names__')) + return package_names diff --git a/vicn/resource/linux/bridge.py b/vicn/resource/linux/bridge.py new file mode 100644 index 00000000..882f0226 --- /dev/null +++ b/vicn/resource/linux/bridge.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging + +from vicn.core.address_mgr import AddressManager +from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.exception import ResourceNotFound +from vicn.core.requirement import Requirement +from vicn.core.task import inline_task +from vicn.resource.channel import Channel +from vicn.resource.linux.bridge_mgr import BridgeManager +from vicn.resource.linux.net_device import BaseNetDevice + +log = logging.getLogger(__name__) + +# FIXME This should use the AddressManager to get allocated a name that does +# not exist +DEFAULT_BRIDGE_NAME = 'br0' + +class Bridge(Channel, BaseNetDevice): + """ + Resource: Bridge + """ + node = Attribute( + reverse_name = 'bridge', + reverse_description = 'Main bridge', + reverse_auto = 'true', + multiplicity = Multiplicity.OneToOne, + requirements = [ + Requirement('bridge_manager') + ]) + device_name = Attribute( + default = DEFAULT_BRIDGE_NAME, + mandatory = False) + + #-------------------------------------------------------------------------- + # Constructor / Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + self.prefix = 'br' + self.netdevice_type = 'bridge' + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inline_task + def __get__(self): + # FIXME we currently force the recreation of the bridge, delegating the + # check to the creation function + raise ResourceNotFound + + def __create__(self): + # FIXME : reserves .1 IP address for the bridge, provided no other + # class uses this trick + AddressManager().get_ip(self) + return self.node.bridge_manager.add_bridge(self.device_name) + + # Everything should be handled by BaseNetDevice + __delete__ = None + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def _add_interface(self, interface, vlan=None): + """ + Returns: + Task + """ + return self.node.bridge_manager.add_interface(self.device_name, + interface.device_name, vlan) + + def __method_add_interface__(self, interface, vlan=None): + return self._add_interface(interface, vlan) + + def _remove_interface(self, interface): + """ + Returns: + Task + """ + log.info('Removing interface {} from bridge {}'.format( + interface.device_name, self.name)) + return self.node.bridge_manager.del_interface(self.device_name, + interface.device_name) + diff --git a/vicn/resource/linux/bridge_mgr.py b/vicn/resource/linux/bridge_mgr.py new file mode 100644 index 00000000..b7035221 --- /dev/null +++ b/vicn/resource/linux/bridge_mgr.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from vicn.resource.linux.application import LinuxApplication +from vicn.core.resource import FactoryResource +from vicn.core.attribute import Attribute, Multiplicity + +class BridgeManager(LinuxApplication): + """ + Resource: Bridge Manager + + A bridge manager is responsible to manage bridges on a node. + """ + __type__ = FactoryResource + + # Overloaded reverse attribute + node = Attribute(reverse_name = 'bridge_manager', + reverse_auto = True, + multiplicity = Multiplicity.OneToOne) diff --git a/vicn/resource/linux/certificate.py b/vicn/resource/linux/certificate.py new file mode 100644 index 00000000..e8750dff --- /dev/null +++ b/vicn/resource/linux/certificate.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os.path + +from netmodel.model.type import String +from vicn.core.attribute import Attribute, Multiplicity, Reference +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import Resource +from vicn.core.task import task, inline_task, BashTask +from vicn.resource.linux.file import File +from vicn.resource.node import Node + +DEFAULT_RSA_LENGTH = '4096' +DEFAULT_SUBJECT = '/CN=www.cisco.com/L=Paris/O=Cisco/C=FR' + +CMD_CREATE='\n'.join([ + '# Generate a new certificate', + 'openssl req -x509 -newkey rsa:' + DEFAULT_RSA_LENGTH + + ' -keyout {self.key} -out {self.cert} -subj ' + DEFAULT_SUBJECT + ' -nodes' +]) + +class Certificate(Resource): + """ + Resource: Certificate + + Implements a SSL certificate. + + Todo: + - ideally, this should be implemented as a pair of tightly coupled files. + """ + node = Attribute(Node, + description = 'Node on which the certificate is created', + mandatory = True, + multiplicity = Multiplicity.ManyToOne) + cert = Attribute(String, description = 'Certificate path', + mandatory = True) + key = Attribute(String, description = 'Key path', + mandatory = True) + + @inline_task + def __initialize__(self): + self._cert_file = File(node = Reference(self, 'node'), + filename = Reference(self, 'cert'), + managed = False) + self._key_file = File(node = Reference(self, 'node'), + filename = Reference(self, 'key'), + managed = False) + + def __get__(self): + return self._cert_file.__get__() | self._key_file.__get__() + + def __create__(self): + return BashTask(None, CMD_CREATE, {'self': self}) + + def __delete__(self): + return self._cert_file.__delete__() | self._key_file.__delete__() + + diff --git a/vicn/resource/linux/dnsmasq.py b/vicn/resource/linux/dnsmasq.py new file mode 100644 index 00000000..e18f750f --- /dev/null +++ b/vicn/resource/linux/dnsmasq.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import ipaddress +import logging + +from string import Template + +from netmodel.model.type import String, Bool +from vicn.core.attribute import Attribute +from vicn.core.resource import EmptyResource +from vicn.resource.dns_server import DnsServer +from vicn.resource.interface import Interface +from vicn.resource.linux.file import TextFile +from vicn.resource.linux.service import Service + +log = logging.getLogger(__name__) + +FN_CONF='/etc/dnsmasq.conf' + +TPL_CONF=''' +# Configuration file for dnsmasq. +# +# Format is one option per line, legal options are the same +# as the long options legal on the command line. See +# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details. + +interface=$interface +dhcp-range=$dhcp_range +dhcp-host=00:0e:c6:81:79:01,192.168.128.200,12h + +#server=$server +$flags +''' + +DHCP_OFFSET = 195 + +class DnsMasq(Service, DnsServer): + """ + + Todo: + - Currently, a single interface is supported. + - DHCP range is hardcoded + """ + __package_names__ = ['dnsmasq'] + __service_name__ = 'dnsmasq' + + interface = Attribute(Interface, + description = 'Interface on which to listen') + lease_interval = Attribute(String, + default = '12h') + server = Attribute(String) + dhcp_authoritative = Attribute(Bool, + description = 'Flag: DHCP authoritative', + default = True) + log_queries = Attribute(Bool, description = 'Flag: log DNS queries', + default = True) + log_dhcp = Attribute(Bool, description = 'Flag: log DHCP queries', + default = True) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.interface: + if self.node.bridge: + self.interface = self.node.bridge + else: + self.interface = self.node.host_interface + + def __subresources__(self): + # Overwrite configuration file + flags = list() + if self.dhcp_authoritative: + flags.append('dhcp-authoritative') + if self.log_queries: + flags.append('log-queries') + if self.log_dhcp: + flags.append('log-dhcp') + network = self._state.manager.get('network') + network = ipaddress.ip_network(network, strict=False) + + dhcp_range = '{},{},{},{},{}'.format( + self.interface.device_name, + str(network[DHCP_OFFSET]), + str(network[DHCP_OFFSET + 5]), # eg. .253 + "255.255.255.0", + self.lease_interval) + + t_dict = { + 'interface' : self.interface.device_name, + 'dhcp_range': dhcp_range, + 'server' : str(network[-2]), # unused so far + 'flags' : '\n'.join(flags) + } + + t = Template(TPL_CONF) + conf = t.substitute(t_dict) + + return TextFile(node = self.node, owner = self, filename = FN_CONF, + content = conf, overwrite = True) diff --git a/vicn/resource/linux/file.py b/vicn/resource/linux/file.py new file mode 100644 index 00000000..cddda8ed --- /dev/null +++ b/vicn/resource/linux/file.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from netmodel.model.type import String, Bool +from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import Resource +from vicn.core.task import BashTask, inline_task +from vicn.resource.node import Node + +CREATE_DIR_CMD = "mkdir -p {dir}" +CREATE_FILE_CMD = "mkdir -p $(dirname {file.filename}) && touch {file.filename}" +DELETE_FILE_CMD = "rm -f {file.filename}" + +GET_FILE_CMD = 'test -f {file.filename} && readlink -e {file.filename}' + +GREP_FILE_CMD = "cat {file.filename}" + +CMD_PRINT_TO_FILE = 'echo -n "{file.content}" > {file.filename}' + +class File(Resource): + """ + Resource: File + """ + filename = Attribute(String, description = 'Path to the file', + mandatory = True) + node = Attribute(Node, description = 'Node on which the file is created', + mandatory = True, + multiplicity = Multiplicity.ManyToOne, + reverse_name = 'files', + reverse_description = 'Files created on the node') + overwrite = Attribute(Bool, + description = 'Determines whether an existing file is overwritten', + default = False) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __get__(self): + + # UGLY + @inline_task + def not_found(): + raise ResourceNotFound + + if self.overwrite: + return not_found() + + def is_path (rv): + if rv is None or rv.stdout is None or len(rv.stdout) == 0 or \ + rv.return_value != 0: + raise ResourceNotFound + return {} # 'filename': rv.stdout} + + test = BashTask(self.node, GET_FILE_CMD, {"file": self}, parse=is_path) + return test + + def __create__(self): + ctask = BashTask(self.node, CREATE_FILE_CMD, {"file": self}) + if self.overwrite: + ctask = BashTask(self.node, DELETE_FILE_CMD, {'file': self}) > ctask + return ctask + + def __delete__(self): + return BashTask(self.node, DELETE_FILE_CMD, { "file" : self}) + +#------------------------------------------------------------------------------ + +class TextFile(File): + """ + Resource: TextFile + + A file with text content. + """ + + content = Attribute(String, default='') + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __create__(self): + return BashTask(self.node, CMD_PRINT_TO_FILE, {'file': self}) + + #-------------------------------------------------------------------------- + # Attributes + #-------------------------------------------------------------------------- + + def _set_content(self): + return self.__create__() + + def _get_content(self): + return BashTask(self.node, GREP_FILE_CMD, {'file': self}, + parse =( lambda x : x.stdout)) diff --git a/vicn/resource/linux/iperf.py b/vicn/resource/linux/iperf.py new file mode 100644 index 00000000..a0780a1c --- /dev/null +++ b/vicn/resource/linux/iperf.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from abc import ABC + +from netmodel.model.type import Integer +from vicn.core.attribute import Attribute +from vicn.resource.linux.application import LinuxApplication + +class Iperf3(LinuxApplication, ABC): + __package_names__ = ['iperf3'] + diff --git a/vicn/resource/linux/link.py b/vicn/resource/linux/link.py new file mode 100644 index 00000000..4304a948 --- /dev/null +++ b/vicn/resource/linux/link.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import random +import string +import logging + +from netmodel.model.type import Integer, String +from vicn.core.attribute import Attribute, Reference +from vicn.core.exception import ResourceNotFound +from vicn.core.state import ResourceState, AttributeState +from vicn.core.task import inline_task, async_task, run_task +from vicn.core.task import get_attributes_task, BashTask +from vicn.resource.channel import Channel +from vicn.resource.interface import Interface +from vicn.resource.linux.net_device import NonTapBaseNetDevice +from vicn.resource.node import Node + +# FIXME remove VPP specific code +from vicn.resource.vpp.interface import VPPInterface + +log = logging.getLogger(__name__) + +CMD_DELETE_IF_EXISTS='ip link show {interface.device_name} && ' \ + 'ip link delete {interface.device_name} || true' + +CMD_CREATE=''' +# Create veth pair in the host node +ip link add name {tmp_src} type veth peer name {tmp_dst} +ip link set dev {tmp_src} netns {pid[0]} name {interface.src.device_name} +ip link set dev {tmp_dst} netns {pid[1]} name {interface.dst.device_name} +''' +CMD_UP=''' +ip link set dev {interface.device_name} up +''' + +class Link(Channel): + """ + Resource: Link + + Implements a virtual wired link between containers. It is a VethPair, both + sides of which sit inside a different container. + + Because of this, the resource only supports passing source and destination + containers, and not interfaces. It also explains the relative complexity of + the current implementation. + """ + + src = Attribute(Interface, description = 'Source interface') + dst = Attribute(Interface, description = 'Destination interface') + + capacity = Attribute(Integer, description = 'Link capacity (Mb/s)') + delay = Attribute(String, description = 'Link propagation delay') + + src_node = Attribute(Node, description = 'Source node', + mandatory = True) + dst_node = Attribute(Node, description = 'Destination node', + mandatory = True) + + def __init__(self, *args, **kwargs): + assert 'src' not in kwargs and 'dst' not in kwargs + assert 'src_node' in kwargs and 'dst_node' in kwargs + super().__init__(*args, **kwargs) + + @inline_task + def __initialize__(self): + + # We create two managed net devices that are pre-setup + # but the resource manager has to take over for IP addresses etc. + # Being done in initialize, those attributes won't be considered as + # dependencies and will thus not block the resource state machine. + self.src = NonTapBaseNetDevice(node = self.src_node, + device_name = self.dst_node.name, + channel = self, + capacity = self.capacity, + owner = self.owner) + self.dst = NonTapBaseNetDevice(node = self.dst_node, + device_name = self.src_node.name, + channel = self, + capacity = self.capacity, + owner = self.owner) + self.dst.remote = self.src + self.src.remote = self.dst + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + async def _commit(self): + manager = self._state.manager + + # We mark the src and dst interfaces created because we are pre-setting + # them up in __create__ using a VethPair + # We go through both INITIALIZED and CREATED stats to raise the proper + # events and satisfy any eventual wait_* command. + await manager._set_resource_state(self.src, ResourceState.INITIALIZED) + await manager._set_resource_state(self.dst, ResourceState.INITIALIZED) + await manager._set_resource_state(self.src, ResourceState.CREATED) + await manager._set_resource_state(self.dst, ResourceState.CREATED) + + # We mark the attribute clean so that it is not updated + await manager._set_attribute_state(self, 'src', AttributeState.CLEAN) + await manager._set_attribute_state(self, 'dst', AttributeState.CLEAN) + + manager.commit_resource(self.src) + manager.commit_resource(self.dst) + + # Disable rp_filtering + # self.src.rp_filter = False + # self.dst.rp_filter = False + + #XXX VPP + if hasattr(self.src_node, 'vpp') and not self.src_node.vpp is None: + vpp_src = VPPInterface(parent = self.src, + vpp = self.src_node.vpp, + ip_address = Reference(self.src, 'ip_address'), + device_name = 'vpp' + self.src.device_name) + manager.commit_resource(vpp_src) + + if hasattr(self.dst_node, 'vpp') and not self.dst_node.vpp is None: + vpp_dst = VPPInterface(parent = self.dst, + vpp = self.dst_node.vpp, + ip_address = Reference(self.dst, 'ip_address'), + device_name = 'vpp' + self.dst.device_name) + manager.commit_resource(vpp_dst) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @async_task + async def __get__(self): + manager = self._state.manager + + try: + await run_task(self.src.__get__(), manager) + await run_task(self.dst.__get__(), manager) + except ResourceNotFound: + # This is raised if any of the two side of the VethPair is missing + raise ResourceNotFound + + # We always need to commit the two endpoints so that their attributes + # are correctly updated + await self._commit() + + def __create__(self): + assert self.src_node.get_type() == 'lxccontainer' + assert self.dst_node.get_type() == 'lxccontainer' + + src_host = self.src_node.node + dst_host = self.dst_node.node + + assert src_host == dst_host + host = src_host + + # Sometimes a down interface persists on one side + delif_src = BashTask(self.src_node, CMD_DELETE_IF_EXISTS, + {'interface': self.src}) + delif_dst = BashTask(self.dst_node, CMD_DELETE_IF_EXISTS, + {'interface': self.dst}) + + pid_src = get_attributes_task(self.src_node, ['pid']) + pid_dst = get_attributes_task(self.dst_node, ['pid']) + + tmp_src = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase + + string.digits) for _ in range(5)) + tmp_dst = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase + + string.digits) for _ in range(5)) + + create = BashTask(host, CMD_CREATE, {'interface': self, + 'tmp_src': tmp_src, 'tmp_dst': tmp_dst}) + + up_src = BashTask(self.src_node, CMD_UP, {'interface': self.src}) + up_dst = BashTask(self.dst_node, CMD_UP, {'interface': self.dst}) + + @async_task + async def set_state(): + # We always need to commit the two endpoints so that their attributes + # are correctly updated + await self._commit() + + delif = delif_src | delif_dst + up = up_src | up_dst + pid = pid_src | pid_dst + return ((delif > (pid @ create)) > up) > set_state() + diff --git a/vicn/resource/linux/macvlan.py b/vicn/resource/linux/macvlan.py new file mode 100644 index 00000000..ea9c37c1 --- /dev/null +++ b/vicn/resource/linux/macvlan.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from netmodel.model.type import String +from vicn.core.attribute import Attribute +from vicn.core.task import BashTask +from vicn.resource.linux.net_device import SlaveBaseNetDevice + +CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \ + 'link {netdevice.parent.device_name} ' \ + 'type {netdevice.netdevice_type} mode {netdevice.mode}' + +class MacVlan(SlaveBaseNetDevice): + """ + Resource: MacVlan + + Implements a MacVlan interface. + """ + + mode = Attribute(String, description = 'MACVLAN mode', + default = 'bridge') + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + self.prefix = 'macvlan' + self.netdevice_type = 'macvlan' + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __create__(self): + return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self}) diff --git a/vicn/resource/linux/macvtap.py b/vicn/resource/linux/macvtap.py new file mode 100644 index 00000000..82002e02 --- /dev/null +++ b/vicn/resource/linux/macvtap.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from netmodel.model.type import String +from vicn.core.attribute import Attribute +from vicn.core.task import BashTask +from vicn.resource.linux.net_device import SlaveBaseNetDevice + +CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \ + 'link {netdevice.parent.device_name} ' \ + 'type {netdevice.netdevice_type} mode {netdevice.mode}' + +class MacVtap(SlaveBaseNetDevice): + """ + Resource: MacVtap + + Implements a MacVtap interface. + """ + + mode = Attribute(String, description = 'MACVTAP mode', + default = 'bridge'), + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + self.prefix = 'macvtap' + self.netdevice_type = 'macvtap' + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __create__(self): + return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self}) diff --git a/vicn/resource/linux/net_device.py b/vicn/resource/linux/net_device.py new file mode 100644 index 00000000..f0a08991 --- /dev/null +++ b/vicn/resource/linux/net_device.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import re +import math +import random +import string + +from netmodel.model.type import Integer, String, Bool +from vicn.core.address_mgr import AddressManager +from vicn.core.attribute import Attribute +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import BaseResource +from vicn.core.task import BashTask, task, EmptyTask +from vicn.resource.application import Application +from vicn.resource.interface import Interface + +# parse_ip_addr inspired from: +# From: https://github.com/ohmu/poni/blob/master/poni/cloud_libvirt.py + +LXD_FIX = lambda cmd: 'sleep 1 && {}'.format(cmd) + +MAX_DEVICE_NAME_SIZE = 15 + +CMD_FLUSH_IP = 'ip addr flush dev {device_name}' + +CMD_INTERFACE_LIST = 'ip link show | grep -A 1 @{}' +RX_INTERFACE_LIST = '.*?(?P[^ ]*)@{}:' +CMD_INTERFACE_GET = 'ip link show | grep -A 1 {}@{}' +RX_INTERFACE_GET = '.*?(?P{})@{}:' + +log = logging.getLogger(__name__) + +CMD_GET = LXD_FIX('ip link show {netdevice.device_name}') +CMD_CREATE = 'ip link add name {netdevice.device_name} ' \ + 'type {netdevice.netdevice_type}' +CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \ + 'link {netdevice.parent.device_name} ' \ + 'type {netdevice.netdevice_type}' +CMD_DELETE = 'ip link delete {netdevice.device_name}' +CMD_SET_MAC_ADDRESS = 'ip link set dev {netdevice.device_name} ' \ + 'address {netdevice.mac_address}' +CMD_GET_IP_ADDRESS = 'ip addr show {netdevice.device_name}' +CMD_SET_IP_ADDRESS = 'ip addr add dev {netdevice.device_name} ' \ + '{netdevice.ip_address} brd + || true' +CMD_SET_PROMISC = 'ip link set dev {netdevice.device_name} promisc {on_off}' +CMD_SET_UP = 'ip link set {netdevice.device_name} {up_down}' +CMD_SET_CAPACITY='\n'.join([ + 'tc qdisc del dev {netdevice.device_name} root || true', + 'tc qdisc add dev {netdevice.device_name} root handle 1: tbf rate ' + '{netdevice.capacity}Mbit burst {burst}kb latency 70ms' + 'tc qdisc add dev {netdevice.device_name} parent 1:1 codel', +]) +CMD_GET_PCI_ADDRESS='ethtool -i {netdevice.device_name} | ' \ + "sed -n '/bus-info/{{s/.*: [^:]*:\(.*\)/\\1/p}}'" +CMD_GET_OFFLOAD='ethtool -k {netdevice.device_name} | ' \ + 'grep rx-checksumming | cut -d " " -f 2' +CMD_SET_OFFLOAD='ethtool -K {netdevice.device_name} rx on tx on' +CMD_UNSET_OFFLOAD='ethtool -K {netdevice.device_name} rx off tx off' + +CMD_UNSET_RP_FILTER = ''' +sysctl -w net.ipv4.conf.all.rp_filter=0 +sysctl -w net.ipv4.conf.{netdevice.device_name}.rp_filter=0 +''' +CMD_SET_RP_FILTER = 'sysctl -w ' \ + 'net.ipv4.conf.{netdevice.device_name}.rp_filter=1' +CMD_GET_RP_FILTER = ''' +sysctl net.ipv4.conf.all.rp_filter +sysctl net.ipv4.conf.{netdevice.device_name}.rp_filter +''' + +#------------------------------------------------------------------------------- + +# FIXME GPL code + +# Copyright 2015 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Utility to parse 'ip link [show]'. + +Example dictionary returned by parse_ip_link(): + +{u'eth0': {u'flags': set([u'BROADCAST', u'LOWER_UP', u'MULTICAST', u'UP']), + u'index': 2, + u'mac': u'80:fa:5c:0d:43:5e', + u'name': u'eth0', + u'settings': {u'group': u'default', + u'mode': u'DEFAULT', + u'mtu': u'1500', + u'qdisc': u'pfifo_fast', + u'qlen': u'1000', + u'state': u'UP'}}, + u'lo': {u'flags': set([u'LOOPBACK', u'LOWER_UP', u'UP']), + u'index': 1, + u'name': u'lo', + u'settings': {u'group': u'default', + u'mode': u'DEFAULT', + u'mtu': u'65536', + u'qdisc': u'noqueue', + u'state': u'UNKNOWN'}}} + +The dictionary above is generated given the following input: + + 1: lo: mtu 65536 qdisc noqueue state UNKNOWN \ +mode DEFAULT group default + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + 2: eth0: mtu 1500 qdisc pfifo_fast \ +state UP mode DEFAULT group default qlen 1000 + link/ether 80:fa:5c:0d:43:5e brd ff:ff:ff:ff:ff:ff +""" + +def _get_settings_dict(settings_line): + """ + Given a string of the format: + "[[ ] ][...]" + Returns a dictionary mapping each key to its corresponding value. + :param settings_line: unicode + :return: dict + """ + settings = settings_line.strip().split() + num_tokens = len(settings) + assert num_tokens % 2 == 0 + return { + settings[2 * i]: settings[2 * i + 1] for i in range(num_tokens // 2) + } + + +def _parse_interface_definition(line): + """Given a string of the format: + : : + Returns a dictionary containing the component parts. + :param line: unicode + :return: dict + :raises: ValueError if a malformed interface definition line is presented + """ + interface = {} + + # This line is in the format: + # : : + [index, name, properties] = map( + lambda s: s.strip(), line.split(':')) + + interface['index'] = int(index) + if '@' in name: + name, parent = name.split('@') + interface['name'] = name + interface['parent'] = parent + else: + interface['name'] = name + interface['parent'] = None + + # Now parse the part from above. + # This will be in the form " key1 value1 key2 value2 ..." + matches = re.match(r"^<(.*)>(.*)", properties) + if matches: + flags = matches.group(1) + if len(flags) > 0: + flags = flags.split(',') + else: + flags = [] + interface['flags'] = set(flags) + interface['settings'] = _get_settings_dict(matches.group(2)) + else: + raise ValueError("Malformed 'ip link' line (%s)" % line) + return interface + + +def _add_additional_interface_properties(interface, line): + """ + Given the specified interface and a specified follow-on line containing + more interface settings, adds any additional settings to the interface + dictionary. (currently, the only relevant setting is the interface MAC.) + :param interface: dict + :param line: unicode + """ + settings = _get_settings_dict(line) + # We only care about the MAC address for Ethernet interfaces. + mac = settings.get('link/ether') + if mac is not None: + interface['mac'] = mac + + +def parse_ip_link(output): + """ + Given the full output from 'ip link [show]', parses it and returns a + dictionary mapping each interface name to its settings. + :param output: string or unicode + :return: dict + """ + interfaces = {} + interface = None + for line in output.splitlines(): + if re.match(r'^[0-9]', line): + interface = _parse_interface_definition(line) + if interface is not None: + interfaces[interface['name']] = interface + else: + if interface is not None: + _add_additional_interface_properties(interface, line) + return interfaces + +#------------------------------------------------------------------------------ + +_IP_ADDR_SPLIT_RE = re.compile("^[0-9]+: ", flags=re.MULTILINE) + +def parse_ip_addr(data): + """ + Parse addresses from 'ip addr' output + """ + + for iface in _IP_ADDR_SPLIT_RE.split(data.strip()): + if not iface: + continue + lines = [l.strip() for l in iface.splitlines()] + # XXX @ in name not supported + name = lines.pop(0).partition(":")[0] + info = { + "ip-addresses": [], + "hardware-address": None, + } + if '@' in name: + name, parent = name.split('@') + info['name'] = name + info['parent'] = parent + else: + info['name'] = name + info['parent'] = None + + for line in lines: + words = line.split() + if words[0].startswith("link/") and len(words) >= 2: + info["hardware-address"] = words[1] + elif words[0] in ("inet", "inet6"): + addrtype = "ipv6" if words[0] == "inet6" else "ipv4" + addr, _, prefix = words[1].partition("/") + if prefix == '': + prefix = 128 if addrtype == "ipv6" else 32 + info["ip-addresses"].append({"ip-address-type": addrtype, + "ip-address": addr, "prefix": int(prefix)}) + yield info + +#------------------------------------------------------------------------------ + +class BaseNetDevice(Interface, Application): + __type__ = BaseResource + + # XXX note: ethtool only required if we need to get the pci address + __package_names__ = ['ethtool'] + + device_name = Attribute(String, description = 'Name of the NetDevice', + default = lambda x : x._default_device_name(), + max_size = MAX_DEVICE_NAME_SIZE) + capacity = Attribute(Integer, + description = 'Capacity for interface shaping (Mb/s)') + mac_address = Attribute(String, description = 'Mac address of the device') + ip_address = Attribute(String, description = 'IP address of the device') + pci_address = Attribute(String, + description = 'PCI bus address of the device', + ro = True) + promiscuous = Attribute(Bool, description = 'Promiscuous', default = False) + up = Attribute(Bool, description = 'Promiscuous', default = True) + netdevice_type = Attribute(String, description = 'Type of the netdevice', + ro = True) + prefix = Attribute(String, default = 'dev') + rp_filter = Attribute(Bool, description = 'Reverse-path filtering enabled', + default = True) + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Dummy member to store the other side of a VethPair + # We use it to disable offloading on interfaces connected to VPP + self.remote = None + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __get__(self): + def check(rv): + if not bool(rv): + raise ResourceNotFound + return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True, + parse=check) + + __create__ = None + + def __delete__(self): + return BashTask(self.node, CMD_DELETE, {'netdevice': self}) + + #-------------------------------------------------------------------------- + # Attributes + #-------------------------------------------------------------------------- + + def _get_mac_address(self): + # Merge into parse_ip_link + def parse(rv): + assert rv is not None + + nds = parse_ip_link(rv.stdout) + # This will raise an exception is the interface does not exist + nd = nds[self.device_name] + attrs = { 'mac_address': nd['mac'], } + return attrs + + return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True, + parse=parse) + + def _set_mac_address(self): + return BashTask(self.node, CMD_SET_MAC_ADDRESS, {'netdevice': self}) + + def _get_ip_address(self): + """ + NOTE: Incidently, this will also give the MAC address, as well as other + attributes... + """ + def parse(rv): + attrs = dict() + + assert rv is not None + + nds = list(parse_ip_addr(rv.stdout)) + + assert nds + assert len(nds) <= 1 + + nd = nds[0] + + assert nd['name'] == self.device_name + + attrs['mac_address'] = nd['hardware-address'] + + # We assume a single IPv4 address for now... + ips = [ip for ip in nd['ip-addresses'] + if ip['ip-address-type'] == 'ipv4'] + if len(ips) >= 1: + if len(ips) > 1: + log.warning('Keeping only first of many IP addresses...') + ip = ips[0] + attrs['ip_address'] = ip['ip-address'] + else: + attrs['ip_address'] = None + return attrs + + return BashTask(self.node, CMD_GET_IP_ADDRESS, + {'netdevice': self}, parse=parse) + + def _set_ip_address(self): + if self.ip_address is None: + # Unset IP + return BashTask(self.node, CMD_FLUSH_IP, + {'device_name': self.device_name}) + return BashTask(self.node, CMD_SET_IP_ADDRESS, + {'netdevice': self}) + + @task + def _get_promiscuous(self): + return {'promiscuous': False} + + def _set_promiscuous(self): + on_off = 'on' if self.promiscuous else 'off' + return BashTask(self.node, CMD_SET_PROMISC, + {'netdevice': self, 'on_off' : on_off}) + + @task + def _get_up(self): + return {'up': False} + + def _set_up(self): + up_down = 'up' if self.up else 'down' + return BashTask(self.node, CMD_SET_UP, + {'netdevice': self, 'up_down': up_down}) + + @task + def _get_capacity(self): + return {'capacity': None} + + def _set_capacity(self): + if self.capacity is None: + log.warning('set_capacity(None) not implemented') + return EmptyTask() + + # http://unix.stackexchange.com/questions/100785/bucket-size-in-tbf + MBPS = 1000000 + KBPS = 1024 + BYTES = 8 + HZ = 250 + + # Round to power of two... see manpage + burst = math.ceil((((self.capacity * MBPS) / HZ) / BYTES) / KBPS) + burst = 1 << (burst - 1).bit_length() + + return BashTask(self.node, CMD_SET_CAPACITY, + {'netdevice': self, 'burst': burst}) + + def _get_rp_filter(self): + def parse(rv): + lines = rv.stdout.splitlines() + return (int(lines[0][-1]) + int(lines[1][-1]) > 0) + return BashTask(self.node, CMD_GET_RP_FILTER, {'netdevice' :self}, + parse = parse) + + def _set_rp_filter(self): + cmd = CMD_SET_RP_FILTER if self.rp_filter else CMD_UNSET_RP_FILTER + return BashTask(self.node, cmd, {'netdevice' : self}) + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + def _remote_node_name(self): + remote_interface = self._remote_interface() + if remote_interface: + return remote_interface.node.name + else: + rnd = ''.join(random.choice(string.ascii_uppercase + string.digits) + for _ in range(3)) + return 'unk{}'.format(rnd) + + def _remote_interface(self): + if not self.channel: + return None + interfaces = self.channel.interfaces + for interface in interfaces: + if interface == self: + continue + return interface + + def _default_device_name(self): + remote_node_name = self._remote_node_name() + if remote_node_name: + return remote_node_name + else: + return AddressManager().get('device_name', self, + prefix = self.prefix, scope = self.prefix) + +#------------------------------------------------------------------------------ + +class NonTapBaseNetDevice(BaseNetDevice): + # Tap devices for instance don't have offload + offload = Attribute(Bool, description = 'Offload', default=True) + + #-------------------------------------------------------------------------- + # Attributes + #-------------------------------------------------------------------------- + + def _get_offload(self): + return BashTask(self.node, CMD_GET_OFFLOAD, {'netdevice': self}, + parse = lambda rv : rv.stdout.strip() == 'on') + + def _set_offload(self): + cmd = None + if self.offload: + cmd = CMD_SET_OFFLOAD + else: + cmd = CMD_UNSET_OFFLOAD + return BashTask(self.node, cmd, {'netdevice' : self}) + +#------------------------------------------------------------------------------ + +class NetDevice(NonTapBaseNetDevice): + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __create__(self): + return BashTask(self.node, CMD_CREATE, {'netdevice': self}) + +#------------------------------------------------------------------------------ + +class SlaveBaseNetDevice(BaseNetDevice): + parent = Attribute(NetDevice, description = 'Parent NetDevice') + + host = Attribute(NetDevice, description = 'Host interface', + default = lambda x : x._default_host()) + + def _default_host(self): + if self.node.__class__.__name__ == 'LxcContainer': + host = self.node.node + else: + host = self.node + max_len = MAX_DEVICE_NAME_SIZE - len(self.node.name) - 1 + device_name = self.device_name[:max_len] + + return NetDevice(node = host, + device_name = '{}-{}'.format(self.node.name, device_name), + managed = False) + +#------------------------------------------------------------------------------ + +class SlaveNetDevice(SlaveBaseNetDevice): + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __create__(self): + return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self}) diff --git a/vicn/resource/linux/netmon.py b/vicn/resource/linux/netmon.py new file mode 100644 index 00000000..8472f308 --- /dev/null +++ b/vicn/resource/linux/netmon.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from vicn.resource.linux.service import Service + +class NetMon(Service): + """ + Resource: NetMon + + Generic network monitoring daemon, used internally by VICN for resource + monitoring. + """ + __package_names__ = ['netmon'] + __service_name__ = 'netmon' diff --git a/vicn/resource/linux/numa_mgr.py b/vicn/resource/linux/numa_mgr.py new file mode 100644 index 00000000..632264ce --- /dev/null +++ b/vicn/resource/linux/numa_mgr.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import re +from itertools import cycle + +from netmodel.model.type import BaseType +from vicn.core.resource import Resource +from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.task import BashTask +from vicn.resource.node import Node + +PATTERN_LSCPU_NUMA = 'NUMA node[0-9]+ CPU\(s\)' +CMD_LSCPU = 'lscpu' + +class CycleType(BaseType, cycle): + """ + Type: CycleType + """ + pass + +#------------------------------------------------------------------------------ + +def parse_lscpu_line(line): + #Format: NUMA node0 CPU(s): 0-17,36-53 + line = line.split(':')[1] + #line = 0-17,36,53 + + def limits_to_list(string): + limits = string.split('-') + lower_limit = int(limits[0]) + #Removes core 0 as it is used the most often by the kernl + if lower_limit is 0 : lower_limit = 1 + return cycle(range(lower_limit, int(limits[1]))) + return cycle(map(limits_to_list, line.split(','))) + +def parse_lscpu_rv(rv): + ret = [] + for line in rv.stdout.splitlines(): + if re.search(PATTERN_LSCPU_NUMA, line): + ret.append(parse_lscpu_line(line)) + return ret + +#------------------------------------------------------------------------------ + +class NumaManager(Resource): + """ + Resource: NumaManager + """ + + node = Attribute(Node, + mandatory = True, + multiplicity = Multiplicity.OneToOne, + reverse_auto = True, + reverse_name = 'numa_mgr') + numa_repartitor = Attribute(CycleType, + description = 'Tool to separate cores/CPUs/sockets', + multiplicity = Multiplicity.OneToMany, + ro = True) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + __create__ = None + __delete__ = None + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.current_numa_node = 0 + + #-------------------------------------------------------------------------- + # Attributes + #-------------------------------------------------------------------------- + + def _get_numa_repartitor(self): + return BashTask(self.node, CMD_LSCPU, parse=parse_lscpu_rv) + + #-------------------------------------------------------------------------- + # Public API + #-------------------------------------------------------------------------- + + def get_numa_core(self, numa_node=None): + if numa_node is None: + numa_node = self.current_numa_node + self.current_numa_node = (self.current_numa_node+1) % \ + len(self.numa_repartitor) + numa_list = self.numa_repartitor[numa_node] + + socket = next(numa_list) + return numa_node, next(socket) + + def get_number_of_numa(self): + return len(self.numa_repartitor) diff --git a/vicn/resource/linux/ovs.py b/vicn/resource/linux/ovs.py new file mode 100644 index 00000000..d67e4bca --- /dev/null +++ b/vicn/resource/linux/ovs.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from vicn.core.task import BashTask +from vicn.resource.linux.bridge_mgr import BridgeManager + +CMD_ADD_BRIDGE = ''' +ovs-vsctl --may-exist add-br {bridge_name} +ip link set dev {bridge_name} up +''' + +CMD_DEL_BRIDGE = 'ovs-vsctl --if-exists del-br {bridge_name}' + +CMD_ADD_INTERFACE = 'ovs-vsctl --may-exist add-port {bridge_name} ' \ + '{interface_name}' +CMD_ADD_INTERFACE_VLAN = CMD_ADD_INTERFACE + ' tag={vlan}' +CMD_DEL_INTERFACE = 'ovs-vsctl --if-exists del-port {bridge_name} ' \ + '{interface_name}' + +class OVS(BridgeManager): + """ + Resource: OVS + + OpenVSwitch bridge manager + """ + + __package_names__ = ['openvswitch-switch'] + + #--------------------------------------------------------------------------- + # BridgeManager API + #--------------------------------------------------------------------------- + + def add_bridge(self, bridge_name): + return BashTask(self.node, CMD_ADD_BRIDGE, + {'bridge_name': bridge_name}, + output = False, as_root = True) + + def del_bridge(self, bridge_name): + return BashTask(self.node, CMD_DEL_BRIDGE, + {'bridge_name': bridge_name}, + output = False, as_root = True) + + def add_interface(self, bridge_name, interface_name, vlan=None): + cmd = CMD_ADD_INTERFACE_VLAN if vlan is not None else CMD_ADD_INTERFACE + return BashTask(self.node, cmd, {'bridge_name': bridge_name, + 'interface_name': interface_name, 'vlan': vlan}, + output = False, as_root = True) + + def del_interface(self, bridge_name, interface_name, vlan=None): + return BashTask(self.node, CMD_DEL_INTERFACE, + {'bridge_name': bridge_name, 'interface_name': interface_name, + 'vlan': vlan}, + output = False, as_root = True) diff --git a/vicn/resource/linux/package_manager.py b/vicn/resource/linux/package_manager.py new file mode 100644 index 00000000..cb149ac6 --- /dev/null +++ b/vicn/resource/linux/package_manager.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import asyncio +import logging + +from netmodel.model.type import String +from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.exception import ResourceNotFound +from vicn.core.requirement import Requirement +from vicn.core.resource import Resource +from vicn.core.task import BashTask, EmptyTask, async_task +from vicn.core.task import inline_task, run_task +from vicn.resource.node import Node + +log = logging.getLogger(__name__) + +CMD_APT_GET_KILL = 'kill -9 $(pidof apt-get) || true' +CMD_DPKG_CONFIGURE_A = 'dpkg --configure -a' + +CMD_APT_GET_UPDATE = ''' +# Force IPv4 +echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4 +# Update package repository on node {node} +apt-get update +''' + +# We need to double { } we want to preserve +CMD_PKG_TEST='dpkg -s {self.package_name}' + +CMD_PKG_INSTALL=''' +# Installing package {package_name} +apt-get -y --allow-unauthenticated install {package_name} +''' + +CMD_PKG_UNINSTALL=''' +# Uninstalling package {self.package_name} +apt-get remove {self.package_name} +''' + +CMD_SETUP_REPO = ''' +# Initialize package repository {repository.repo_name} on node {self.node.name} +echo "{deb_source}" > {path} +''' + +class PackageManager(Resource): + """ + Resource: PackageManager + + APT package management wrapper. + + Todo: + - We assume a package manager is always installed on every machine. + - Currently, we limit ourselves to debian/ubuntu, and voluntarily don't + subclass this as we have (so far) no code for selecting the right + subclass, eg choising dynamically between DebRepositoryManager and + RpmRepositoryManager. + - We currently don't use package version numbers, which means a package + can be installed but not be up to date. + """ + + node = Attribute(Node, + reverse_name = 'package_manager', + reverse_auto = True, + mandatory = True, + multiplicity = Multiplicity.OneToOne) + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._up_to_date = False + self.apt_lock = asyncio.Lock() + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __after__(self): + if self.node.__class__.__name__ == 'Physical': + # UGLY : This blocking code is currently needed + task = self.node.host_interface._get_ip_address() + ip_dict = task.execute_blocking() + self.node.host_interface.ip_address = ip_dict['ip_address'] + return ('Repository',) + else: + return ('Repository', 'CentralIP', 'RoutingTable') + + @inline_task + def __get__(self): + raise ResourceNotFound + + def __create__(self): + repos = EmptyTask() + for repository in self._state.manager.by_type_str('Repository'): + deb_source = self._get_deb_source(repository) + path = self._get_path(repository) + repo = BashTask(self.node, CMD_SETUP_REPO, + {'deb_source': deb_source, 'path': path}) + repos = repos | repo + + return repos + + #--------------------------------------------------------------------------- + # Methods + #--------------------------------------------------------------------------- + + def __method_update__(self): + kill = BashTask(self.node, CMD_APT_GET_KILL, {'node': self.node.name}, + lock = self.apt_lock) + + # Setup during a reattempt + if hasattr(self, '_dpkg_configure_a'): + dpkg_configure_a = BashTask(self.node, CMD_DPKG_CONFIGURE_A, + lock = self.apt_lock) + else: + dpkg_configure_a = EmptyTask() + + if not self.node.package_manager._up_to_date: + update = BashTask(self.node, CMD_APT_GET_UPDATE, {'node': self.node.name}, + lock = self.apt_lock, post = self._mark_updated) + else: + update = EmptyTask() + + return (kill > dpkg_configure_a) > update + + def __method_install__(self, package_name): + update = self.__method_update__() + install = BashTask(self.node, CMD_PKG_INSTALL, {'package_name': + package_name}, lock = self.apt_lock) + return update > install + + #--------------------------------------------------------------------------- + # Internal methods + #--------------------------------------------------------------------------- + + def _mark_updated(self): + self._up_to_date = True + + def _get_path(self, repository): + return '/etc/apt/sources.list.d/{}.list'.format(repository.repo_name) + + def _get_deb_source(self, repository): + path = repository.node.host_interface.ip_address + '/' + if repository.directory: + path += repository.directory + '/' + return 'deb http://{} {}/'.format(path, self.node.dist) + +#------------------------------------------------------------------------------ + +class Package(Resource): + """ + Resource: Package + + deb package support + """ + + package_name = Attribute(String, mandatory = True) + node = Attribute(Node, + mandatory = True, + requirements=[ + Requirement('package_manager') + ]) + + #--------------------------------------------------------------------------- + # Resource lifecycle + #--------------------------------------------------------------------------- + + def __get__(self): + return BashTask(self.node, CMD_PKG_TEST, {'self': self}) + + def __create__(self): + return self.node.package_manager.__method_install__(self.package_name) + + @async_task + async def __delete__(self): + with await self.node.package_manager._lock: + task = BashTask(self.node, CMD_PKG_UNINSTALL, {'self': self}) + ret = await run_task(task, self._state.manager) + return ret + +#------------------------------------------------------------------------------ + +class Packages(Resource): + """ + Resource: Packages + + Todo: + - The number of concurrent subresources is not dynamically linked to the + nodes. We may need to link subresources to the attribute in general, but + since package_names are static for a resource, this is not a problem here. + """ + names = Attribute(String, multiplicity = Multiplicity.OneToMany) + node = Attribute(Node, + mandatory = True, + requirements=[ + Requirement('package_manager') + ]) + + #--------------------------------------------------------------------------- + # Resource lifecycle + #--------------------------------------------------------------------------- + + def __subresources__(self): + """ + Note: Although packages are (rightfully) specified concurrent, apt tasks + will be exlusive thanks to the use of a lock in the package manager. + """ + if self.names: + packages = [Package(node=self.node, package_name=name, owner=self) + for name in self.names] + return Resource.__concurrent__(*packages) + else: + return None + diff --git a/vicn/resource/linux/phy_interface.py b/vicn/resource/linux/phy_interface.py new file mode 100644 index 00000000..c1aef27e --- /dev/null +++ b/vicn/resource/linux/phy_interface.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from netmodel.model.type import String +from vicn.core.attribute import Attribute +from vicn.core.resource import BaseResource +from vicn.resource.interface import Interface + +class PhyInterface(Interface): + """ + Resource: PhyInterface + + Physical network interface. + """ + + __type__ = BaseResource + + device_name = Attribute(String, description = 'Name of the DpdkDevice', + mandatory = True) + pci_address = Attribute(String, description = "Device's PCI bus address", + mandatory = True) + mac_address = Attribute(String, description = "Device's MAC address", + mandatory=True) + ip_address = Attribute(String, description = "Device's IP address") + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if not self.name: + self.name = self.node.name + '-' + self.device_name diff --git a/vicn/resource/linux/phy_link.py b/vicn/resource/linux/phy_link.py new file mode 100644 index 00000000..878cf7c6 --- /dev/null +++ b/vicn/resource/linux/phy_link.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from vicn.core.attribute import Attribute +from vicn.core.task import inline_task +from vicn.resource.channel import Channel +from vicn.resource.linux.phy_interface import PhyInterface + +class PhyLink(Channel): + """ + Resource: PhyLink + + Physical Link to inform the orchestrator about Layer2 connectivity. + """ + + src = Attribute(PhyInterface, description = 'Source interface', + mandatory = True) + dst = Attribute(PhyInterface, description = 'Destination interface', + mandatory = True) + + @inline_task + def __initialize__(self): + self.src.set('channel', self) + self.dst.set('channel', self) diff --git a/vicn/resource/linux/physical.py b/vicn/resource/linux/physical.py new file mode 100644 index 00000000..e5eba2d3 --- /dev/null +++ b/vicn/resource/linux/physical.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import stat +import logging +import subprocess +import shlex + +from netmodel.model.type import String, Integer +from netmodel.util.misc import is_local_host +from netmodel.util.socket import check_port +from vicn.core.attribute import Attribute +from vicn.core.commands import Command, ReturnValue +from vicn.core.exception import ResourceNotFound +from vicn.core.task import Task, task +from vicn.resource.node import Node, DEFAULT_USERNAME +from vicn.resource.node import DEFAULT_SSH_PUBLIC_KEY +from vicn.resource.node import DEFAULT_SSH_PRIVATE_KEY + +log = logging.getLogger(__name__) + +CMD_SSH_COPY_ID = 'ssh-copy-id {ssh_options} -i {public_key} -p {port} ' \ + '{user}@{host}' +CMD_SSH = 'ssh {ssh_options} -i {private_key} -p {port} ' \ + '{user}@{host} {command}' +CMD_SSH_NF = 'ssh -n -f {ssh_options} -i {private_key} -p {port} ' \ + '{user}@{host} {command}' + +class Physical(Node): + """ + Resource: Physical + + Physical node + """ + + hostname = Attribute(String, description = 'Hostname or IP address', + mandatory = True) + ssh_port = Attribute(Integer, description = 'SSH port number', + default = 22) + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.node_with_kernel = self + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @task + def __get__(self, attributes=None): + if not check_port(self.hostname, self.ssh_port): + raise ResourceNotFound + + def __create__(self): + tasks = list() + modes = (True, False) if DEFAULT_USERNAME != 'root' else (True,) + for as_root in modes: + tasks.append(self._setup_ssh_key(as_root)) + return Task.__concurrent__(*tasks) + + __delete__ = None + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + @task + def _setup_ssh_key(self, as_root): + os.chmod(DEFAULT_SSH_PUBLIC_KEY, stat.S_IRUSR | stat.S_IWUSR) + os.chmod(DEFAULT_SSH_PRIVATE_KEY, stat.S_IRUSR | stat.S_IWUSR) + cmd_params = { + 'public_key' : DEFAULT_SSH_PUBLIC_KEY, + 'ssh_options' : '', + 'port' : self.ssh_port, + 'user' : 'root' if as_root else DEFAULT_USERNAME, + 'host' : self.hostname, + } + + c = Command(CMD_SSH_COPY_ID, parameters = cmd_params) + + return self._do_execute_process(c.full_commandline, output=False) + + #-------------------------------------------------------------------------- + # Public API + #-------------------------------------------------------------------------- + + def _do_execute_process(self, command, output = False): + p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) + if output: + out, err = p.communicate() + return ReturnValue(p.returncode, stdout=out, stderr=err) + + p.wait() + return ReturnValue(p.returncode) + + def _do_execute_ssh(self, command, output=False, as_root=False, + ssh_options=None): + if not ssh_options: + ssh_options = dict() + cmd_params = { + 'private_key' : DEFAULT_SSH_PRIVATE_KEY, + 'ssh_options' : ' '.join(['-o {}={}'.format(k, v) + for k, v in ssh_options.items()]), + 'port' : self.ssh_port, + 'user' : 'root' if as_root else DEFAULT_USERNAME, + 'host' : self.hostname, + 'command' : shlex.quote(command), + } + + if (command[-1] != '&'): + c = Command(CMD_SSH, parameters = cmd_params) + else: + c = Command(CMD_SSH_NF, parameters = cmd_params) + + return self._do_execute_process(c.full_commandline_nobashize, output) + + def execute(self, command, output=False, as_root=False): + if is_local_host(self.hostname): + rv = self._do_execute_process(command, output = output) + else: + rv = self._do_execute_ssh(command, output = output, + as_root = as_root) + return rv diff --git a/vicn/resource/linux/repository.py b/vicn/resource/linux/repository.py new file mode 100644 index 00000000..f3e70565 --- /dev/null +++ b/vicn/resource/linux/repository.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from netmodel.model.type import String +from vicn.core.attribute import Attribute, Multiplicity +from vicn.resource.application import Application + +class Repository(Application): + """ + Resource: Repository + + deb package repository + + Note: As PackageManager uses a Repository, this resource cannot be a + LinuxApplication resource. We have no package to install since they are + part of any basic distribution install. + """ + + repo_name = Attribute(String, description = 'Name of the repository', + default = 'vicn') + directory = Attribute(String, description = 'Directory holding packages', + default = '') + distributions = Attribute(String, + description = 'List of distributions served by this repository', + multiplicity = Multiplicity.ManyToMany, + default = ['sid', 'trusty', 'xenial']) diff --git a/vicn/resource/linux/service.py b/vicn/resource/linux/service.py new file mode 100644 index 00000000..3eb753fc --- /dev/null +++ b/vicn/resource/linux/service.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging + +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import CategoryResource +from vicn.core.task import inline_task, BashTask, EmptyTask +from vicn.resource.linux.application import LinuxApplication + +log = logging.getLogger(__name__) + +CMD_START = 'service {service_name} start' +CMD_STOP = 'service {service_name} stop' +CMD_RESTART = 'service {service_name} restart' +CMD_STOP_START = 'service {service_name} stop && sleep 1 && ' \ + 'service {service_name} start' + +class Service(LinuxApplication): + """Service resource + + This resource wraps a Linux Service, and ensure the service is started + (resp. stopped) during setup (resp. teardown). + + Required tags: + __service_name__ (str): all classes that inherit from Service should + inform this tag which gives the name of the service known to the + system. + + TODO: + * Support for upstart, sysvinit and systemd services. + * Start and Stop method + * Status attribute + """ + + __type__ = CategoryResource + + + + @inline_task + def __get__(self): + raise ResourceNotFound + + def __method_restart__(self): + return BashTask(self.node, CMD_RESTART, + {'service_name': self.__service_name__}) + + def __method_start__(self): + return BashTask(self.node, CMD_START, + {'service_name': self.__service_name__}) + + + def __create__(self): + if self.__service_name__ == 'lxd': + log.warning('Not restarting LXD') + return EmptyTask() + + if self.__service_name__ == 'dnsmasq': + return BashTask(self.node, CMD_STOP_START, + {'service_name': self.__service_name__}) + + return self.__method_restart__() + + + def __delete__(self): + return BashTask(self.node, CMD_STOP, + {'service_name': self.__service_name__}) + diff --git a/vicn/resource/linux/sym_veth_pair.py b/vicn/resource/linux/sym_veth_pair.py new file mode 100644 index 00000000..bf79a69b --- /dev/null +++ b/vicn/resource/linux/sym_veth_pair.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import random +import string + +from netmodel.model.type import Integer +from vicn.core.attribute import Attribute +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import Resource +from vicn.core.state import ResourceState, AttributeState +from vicn.core.task import BashTask, get_attributes_task +from vicn.core.task import async_task, task, inline_task +from vicn.core.task import run_task +from vicn.resource.interface import Interface +from vicn.resource.node import Node +from vicn.resource.linux.net_device import NonTapBaseNetDevice +from vicn.resource.linux.link import CMD_DELETE_IF_EXISTS +from vicn.resource.linux.link import CMD_UP + +CMD_CREATE=''' +# Create veth pair in the host node +ip link add name {tmp_side1} type veth peer name {tmp_side2} +ip link set dev {tmp_side1} netns {pid[0]} name {side1_device_name} +ip link set dev {tmp_side2} netns {pid[1]} name {side2_device_name} +''' + +class SymVethPair(Resource): + """ + Resource: SymVethPair + + This resource is used in VPPBridge. The main difference with the Link + resource is that is it not a channel. + """ + + node1 = Attribute(Node, + description = 'Node on which one side of the veth will sit', + mandatory = True) + node2 = Attribute(Node, + description = 'Node on which the other side of the veth will sit', + mandatory = True) + capacity = Attribute(Integer, + description = 'Capacity of the veth pair (Mb/s)') + side1 = Attribute(Interface, description = 'Source interface') + side2 = Attribute(Interface, description = 'Destination interface') + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + async def _commit(self): + # see link.py for explanations + manager = self._state.manager + await manager._set_resource_state(self.side1, + ResourceState.INITIALIZED) + await manager._set_resource_state(self.side2, + ResourceState.INITIALIZED) + await manager._set_resource_state(self.side1, ResourceState.CREATED) + await manager._set_resource_state(self.side2, ResourceState.CREATED) + await manager._set_attribute_state(self, 'side1', AttributeState.CLEAN) + await manager._set_attribute_state(self, 'side2', AttributeState.CLEAN) + manager.commit_resource(self.side1) + manager.commit_resource(self.side2) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inline_task + def __initialize__(self): + self.side1 = NonTapBaseNetDevice(node = self.node1, + device_name = self.node2.name, + capacity = self.capacity, + owner = self.owner) + self.side2 = NonTapBaseNetDevice(node = self.node2, + device_name = self.node1.name, + capacity = self.capacity, + owner = self.owner) + self.side1.remote = self.side2 + self.side2.remote = self.side1 + + @async_task + async def __get__(self): + manager = self._state.manager + + try: + await run_task(self.side1.__get__(), manager) + await run_task(self.side2.__get__(), manager) + except ResourceNotFound: + raise ResourceNotFound + + await self._commit() + + def __create__(self): + assert self.node1.get_type() == 'lxccontainer' + assert self.node2.get_type() == 'lxccontainer' + + node1_host = self.node1.node + node2_host = self.node2.node + + assert node1_host == node2_host + host = node1_host + + # Sometimes a down interface persists on one side + delif_side1 = BashTask(self.node1, CMD_DELETE_IF_EXISTS, + {'interface': self.side1}) + delif_side2 = BashTask(self.node2, CMD_DELETE_IF_EXISTS, + {'interface': self.side2}) + + pid_node1 = get_attributes_task(self.node1, ['pid']) + pid_node2 = get_attributes_task(self.node2, ['pid']) + + tmp_side1 = 'tmp-veth-' + ''.join(random.choice( + string.ascii_uppercase + string.digits) for _ in range(5)) + tmp_side2 = 'tmp-veth-' + ''.join(random.choice( + string.ascii_uppercase + string.digits) for _ in range(5)) + + create = BashTask(host, CMD_CREATE, + {'side1_device_name': self.side1.device_name, + 'side2_device_name': self.side2.device_name, + 'tmp_side1': tmp_side1, 'tmp_side2': tmp_side2}) + + up_side1 = BashTask(self.node1, CMD_UP, {'interface': self.side1}) + up_side2 = BashTask(self.node2, CMD_UP, {'interface': self.side2}) + + @async_task + async def set_state(): + await self._commit() + + delif = delif_side1 | delif_side2 + up = up_side1 | up_side2 + pid = pid_node1 | pid_node2 + return ((delif > (pid @ create)) > up) > set_state() + + def __delete__(self): + raise NotImplementedError diff --git a/vicn/resource/linux/tap_device.py b/vicn/resource/linux/tap_device.py new file mode 100644 index 00000000..68c0b9c7 --- /dev/null +++ b/vicn/resource/linux/tap_device.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from netmodel.model.type import String +from vicn.core.attribute import Attribute +from vicn.core.task import BashTask +from vicn.resource.linux.net_device import BaseNetDevice + +CMD_CREATE='ip tuntap add name {netdevice.device_name} mode tap' +CMD_FLUSH_IP='ip addr flush dev {device_name}' +CMD_SET_IP_ADDRESS='ip addr add dev {netdevice.device_name} 0.0.0.0' + +class TapDevice(BaseNetDevice): + + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + self.prefix = 'tap' + self.netdevice_type = 'tap' + + def __create__(self): + return BashTask(self.node, CMD_CREATE, {'netdevice': self}) + + def _set_ip_address(self): + if self.ip_address is None: + # Unset IP + return BashTask(self.node, CMD_FLUSH_IP, + {'device_name': self.device_name}) + return BashTask(self.node, CMD_SET_IP_ADDRESS, + {'netdevice': self}) + +class TapChannel(TapDevice): + station_name = Attribute(String) + channel_name = Attribute(String) diff --git a/vicn/resource/linux/traceroute.py b/vicn/resource/linux/traceroute.py new file mode 100644 index 00000000..2f8cb2c9 --- /dev/null +++ b/vicn/resource/linux/traceroute.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from vicn.resource.linux.application import LinuxApplication + +class Traceroute(LinuxApplication): + __package_names__ = ['traceroute'] + diff --git a/vicn/resource/linux/veth_pair.py b/vicn/resource/linux/veth_pair.py new file mode 100644 index 00000000..53fa9bf8 --- /dev/null +++ b/vicn/resource/linux/veth_pair.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import random +import string + +from vicn.resource.linux.net_device import SlaveBaseNetDevice +from vicn.core.task import BashTask, get_attributes_task + +# ip link add veth0 type veth peer name veth1 + +CMD_CREATE=''' +# Create veth pair in the host node +ip link add name {interface.host.device_name} type veth peer name {tmp_name} +# The host interface will always be up... +ip link set dev {interface.host.device_name} up +# Move interface into container and rename it +ip link set dev {tmp_name} netns {pid} name {interface.device_name} +''' +CMD_UP=''' +ip link set dev {interface.device_name} up +''' + +# see: +# http://stackoverflow.com/questions/22780927/lxc-linux-containers-add-new-network-interface-without-restarting + +class VethPair(SlaveBaseNetDevice): + # Do not need the parent attribute... + + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + self.prefix = 'veth' + self.netdevice_type = 'veth' + + def __create__(self): + assert self.node.__class__.__name__ == 'LxcContainer' + host = self.node.node + pid = get_attributes_task(self.node, ['pid']) + tmp_name = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase \ + + string.digits) for _ in range(5)) + create = BashTask(host, CMD_CREATE, {'tmp_name': tmp_name, + 'interface': self}) + up = BashTask(self.node, CMD_UP, {'interface': self}) + bridge = host.bridge_manager.add_interface(host.bridge.device_name, + self.host.device_name) + return ((pid @ create) > up) > bridge + + # ... IP and UP missing... -- cgit 1.2.3-korg