diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/airbag.py | 17 | ||||
-rw-r--r-- | python/vyos/ifconfig/dhcp.py | 129 | ||||
-rw-r--r-- | python/vyos/template.py | 10 | ||||
-rw-r--r-- | python/vyos/util.py | 87 |
4 files changed, 124 insertions, 119 deletions
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index 6698aa404..b7838d8a2 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -26,6 +26,17 @@ from vyos.version import get_full_version_data DISABLE = False +_noteworthy = [] + +def noteworthy(msg): + """ + noteworthy can be use to take note things which we may not want to + report to the user may but be worth including in bug report + if something goes wrong later on + """ + _noteworthy.append(msg) + + # emulate a file object class _IO(object): def __init__(self, std, log): @@ -58,11 +69,16 @@ def bug_report(dtype, value, trace): information = get_full_version_data() trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n') + note = '' + if _noteworthy: + note = 'noteworthy:\n' + note += '\n'.join(_noteworthy) information.update({ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'trace': trace, 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + 'note': note, }) sys.stdout.write(INTRO.format(**information)) @@ -145,6 +161,7 @@ Hardware S/N: {hardware_serial} Hardware UUID: {hardware_uuid} {trace} +{note} """ INTRO = """\ diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py index 3122147a3..bf6566c07 100644 --- a/python/vyos/ifconfig/dhcp.py +++ b/python/vyos/ifconfig/dhcp.py @@ -19,28 +19,20 @@ from vyos.dicts import FixedDict from vyos.ifconfig.control import Control from vyos.template import render +config_base = r'/var/lib/dhcp/dhclient_' -class _DHCP (Control): - client_base = r'/var/lib/dhcp/dhclient_' - - def __init__(self, ifname, version, **kargs): - super().__init__(**kargs) - 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', - } - -class _DHCPv4 (_DHCP): +class _DHCPv4 (Control): def __init__(self, ifname): - super().__init__(ifname, '') + super().__init__() self.options = FixedDict(**{ 'ifname': ifname, 'hostname': '', 'client_id': '', - 'vendor_class_id': '' + 'vendor_class_id': '', + 'conf_file': config_base + f'{ifname}.conf', + 'options_file': config_base + f'{ifname}.options', + 'pid_file': config_base + f'{ifname}.pid', + 'lease_file': config_base + f'{ifname}.leases', }) # replace dhcpv4/v6 with systemd.networkd? @@ -55,25 +47,16 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.set() """ - if not self.options['hostname']: # read configured system hostname. # maybe change to vyos hostd client ??? with open('/etc/hostname', 'r') as f: self.options['hostname'] = f.read().rstrip('\n') - render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options) + render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) + render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options) - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - cmd += ' --exec /sbin/dhclient' - cmd += ' --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}' - return self._cmd(cmd.format(**self.file)) + return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -86,44 +69,29 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.delete() """ - if not os.path.isfile(self.file['pid']): + if not os.path.isfile(self.options['pid_file']): self._debug_msg('No DHCP client PID found') return None - # 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: - # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000) - # Client-IP 172.16.35.103 - # Client-Ethernet-Address 00:50:56:9d:11:df - # Vendor-rfc1048 Extensions - # Magic Cookie 0x63825363 - # DHCP-Message Option 53, length 1: Release - # Server-ID Option 54, length 4: 172.16.35.254 - # Hostname Option 12, length 10: "vyos" - # - cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}' - self._cmd(cmd.format(**self.file)) + self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options)) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) + for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): + if os.path.isfile(self.options[name]): + os.remove(self.options[name]) - -class _DHCPv6 (_DHCP): +class _DHCPv6 (Control): def __init__(self, ifname): - super().__init__(ifname, 'v6') + super().__init__() self.options = FixedDict(**{ 'ifname': ifname, + 'conf_file': config_base + f'v6_{ifname}.conf', + 'options_file': config_base + f'v6_{ifname}.options', + 'pid_file': config_base + f'v6_{ifname}.pid', + 'lease_file': config_base + f'v6_{ifname}.leases', 'dhcpv6_prm_only': False, 'dhcpv6_temporary': False, }) - self.file.update({ - 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra', - }) def set(self): """ @@ -134,7 +102,7 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.set_dhcpv6() + >>> j.dhcp.v6.set() """ # better save then sorry .. should be checked in interface script @@ -143,29 +111,13 @@ class _DHCPv6 (_DHCP): raise Exception( 'DHCPv6 temporary and parameters-only options are mutually exclusive!') - render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options) + render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) + render(self.options['conf_file'], 'dhcp-client/ipv6.tmpl', self.options) # no longer accept router announcements on this interface - self._write_sysfs(self.file['accept_ra'], 0) - - # assemble command-line to start DHCPv6 client (dhclient) - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - cmd += ' --exec /sbin/dhclient' - cmd += ' --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}' - # add optional arguments - if self.options['dhcpv6_prm_only']: - cmd += ' -S' - if self.options['dhcpv6_temporary']: - cmd += ' -T' - cmd += ' {ifname}' - - return self._cmd(cmd.format(**self.file)) + self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 0) + + return self._cmd('systemctl restart dhclient6@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -176,33 +128,24 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.del_dhcpv6() + >>> j.dhcp.v6.delete() """ - if not os.path.isfile(self.file['pid']): + if not os.path.isfile(self.options['pid_file']): self._debug_msg('No DHCPv6 client PID found') return None - # with open(self.file['pid'], 'r') as f: - # pid = int(f.read()) - - # stop dhclient - cmd = 'start-stop-daemon' - cmd += ' --stop' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - self._cmd(cmd.format(**self.file)) + self._cmd('systemctl stop dhclient6@{ifname}.service'.format(**self.options)) # accept router announcements on this interface - self._write_sysfs(self.file['accept_ra'], 1) + self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 1) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) + for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): + if os.path.isfile(self.options[name]): + os.remove(self.options[name]) -class DHCP (object): +class DHCP(object): def __init__(self, ifname): self.v4 = _DHCPv4(ifname) self.v6 = _DHCPv6(ifname) diff --git a/python/vyos/template.py b/python/vyos/template.py index 6c73ce753..e4b253ed3 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -19,6 +19,7 @@ from jinja2 import Environment from jinja2 import FileSystemLoader from vyos.defaults import directories +from vyos.util import chmod, chown, makedir # reuse the same Environment to improve performance @@ -32,7 +33,7 @@ _templates_mem = { } -def render(destination, template, content, trim_blocks=False, formater=None): +def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None): """ render a template from the template directory, it will raise on any errors destination: the file where the rendered template must be saved @@ -46,6 +47,10 @@ def render(destination, template, content, trim_blocks=False, formater=None): (recovering the load time and overhead caused by having the file out of the code) """ + # Create the directory if it does not exists + folder = os.path.dirname(destination) + makedir(folder, user, group) + # Setup a renderer for the given template # This is cached and re-used for performance if template not in _templates_mem[trim_blocks]: @@ -63,3 +68,6 @@ def render(destination, template, content, trim_blocks=False, formater=None): # Write client config file with open(destination, 'w') as f: f.write(content) + + chmod(destination, permission) + chown(destination, user, group) diff --git a/python/vyos/util.py b/python/vyos/util.py index 4340332d3..92b6f7992 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import sys # # NOTE: Do not import full classes here, move your import to the function @@ -25,7 +26,7 @@ import os # which all have slighty different behaviour from subprocess import Popen, PIPE, STDOUT, DEVNULL def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ popen is a wrapper helper aound subprocess.Popen with it default setting it will return a tuple (out, err) @@ -48,12 +49,14 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - STDOUT, send the data to be merged with stdout - DEVNULL, discard the output decode: specify the expected text encoding (utf-8, ascii, ...) + the default is explicitely utf-8 which is python's own default 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) """ from vyos import debug + from vyos import airbag # log if the flag is set, otherwise log if command is set if not debug.enabled(flag): flag = 'command' @@ -77,27 +80,39 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, stdin=stdin, stdout=stdout, stderr=stderr, env=env, shell=use_shell, ) - tmp = p.communicate(input, timeout) - out1 = b'' - out2 = b'' + + pipe = p.communicate(input, timeout) + + pipe_out = b'' if stdout == PIPE: - out1 = tmp[0] + pipe_out = pipe[0] + + pipe_err = b'' 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: - ret_msg = f"returned:\n{decoded}" - debug.message(ret_msg, flag) - return decoded, p.returncode + pipe_err = pipe[1] + + str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip() + str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip() + + out_msg = f"returned (out):\n{str_out}" + if str_out: + debug.message(out_msg, flag) + + if str_err: + err_msg = f"returned (err):\n{str_err}" + # this message will also be send to syslog via airbag + debug.message(err_msg, flag, destination=sys.stderr) + + # should something go wrong, report this too via airbag + airbag.noteworthy(cmd_msg) + airbag.noteworthy(out_msg) + airbag.noteworthy(err_msg) + + return str_out, p.returncode def run(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=DEVNULL, stderr=None, decode=None): + stdout=DEVNULL, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which discard the stdout and will return the error code of a command @@ -113,14 +128,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None, def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None, - raising=None, message=''): + stdout=PIPE, stderr=PIPE, decode='utf-8', + raising=None, message='', expect=[0]): """ 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 + expect: a list of error codes to consider as normal """ decoded, code = popen( command, flag, @@ -129,7 +145,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, env=env, shell=shell, decode=decode, ) - if code != 0: + if code not in expect: feedback = message + '\n' if message else '' feedback += f'failed to run command: {command}\n' feedback += f'returned: {decoded}\n' @@ -143,7 +159,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, def call(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which print the stdout and will return the error code of a command @@ -197,10 +213,24 @@ def chown(path, user, group): from pwd import getpwnam from grp import getgrnam - if os.path.exists(path): - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(path, uid, gid) + if user is None or group is None: + return False + + if not os.path.exists(path): + return False + + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(path, uid, gid) + return True + + +def chmod(path, bitmask): + if not os.path.exists(path): + return + if bitmask is None: + return + os.chmod(path, bitmask) def chmod_600(path): @@ -231,6 +261,13 @@ def chmod_755(path): os.chmod(path, bitmask) +def makedir(path, user=None, group=None): + if os.path.exists(path): + return + os.mkdir(path) + chown(path, user, group) + + def colon_separated_to_dict(data_string, uniquekeys=False): """ Converts a string containing newline-separated entries of colon-separated key-value pairs into a dict. |