diff options
Diffstat (limited to 'python')
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','') |