aboutsummaryrefslogtreecommitdiffstats
path: root/vicn/resource/linux
diff options
context:
space:
mode:
Diffstat (limited to 'vicn/resource/linux')
-rw-r--r--vicn/resource/linux/__init__.py0
-rw-r--r--vicn/resource/linux/application.py56
-rw-r--r--vicn/resource/linux/bridge.py104
-rw-r--r--vicn/resource/linux/bridge_mgr.py34
-rw-r--r--vicn/resource/linux/certificate.py74
-rw-r--r--vicn/resource/linux/dnsmasq.py118
-rw-r--r--vicn/resource/linux/file.py110
-rw-r--r--vicn/resource/linux/iperf.py27
-rw-r--r--vicn/resource/linux/link.py201
-rw-r--r--vicn/resource/linux/macvlan.py52
-rw-r--r--vicn/resource/linux/macvtap.py52
-rw-r--r--vicn/resource/linux/net_device.py519
-rw-r--r--vicn/resource/linux/netmon.py29
-rw-r--r--vicn/resource/linux/numa_mgr.py113
-rw-r--r--vicn/resource/linux/ovs.py68
-rw-r--r--vicn/resource/linux/package_manager.py232
-rw-r--r--vicn/resource/linux/phy_interface.py49
-rw-r--r--vicn/resource/linux/phy_link.py39
-rw-r--r--vicn/resource/linux/physical.py144
-rw-r--r--vicn/resource/linux/repository.py41
-rw-r--r--vicn/resource/linux/service.py83
-rw-r--r--vicn/resource/linux/sym_veth_pair.py151
-rw-r--r--vicn/resource/linux/tap_device.py48
-rw-r--r--vicn/resource/linux/traceroute.py23
-rw-r--r--vicn/resource/linux/veth_pair.py62
25 files changed, 2429 insertions, 0 deletions
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...