summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/__init__.py2
-rw-r--r--python/vyos/airbag.py168
-rw-r--r--python/vyos/authutils.py2
-rw-r--r--python/vyos/debug.py182
-rw-r--r--python/vyos/defaults.py1
-rw-r--r--python/vyos/dicts.py50
-rw-r--r--python/vyos/ifconfig/__init__.py1
-rw-r--r--python/vyos/ifconfig/bond.py3
-rw-r--r--python/vyos/ifconfig/bridge.py3
-rw-r--r--python/vyos/ifconfig/control.py13
-rw-r--r--python/vyos/ifconfig/dhcp.py194
-rw-r--r--python/vyos/ifconfig/ethernet.py2
-rw-r--r--python/vyos/ifconfig/input.py31
-rw-r--r--python/vyos/ifconfig/interface.py46
-rw-r--r--python/vyos/ifconfig/macvlan.py11
-rw-r--r--python/vyos/ifconfig/section.py (renamed from python/vyos/ifconfig/register.py)72
-rw-r--r--python/vyos/ifconfig/stp.py2
-rw-r--r--python/vyos/ifconfig/tunnel.py16
-rw-r--r--python/vyos/ifconfig/vti.py31
-rw-r--r--python/vyos/ifconfig/vxlan.py25
-rw-r--r--python/vyos/ifconfig_vlan.py22
-rw-r--r--python/vyos/ioctl.py8
-rw-r--r--python/vyos/remote.py15
-rw-r--r--python/vyos/template.py65
-rw-r--r--python/vyos/util.py158
-rw-r--r--python/vyos/version.py15
26 files changed, 877 insertions, 261 deletions
diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py
index 9b5ed21c9..e3e14fdd8 100644
--- a/python/vyos/__init__.py
+++ b/python/vyos/__init__.py
@@ -1 +1 @@
-from .base import *
+from .base import ConfigError
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
new file mode 100644
index 000000000..b0565192d
--- /dev/null
+++ b/python/vyos/airbag.py
@@ -0,0 +1,168 @@
+# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import logging
+import logging.handlers
+from datetime import datetime
+
+from vyos import debug
+from vyos.config import Config
+from vyos.version import get_version
+from vyos.util import run
+
+
+# we allow to disable the extra logging
+DISABLE = False
+
+
+# emulate a file object
+class _IO(object):
+ def __init__(self, std, log):
+ self.std = std
+ self.log = log
+
+ def write(self, message):
+ self.std.write(message)
+ if DISABLE:
+ return
+ for line in message.split('\n'):
+ s = line.rstrip()
+ if s:
+ self.log(s)
+
+ def flush(self):
+ self.std.flush()
+
+ def close(self):
+ pass
+
+
+# The function which will be used to report information
+# to users when an exception is unhandled
+def bug_report(dtype, value, trace):
+ from traceback import format_exception
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ information = {
+ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'version': get_version(),
+ 'trace': format_exception(dtype, value, trace),
+ 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ }
+
+ sys.stdout.write(INTRO.format(**information))
+ sys.stdout.flush()
+
+ sys.stderr.write(FAULT.format(**information))
+ sys.stderr.flush()
+
+
+# define an exception handler to be run when an exception
+# reach the end of __main__ and was not intercepted
+def intercepter(dtype, value, trace):
+ bug_report(dtype, value, trace)
+ if debug.enabled('developer'):
+ import pdb
+ pdb.pm()
+
+
+def InterceptingLogger(address, _singleton=[False]):
+ skip = _singleton.pop()
+ _singleton.append(True)
+ if skip:
+ return
+
+ logger = logging.getLogger('VyOS')
+ logger.setLevel(logging.DEBUG)
+ handler = logging.handlers.SysLogHandler(address='/dev/log', facility='syslog')
+ logger.addHandler(handler)
+
+ # log to syslog any message sent to stderr
+ sys.stderr = _IO(sys.stderr, logger.critical)
+
+
+# lists as default arguments in function is normally dangerous
+# as they will keep any modification performed, unless this is
+# what you want to do (in that case to only run the code once)
+def InterceptingException(excepthook,_singleton=[False]):
+ skip = _singleton.pop()
+ _singleton.append(True)
+ if skip:
+ return
+
+ # install the handler to replace the default behaviour
+ # which just prints the exception trace on screen
+ sys.excepthook = excepthook
+
+
+# Do not attempt the extra logging for operational commands
+try:
+ # This fails during boot
+ insession = Config().in_session()
+except:
+ # we save info on boot to help debugging
+ insession = True
+
+
+# Installing the interception, it currently does not work when
+# running testing so we are checking that we are on the router
+# as otherwise it prevents dpkg-buildpackage to work
+if get_version() and insession:
+ InterceptingLogger('/run/systemd/journal/dev-log')
+ InterceptingException(intercepter)
+
+
+# Messages to print
+
+FAULT = """\
+Date: {date}
+VyOS image: {version}
+
+{trace}
+"""
+
+INTRO = """\
+VyOS had an issue completing a command.
+
+We are sorry that you encountered a problem with VyOS.
+There are a few things you can do to help us (and yourself):
+{instructions}
+
+PLEASE, when reporting, do include as much information as you can:
+- do not obfuscate any data (feel free to send us a private communication with
+ the extra information if your business policy is strict on information sharing)
+- and include all the information presented below
+
+"""
+
+COMMUNITY = """\
+- Make sure you are running the latest version of the code available at
+ https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso
+- Consult the forum to see how to handle this issue
+ https://forum.vyos.io
+- Join our community on slack where our users exchange help and advice
+ https://vyos.slack.com
+""".strip()
+
+SUPPORTED = """\
+- Make sure you are running the latest stable version of VyOS
+ the code is available at https://downloads.vyos.io/?dir=release/current
+- Contact us on our online help desk
+ https://support.vyos.io/
+""".strip()
diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py
index 90a46ffb4..66b5f4a74 100644
--- a/python/vyos/authutils.py
+++ b/python/vyos/authutils.py
@@ -22,7 +22,7 @@ def make_password_hash(password):
""" Makes a password hash for /etc/shadow using mkpasswd """
mkpassword = 'mkpasswd --method=sha-512 --stdin'
- return cmd(mkpassword, input=password.encode(), timeout=5)
+ return cmd(mkpassword, input=password, timeout=5)
def split_ssh_public_key(key_string, defaultname=""):
""" Splits an SSH public key into its components """
diff --git a/python/vyos/debug.py b/python/vyos/debug.py
new file mode 100644
index 000000000..20090fb85
--- /dev/null
+++ b/python/vyos/debug.py
@@ -0,0 +1,182 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+
+
+def message(message, flag='', destination=sys.stdout):
+ """
+ print a debug message line on stdout if debugging is enabled for the flag
+ also log it to a file if the flag 'log' is enabled
+
+ message: the message to print
+ flag: which flag must be set for it to print
+ destination: which file like object to write to (default: sys.stdout)
+
+ returns if any message was logged or not
+ """
+ enable = enabled(flag)
+ if enable:
+ destination.write(_format(flag,message))
+
+ # the log flag is special as it logs all the commands
+ # executed to a log
+ logfile = _logfile('log', '/tmp/developer-log')
+ if not logfile:
+ return enable
+
+ try:
+ # at boot the file is created as root:vyattacfg
+ # at runtime the file is created as user:vyattacfg
+ # the default permission are 644
+ mask = os.umask(0o113)
+
+ with open(logfile, 'a') as f:
+ f.write(_format('log', message))
+ finally:
+ os.umask(mask)
+
+ return enable
+
+
+def enabled(flag):
+ """
+ a flag can be set by touching the file in /tmp or /config
+
+ The current flags are:
+ - developer: the code will drop into PBD on un-handled exception
+ - log: the code will log all command to a file
+ - ifconfig: when modifying an interface,
+ prints command with result and sysfs access on stdout for interface
+ - command: print command run with result
+
+ Having the flag setup on the filesystem is required to have
+ debuging at boot time, however, setting the flag via environment
+ does not require a seek to the filesystem and is more efficient
+ it can be done on the shell on via .bashrc for the user
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns either the file or environment name used to set it up
+ """
+
+ # this is to force all new flags to be registered here to be
+ # documented both here and a reminder to update readthedocs :-)
+ if flag not in ['developer', 'log', 'ifconfig', 'command']:
+ return ''
+
+ return _fromenv(flag) or _fromfile(flag)
+
+
+def _format(flag, message):
+ """
+ format a log message
+ """
+ return f'DEBUG/{flag.upper():<7} {message}\n'
+
+
+def _fromenv(flag):
+ """
+ check if debugging is set for this flag via environment
+
+ For a given debug flag named "test"
+ The presence of the environment VYOS_TEST_DEBUG (uppercase) enables it
+
+ return empty string if not
+ return content of env value it is
+ """
+
+ flagname = f'VYOS_{flag.upper()}_DEBUG'
+ flagenv = os.environ.get(flagname, None)
+
+ if flagenv is None:
+ return ''
+ return flagenv
+
+
+def _fromfile(flag):
+ """
+ Check if debug exist for a given debug flag name
+
+ Check is a debug flag was set by the user. the flag can be set either:
+ - in /tmp for a non-persistent presence between reboot
+ - in /config for always on (an existence at boot time)
+
+ For a given debug flag named "test"
+ The presence of the file vyos.test.debug (all lowercase) enables it
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns the full flagname
+ """
+
+ for folder in ('/tmp', '/config'):
+ flagfile = f'{folder}/vyos.{flag}.debug'
+ if os.path.isfile(flagfile):
+ return flagfile
+
+ return ''
+
+
+def _contentenv(flag):
+ return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip()
+
+
+def _contentfile(flag):
+ """
+ Check if debug exist for a given debug flag name
+
+ Check is a debug flag was set by the user. the flag can be set either:
+ - in /tmp for a non-persistent presence between reboot
+ - in /config for always on (an existence at boot time)
+
+ For a given debug flag named "test"
+ The presence of the file vyos.test.debug (all lowercase) enables it
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns the full flagname
+ """
+
+ for folder in ('/tmp', '/config'):
+ flagfile = f'{folder}/vyos.{flag}.debug'
+ if not os.path.isfile(flagfile):
+ continue
+ with open(flagfile) as f:
+ return f.readline().strip()
+
+ return ''
+
+
+def _logfile(flag, default):
+ """
+ return the name of the file to use for logging when the flag 'log' is set
+ if it could not be established or the location is invalid it returns
+ an empty string
+ """
+
+ # For log we return the location of the log file
+ log_location = _contentenv(flag) or _contentfile(flag)
+
+ # it was not set
+ if not log_location:
+ return ''
+
+ # Make sure that the logs can only be in /tmp, /var/log, or /tmp
+ if not log_location.startswith('/tmp/') and \
+ not log_location.startswith('/config/') and \
+ not log_location.startswith('/var/log/'):
+ return default
+ if '..' in log_location:
+ return default
+ return log_location
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index a2ad142bc..88894674f 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -21,6 +21,7 @@ directories = {
"current": "/opt/vyatta/etc/config-migrate/current",
"migrate": "/opt/vyatta/etc/config-migrate/migrate",
"log": "/var/log/vyatta",
+ "templates": "/usr/share/vyos/templates/"
}
cfg_group = 'vyattacfg'
diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py
new file mode 100644
index 000000000..79cab4a08
--- /dev/null
+++ b/python/vyos/dicts.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+class FixedDict(dict):
+ """
+ FixedDict: A dictionnary not allowing new keys to be created after initialisation.
+
+ >>> f = FixedDict(**{'count':1})
+ >>> f['count'] = 2
+ >>> f['king'] = 3
+ File "...", line ..., in __setitem__
+ raise ConfigError(f'Option "{k}" has no defined default')
+ """
+
+ def __init__(self, **options):
+ self._allowed = options.keys()
+ super().__init__(**options)
+
+ def __setitem__(self, k, v):
+ """
+ __setitem__ is a builtin which is called by python when setting dict values:
+ >>> d = dict()
+ >>> d['key'] = 'value'
+ >>> d
+ {'key': 'value'}
+
+ is syntaxic sugar for
+
+ >>> d = dict()
+ >>> d.__setitem__('key','value')
+ >>> d
+ {'key': 'value'}
+ """
+ if k not in self._allowed:
+ raise ConfigError(f'Option "{k}" has no defined default')
+ super().__setitem__(k, v)
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
index 1f9956af0..cd1696ca1 100644
--- a/python/vyos/ifconfig/__init__.py
+++ b/python/vyos/ifconfig/__init__.py
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+from vyos.ifconfig.section import Section
from vyos.ifconfig.interface import Interface
from vyos.ifconfig.bond import BondIf
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
index e2ff71490..47dd4ff34 100644
--- a/python/vyos/ifconfig/bond.py
+++ b/python/vyos/ifconfig/bond.py
@@ -18,7 +18,8 @@ import os
from vyos.ifconfig.interface import Interface
from vyos.ifconfig.vlan import VLAN
-from vyos.validate import *
+from vyos.validate import assert_list
+from vyos.validate import assert_positive
@Interface.register
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index 94b0075d8..44b92c1db 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -16,7 +16,8 @@
from vyos.ifconfig.interface import Interface
-from vyos.validate import *
+from vyos.validate import assert_boolean
+from vyos.validate import assert_positive
@Interface.register
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
index c7a2fa2d6..7bb63beed 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -16,12 +16,13 @@
import os
-from vyos.util import debug, debug_msg
-from vyos.util import popen, cmd
-from vyos.ifconfig.register import Register
+from vyos import debug
+from vyos.util import popen
+from vyos.util import cmd
+from vyos.ifconfig.section import Section
-class Control(Register):
+class Control(Section):
_command_get = {}
_command_set = {}
@@ -35,10 +36,10 @@ class Control(Register):
# if debug is not explicitely disabled the the config, enable it
self.debug = ''
if kargs.get('debug', True):
- self.debug = debug('ifconfig')
+ self.debug = debug.enabled('ifconfig')
def _debug_msg (self, message):
- return debug_msg(message, self.debug)
+ return debug.message(message, self.debug)
def _popen(self, command):
return popen(command, self.debug)
diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py
index 8ec8263b5..d4ff9c2cd 100644
--- a/python/vyos/ifconfig/dhcp.py
+++ b/python/vyos/ifconfig/dhcp.py
@@ -14,105 +14,37 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
-import jinja2
+from vyos.dicts import FixedDict
from vyos.ifconfig.control import Control
+from vyos.template import render
-template_v4 = """
-# generated by ifconfig.py
-option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
-timeout 60;
-retry 300;
-
-interface "{{ intf }}" {
- send host-name "{{ hostname }}";
- {% if client_id -%}
- send dhcp-client-identifier "{{ client_id }}";
- {% endif -%}
- {% if vendor_class_id -%}
- send vendor-class-identifier "{{ vendor_class_id }}";
- {% endif -%}
- request subnet-mask, broadcast-address, routers, domain-name-servers,
- rfc3442-classless-static-routes, domain-name, interface-mtu;
- require subnet-mask;
-}
-
-"""
-
-template_v6 = """
-# generated by ifconfig.py
-interface "{{ intf }}" {
- request routers, domain-name-servers, domain-name;
-}
-
-"""
-
-class DHCP (Control):
+
+class _DHCP (Control):
client_base = r'/var/lib/dhcp/dhclient_'
- def __init__ (self, ifname, **kargs):
+ def __init__(self, ifname, version, **kargs):
super().__init__(**kargs)
-
- # per interface DHCP config files
- self._dhcp = {
- 4: {
- 'ifname': ifname,
- 'conf': self.client_base + ifname + '.conf',
- 'pid': self.client_base + ifname + '.pid',
- 'lease': self.client_base + ifname + '.leases',
- 'options': {
- 'intf': ifname,
- 'hostname': '',
- 'client_id': '',
- 'vendor_class_id': ''
- },
- },
- 6: {
- 'ifname': ifname,
- 'conf': self.client_base + ifname + '.v6conf',
- 'pid': self.client_base + ifname + '.v6pid',
- 'lease': self.client_base + ifname + '.v6leases',
- 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
- 'options': {
- 'intf': ifname,
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False
- },
- },
+ self.version = version
+ self.file = {
+ 'ifname': ifname,
+ 'conf': self.client_base + ifname + '.' + version + 'conf',
+ 'pid': self.client_base + ifname + '.' + version + 'pid',
+ 'lease': self.client_base + ifname + '.' + version + 'leases',
}
- def get_dhcp_options(self):
- """
- Return dictionary with supported DHCP options.
-
- Dictionary should be altered and send back via set_dhcp_options()
- so those options are applied when DHCP is run.
- """
- return self._dhcp[4]['options']
-
- def set_dhcp_options(self, options):
- """
- Store new DHCP options used by next run of DHCP client.
- """
- self._dhcp[4]['options'] = options
-
- def get_dhcpv6_options(self):
- """
- Return dictionary with supported DHCPv6 options.
-
- Dictionary should be altered and send back via set_dhcp_options()
- so those options are applied when DHCP is run.
- """
- return self._dhcp[6]['options']
-
- def set_dhcpv6_options(self, options):
- """
- Store new DHCP options used by next run of DHCP client.
- """
- self._dhcp[6]['options'] = options
+class _DHCPv4 (_DHCP):
+ def __init__(self, ifname):
+ super().__init__(ifname, '')
+ self.options = FixedDict(**{
+ 'ifname': ifname,
+ 'hostname': '',
+ 'client_id': '',
+ 'vendor_class_id': ''
+ })
# replace dhcpv4/v6 with systemd.networkd?
- def _set_dhcp(self):
+ def set(self):
"""
Configure interface as DHCP client. The dhclient binary is automatically
started in background!
@@ -121,21 +53,16 @@ class DHCP (Control):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.set_dhcp()
+ >>> j.dhcp.v4.set()
"""
- dhcp = self.get_dhcp_options()
- if not dhcp['hostname']:
+ if not self.options['hostname']:
# read configured system hostname.
# maybe change to vyos hostd client ???
with open('/etc/hostname', 'r') as f:
- dhcp['hostname'] = f.read().rstrip('\n')
+ self.options['hostname'] = f.read().rstrip('\n')
- # render DHCP configuration
- tmpl = jinja2.Template(template_v4)
- dhcp_text = tmpl.render(dhcp)
- with open(self._dhcp[4]['conf'], 'w') as f:
- f.write(dhcp_text)
+ render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options)
cmd = 'start-stop-daemon'
cmd += ' --start'
@@ -146,9 +73,9 @@ class DHCP (Control):
cmd += ' --'
# now pass arguments to dhclient binary
cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}'
- return self._cmd(cmd.format(**self._dhcp[4]))
+ return self._cmd(cmd.format(**self.file))
- def _del_dhcp(self):
+ def delete(self):
"""
De-configure interface as DHCP clinet. All auto generated files like
pid, config and lease will be removed.
@@ -157,14 +84,14 @@ class DHCP (Control):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.del_dhcp()
+ >>> j.dhcp.v4.delete()
"""
- if not os.path.isfile(self._dhcp[4]['pid']):
+ if not os.path.isfile(self.file['pid']):
self._debug_msg('No DHCP client PID found')
return None
- # with open(self._dhcp[4]['pid'], 'r') as f:
- # pid = int(f.read())
+ # with open(self.file['pid'], 'r') as f:
+ # pid = int(f.read())
# stop dhclient, we need to call dhclient and tell it should release the
# aquired IP address. tcpdump tells me:
@@ -178,14 +105,27 @@ class DHCP (Control):
# Hostname Option 12, length 10: "vyos"
#
cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}'
- self._cmd(cmd.format(**self._dhcp[4]))
+ self._cmd(cmd.format(**self.file))
# cleanup old config files
for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self._dhcp[4][name]):
- os.remove(self._dhcp[4][name])
+ if os.path.isfile(self.file[name]):
+ os.remove(self.file[name])
+
- def _set_dhcpv6(self):
+class _DHCPv6 (_DHCP):
+ def __init__(self, ifname):
+ super().__init__(ifname, 'v6')
+ self.options = FixedDict(**{
+ 'ifname': ifname,
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False,
+ })
+ self.file.update({
+ 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ })
+
+ def set(self):
"""
Configure interface as DHCPv6 client. The dhclient binary is automatically
started in background!
@@ -196,22 +136,17 @@ class DHCP (Control):
>>> j = Interface('eth0')
>>> j.set_dhcpv6()
"""
- dhcpv6 = self.get_dhcpv6_options()
# better save then sorry .. should be checked in interface script
# but if you missed it we are safe!
- if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']:
+ if self.options['dhcpv6_prm_only'] and self.options['dhcpv6_temporary']:
raise Exception(
'DHCPv6 temporary and parameters-only options are mutually exclusive!')
- # render DHCP configuration
- tmpl = jinja2.Template(template_v6)
- dhcpv6_text = tmpl.render(dhcpv6)
- with open(self._dhcp[6]['conf'], 'w') as f:
- f.write(dhcpv6_text)
+ render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options)
# no longer accept router announcements on this interface
- self._write_sysfs(self._dhcp[6]['accept_ra'], 0)
+ self._write_sysfs(self.file['accept_ra'], 0)
# assemble command-line to start DHCPv6 client (dhclient)
cmd = 'start-stop-daemon'
@@ -224,15 +159,15 @@ class DHCP (Control):
# now pass arguments to dhclient binary
cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}'
# add optional arguments
- if dhcpv6['dhcpv6_prm_only']:
+ if self.options['dhcpv6_prm_only']:
cmd += ' -S'
- if dhcpv6['dhcpv6_temporary']:
+ if self.options['dhcpv6_temporary']:
cmd += ' -T'
cmd += ' {ifname}'
- return self._cmd(cmd.format(**self._dhcp[6]))
+ return self._cmd(cmd.format(**self.file))
- def _del_dhcpv6(self):
+ def delete(self):
"""
De-configure interface as DHCPv6 clinet. All auto generated files like
pid, config and lease will be removed.
@@ -243,12 +178,12 @@ class DHCP (Control):
>>> j = Interface('eth0')
>>> j.del_dhcpv6()
"""
- if not os.path.isfile(self._dhcp[6]['pid']):
+ if not os.path.isfile(self.file['pid']):
self._debug_msg('No DHCPv6 client PID found')
return None
- # with open(self._dhcp[6]['pid'], 'r') as f:
- # pid = int(f.read())
+ # with open(self.file['pid'], 'r') as f:
+ # pid = int(f.read())
# stop dhclient
cmd = 'start-stop-daemon'
@@ -256,13 +191,18 @@ class DHCP (Control):
cmd += ' --oknodo'
cmd += ' --quiet'
cmd += ' --pidfile {pid}'
- self._cmd(cmd.format(**self._dhcp[6]))
+ self._cmd(cmd.format(**self.file))
# accept router announcements on this interface
- self._write_sysfs(self._dhcp[6]['accept_ra'], 1)
+ self._write_sysfs(self.options['accept_ra'], 1)
# cleanup old config files
for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self._dhcp[6][name]):
- os.remove(self._dhcp[6][name])
+ if os.path.isfile(self.file[name]):
+ os.remove(self.file[name])
+
+class DHCP (object):
+ def __init__(self, ifname):
+ self.v4 = _DHCPv4(ifname)
+ self.v6 = _DHCPv6(ifname)
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 291b326bf..542de4f59 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -18,8 +18,8 @@ import re
from vyos.ifconfig.interface import Interface
from vyos.ifconfig.vlan import VLAN
+from vyos.validate import assert_list
from vyos.util import run
-from vyos.validate import *
@Interface.register
diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py
new file mode 100644
index 000000000..bfab36335
--- /dev/null
+++ b/python/vyos/ifconfig/input.py
@@ -0,0 +1,31 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class InputIf(Interface):
+ default = {
+ 'type': '',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'input',
+ 'prefixes': ['ifb', ],
+ },
+ }
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 96057a943..43f823eca 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -18,23 +18,33 @@ import re
import json
import glob
import time
+from time import sleep
+from os.path import isfile
from copy import deepcopy
+from datetime import timedelta
-from vyos.validate import * # should not * include
-from vyos.util import mac2eui64
-from vyos import ConfigError
-
+from hurry.filesize import size, alternative
from ipaddress import IPv4Network, IPv6Address, IPv6Network
from netifaces import ifaddresses, AF_INET, AF_INET6
-from time import sleep
-from os.path import isfile
from tabulate import tabulate
-from hurry.filesize import size,alternative
-from datetime import timedelta
+from vyos.util import mac2eui64
+from vyos import ConfigError
from vyos.ifconfig.dhcp import DHCP
+from vyos.validate import is_ipv4
+from vyos.validate import is_ipv6
+from vyos.validate import is_intf_addr_assigned
+from vyos.validate import assert_boolean
+from vyos.validate import assert_list
+from vyos.validate import assert_mac
+from vyos.validate import assert_mtu
+from vyos.validate import assert_positive
+from vyos.validate import assert_range
+
+from vyos.ifconfig.control import Control
+
-class Interface(DHCP):
+class Interface(Control):
options = []
required = []
default = {
@@ -173,7 +183,8 @@ class Interface(DHCP):
self.config['ifname'] = ifname
# we must have updated config before initialising the Interface
- super().__init__(ifname, **kargs)
+ super().__init__(**kargs)
+ self.dhcp = DHCP(ifname)
if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])):
# Any instance of Interface, such as Interface('eth0')
@@ -216,8 +227,8 @@ class Interface(DHCP):
>>> i.remove()
"""
# stop DHCP(v6) if running
- self._del_dhcp()
- self._del_dhcpv6()
+ self.dhcp.v4.delete()
+ self.dhcp.v6.delete()
# remove all assigned IP addresses from interface - this is a bit redundant
# as the kernel will remove all addresses on interface deletion, but we
@@ -660,12 +671,13 @@ class Interface(DHCP):
# do not change below 'if' ordering esle you will get an exception as:
# ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address
if addr != 'dhcp' and is_ipv4(addr):
- raise ConfigError("Can't configure both static IPv4 and DHCP address on the same interface")
+ raise ConfigError(
+ "Can't configure both static IPv4 and DHCP address on the same interface")
if addr == 'dhcp':
- self._set_dhcp()
+ self.dhcp.v4.set()
elif addr == 'dhcpv6':
- self._set_dhcpv6()
+ self.dhcp.v6.set()
else:
if not is_intf_addr_assigned(self.config['ifname'], addr):
cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname'])
@@ -694,9 +706,9 @@ class Interface(DHCP):
['2001:db8::ffff/64']
"""
if addr == 'dhcp':
- self._del_dhcp()
+ self.dhcp.v4.delete()
elif addr == 'dhcpv6':
- self._del_dhcpv6()
+ self.dhcp.v6.delete()
else:
if is_intf_addr_assigned(self.config['ifname'], addr):
cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname'])
diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py
index 4e4b563a1..55b1a3e91 100644
--- a/python/vyos/ifconfig/macvlan.py
+++ b/python/vyos/ifconfig/macvlan.py
@@ -35,10 +35,10 @@ class MACVLANIf(Interface):
'prefixes': ['peth', ],
},
}
- options = Interface.options + ['link', 'mode']
+ options = Interface.options + ['source_interface', 'mode']
def _create(self):
- cmd = 'ip link add {ifname} link {link} type macvlan mode {mode}'.format(
+ cmd = 'ip link add {ifname} link {source_interface} type macvlan mode {mode}'.format(
**self.config)
self._cmd(cmd)
@@ -54,7 +54,7 @@ class MACVLANIf(Interface):
"""
config = {
'address': '',
- 'link': 0,
+ 'source_interface': '',
'mode': ''
}
return config
@@ -62,7 +62,6 @@ class MACVLANIf(Interface):
def set_mode(self, mode):
"""
"""
-
- cmd = 'ip link set dev {} type macvlan mode {}'.format(
- self.config['ifname'], mode)
+ ifname = self.config['ifname']
+ cmd = f'ip link set dev {ifname} type macvlan mode {mode}'
return self._cmd(cmd)
diff --git a/python/vyos/ifconfig/register.py b/python/vyos/ifconfig/section.py
index c90782b70..ab340d247 100644
--- a/python/vyos/ifconfig/register.py
+++ b/python/vyos/ifconfig/section.py
@@ -16,9 +16,10 @@
import netifaces
-class Register:
+class Section:
# the known interface prefixes
_prefixes = {}
+ _classes = []
# class need to define: definition['prefixes']
# the interface prefixes declared by a class used to name interface with
@@ -26,9 +27,16 @@ class Register:
@classmethod
def register(cls, klass):
+ """
+ A function to use as decorator the interfaces classes
+ It register the prefix for the interface (eth, dum, vxlan, ...)
+ with the class which can handle it (EthernetIf, DummyIf,VXLANIf, ...)
+ """
if not klass.definition.get('prefixes',[]):
raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}')
+ cls._classes.append(klass)
+
for ifprefix in klass.definition['prefixes']:
if ifprefix in cls._prefixes:
raise RuntimeError(f'only one class can be registered for prefix "{ifprefix}" type')
@@ -38,7 +46,11 @@ class Register:
@classmethod
def _basename (cls, name, vlan):
- # remove number from interface name
+ """
+ remove the number at the end of interface name
+ name: name of the interface
+ vlan: if vlan is True, do not stop at the vlan number
+ """
name = name.rstrip('0123456789')
name = name.rstrip('.')
if vlan:
@@ -47,15 +59,13 @@ class Register:
@classmethod
def section(cls, name, vlan=True):
- # return the name of a section an interface should be under
+ """
+ return the name of a section an interface should be under
+ name: name of the interface (eth0, dum1, ...)
+ vlan: should we try try to remove the VLAN from the number
+ """
name = cls._basename(name, vlan)
- # XXX: To leave as long as vti and input are not moved to vyos
- if name == 'vti':
- return 'vti'
- if name == 'ifb':
- return 'input'
-
if name in cls._prefixes:
return cls._prefixes[name].definition['section']
return ''
@@ -68,15 +78,13 @@ class Register:
raise ValueError(f'No type found for interface name: {name}')
@classmethod
- def _listing (cls,section=''):
+ def _intf_under_section (cls,section=''):
+ """
+ return a generator with the name of the interface which are under a section
+ """
interfaces = netifaces.interfaces()
for ifname in interfaces:
- # XXX: Temporary hack as vti and input are not yet moved from vyatta to vyos
- if ifname.startswith('vti') or ifname.startswith('input'):
- yield ifname
- continue
-
ifsection = cls.section(ifname)
if not ifsection:
continue
@@ -87,9 +95,37 @@ class Register:
yield ifname
@classmethod
- def listing(cls, section=''):
- return list(cls._listing(section))
+ def interfaces(cls, section=''):
+ """
+ return a list of the name of the interface which are under a section
+ if no section is provided, then it returns all configured interfaces
+ """
+ return list(cls._intf_under_section(section))
+ @classmethod
+ def _intf_with_feature(cls, feature=''):
+ """
+ return a generator with the name of the interface which have
+ a particular feature set in their definition such as:
+ bondable, broadcast, bridgeable, ...
+ """
+ for klass in cls._classes:
+ if klass.definition[feature]:
+ yield klass.definition['section']
-# XXX: TODO - limit name for VRF interfaces
+ @classmethod
+ def feature(cls, feature=''):
+ """
+ return list with the name of the interface which have
+ a particular feature set in their definition such as:
+ bondable, broadcast, bridgeable, ...
+ """
+ return list(cls._intf_with_feature(feature))
+ @classmethod
+ def reserved(cls):
+ """
+ return list with the interface name prefixes
+ eth, lo, vxlan, dum, ...
+ """
+ return list(cls._prefixes.keys())
diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py
index 97a3c1ff3..5e83206c2 100644
--- a/python/vyos/ifconfig/stp.py
+++ b/python/vyos/ifconfig/stp.py
@@ -16,7 +16,7 @@
from vyos.ifconfig.interface import Interface
-from vyos.validate import *
+from vyos.validate import assert_positive
class STP:
diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py
index 1bbb9eb6a..009a53a82 100644
--- a/python/vyos/ifconfig/tunnel.py
+++ b/python/vyos/ifconfig/tunnel.py
@@ -31,7 +31,7 @@ def enable_to_on(value):
raise ValueError(f'expect enable or disable but got "{value}"')
-
+@Interface.register
class _Tunnel(Interface):
"""
_Tunnel: private base class for tunnels
@@ -143,7 +143,7 @@ class GREIf(_Tunnel):
options = ['local', 'remote', 'ttl', 'tos', 'key']
updates = ['local', 'remote', 'ttl', 'tos',
- 'multicast', 'allmulticast']
+ 'mtu', 'multicast', 'allmulticast']
create = 'ip tunnel add {ifname} mode {type}'
change = 'ip tunnel cha {ifname}'
@@ -167,7 +167,7 @@ class GRETapIf(_Tunnel):
required = ['local', ]
options = ['local', 'remote', ]
- updates = []
+ updates = ['mtu', ]
create = 'ip link add {ifname} type {type}'
change = ''
@@ -193,7 +193,7 @@ class IP6GREIf(_Tunnel):
'hoplimit', 'tclass', 'flowlabel']
updates = ['local', 'remote', 'encaplimit',
'hoplimit', 'tclass', 'flowlabel',
- 'multicast', 'allmulticast']
+ 'mtu', 'multicast', 'allmulticast']
create = 'ip tunnel add {ifname} mode {type}'
change = 'ip tunnel cha {ifname} mode {type}'
@@ -227,7 +227,7 @@ class IPIPIf(_Tunnel):
options = ['local', 'remote', 'ttl', 'tos', 'key']
updates = ['local', 'remote', 'ttl', 'tos',
- 'multicast', 'allmulticast']
+ 'mtu', 'multicast', 'allmulticast']
create = 'ip tunnel add {ifname} mode {type}'
change = 'ip tunnel cha {ifname}'
@@ -252,7 +252,7 @@ class IPIP6If(_Tunnel):
'hoplimit', 'tclass', 'flowlabel']
updates = ['local', 'remote', 'encaplimit',
'hoplimit', 'tclass', 'flowlabel',
- 'multicast', 'allmulticast']
+ 'mtu', 'multicast', 'allmulticast']
create = 'ip -6 tunnel add {ifname} mode {type}'
change = 'ip -6 tunnel cha {ifname}'
@@ -288,7 +288,7 @@ class SitIf(_Tunnel):
options = ['local', 'remote', 'ttl', 'tos', 'key']
updates = ['local', 'remote', 'ttl', 'tos',
- 'multicast', 'allmulticast']
+ 'mtu', 'multicast', 'allmulticast']
create = 'ip tunnel add {ifname} mode {type}'
change = 'ip tunnel cha {ifname}'
@@ -309,7 +309,7 @@ class Sit6RDIf(SitIf):
# TODO: check if key can really be used with 6RD
options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix']
updates = ['remote', 'ttl', 'tos',
- 'multicast', 'allmulticast']
+ 'mtu', 'multicast', 'allmulticast']
def _create(self):
# do not call _Tunnel.create, building fully here
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
new file mode 100644
index 000000000..56ebe01d1
--- /dev/null
+++ b/python/vyos/ifconfig/vti.py
@@ -0,0 +1,31 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class VTIIf(Interface):
+ default = {
+ 'type': 'vti',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'vti',
+ 'prefixes': ['vti', ],
+ },
+ }
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
index 5678ad62e..f47ae17cc 100644
--- a/python/vyos/ifconfig/vxlan.py
+++ b/python/vyos/ifconfig/vxlan.py
@@ -43,12 +43,13 @@ class VXLANIf(Interface):
default = {
'type': 'vxlan',
- 'vni': 0,
- 'dev': '',
'group': '',
- 'remote': '',
'port': 8472, # The Linux implementation of VXLAN pre-dates
# the IANA's selection of a standard destination port
+ 'remote': '',
+ 'src_address': '',
+ 'src_interface': '',
+ 'vni': 0
}
definition = {
**Interface.definition,
@@ -58,24 +59,30 @@ class VXLANIf(Interface):
'bridgeable': True,
}
}
- options = ['group', 'remote', 'dev', 'port', 'vni']
+ options = ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address']
mapping = {
'ifname': 'add',
'vni': 'id',
'port': 'dstport',
+ 'src_address': 'nolearning local',
}
def _create(self):
cmdline = set()
if self.config['remote']:
- cmdline = ('ifname', 'type', 'remote', 'dev', 'vni', 'port')
- elif self.config['group'] and self.config['dev']:
- cmdline = ('ifname', 'type', 'group', 'dev', 'vni', 'port')
+ cmdline = ('ifname', 'type', 'remote', 'src_interface', 'vni', 'port')
+
+ elif self.config['src_address']:
+ cmdline = ('ifname', 'type', 'src_address', 'vni', 'port')
+
+ elif self.config['group'] and self.config['src_interface']:
+ cmdline = ('ifname', 'type', 'group', 'src_interface', 'vni', 'port')
+
else:
- intf = self.config['intf']
+ ifname = self.config['ifname']
raise ConfigError(
- f'VXLAN "{intf}" is missing mandatory underlay interface for a multicast network.')
+ f'VXLAN "{ifname}" is missing mandatory underlay interface for a multicast network.')
cmd = 'ip link'
for key in cmdline:
diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py
index ed22646c1..899fd17da 100644
--- a/python/vyos/ifconfig_vlan.py
+++ b/python/vyos/ifconfig_vlan.py
@@ -25,32 +25,20 @@ def apply_vlan_config(vlan, config):
if not vlan.definition['vlan']:
raise TypeError()
- # get DHCP config dictionary and update values
- opt = vlan.get_dhcp_options()
-
if config['dhcp_client_id']:
- opt['client_id'] = config['dhcp_client_id']
+ vlan.dhcp.v4.options['client_id'] = config['dhcp_client_id']
if config['dhcp_hostname']:
- opt['hostname'] = config['dhcp_hostname']
+ vlan.dhcp.v4.options['hostname'] = config['dhcp_hostname']
if config['dhcp_vendor_class_id']:
- opt['vendor_class_id'] = config['dhcp_vendor_class_id']
-
- # store DHCP config dictionary - used later on when addresses are aquired
- vlan.set_dhcp_options(opt)
-
- # get DHCPv6 config dictionary and update values
- opt = vlan.get_dhcpv6_options()
+ vlan.dhcp.v4.options['vendor_class_id'] = config['dhcp_vendor_class_id']
if config['dhcpv6_prm_only']:
- opt['dhcpv6_prm_only'] = True
+ vlan.dhcp.v6.options['dhcpv6_prm_only'] = True
if config['dhcpv6_temporary']:
- opt['dhcpv6_temporary'] = True
-
- # store DHCPv6 config dictionary - used later on when addresses are aquired
- vlan.set_dhcpv6_options(opt)
+ vlan.dhcp.v6.options['dhcpv6_temporary'] = True
# update interface description used e.g. within SNMP
vlan.set_alias(config['description'])
diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py
index e57d261e4..cfa75aac6 100644
--- a/python/vyos/ioctl.py
+++ b/python/vyos/ioctl.py
@@ -13,9 +13,11 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+import sys
import os
-import fcntl, struct, sys
-from socket import *
+import socket
+import fcntl
+import struct
SIOCGIFFLAGS = 0x8913
@@ -28,7 +30,7 @@ def get_terminal_size():
def get_interface_flags(intf):
""" Pull the SIOCGIFFLAGS """
nullif = '\0'*256
- sock = socket(AF_INET, SOCK_DGRAM)
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
raw = fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, intf + nullif)
flags, = struct.unpack('H', raw[16:18])
return flags
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index f0bf41cd4..1b4d3876e 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -18,7 +18,8 @@ import os
import re
import fileinput
-from vyos.util import cmd, DEVNULL
+from vyos.util import cmd
+from vyos.util import DEVNULL
def check_and_add_host_key(host_name):
@@ -31,10 +32,10 @@ def check_and_add_host_key(host_name):
mode = 0o600
os.mknod(known_hosts, 0o600)
- keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name)
+ keyscan_cmd = 'ssh-keyscan -t rsa {}'.format(host_name)
try:
- host_key = cmd(keyscan_cmd, stderr=DEVNULL, universal_newlines=True)
+ host_key = cmd(keyscan_cmd, stderr=DEVNULL)
except OSError:
sys.exit("Can not get RSA host key")
@@ -61,9 +62,9 @@ def check_and_add_host_key(host_name):
print("Host key has changed!")
print("If you trust the host key fingerprint below, continue.")
- fingerprint_cmd = 'ssh-keygen -lf /dev/stdin <<< "{}"'.format(host_key)
+ fingerprint_cmd = 'ssh-keygen -lf /dev/stdin'
try:
- fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, universal_newlines=True)
+ fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, input=host_key)
except OSError:
sys.exit("Can not get RSA host key fingerprint.")
@@ -125,7 +126,7 @@ def get_remote_config(remote_file):
# Try header first, and look for 'OK' or 'Moved' codes:
curl_cmd = 'curl {0} -q -I {1}'.format(redirect_opt, remote_file)
try:
- curl_output = cmd(curl_cmd, shell=True, universal_newlines=True)
+ curl_output = cmd(curl_cmd)
except OSError:
sys.exit(1)
@@ -142,6 +143,6 @@ def get_remote_config(remote_file):
curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file)
try:
- return cmd(curl_cmd, universal_newlines=True)
+ return cmd(curl_cmd, stderr=None)
except OSError:
return None
diff --git a/python/vyos/template.py b/python/vyos/template.py
new file mode 100644
index 000000000..6c73ce753
--- /dev/null
+++ b/python/vyos/template.py
@@ -0,0 +1,65 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from jinja2 import Environment
+from jinja2 import FileSystemLoader
+
+from vyos.defaults import directories
+
+
+# reuse the same Environment to improve performance
+_templates_env = {
+ False: Environment(loader=FileSystemLoader(directories['templates'])),
+ True: Environment(loader=FileSystemLoader(directories['templates']), trim_blocks=True),
+}
+_templates_mem = {
+ False: {},
+ True: {},
+}
+
+
+def render(destination, template, content, trim_blocks=False, formater=None):
+ """
+ render a template from the template directory, it will raise on any errors
+ destination: the file where the rendered template must be saved
+ template: the path to the template relative to the template folder
+ content: the dictionary to use to render the template
+
+ This classes cache the renderer, so rendering the same file multiple time
+ does not cause as too much overhead. If use everywhere, it could be changed
+ and load the template from python environement variables from an import
+ python module generated when the debian package is build
+ (recovering the load time and overhead caused by having the file out of the code)
+ """
+
+ # Setup a renderer for the given template
+ # This is cached and re-used for performance
+ if template not in _templates_mem[trim_blocks]:
+ _templates_mem[trim_blocks][template] = _templates_env[trim_blocks].get_template(template)
+ template = _templates_mem[trim_blocks][template]
+
+ # As we are opening the file with 'w', we are performing the rendering
+ # before calling open() to not accidentally erase the file if the
+ # templating fails
+ content = template.render(content)
+
+ if formater:
+ content = formater(content)
+
+ # Write client config file
+ with open(destination, 'w') as f:
+ f.write(content)
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 16cfae92d..49c47cd85 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -16,61 +16,121 @@
import os
import re
import sys
-from subprocess import Popen, PIPE, STDOUT, DEVNULL
+from subprocess import Popen
+from subprocess import PIPE
+from subprocess import STDOUT
+from subprocess import DEVNULL
-def debug(flag):
- return flag if os.path.isfile(f'/tmp/vyos.{flag}.debug') else ''
+from vyos import debug
+# There is many (too many) ways to run command with python
+# os.system, subprocess.Popen, subproces.{run,call,check_output}
+# which all have slighty different behaviour
-def debug_msg(message, section=''):
- if section:
- print(f'DEBUG/{section:<6} {message}')
+def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=None, decode=None):
+ """
+ popen is a wrapper helper aound subprocess.Popen
+ with it default setting it will return a tuple (out, err)
+ out: the output of the program run
+ err: the error code returned by the program
+
+ it can be affected by the following flags:
+ shell: do not try to auto-detect if a shell is required
+ for example if a pipe (|) or redirection (>, >>) is used
+ input: data to sent to the child process via STDIN
+ the data should be bytes but string will be converted
+ timeout: time after which the command will be considered to have failed
+ env: mapping that defines the environment variables for the new process
+ stdout: define how the output of the program should be handled
+ - PIPE (default), sends stdout to the output
+ - DEVNULL, discard the output
+ stderr: define how the output of the program should be handled
+ - None (default), send/merge the data to/with stderr
+ - PIPE, popen will append it to output
+ - STDOUT, send the data to be merged with stdout
+ - DEVNULL, discard the output
+ decode: specify the expected text encoding (utf-8, ascii, ...)
+
+ usage:
+ to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
+ to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
+ """
+
+ # log if the flag is set, otherwise log if command is set
+ if not debug.enabled(flag):
+ flag = 'command'
+
+ cmd_msg = f"cmd '{command}'"
+ debug.message(cmd_msg, flag)
-def popen(command, section='', shell=None, input=None, timeout=None, env=None,
- universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None):
- """ popen does not raise, returns the output and error code of command """
use_shell = shell
+ stdin = None
if shell is None:
- use_shell = True if ' ' in command else False
+ use_shell = False
+ if ' ' in command:
+ use_shell = True
+ if env:
+ use_shell = True
+ if input:
+ stdin = PIPE
+ input = input.encode() if type(input) is str else input
p = Popen(
command,
- stdout=stdout, stderr=stderr,
+ stdin=stdin, stdout=stdout, stderr=stderr,
env=env, shell=use_shell,
- universal_newlines=universal_newlines,
)
- tmp = p.communicate(input, timeout)[0].strip()
- debug_msg(f"cmd '{command}'", section)
- decoded = tmp.decode(decode) if decode else tmp.decode()
+ tmp = p.communicate(input, timeout)
+ out1 = b''
+ out2 = b''
+ if stdout == PIPE:
+ out1 = tmp[0]
+ if stderr == PIPE:
+ out2 += tmp[1]
+ decoded1 = out1.decode(decode) if decode else out1.decode()
+ decoded2 = out2.decode(decode) if decode else out2.decode()
+ decoded1 = decoded1.replace('\r\n', '\n').strip()
+ decoded2 = decoded2.replace('\r\n', '\n').strip()
+ nl = '\n' if decoded1 and decoded2 else ''
+ decoded = decoded1 + nl + decoded2
if decoded:
- debug_msg(f"returned:\n{decoded}", section)
+ ret_msg = f"returned:\n{decoded}"
+ debug.message(ret_msg, flag)
return decoded, p.returncode
-def run(command, section='', shell=None, input=None, timeout=None, env=None,
- universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None):
- """ does not raise exception on error, returns error code """
+def run(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=DEVNULL, stderr=None, decode=None):
+ """
+ A wrapper around vyos.util.popen, which discard the stdout and
+ will return the error code of a command
+ """
_, code = popen(
- command, section,
+ command, flag,
stdout=stdout, stderr=stderr,
input=input, timeout=timeout,
env=env, shell=shell,
- universal_newlines=universal_newlines,
decode=decode,
)
return code
-def cmd(command, section='', shell=None, input=None, timeout=None, env=None,
- universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None,
+def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=None, decode=None,
raising=None, message=''):
- """ does raise exception, returns output of command """
+ """
+ A wrapper around vyos.util.popen, which returns the stdout and
+ will raise the error code of a command
+
+ raising: specify which call should be used when raising (default is OSError)
+ the class should only require a string as parameter
+ """
decoded, code = popen(
- command, section,
+ command, flag,
stdout=stdout, stderr=stderr,
input=input, timeout=timeout,
env=env, shell=shell,
- universal_newlines=universal_newlines,
decode=decode,
)
if code != 0:
@@ -86,6 +146,23 @@ def cmd(command, section='', shell=None, input=None, timeout=None, env=None,
return decoded
+def call(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=None, decode=None):
+ """
+ A wrapper around vyos.util.popen, which print the stdout and
+ will return the error code of a command
+ """
+ out, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ print(out)
+ return code
+
+
def read_file(path):
""" Read a file to string """
with open(path, 'r') as f:
@@ -93,18 +170,37 @@ def read_file(path):
return data
-def chown_file(path, user, group):
- """ change file owner """
+def chown(path, user, group):
+ """ change file/directory owner """
from pwd import getpwnam
from grp import getgrnam
- if os.path.isfile(path):
+ if os.path.exists(path):
uid = getpwnam(user).pw_uid
gid = getgrnam(group).gr_gid
os.chown(path, uid, gid)
-def chmod_x(path):
- """ make file executable """
+
+def chmod_600(path):
+ """ make file only read/writable by owner """
+ from stat import S_IRUSR, S_IWUSR
+
+ if os.path.exists(path):
+ bitmask = S_IRUSR | S_IWUSR
+ os.chmod(path, bitmask)
+
+
+def chmod_750(path):
+ """ make file/directory only executable to user and group """
+ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP
+
+ if os.path.exists(path):
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
+ os.chmod(path, bitmask)
+
+
+def chmod_755(path):
+ """ make file executable by all """
from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
if os.path.exists(path):
diff --git a/python/vyos/version.py b/python/vyos/version.py
index 383efbc1e..d51a940d6 100644
--- a/python/vyos/version.py
+++ b/python/vyos/version.py
@@ -44,7 +44,7 @@ def get_version_data(file=version_file):
file (str): path to the version file
Returns:
- dict: version data
+ dict: version data, if it can not be found and empty dict
The optional ``file`` argument comes in handy in upgrade scripts
that need to retrieve information from images other than the running image.
@@ -52,17 +52,20 @@ def get_version_data(file=version_file):
is an implementation detail and may change in the future, while the interface
of this module will stay the same.
"""
- with open(file, 'r') as f:
- version_data = json.load(f)
- return version_data
+ try:
+ with open(file, 'r') as f:
+ version_data = json.load(f)
+ return version_data
+ except FileNotFoundError:
+ return {}
def get_version(file=None):
"""
- Get the version number
+ Get the version number, or an empty string if it could not be determined
"""
version_data = None
if file:
version_data = get_version_data(file=file)
else:
version_data = get_version_data()
- return version_data["version"]
+ return version_data.get('version','')