diff options
Diffstat (limited to 'src')
27 files changed, 2148 insertions, 303 deletions
diff --git a/src/conf_mode/accel_pppoe.py b/src/conf_mode/accel_pppoe.py index 3b3bf8cac..9c879502a 100755 --- a/src/conf_mode/accel_pppoe.py +++ b/src/conf_mode/accel_pppoe.py @@ -47,6 +47,8 @@ pppoe ippool {% if client_ipv6_pool %} ipv6pool +ipv6_nd +ipv6_dhcp {% endif %} chap-secrets auth_pap @@ -81,6 +83,7 @@ master=1 [client-ip-range] disable +{% if ppp_gw %} [ip-pool] gw-ip-address={{ppp_gw}} {% if client_ip_pool %} @@ -92,6 +95,7 @@ gw-ip-address={{ppp_gw}} {{sn}} {% endfor %} {% endif %} +{% endif -%} {% if client_ipv6_pool %} [ipv6-pool] @@ -101,7 +105,7 @@ gw-ip-address={{ppp_gw}} {% for prfx in client_ipv6_pool['delegate-prefix']: %} delegate={{prfx}} {% endfor %} -{% endif -%} +{% endif %} {% if dns %} [dns] @@ -111,14 +115,14 @@ dns1={{dns[0]}} {% if dns[1] %} dns2={{dns[1]}} {% endif -%} -{% endif -%} +{% endif %} {% if dnsv6 %} -[dnsv6] +[ipv6-dns] {% for srv in dnsv6: %} -dns={{srv}} +{{srv}} {% endfor %} -{% endif -%} +{% endif %} {% if wins %} [wins] @@ -169,6 +173,9 @@ verbose=1 [shaper] verbose=1 attr={{authentication['radiusopt']['shaper']['attr']}} +{% if authentication['radiusopt']['shaper']['vendor'] %} +vendor={{authentication['radiusopt']['shaper']['vendor']}} +{% endif -%} {% endif -%} {% endif %} @@ -208,6 +215,10 @@ lcp-echo-failure=3 {% if ppp_options['ipv4'] %} ipv4={{ppp_options['ipv4']}} {% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + {% if ppp_options['ipv6'] %} ipv6={{ppp_options['ipv6']}} {% if ppp_options['ipv6-intf-id'] %} @@ -220,6 +231,7 @@ ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}} ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}} {% endif %} {% endif %} + mtu={{mtu}} [pppoe] @@ -230,13 +242,16 @@ ac-name={{concentrator}} {% if interface %} {% for int in interface %} interface={{int}} -{% endfor %} +{% if interface[int]['vlans'] %} +vlan_mon={{interface[int]['vlans']|join(',')}} +interface=re:{{int}}\.(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2}) {% endif %} +{% endfor -%} +{% endif -%} {% if svc_name %} service-name={{svc_name}} -{% endif %} +{% endif -%} pado-delay=0 -# maybe: called-sid, tr101, padi-limit etc. {% if limits %} [connlimit] @@ -326,7 +341,7 @@ def get_config(): 'client_ip_pool' : '', 'client_ip_subnets' : [], 'client_ipv6_pool' : {}, - 'interface' : [], + 'interface' : {}, 'ppp_gw' : '', 'svc_name' : '', 'dns' : [], @@ -345,7 +360,12 @@ def get_config(): if c.exists('service-name'): config_data['svc_name'] = c.return_value('service-name') if c.exists('interface'): - config_data['interface'] = c.return_values('interface') + for intfc in c.list_nodes('interface'): + config_data['interface'][intfc] = {'vlans' : []} + if c.exists('interface ' + intfc + ' vlan-id'): + config_data['interface'][intfc]['vlans'] += c.return_values('interface ' + intfc + ' vlan-id') + if c.exists('interface ' + intfc + ' vlan-range'): + config_data['interface'][intfc]['vlans'] +=c.return_values('interface ' + intfc + ' vlan-range') if c.exists('local-ip'): config_data['ppp_gw'] = c.return_value('local-ip') if c.exists('dns-servers'): @@ -476,6 +496,8 @@ def get_config(): config_data['authentication']['radiusopt']['shaper'] = { 'attr' : c.return_value('authentication radius-settings rate-limit attribute') } + if c.exists('authentication radius-settings rate-limit vendor'): + config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor') if c.exists('mtu'): config_data['mtu'] = c.return_value('mtu') @@ -543,18 +565,14 @@ def verify(c): if c['authentication']['radiussrv'][rsrv]['secret'] == None: raise ConfigError('radius server ' + rsrv + ' needs a secret configured') - ### local ippool and gateway settings - - if not c['ppp_gw']: - raise ConfigError('pppoe-server local-ip required') - - if not c['client_ip_subnets'] and not c['client_ip_pool']: - print ("Warning: No pppoe client IP pool defined") + ### local ippool and gateway settings config checks - ### activate as soon as it is clear what to do migrate or depricate. - #if c['client_ip_pool']: - # print ("Warning: client-ip-pool (start|stop) is depricated, please use client-ip-pool subnet") - # sl.syslog(sl.LOG_NOTICE, "client-ip-pool start stop is depricated, please use client-ip-pool subnet") + if c['client_ip_subnets'] or c['client_ip_pool']: + if not c['ppp_gw']: + raise ConfigError('pppoe-server local-ip required') + + if c['ppp_gw'] and not c['client_ip_subnets'] and not c['client_ip_pool']: + print ("Warning: No pppoe client IPv4 pool defined") def generate(c): if c == None: diff --git a/src/conf_mode/accel_pptp.py b/src/conf_mode/accel_pptp.py index 6c53e8dd4..1dd7efb3e 100755 --- a/src/conf_mode/accel_pptp.py +++ b/src/conf_mode/accel_pptp.py @@ -84,7 +84,9 @@ wins2={{wins[1]}} {% endif %} [pptp] +{% if outside_addr %} bind={{outside_addr}} +{% endif %} verbose=5 ppp-max-mtu={{mtu}} mppe={{authentication['mppe']}} @@ -294,7 +296,7 @@ def verify(c): if c['authentication']['mode'] == 'local': if not c['authentication']['local-users']: - raise ConfigError('pppoe-server authentication local-users required') + raise ConfigError('pptp-server authentication local-users required') for usr in c['authentication']['local-users']: if not c['authentication']['local-users'][usr]['passwd']: raise ConfigError('user ' + usr + ' requires a password') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index c8ae2fa91..3e1381cd0 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -186,8 +186,10 @@ shared-network {{ network.name }} { {%- endif -%} {%- for host in subnet.static_mapping %} {% if not host.disabled -%} - host {{ network.name }}_{{ host.name }} { + host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { + {%- if host.ip_address %} fixed-address {{ host.ip_address }}; + {%- endif %} hardware ethernet {{ host.mac_address }}; {%- if host.static_parameters %} # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated @@ -728,22 +730,19 @@ def verify(dhcp): raise ConfigError('No DHCP address range or active static-mapping set\n' \ 'for subnet {0}!'.format(subnet['network'])) - # Static IP address mappings require both an IP address and MAC address + # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set) for mapping in subnet['static_mapping']: - # Static IP address must be configured - if not mapping['ip_address']: - raise ConfigError('DHCP static lease IP address not specified for static mapping\n' \ - '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) - # Static IP address must be in bound - if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \ - 'in shared network {2} is outside DHCP lease subnet {3}!' \ - .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) + if mapping['ip_address']: + # Static IP address must be in bound + if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside DHCP lease subnet {3}!' \ + .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) # Static mapping requires MAC address if not mapping['mac_address']: - raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \ + raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \ '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) # There must be one subnet connected to a listen interface. @@ -754,7 +753,7 @@ def verify(dhcp): # Subnets must be non overlapping if subnet['network'] in subnets: - raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet)) + raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) else: subnets.append(subnet['network']) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index bb3e6e90d..039321430 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -27,8 +27,8 @@ import vyos.validate from vyos.config import Config from vyos import ConfigError -config_file = r'/etc/dhcp/dhcpd6.conf' -lease_file = r'/config/dhcpd6.leases' +config_file = r'/etc/dhcp/dhcpdv6.conf' +lease_file = r'/config/dhcpdv6.leases' daemon_config_file = r'/etc/default/isc-dhcpv6-server' # Please be careful if you edit the template. @@ -94,13 +94,20 @@ shared-network {{ network.name }} { {%- for host in subnet.static_mapping %} {% if not host.disabled -%} host {{ network.name }}_{{ host.name }} { - host-identifier option dhcp6.client-id "{{ host.client_identifier }}"; + {%- if host.client_identifier %} + host-identifier option dhcp6.client-id {{ host.client_identifier }}; + {%- endif %} + {%- if host.ipv6_address %} fixed-address6 {{ host.ipv6_address }}; + {%- endif %} } {%- endif %} {%- endfor %} } {%- endfor %} + on commit { + set shared-networkname = "{{ network.name }}"; + } } {%- endif %} {% endfor %} @@ -112,8 +119,8 @@ daemon_tmpl = """ # sourced by /etc/init.d/isc-dhcpv6-server -DHCPD_CONF=/etc/dhcp/dhcpd6.conf -DHCPD_PID=/var/run/dhcpd6.pid +DHCPD_CONF=/etc/dhcp/dhcpdv6.conf +DHCPD_PID=/var/run/dhcpdv6.pid OPTIONS="-6 -lf {{ lease_file }}" INTERFACES="" """ @@ -381,7 +388,25 @@ def verify(dhcpv6): raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \ 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name'])) + # Static mappings don't require anything (but check if IP is in subnet if it's set) + for mapping in subnet['static_mapping']: + if mapping['ipv6_address']: + # Static address must be in subnet + if not ipaddress.ip_address(mapping['ipv6_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 static mapping IPv6 address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside subnet {3}!' \ + .format(mapping['ipv6_address'], mapping['name'], network['name'], subnet['network'])) + + # Subnets must be unique + if subnet['network'] in subnets: + raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) + else: + subnets.append(subnet['network']) + # DHCPv6 requires at least one configured address range or one static mapping + # (FIXME: is not actually checked right now?) + + # There must be one subnet connected to a listen interface if network is not disabled. if not network['disabled']: if vyos.validate.is_subnet_connected(subnet['network']): listen_ok = True diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 135f6fec0..3ca77adee 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -19,12 +19,20 @@ import sys import os -import netifaces +import argparse import jinja2 +import netifaces + +import vyos.util from vyos.config import Config from vyos import ConfigError + +parser = argparse.ArgumentParser() +parser.add_argument("--dhclient", action="store_true", + help="Started from dhclient-script") + config_file = r'/etc/powerdns/recursor.conf' # XXX: pdns recursor doesn't like whitespace near entry separators, @@ -54,24 +62,22 @@ export-etc-hosts={{ export_hosts_file }} # listen-on local-address={{ listen_on | join(',') }} -# domain ... server ... -{% if domains -%} - -forward-zones={% for d in domains %} -{{ d.name }}={{ d.servers | join(";") }} -{{- "," if not loop.last -}} -{% endfor %} - -{% endif %} - # dnssec dnssec={{ dnssec }} -{% if name_servers -%} -# name-server -forward-zones-recurse=.={{ name_servers | join(';') }} -{% else %} -# no name-servers specified - start full recursor +# forward-zones / recursion +# +# statement is only inserted if either one forwarding domain or nameserver is configured +# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own +# +{% if name_servers or domains %}forward-zones-recurse= +{%- for d in domains %} +{{ d.name }}={{ d.servers | join(";") }} +{{- ", " if not loop.last -}} +{%- endfor -%} +{%- if name_servers -%} +{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }} +{% endif %} {% endif %} """ @@ -84,31 +90,36 @@ default_config_data = { 'name_servers': [], 'negative_ttl': 3600, 'domains': [], - 'dnssec' : 'process-no-validate' + 'dnssec': 'process-no-validate' } # borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! def get_resolvers(file): - resolvers = [] try: with open(file, 'r') as resolvconf: - for line in resolvconf.readlines(): - line = line.split('#',1)[0]; - line = line.rstrip(); - if 'nameserver' in line: - resolvers.append(line.split()[1]) + lines = [line.split('#', 1)[0].rstrip() + for line in resolvconf.readlines()] + resolvers = [line.split()[1] + for line in lines if 'nameserver' in line] return resolvers except IOError: return [] -def get_config(): + +def get_config(arguments): dns = default_config_data conf = Config() + + if arguments.dhclient: + conf.exists = conf.exists_effective + conf.return_value = conf.return_effective_value + conf.return_values = conf.return_effective_values + if not conf.exists('service dns forwarding'): return None - else: - conf.set_level('service dns forwarding') + + conf.set_level('service dns forwarding') if conf.exists('cache-size'): cache_size = conf.return_value('cache-size') @@ -139,7 +150,8 @@ def get_config(): system_name_servers = [] system_name_servers = conf.return_values('name-server') if not system_name_servers: - print("DNS forwarding warning: No name-servers set under 'system name-server'\n") + print( + "DNS forwarding warning: No name-servers set under 'system name-server'\n") else: dns['name_servers'] = dns['name_servers'] + system_name_servers conf.set_level('service dns forwarding') @@ -171,9 +183,10 @@ def get_config(): try: addrs = netifaces.ifaddresses(interface) except ValueError: - print("WARNING: interface {0} does not exist".format(interface)) + print( + "WARNING: interface {0} does not exist".format(interface)) continue - + if netifaces.AF_INET in addrs.keys(): for ip4 in addrs[netifaces.AF_INET]: listen4.append(ip4['addr']) @@ -183,7 +196,8 @@ def get_config(): listen6.append(ip6['addr']) if (not listen4) and (not (listen6)): - print("WARNING: interface {0} has no configured addresses".format(interface)) + print( + "WARNING: interface {0} has no configured addresses".format(interface)) dns['listen_on'] = dns['listen_on'] + listen4 + listen6 @@ -195,56 +209,69 @@ def get_config(): interfaces = [] interfaces = conf.return_values('dhcp') for interface in interfaces: - dhcp_resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface)) + dhcp_resolvers = get_resolvers( + "/etc/resolv.conf.dhclient-new-{0}".format(interface)) if dhcp_resolvers: dns['name_servers'] = dns['name_servers'] + dhcp_resolvers return dns + def bracketize_ipv6_addrs(addrs): """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched.""" return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] + def verify(dns): # bail out early - looks like removal from running config if dns is None: return None if not dns['listen_on']: - raise ConfigError("Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") + raise ConfigError( + "Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") if dns['domains']: for domain in dns['domains']: if not domain['servers']: - raise ConfigError('Error: No server configured for domain {0}'.format(domain['name'])) + raise ConfigError( + 'Error: No server configured for domain {0}'.format(domain['name'])) return None + def generate(dns): # bail out early - looks like removal from running config if dns is None: return None tmpl = jinja2.Template(config_tmpl, trim_blocks=True) - config_text = tmpl.render(dns) with open(config_file, 'w') as f: f.write(config_text) return None + def apply(dns): if dns is not None: os.system("systemctl restart pdns-recursor") else: # DNS forwarding is removed in the commit os.system("systemctl stop pdns-recursor") - os.unlink(config_file) + if os.path.isfile(config_file): + os.unlink(config_file) - return None if __name__ == '__main__': + args = parser.parse_args() + + if args.dhclient: + # There's a big chance it was triggered by a commit still in progress + # so we need to wait until the new values are in the running config + vyos.util.wait_for_commit_lock() + try: - c = get_config() + c = get_config(args) verify(c) generate(c) apply(c) diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py index e2c306904..2be80cdbf 100755 --- a/src/conf_mode/firewall_options.py +++ b/src/conf_mode/firewall_options.py @@ -32,7 +32,8 @@ def get_config(): opts = copy.deepcopy(default_config_data) conf = Config() if not conf.exists('firewall options'): - return None + # bail out early + return opts else: conf.set_level('firewall options') diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 81a52e87f..16467c8df 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -23,20 +23,29 @@ conf-mode script for 'system host-name' and 'system domain-name'. import os import re import sys -import subprocess import copy -import jinja2 import glob +import subprocess +import argparse +import jinja2 + +import vyos.util from vyos.config import Config from vyos import ConfigError + +parser = argparse.ArgumentParser() +parser.add_argument("--dhclient", action="store_true", + help="Started from dhclient-script") + config_file_hosts = '/etc/hosts' config_file_resolv = '/etc/resolv.conf' config_tmpl_hosts = """ ### Autogenerated by host_name.py ### -127.0.0.1 localhost {{ hostname }}{% if domain_name %}.{{ domain_name }}{% endif %} +127.0.0.1 localhost +127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }}{% endif %} # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback @@ -72,32 +81,6 @@ search {{ domain_search | join(" ") }} """ -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - resolvers = [] - try: - with open(file, 'r') as resolvconf: - for line in resolvconf.readlines(): - line = line.split('#',1)[0]; - line = line.rstrip(); - if 'nameserver' in line: - resolvers.append(line.split()[1]) - return resolvers - except IOError: - return [] - -def get_dhcp_search_doms(file): - search_doms = [] - try: - with open(file, 'r') as resolvconf: - for line in resolvconf.readlines(): - line = line.split('#',1)[0]; - line = line.rstrip(); - if 'search' in line: - return re.sub('^search','',line).lstrip().split() - except IOError: - return [] - default_config_data = { 'hostname': 'vyos', 'domain_name': '', @@ -106,158 +89,218 @@ default_config_data = { 'no_dhcp_ns': False } -def get_config(): - conf = Config() - hosts = copy.deepcopy(default_config_data) - - if conf.exists("system host-name"): - hosts['hostname'] = conf.return_value("system host-name") - - if conf.exists("system domain-name"): - hosts['domain_name'] = conf.return_value("system domain-name") - hosts['domain_search'].append(hosts['domain_name']) - - for search in conf.return_values("system domain-search domain"): - hosts['domain_search'].append(search) +# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! +def get_resolvers(file): + resolv = {} + try: + with open(file, 'r') as resolvconf: + lines = [line.split('#', 1)[0].rstrip() + for line in resolvconf.readlines()] + resolvers = [line.split()[1] + for line in lines if 'nameserver' in line] + domains = [line.split()[1] for line in lines if 'search' in line] + resolv['resolvers'] = resolvers + resolv['domains'] = domains + return resolv + except IOError: + return [] + + +def get_config(arguments): + conf = Config() + hosts = copy.deepcopy(default_config_data) + + if arguments.dhclient: + conf.exists = conf.exists_effective + conf.return_value = conf.return_effective_value + conf.return_values = conf.return_effective_values + + if conf.exists("system host-name"): + hosts['hostname'] = conf.return_value("system host-name") + # This may happen if the config is not loaded yet, + # e.g. if run by cloud-init + if not hosts['hostname']: + hosts['hostname'] = default_config_data['hostname'] + + if conf.exists("system domain-name"): + hosts['domain_name'] = conf.return_value("system domain-name") + hosts['domain_search'].append(hosts['domain_name']) + + for search in conf.return_values("system domain-search domain"): + hosts['domain_search'].append(search) + + if conf.exists("system name-server"): + hosts['nameserver'] = conf.return_values("system name-server") + + if conf.exists("system disable-dhcp-nameservers"): + hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') + + # system static-host-mapping + hosts['static_host_mapping'] = {'hostnames': {}} + + if conf.exists('system static-host-mapping host-name'): + for hn in conf.list_nodes('system static-host-mapping host-name'): + hosts['static_host_mapping']['hostnames'][hn] = { + 'ipaddr': conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), + 'alias': '' + } + + if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): + a = conf.return_values( + 'system static-host-mapping host-name ' + hn + ' alias') + hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join(a) + + return hosts - if conf.exists("system name-server"): - hosts['nameserver'] = conf.return_values("system name-server") - if conf.exists("system disable-dhcp-nameservers"): - hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') +def verify(config): + if config is None: + return None + + # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" + hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") + if not hostname_regex.match(config['hostname']): + raise ConfigError('Invalid host name ' + config["hostname"]) + + # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" + length = len(config['hostname']) + if length < 1 or length > 63: + raise ConfigError( + 'Invalid host-name length, must be less than 63 characters') + + # The search list is currently limited to six domains with a total of 256 characters. + # https://linux.die.net/man/5/resolv.conf + if len(config['domain_search']) > 6: + raise ConfigError( + 'The search list is currently limited to six domains') + + tmp = ' '.join(config['domain_search']) + if len(tmp) > 256: + raise ConfigError( + 'The search list is currently limited to 256 characters') + + # static mappings alias hostname + if config['static_host_mapping']['hostnames']: + for hn in config['static_host_mapping']['hostnames']: + if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: + raise ConfigError('IP address required for ' + hn) + for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): + if not hostname_regex.match(hn_alias) and len(hn_alias) != 0: + raise ConfigError('Invalid hostname alias ' + hn_alias) - ## system static-host-mapping - hosts['static_host_mapping'] = { 'hostnames' : {}} + return None - if conf.exists('system static-host-mapping host-name'): - for hn in conf.list_nodes('system static-host-mapping host-name'): - hosts['static_host_mapping']['hostnames'][hn] = { - 'ipaddr' : conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), - 'alias' : '' - } - - if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): - a = conf.return_values('system static-host-mapping host-name ' + hn + ' alias') - hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join( conf.return_values('system static-host-mapping host-name ' + hn + ' alias') ) - return hosts +def generate(config): + if config is None: + return None + + # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers + # received via dhclient should not be added into the final 'resolv.conf'. + # + # We iterate over every resolver file and retrieve the received nameservers + # for later adjustment of the system nameservers + dhcp_ns = [] + dhcp_sd = [] + for file in glob.glob('/etc/resolv.conf.dhclient-new*'): + for key, value in get_resolvers(file).items(): + ns = [r for r in value if key == 'resolvers'] + dhcp_ns.extend(ns) + sd = [d for d in value if key == 'domains'] + dhcp_sd.extend(sd) + + if not config['no_dhcp_ns']: + config['nameserver'] += dhcp_ns + config['domain_search'] += dhcp_sd + + # Prune duplicate values + # Not order preserving, but then when multiple DHCP clients are used, + # there can't be guarantees about the order anyway + dhcp_ns = list(set(dhcp_ns)) + dhcp_sd = list(set(dhcp_sd)) + + # We have third party scripts altering /etc/hosts, too. + # One example are the DHCP hostname update scripts thus we need to cache in + # every modification first - so changing domain-name, domain-search or hostname + # during runtime works + old_hosts = "" + with open(config_file_hosts, 'r') as f: + # Skips text before the beginning of our marker. + # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts + for line in f: + if line.strip() == '### modifications from other scripts should be added below': + break + + for line in f: + # This additional line.strip() filters empty lines + if line.strip(): + old_hosts += line + + # Add an additional newline + old_hosts += '\n' + + tmpl = jinja2.Template(config_tmpl_hosts) + config_text = tmpl.render(config) + + with open(config_file_hosts, 'w') as f: + f.write(config_text) + f.write(old_hosts) + + tmpl = jinja2.Template(config_tmpl_resolv) + config_text = tmpl.render(config) + with open(config_file_resolv, 'w') as f: + f.write(config_text) -def verify(config): - if config is None: return None - # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" - hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") - if not hostname_regex.match(config['hostname']): - raise ConfigError('Invalid host name ' + config["hostname"]) - # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" - length = len(config['hostname']) - if length < 1 or length > 63: - raise ConfigError('Invalid host-name length, must be less than 63 characters') +def apply(config): + if config is None: + return None - # The search list is currently limited to six domains with a total of 256 characters. - # https://linux.die.net/man/5/resolv.conf - if len(config['domain_search']) > 6: - raise ConfigError('The search list is currently limited to six domains') + # No domain name -- the Debian way. + hostname_new = config['hostname'] - tmp = ' '.join(config['domain_search']) - if len(tmp) > 256: - raise ConfigError('The search list is currently limited to 256 characters') + # rsyslog runs into a race condition at boot time with systemd + # restart rsyslog only if the hostname changed. + hostname_old = subprocess.check_output(['hostnamectl', '--static']).decode().strip() - # static mappings alias hostname - if config['static_host_mapping']['hostnames']: - for hn in config['static_host_mapping']['hostnames']: - if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: - raise ConfigError('IP address required for ' + hn) - for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): - if not hostname_regex.match(hn_alias) and len (hn_alias) !=0: - raise ConfigError('Invalid hostname alias ' + hn_alias) + os.system("hostnamectl set-hostname --static {0}".format(hostname_new)) - return None + # Restart services that use the hostname + if hostname_new != hostname_old: + os.system("systemctl restart rsyslog.service") -def generate(config): - if config is None: - return None + # If SNMP is running, restart it too + if os.system("pgrep snmpd > /dev/null") == 0: + os.system("systemctl restart snmpd.service") - # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers - # received via dhclient should not be added into the final 'resolv.conf'. - # - # We iterate over every resolver file and retrieve the received nameservers - # for later adjustment of the system nameservers - dhcp_ns = [] - for file in glob.glob('/etc/resolv.conf.dhclient-new*'): - for r in get_resolvers(file): - dhcp_ns.append(r) - - if not config['no_dhcp_ns']: - config['nameserver'] += dhcp_ns - for file in glob.glob('/etc/resolv.conf.dhclient-new*'): - config['domain_search'] = get_dhcp_search_doms(file) - - # We have third party scripts altering /etc/hosts, too. - # One example are the DHCP hostname update scripts thus we need to cache in - # every modification first - so changing domain-name, domain-search or hostname - # during runtime works - old_hosts = "" - with open(config_file_hosts, 'r') as f: - # Skips text before the beginning of our marker. - # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts - for line in f: - if line.strip() == '### modifications from other scripts should be added below': - break - - for line in f: - # This additional line.strip() filters empty lines - if line.strip(): - old_hosts += line - - # Add an additional newline - old_hosts += '\n' - - tmpl = jinja2.Template(config_tmpl_hosts) - config_text = tmpl.render(config) - - with open(config_file_hosts, 'w') as f: - f.write(config_text) - f.write(old_hosts) - - tmpl = jinja2.Template(config_tmpl_resolv) - config_text = tmpl.render(config) - with open(config_file_resolv, 'w') as f: - f.write(config_text) - - return None + # restart pdns if it is used + if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0: + os.system("/etc/init.d/pdns-recursor restart >/dev/null") -def apply(config): - if config is None: return None - fqdn = config['hostname'] - if config['domain_name']: - fqdn += '.' + config['domain_name'] - - os.system("hostnamectl set-hostname --static {0}".format(fqdn.rstrip('.'))) - - # Restart services that use the hostname - os.system("systemctl restart rsyslog.service") - - # If SNMP is running, restart it too - if os.system("pgrep snmpd > /dev/null") == 0: - os.system("systemctl restart snmpd.service") - - # restart pdns if it is used - if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0: - os.system("/etc/init.d/pdns-recursor restart >/dev/null") - - return None if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + args = parser.parse_args() + + if args.dhclient: + # There's a big chance it was triggered by a commit still in progress + # so we need to wait until the new values are in the running config + vyos.util.wait_for_commit_lock() + + + try: + c = get_config(args) + # If it's called from dhclient, then either: + # a) verification was already done at commit time + # b) it's run on an unconfigured system, e.g. by cloud-init + # Therefore, verification is either redundant or useless + if not args.dhclient: + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py new file mode 100755 index 000000000..7d618dded --- /dev/null +++ b/src/conf_mode/http-api.py @@ -0,0 +1,104 @@ +#!/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/>. +# +# + +import sys +import os +import subprocess +import json + +import vyos.defaults +from vyos.config import Config +from vyos import ConfigError + +config_file = '/etc/vyos/http-api.conf' + +default_config_data = { + 'listen_address' : '127.0.0.1', + 'port' : '8080', + 'strict' : 'false', + 'debug' : 'false', + 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] +} + +vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] + +# XXX: this model will need to be extended for tag nodes +dependencies = [ + 'https.py', +] + +def get_config(): + http_api = default_config_data + conf = Config() + if not conf.exists('service https api'): + return None + else: + conf.set_level('service https api') + + if conf.exists('strict'): + http_api['strict'] = 'true' + + if conf.exists('debug'): + http_api['debug'] = 'true' + + if conf.exists('port'): + port = conf.return_value('port') + http_api['port'] = port + + if conf.exists('keys'): + for name in conf.list_nodes('keys id'): + if conf.exists('keys id {0} key'.format(name)): + key = conf.return_value('keys id {0} key'.format(name)) + new_key = { 'id': name, 'key': key } + http_api['api_keys'].append(new_key) + + return http_api + +def verify(http_api): + return None + +def generate(http_api): + if http_api is None: + return None + + with open(config_file, 'w') as f: + json.dump(http_api, f, indent=2) + + return None + +def apply(http_api): + if http_api is not None: + os.system('sudo systemctl restart vyos-http-api.service') + for dep in dependencies: + cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + raise ConfigError("{}.".format(err)) + else: + os.system('sudo systemctl stop vyos-http-api.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py new file mode 100755 index 000000000..dae51dd7d --- /dev/null +++ b/src/conf_mode/https.py @@ -0,0 +1,132 @@ +#!/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/>. +# +# + +import sys +import os + +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = '/etc/nginx/sites-available/default' + +# Please be careful if you edit the template. +config_tmpl = """ + +### Autogenerated by http-api.py ### +# Default server configuration +# +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 302 https://$server_name$request_uri; +} + +server { + + # SSL configuration + # + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + include snippets/snakeoil.conf; + +{% for l_addr in listen_address %} + server_name {{ l_addr }}; +{% endfor %} + + location / { +{% if api %} + proxy_pass http://localhost:{{ api.port }}; + proxy_buffering off; +{% endif %} + } + + error_page 501 502 503 =200 @50*_json; + + location @50*_json { + default_type application/json; + return 200 '{"error": "Start service in configuration mode: set service https api"}'; + } + +} +""" + +default_config_data = { + 'listen_address' : [ '127.0.0.1' ] +} + +default_api_config_data = { + 'port' : '8080', +} + +def get_config(): + https = default_config_data + conf = Config() + if not conf.exists('service https'): + return None + else: + conf.set_level('service https') + + if conf.exists('listen-address'): + addrs = conf.return_values('listen-address') + https['listen_address'] = addrs[:] + + if conf.exists('api'): + https['api'] = default_api_config_data + + if conf.exists('api port'): + port = conf.return_value('api port') + https['api']['port'] = port + + return https + +def verify(https): + return None + +def generate(https): + if https is None: + return None + + tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + config_text = tmpl.render(https) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(https): + if https is not None: + os.system('sudo systemctl restart nginx.service') + else: + os.system('sudo systemctl stop nginx.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/ipoe_server.py b/src/conf_mode/ipoe_server.py new file mode 100755 index 000000000..ca6b423e5 --- /dev/null +++ b/src/conf_mode/ipoe_server.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 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/>. +# +# + +import sys +import os +import re +import time +import socket +import subprocess +import jinja2 +import syslog as sl + +from vyos.config import Config +from vyos import ConfigError + +ipoe_cnf_dir = r'/etc/accel-ppp/ipoe' +ipoe_cnf = ipoe_cnf_dir + r'/ipoe.config' + +pidfile = r'/var/run/accel_ipoe.pid' +cmd_port = r'2002' + +chap_secrets = ipoe_cnf_dir + '/chap-secrets' +## accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p /var/run/accel_pppoe.pid + +ipoe_config = ''' +### generated by ipoe.py ### +[modules] +log_syslog +ippool +ipoe +shaper +ipv6pool +ipv6_nd +ipv6_dhcp +{% if auth['mech'] == 'radius' %} +radius +{% endif -%} +{% if auth['mech'] == 'local' %} +chap-secrets +{% endif %} + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-ipoe,daemon +copy=1 +level=5 + +[ipoe] +verbose=1 +{% for intfc in interfaces %} +interface={{intfc}},\ +shared={{interfaces[intfc]['shared']}},\ +mode={{interfaces[intfc]['mode']}},\ +ifcfg={{interfaces[intfc]['ifcfg']}},\ +range={{interfaces[intfc]['range']}},\ +start={{interfaces[intfc]['sess_start']}},\ +ipv6=1 +{% endfor %} +{% if auth['mech'] == 'noauth' %} +noauth=1 +{% endif %} +{% if auth['mech'] == 'local' %} +username=ifname +password=csid +{% endif %} + +{%- for intfc in interfaces %} +{% if (interfaces[intfc]['shared'] == '0') and (interfaces[intfc]['vlan_mon']) %} +vlan_mon={{interfaces[intfc]['vlan_mon']|join(',')}} +interface=re:{{intfc}}\.(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2}) +{% endif %} +{% endfor %} + +{% if (dns['server1']) or (dns['server2']) %} +[dns] +{% if dns['server1'] %} +dns1={{dns['server1']}} +{% endif -%} +{% if dns['server2'] %} +dns2={{dns['server2']}} +{% endif -%} +{% endif -%} + +{% if (dnsv6['server1']) or (dnsv6['server2']) or (dnsv6['server3']) %} +[dnsv6] +dns={{dnsv6['server1']}} +dns={{dnsv6['server2']}} +dns={{dnsv6['server3']}} +{% endif %} + +[ipv6-nd] +verbose=1 + +[ipv6-dhcp] +verbose=1 + +{% if ipv6['prfx'] %} +[ipv6-pool] +{% for prfx in ipv6['prfx'] %} +{{prfx}} +{% endfor %} +{% for pd in ipv6['pd'] %} +delegate={{pd}} +{% endfor %} +{% endif %} + +{% if auth['mech'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/ipoe/chap-secrets +{% endif %} + +{% if auth['mech'] == 'radius' %} +[radius] +verbose=1 +{% for srv in auth['radius'] %} +server={{srv}},{{auth['radius'][srv]['secret']}},\ +req-limit={{auth['radius'][srv]['req-limit']}},\ +fail-time={{auth['radius'][srv]['fail-time']}} +{% endfor %} +{% if auth['radsettings']['dae-server']['ip-address'] %} +dae-server={{auth['radsettings']['dae-server']['ip-address']}}:\ +{{auth['radsettings']['dae-server']['port']}},\ +{{auth['radsettings']['dae-server']['secret']}} +{% endif -%} +{% if auth['radsettings']['acct-timeout'] %} +acct-timeout={{auth['radsettings']['acct-timeout']}} +{% endif -%} +{% if auth['radsettings']['max-try'] %} +max-try={{auth['radsettings']['max-try']}} +{% endif -%} +{% if auth['radsettings']['timeout'] %} +timeout={{auth['radsettings']['timeout']}} +{% endif -%} +{% if auth['radsettings']['nas-ip-address'] %} +nas-ip-address={{auth['radsettings']['nas-ip-address']}} +{% endif -%} +{% if auth['radsettings']['nas-identifier'] %} +nas-identifier={{auth['radsettings']['nas-identifier']}} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2002 +''' + +### pppoe chap secrets +chap_secrets_conf = ''' +# username server password acceptable local IP addresses shaper +{% for aifc in auth['auth_if'] %} +{% for mac in auth['auth_if'][aifc] %} +{% if (auth['auth_if'][aifc][mac]['up']) and (auth['auth_if'][aifc][mac]['down']) %} +{{aifc}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} +{% else %} +{{aifc}}\t*\t{{mac.lower()}}\t* +{% endif %} +{% endfor %} +{% endfor %} +''' + +##### Inline functions start #### +### config path creation +if not os.path.exists(ipoe_cnf_dir): + os.makedirs(ipoe_cnf_dir) + sl.syslog(sl.LOG_NOTICE, ipoe_cnf_dir + " created") + +def get_cpu(): + cpu_cnt = 1 + if os.cpu_count() == 1: + cpu_cnt = 1 + else: + cpu_cnt = int(os.cpu_count()/2) + return cpu_cnt + +def chk_con(): + cnt = 0 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", int(cmd_port))) + break + except ConnectionRefusedError: + time.sleep(0.5) + cnt +=1 + if cnt == 100: + raise("failed to start pppoe server") + break + +def accel_cmd(cmd=''): + if not cmd: + return None + try: + ret = subprocess.check_output(['/usr/bin/accel-cmd', '-p', cmd_port, cmd]).decode().strip() + return ret + except: + return 1 + +### chap_secrets file if auth mode local +def gen_chap_secrets(c): + tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(chap_secrets,'w').write(chap_secrets_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') + +##### Inline functions end #### + +def get_config(): + c = Config() + if not c.exists('service ipoe-server'): + return None + + config_data = {} + + c.set_level('service ipoe-server') + config_data['interfaces'] = {} + for intfc in c.list_nodes('interface'): + config_data['interfaces'][intfc] = { + 'mode' : 'L2', + 'shared' : '1', + 'sess_start' : 'dhcpv4', ### may need a conifg option, can be dhcpv4 or up for unclassified pkts + 'range' : None, + 'ifcfg' : '1', + 'vlan_mon' : [] + } + config_data['dns'] = { + 'server1' : None, + 'server2' : None + } + config_data['dnsv6'] = { + 'server1' : None, + 'server2' : None, + 'server3' : None + } + config_data['ipv6'] = { + 'prfx' : [], + 'pd' : [], + } + config_data['auth'] = { + 'auth_if' : {}, + 'mech' : 'noauth', + 'radius' : {}, + 'radsettings' : { + 'dae-server' : {} + } + } + + if c.exists('interface ' + intfc + ' network-mode'): + config_data['interfaces'][intfc]['mode'] = c.return_value('interface ' + intfc + ' network-mode') + if c.return_value('interface ' + intfc + ' network') == 'vlan': + config_data['interfaces'][intfc]['shared'] = '0' + if c.exists('interface ' + intfc + ' vlan-id'): + config_data['interfaces'][intfc]['vlan_mon'] += c.return_values('interface ' + intfc + ' vlan-id') + if c.exists('interface ' + intfc + ' vlan-range'): + config_data['interfaces'][intfc]['vlan_mon'] += c.return_values('interface ' + intfc + ' vlan-range') + if c.exists('interface ' + intfc + ' client-subnet'): + config_data['interfaces'][intfc]['range'] = c.return_value('interface ' + intfc + ' client-subnet') + if c.exists('dns-server server-1'): + config_data['dns']['server1'] = c.return_value('dns-server server-1') + if c.exists('dns-server server-2'): + config_data['dns']['server2'] = c.return_value('dns-server server-2') + if c.exists('dnsv6-server server-1'): + config_data['dnsv6']['server1'] = c.return_value('dnsv6-server server-1') + if c.exists('dnsv6-server server-2'): + config_data['dnsv6']['server2'] = c.return_value('dnsv6-server server-2') + if c.exists('dnsv6-server server-3'): + config_data['dnsv6']['server3'] = c.return_value('dnsv6-server server-3') + if not c.exists('authentication mode noauth'): + config_data['auth']['mech'] = c.return_value('authentication mode') + if c.exists('authentication mode local'): + for auth_int in c.list_nodes('authentication interface'): + for mac in c.list_nodes('authentication interface ' + auth_int + ' mac-address'): + config_data['auth']['auth_if'][auth_int] = {} + if c.exists('authentication interface ' + auth_int + ' mac-address ' + mac + ' rate-limit'): + config_data['auth']['auth_if'][auth_int][mac] = {} + config_data['auth']['auth_if'][auth_int][mac]['up'] = c.return_value('authentication interface ' + auth_int + ' mac-address ' + mac + ' rate-limit upload') + config_data['auth']['auth_if'][auth_int][mac]['down'] = c.return_value('authentication interface ' + auth_int + ' mac-address ' + mac + ' rate-limit download') + else: + config_data['auth']['auth_if'][auth_int][mac] = {} + config_data['auth']['auth_if'][auth_int][mac]['up'] = None + config_data['auth']['auth_if'][auth_int][mac]['down'] = None + if c.exists('authentication mode radius'): + for rsrv in c.list_nodes('authentication radius-server'): + config_data['auth']['radius'][rsrv] = {} + if c.exists('authentication radius-server ' + rsrv + ' secret'): + config_data['auth']['radius'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret') + else: + config_data['auth']['radius'][rsrv]['secret'] = None + if c.exists('authentication radius-server ' + rsrv + ' fail-time'): + config_data['auth']['radius'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time') + else: + config_data['auth']['radius'][rsrv]['fail-time'] = '0' + if c.exists('authentication radius-server ' + rsrv + ' req-limit'): + config_data['auth']['radius'][rsrv]['req-limit'] = c.return_value('authentication radius-server ' + rsrv + ' req-limit') + else: + config_data['auth']['radius'][rsrv]['req-limit'] = '0' + if c.exists('authentication radius-settings'): + if c.exists('authentication radius-settings timeout'): + config_data['auth']['radsettings']['timeout'] = c.return_value('authentication radius-settings timeout') + if c.exists('authentication radius-settings nas-ip-address'): + config_data['auth']['radsettings']['nas-ip-address'] = c.return_value('authentication radius-settings nas-ip-address') + if c.exists('authentication radius-settings nas-identifier'): + config_data['auth']['radsettings']['nas-identifier'] = c.return_value('authentication radius-settings nas-identifier') + if c.exists('authentication radius-settings max-try'): + config_data['auth']['radsettings']['max-try'] = c.return_value('authentication radius-settings max-try') + if c.exists('authentication radius-settings acct-timeout'): + config_data['auth']['radsettings']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') + if c.exists('authentication radius-settings dae-server ip-address'): + config_data['auth']['radsettings']['dae-server']['ip-address'] = c.return_value('authentication radius-settings dae-server ip-address') + if c.exists('authentication radius-settings dae-server port'): + config_data['auth']['radsettings']['dae-server']['port'] = c.return_value('authentication radius-settings dae-server port') + if c.exists('authentication radius-settings dae-server secret'): + config_data['auth']['radsettings']['dae-server']['secret'] = c.return_value('authentication radius-settings dae-server secret') + + if c.exists('client-ipv6-pool prefix'): + config_data['ipv6']['prfx'] = c.return_values('client-ipv6-pool prefix') + if c.exists('client-ipv6-pool delegate-prefix'): + config_data['ipv6']['pd'] = c.return_values('client-ipv6-pool delegate-prefix') + + return config_data + +def generate(c): + if c == None or not c: + return None + + c['thread_cnt'] = get_cpu() + + if c['auth']['mech'] == 'local': + gen_chap_secrets(c) + + tmpl = jinja2.Template(ipoe_config, trim_blocks=True) + config_text = tmpl.render(c) + open(ipoe_cnf,'w').write(config_text) + return c + +def verify(c): + if c == None or not c: + return None + + for intfc in c['interfaces']: + if not c['interfaces'][intfc]['range']: + raise ConfigError("service ipoe-server interface " + intfc + " client-subnet needs a value") + + if c['auth']['mech'] == 'radius': + if not c['auth']['radius']: + raise ConfigError("service ipoe-server authentication radius-server requires a value for authentication mode radius") + else: + for radsrv in c['auth']['radius']: + if not c['auth']['radius'][radsrv]['secret']: + raise ConfigError("service ipoe-server authentication radius-server " + radsrv + " secret requires a value") + + if c['auth']['radsettings']['dae-server']: + try: + if c['auth']['radsettings']['dae-server']['ip-address']: + pass + except: + raise ConfigError("service ipoe-server authentication radius-settings dae-server ip-address value required") + try: + if c['auth']['radsettings']['dae-server']['secret']: + pass + except: + raise ConfigError("service ipoe-server authentication radius-settings dae-server secret value required") + try: + if c['auth']['radsettings']['dae-server']['port']: + pass + except: + raise ConfigError("service ipoe-server authentication radius-settings dae-server port value required") + + if len(c['ipv6']['pd']) != 0 and len(c['ipv6']['prfx']) == 0: + raise ConfigError("service ipoe-server client-ipv6-pool prefix needs a value") + + return c + +def apply(c): + if c == None: + if os.path.exists(pidfile): + accel_cmd('shutdown hard') + if os.path.exists(pidfile): + os.remove(pidfile) + return None + + if not os.path.exists(pidfile): + ret = subprocess.call(['/usr/sbin/accel-pppd', '-c', ipoe_cnf, '-p', pidfile, '-d']) + chk_con() + if ret !=0 and os.path.exists(pidfile): + os.remove(pidfile) + raise ConfigError('accel-pppd failed to start') + else: + accel_cmd('restart') + sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py new file mode 100755 index 000000000..04549f4b4 --- /dev/null +++ b/src/conf_mode/protocols_bfd.py @@ -0,0 +1,166 @@ +#!/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/>. +# + +import sys +import jinja2 +import copy +import os +import vyos.validate + +from vyos import ConfigError +from vyos.config import Config + +config_file = r'/tmp/bfd.frr' + +# Please be careful if you edit the template. +config_tmpl = """ +! +bfd +{% for peer in old_peers -%} + no peer {{ peer }} +{% endfor -%} +! +{% for peer in new_peers -%} + peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} + detect-multiplier {{ peer.multiplier }} + receive-interval {{ peer.rx_interval }} + transmit-interval {{ peer.tx_interval }} + {% if not peer.shutdown %}no {% endif %}shutdown +{% endfor -%} +! +""" + +default_config_data = { + 'new_peers': [], + 'old_peers' : [] +} + +def get_config(): + bfd = copy.deepcopy(default_config_data) + conf = Config() + if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')): + return None + else: + conf.set_level('protocols bfd') + + # as we have to use vtysh to talk to FRR we also need to know + # which peers are gone due to a config removal - thus we read in + # all peers (active or to delete) + bfd['old_peers'] = conf.list_effective_nodes('peer') + + for peer in conf.list_nodes('peer'): + conf.set_level('protocols bfd peer {0}'.format(peer)) + bfd_peer = { + 'remote': peer, + 'shutdown': False, + 'src_if': '', + 'src_addr': '', + 'multiplier': '3', + 'rx_interval': '300', + 'tx_interval': '300', + 'multihop': False + } + + # Check if individual peer is disabled + if conf.exists('shutdown'): + bfd_peer['shutdown'] = True + + # Check if peer has a local source interface configured + if conf.exists('source interface'): + bfd_peer['src_if'] = conf.return_value('source interface') + + # Check if peer has a local source address configured - this is mandatory for IPv6 + if conf.exists('source address'): + bfd_peer['src_addr'] = conf.return_value('source address') + + # Tell BFD daemon that we should expect packets with TTL less than 254 + # (because it will take more than one hop) and to listen on the multihop + # port (4784) + if conf.exists('multihop'): + bfd_peer['multihop'] = True + + # Configures the minimum interval that this system is capable of receiving + # control packets. The default value is 300 milliseconds. + if conf.exists('interval receive'): + bfd_peer['rx_interval'] = conf.return_value('interval receive') + + # The minimum transmission interval (less jitter) that this system wants + # to use to send BFD control packets. + if conf.exists('interval transmit'): + bfd_peer['tx_interval'] = conf.return_value('interval transmit') + + # Configures the detection multiplier to determine packet loss. The remote + # transmission interval will be multiplied by this value to determine the + # connection loss detection timer. The default value is 3. + if conf.exists('interval multiplier'): + bfd_peer['multiplier'] = conf.return_value('interval multiplier') + + bfd['new_peers'].append(bfd_peer) + + return bfd + +def verify(bfd): + if bfd is None: + return None + + for peer in bfd['new_peers']: + # Bail out early if peer is shutdown + if peer['shutdown']: + continue + + # IPv6 peers require an explicit local address/interface combination + if vyos.validate.is_ipv6(peer['remote']): + if not (peer['src_if'] and peer['src_addr']): + raise ConfigError('BFD IPv6 peers require explicit local address/interface setting') + + # multihop doesn't accept interface names + if peer['multihop'] and peer['src_if']: + raise ConfigError('multihop does not accept interface names') + + + return None + +def generate(bfd): + if bfd is None: + return None + + return None + +def apply(bfd): + if bfd is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(bfd) + with open(config_file, 'w') as f: + f.write(config_text) + + os.system("sudo vtysh -d bfdd -f " + config_file) + if os.path.exists(config_file): + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 06d2e253a..0ddab2129 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -206,6 +206,13 @@ group {{ u.group }} usm {{ u.name }} group {{ u.group }} tsm {{ u.name }} {% endfor %} {%- endif %} + +{% if script_ext %} +# extension scripts +{%- for ext in script_ext|sort %} +extend\t{{ext}}\t{{script_ext[ext]}} +{%- endfor %} +{% endif %} """ # SNMP template (/etc/default/snmpd) - be careful if you edit the template. @@ -240,7 +247,8 @@ default_config_data = { 'v3_tsm_key': '', 'v3_tsm_port': '10161', 'v3_users': [], - 'v3_views': [] + 'v3_views': [], + 'script_ext': {} } def rmfile(file): @@ -345,6 +353,14 @@ def get_config(): snmp['trap_targets'].append(trap_tgt) + # + # 'set service snmp script-extensions' + # + if conf.exists('script-extensions'): + for extname in conf.list_nodes('script-extensions extension-name'): + snmp['script_ext'][extname] = '/config/user-data/' + conf.return_value('script-extensions extension-name ' + extname + ' script') + + ######################################################################### # ____ _ _ __ __ ____ _____ # # / ___|| \ | | \/ | _ \ __ _|___ / # @@ -581,6 +597,14 @@ def verify(snmp): if snmp is None: return None + ### check if the configured script actually exist under /config/user-data + if snmp['script_ext']: + for ext in snmp['script_ext']: + if not os.path.isfile(snmp['script_ext'][ext]): + print ("WARNING: script: " + snmp['script_ext'][ext] + " doesn\'t exist") + else: + os.chmod(snmp['script_ext'][ext], 0o555) + # bail out early if SNMP v3 is not configured if not snmp['v3_enabled']: return None @@ -633,7 +657,6 @@ def verify(snmp): if not 'seclevel' in group.keys(): raise ConfigError('"seclevel" must be specified') - if 'v3_traps' in snmp.keys(): for trap in snmp['v3_traps']: if trap['authPassword'] and trap['authMasterKey']: diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index c8541a4a0..7b79c701b 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -72,8 +72,8 @@ logrotate_configs = ''' missingok notifempty create - rotate {{files[file]['max-files']}} - size={{ files[file]['max-size']//1024}}k + rotate {{files[file]['max-files']}} + size={{files[file]['max-size']//1024}}k postrotate invoke-rc.d rsyslog rotate > /dev/null endscript @@ -120,8 +120,8 @@ def get_config(): config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility') if c.exists('global archive size'): config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024 - if c.exists('global archive files'): - config_data['files']['global']['max-files'] = c.return_value('global archive files') + if c.exists('global archive file'): + config_data['files']['global']['max-files'] = c.return_value('global archive file') if c.exists('global preserve-fqdn'): config_data['files']['global']['preserver_fqdn'] = True @@ -196,7 +196,7 @@ def get_config(): } } ) - + return config_data def generate_selectors(c, config_node): @@ -279,7 +279,7 @@ def verify(c): raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item) def apply(c): - if not os.path.exits('/var/run/rsyslogd.pid'): + if not os.path.exists('/var/run/rsyslogd.pid'): os.system("sudo systemctl start rsyslog >/dev/null") else: os.system("sudo systemctl restart rsyslog >/dev/null") diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index bc833b63f..a08493309 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -39,7 +39,7 @@ config_tmpl = """ {% if group.health_check_script -%} vrrp_script healthcheck_{{ group.name }} { - script {{ group.health_check_script }} + script "{{ group.health_check_script }}" interval {{ group.health_check_interval }} fall {{ group.health_check_count }} rise 1 @@ -85,7 +85,7 @@ vrrp_instance {{ group.name }} { {% if group.auth_password -%} authentication { - auth_pass {{ group.auth_password }} + auth_pass "{{ group.auth_password }}" auth_type {{ group.auth_type }} } {% endif -%} diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py index e893dba47..8234fad0b 100755 --- a/src/conf_mode/wireguard.py +++ b/src/conf_mode/wireguard.py @@ -63,6 +63,7 @@ def get_config(): 'lport' : '', 'status' : 'exists', 'state' : 'enabled', + 'fwmark' : 0x00, 'mtu' : '1420', 'peer' : {} } @@ -95,6 +96,9 @@ def get_config(): ### listen port if c.exists(cnf + ' port'): config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') + ### fwmark + if c.exists(cnf + ' fwmark'): + config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark') ### description if c.exists(cnf + ' description'): config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') @@ -221,7 +225,7 @@ def apply(c): ### config updates if c['interfaces'][intf]['status'] == 'exists': ### IP address change - addr_eff = re.sub("\'", "", c_eff.return_effective_values(intf + ' address')).split() + addr_eff = c_eff.return_effective_values(intf + ' address') addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) @@ -296,6 +300,10 @@ def configure_interface(c, intf): if c['interfaces'][intf]['lport']: wg_config['port'] = c['interfaces'][intf]['lport'] + ## fwmark + if c['interfaces'][intf]['fwmark']: + wg_config['fwmark'] = c['interfaces'][intf]['fwmark'] + ## endpoint if c['interfaces'][intf]['peer'][p]['endpoint']: wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] @@ -314,6 +322,7 @@ def configure_interface(c, intf): ### assemble wg command cmd = "sudo wg set " + intf cmd += " listen-port " + str(wg_config['port']) + cmd += " fwmark " + str(wg_config['fwmark']) cmd += " private-key " + wg_config['private-key'] cmd += " peer " + wg_config['pubkey'] cmd += " preshared-key " + wg_config['psk'] diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py new file mode 100755 index 000000000..a57a19cdf --- /dev/null +++ b/src/helpers/run-config-migration.py @@ -0,0 +1,83 @@ +#!/usr/bin/python3 + +# 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 +import argparse +import datetime +import subprocess +from vyos.migrator import Migrator, VirtualMigrator + +def main(): + argparser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter) + argparser.add_argument('config_file', type=str, + help="configuration file to migrate") + argparser.add_argument('--force', action='store_true', + help="Force calling of all migration scripts.") + argparser.add_argument('--set-vintage', type=str, + choices=['vyatta', 'vyos'], + help="Set the format for the config version footer in config" + " file:\n" + "set to 'vyatta':\n" + "(for '/* === vyatta-config-version ... */' format)\n" + "or 'vyos':\n" + "(for '// vyos-config-version ...' format).") + argparser.add_argument('--virtual', action='store_true', + help="Update the format of the trailing comments in" + " config file,\nfrom 'vyatta' to 'vyos'; no migration" + " scripts are run.") + args = argparser.parse_args() + + config_file_name = args.config_file + force_on = args.force + vintage = args.set_vintage + virtual = args.virtual + + if not os.access(config_file_name, os.R_OK): + print("Read error: {}.".format(config_file_name)) + sys.exit(1) + + if not os.access(config_file_name, os.W_OK): + print("Write error: {}.".format(config_file_name)) + sys.exit(1) + + separator = "." + backup_file_name = separator.join([config_file_name, + '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()), + 'pre-migration']) + + try: + subprocess.check_call(['cp', '-p', config_file_name, + backup_file_name]) + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) + sys.exit(1) + + if not virtual: + migration = Migrator(config_file_name, force=force_on, + set_vintage=vintage) + else: + migration = VirtualMigrator(config_file_name) + + migration.run() + + if not migration._changed: + os.remove(backup_file_name) + +if __name__ == '__main__': + main() diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py new file mode 100755 index 000000000..c33e41d79 --- /dev/null +++ b/src/helpers/system-versions-foot.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 + +# 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 sys +import vyos.formatversions as formatversions +import vyos.systemversions as systemversions +import vyos.defaults +import vyos.version + +sys_versions = systemversions.get_system_versions() + +component_string = formatversions.format_versions_string(sys_versions) + +os_version_string = vyos.version.get_version() + +sys.stdout.write("\n\n") +if vyos.defaults.cfg_vintage == 'vyos': + formatversions.write_vyos_versions_foot(None, component_string, + os_version_string) +elif vyos.defaults.cfg_vintage == 'vyatta': + formatversions.write_vyatta_versions_foot(None, component_string, + os_version_string) +else: + formatversions.write_vyatta_versions_foot(None, component_string, + os_version_string) diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py new file mode 100755 index 000000000..e9a14ae98 --- /dev/null +++ b/src/helpers/vyos-merge-config.py @@ -0,0 +1,115 @@ +#!/usr/bin/python3 + +# 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 sys +import os +import subprocess +import tempfile +import vyos.defaults +import vyos.remote +import vyos.migrator +from vyos.config import Config +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 2): + print("Need config file name to merge.") + print("Usage: merge <config file> [config path]") + sys.exit(0) + +file_name = sys.argv[1] + +configdir = vyos.defaults.directories['config'] + +protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + +if any(x in file_name for x in protocols): + config_file = vyos.remote.get_remote_config(file_name) + if not config_file: + sys.exit("No config file by that name.") +else: + canonical_path = "{0}/{1}".format(configdir, file_name) + first_err = None + try: + with open(canonical_path, 'r') as f: + config_file = f.read() + except Exception as err: + first_err = err + try: + with open(file_name, 'r') as f: + config_file = f.read() + except Exception as err: + print(first_err) + print(err) + sys.exit(1) + +with tempfile.NamedTemporaryFile() as file_to_migrate: + with open(file_to_migrate.name, 'w') as fd: + fd.write(config_file) + + migration = vyos.migrator.Migrator(file_to_migrate.name) + migration.run() + if migration.config_changed(): + with open(file_to_migrate.name, 'r') as fd: + config_file = fd.read() + +merge_config_tree = ConfigTree(config_file) + +effective_config = Config() + +output_effective_config = effective_config.show_config() +# showConfig (called by config.show_config() does not escape +# backslashes, which configtree expects; cf. T1001. +output_effective_config = output_effective_config.replace("\\", "\\\\") + +effective_config_tree = ConfigTree(output_effective_config) + +effective_cmds = effective_config_tree.to_commands() +merge_cmds = merge_config_tree.to_commands() + +effective_cmd_list = effective_cmds.splitlines() +merge_cmd_list = merge_cmds.splitlines() + +effective_cmd_set = set(effective_cmd_list) +add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ] + +path = None +if (len(sys.argv) > 2): + path = sys.argv[2:] + if (not effective_config_tree.exists(path) and not + merge_config_tree.exists(path)): + print("path {} does not exist in either effective or merge" + " config; will use root.".format(path)) + path = None + else: + path = " ".join(path) + +if path: + add_cmds = [ cmd for cmd in add_cmds if path in cmd ] + +for cmd in add_cmds: + cmd = "/opt/vyatta/sbin/my_" + cmd + + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) + +if effective_config.session_changed(): + print("Merge complete. Use 'commit' to make changes effective.") +else: + print("No configuration changes to commit.") diff --git a/src/migration-scripts/pppoe-server/1-to-2 b/src/migration-scripts/pppoe-server/1-to-2 new file mode 100755 index 000000000..fa83896d3 --- /dev/null +++ b/src/migration-scripts/pppoe-server/1-to-2 @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +# Convert "service pppoe-server interface ethX" +# to: +# "service pppoe-server interface ethX {}" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +ctree = ConfigTree(config_file) +cbase = ['service', 'pppoe-server','interface'] + +if not ctree.exists(cbase): + sys.exit(0) +else: + nics = ctree.return_values(cbase) + # convert leafNode to a tagNode + ctree.set(cbase) + ctree.set_tag(cbase) + for nic in nics: + ctree.set(cbase + [nic]) + + try: + open(file_name,'w').write(ctree.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) + diff --git a/src/migration-scripts/system/6-to-7 b/src/migration-scripts/system/6-to-7 new file mode 100755 index 000000000..bf07abf3a --- /dev/null +++ b/src/migration-scripts/system/6-to-7 @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +# Change smp_affinity to smp-affinity + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +update_required = False + +intf_types = config.list_nodes(["interfaces"]) + +for intf_type in intf_types: + intf_type_path = ["interfaces", intf_type] + intfs = config.list_nodes(intf_type_path) + + for intf in intfs: + intf_path = intf_type_path + [intf] + if not config.exists(intf_path + ["smp_affinity"]): + # Nothing to do. + continue + else: + # Rename the node. + old_smp_affinity_path = intf_path + ["smp_affinity"] + config.rename(old_smp_affinity_path, "smp-affinity") + update_required = True + +if update_required: + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("failed to save the modified config: {}".format(e)) + sys.exit(1) + + + diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index 4c4ee6355..c2a05f516 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -20,6 +20,9 @@ import argparse import ipaddress import tabulate import sys +import collections +import os +from datetime import datetime from vyos.config import Config from isc_dhcp_leases import Lease, IscDhcpLeases @@ -27,6 +30,18 @@ from isc_dhcp_leases import Lease, IscDhcpLeases lease_file = "/config/dhcpd.leases" pool_key = "shared-networkname" +lease_display_fields = collections.OrderedDict() +lease_display_fields['ip'] = 'IP address' +lease_display_fields['hardware_address'] = 'Hardware address' +lease_display_fields['state'] = 'State' +lease_display_fields['start'] = 'Lease start' +lease_display_fields['end'] = 'Lease expiration' +lease_display_fields['remaining'] = 'Remaining' +lease_display_fields['pool'] = 'Pool' +lease_display_fields['hostname'] = 'Hostname' + +lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] + def in_pool(lease, pool): if pool_key in lease.sets: if lease.sets[pool_key] == pool: @@ -34,17 +49,47 @@ def in_pool(lease, pool): return False +def utc_to_local(utc_dt): + return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) + def get_lease_data(lease): data = {} - # End time may not be present in backup leases + # isc-dhcp lease times are in UTC so we need to convert them to local time to display + try: + data["start"] = utc_to_local(lease.start).strftime("%Y/%m/%d %H:%M:%S") + except: + data["start"] = "" + + try: + data["end"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") + except: + data["end"] = "" + try: - data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + data["remaining"] = lease.end - datetime.utcnow() + # negative timedelta prints wrong so bypass it + if (data["remaining"].days >= 0): + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data["remaining"] = str(data["remaining"]).split('.')[0] + else: + data["remaining"] = "" except: - data["expires"] = "" + data["remaining"] = "" + + # currently not used but might come in handy + # todo: parse into datetime string + for prop in ['tstp', 'tsfp', 'atsfp', 'cltt']: + if prop in lease.data: + data[prop] = lease.data[prop] + else: + data[prop] = '' data["hardware_address"] = lease.ethernet data["hostname"] = lease.hostname + + data["state"] = lease.binding_state data["ip"] = lease.ip try: @@ -54,26 +99,54 @@ def get_lease_data(lease): return data -def get_leases(leases, state=None, pool=None): +def get_leases(leases, state, pool=None, sort='ip'): + # get leases from file leases = IscDhcpLeases(lease_file).get() - if state is not None: - leases = list(filter(lambda x: x.binding_state == 'active', leases)) + # filter leases by state + if 'all' not in state: + leases = list(filter(lambda x: x.binding_state in state, leases)) + # filter leases by pool name if pool is not None: - leases = list(filter(lambda x: in_pool(x, pool), leases)) + if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): + leases = list(filter(lambda x: in_pool(x, pool), leases)) + else: + print("Pool {0} does not exist.".format(pool)) + sys.exit(0) - return list(map(get_lease_data, leases)) + # should maybe filter all state=active by lease.valid here? -def show_leases(leases): - headers = ["IP address", "Hardware address", "Lease expiration", "Pool", "Client Name"] + # sort by start time to dedupe (newest lease overrides older) + leases = sorted(leases, key = lambda lease: lease.start) + + # dedupe by converting to dict + leases_dict = {} + for lease in leases: + # dedupe by IP + leases_dict[lease.ip] = lease + # convert the lease data + leases = list(map(get_lease_data, leases_dict.values())) + + # apply output/display sort + if sort == 'ip': + leases = sorted(leases, key = lambda lease: int(ipaddress.ip_address(lease['ip']))) + else: + leases = sorted(leases, key = lambda lease: lease[sort]) + + return leases + +def show_leases(leases): lease_list = [] for l in leases: - lease_list.append([l["ip"], l["hardware_address"], l["expires"], l["pool"], l["hostname"]]) + lease_list_params = [] + for k in lease_display_fields.keys(): + lease_list_params.append(l[k]) + lease_list.append(lease_list_params) + + output = tabulate.tabulate(lease_list, lease_display_fields.values()) - output = tabulate.tabulate(lease_list, headers) - print(output) def get_pool_size(config, pool): @@ -85,7 +158,7 @@ def get_pool_size(config, pool): start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) - size += int(ipaddress.IPv4Address(stop)) - int(ipaddress.IPv4Address(start)) + size += int(ipaddress.ip_address(stop)) - int(ipaddress.ip_address(start)) return size @@ -101,35 +174,33 @@ if __name__ == '__main__': group = parser.add_mutually_exclusive_group() group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") - parser.add_argument("-e", "--expired", action="store_true", help="Show expired leases") - parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") - parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") + parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") args = parser.parse_args() # Do nothing if service is not configured config = Config() if not config.exists_effective('service dhcp-server'): - print("DHCP service is not configured") + print("DHCP service is not configured.") sys.exit(0) + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if os.system('systemctl -q is-active isc-dhcp-server.service') != 0: + print("WARNING: DHCP server is configured but not started. Data may be stale.") + if args.leases: - if args.expired: - if args.pool: - leases = get_leases(lease_file, state='free', pool=args.pool) - else: - leases = get_leases(lease_file, state='free') - else: - if args.pool: - leases = get_leases(lease_file, state='active', pool=args.pool) - else: - leases = get_leases(lease_file, state='active') + leases = get_leases(lease_file, args.state, args.pool, args.sort) if args.json: print(json.dumps(leases, indent=4)) else: show_leases(leases) + elif args.statistics: pools = [] @@ -143,10 +214,10 @@ if __name__ == '__main__': stats = [] for p in pools: size = get_pool_size(config, p) - leases = len(get_leases(lease_file, state='active', pool=args.pool)) + leases = len(get_leases(lease_file, state='active', pool=p)) if size != 0: - use_percentage = round(leases / size) * 100 + use_percentage = round(leases / size * 100) else: use_percentage = 0 @@ -163,5 +234,12 @@ if __name__ == '__main__': print(json.dumps(stats, indent=4)) else: show_pool_stats(stats) + + elif args.allowed == 'pool': + print(' '.join(config.list_effective_nodes("service dhcp-server shared-network-name"))) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) else: - print("Use either --leases or --statistics option") + parser.print_help() diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index bf73b92ea..1a6ee62e6 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -20,42 +20,138 @@ import argparse import ipaddress import tabulate import sys +import collections +import os +from datetime import datetime from vyos.config import Config from isc_dhcp_leases import Lease, IscDhcpLeases lease_file = "/config/dhcpdv6.leases" +pool_key = "shared-networkname" + +lease_display_fields = collections.OrderedDict() +lease_display_fields['ip'] = 'IPv6 address' +lease_display_fields['state'] = 'State' +lease_display_fields['last_comm'] = 'Last communication' +lease_display_fields['expires'] = 'Lease expiration' +lease_display_fields['remaining'] = 'Remaining' +lease_display_fields['type'] = 'Type' +lease_display_fields['pool'] = 'Pool' +lease_display_fields['iaid_duid'] = 'IAID_DUID' + +lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] + +def in_pool(lease, pool): + if pool_key in lease.sets: + if lease.sets[pool_key] == pool: + return True + + return False + +def format_hex_string(in_str): + out_str = "" + + # if input is divisible by 2, add : every 2 chars + if len(in_str) > 0 and len(in_str) % 2 == 0: + out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2])) + else: + out_str = in_str + + return out_str + +def utc_to_local(utc_dt): + return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) def get_lease_data(lease): data = {} - # End time may not be present in backup leases + # isc-dhcp lease times are in UTC so we need to convert them to local time to display try: - data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + data["expires"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") except: data["expires"] = "" - data["duid"] = lease.host_identifier_string + try: + data["last_comm"] = utc_to_local(lease.last_communication).strftime("%Y/%m/%d %H:%M:%S") + except: + data["last_comm"] = "" + + try: + data["remaining"] = lease.end - datetime.utcnow() + # negative timedelta prints wrong so bypass it + if (data["remaining"].days >= 0): + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data["remaining"] = str(data["remaining"]).split('.')[0] + else: + data["remaining"] = "" + except: + data["remaining"] = "" + + # isc-dhcp records lease declarations as ia_{na|ta|pd} IAID_DUID {...} + # where IAID_DUID is the combined IAID and DUID + data["iaid_duid"] = format_hex_string(lease.host_identifier_string) + + lease_types_long = {"na": "non-temporary", "ta": "temporary", "pd": "prefix delegation"} + data["type"] = lease_types_long[lease.type] + + data["state"] = lease.binding_state data["ip"] = lease.ip + try: + data["pool"] = lease.sets[pool_key] + except: + data["pool"] = "" + return data -def get_leases(leases, state=None): +def get_leases(leases, state, pool=None, sort='ip'): leases = IscDhcpLeases(lease_file).get() - if state is not None: - leases = list(filter(lambda x: x.binding_state == 'active', leases)) + # filter leases by state + if 'all' not in state: + leases = list(filter(lambda x: x.binding_state in state, leases)) - return list(map(get_lease_data, leases)) + # filter leases by pool name + if pool is not None: + if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): + leases = list(filter(lambda x: in_pool(x, pool), leases)) + else: + print("Pool {0} does not exist.".format(pool)) + sys.exit(0) -def show_leases(leases): - headers = ["IPv6 address", "Lease expiration", "DUID"] + # should maybe filter all state=active by lease.valid here? + + # sort by last_comm time to dedupe (newest lease overrides older) + leases = sorted(leases, key = lambda lease: lease.last_communication) + + # dedupe by converting to dict + leases_dict = {} + for lease in leases: + # dedupe by IP + leases_dict[lease.ip] = lease + # convert the lease data + leases = list(map(get_lease_data, leases_dict.values())) + + # apply output/display sort + if sort == 'ip': + leases = sorted(leases, key = lambda k: int(ipaddress.ip_address(k['ip']))) + else: + leases = sorted(leases, key = lambda k: k[sort]) + + return leases + +def show_leases(leases): lease_list = [] for l in leases: - lease_list.append([l["ip"], l["expires"], l["duid"]]) + lease_list_params = [] + for k in lease_display_fields.keys(): + lease_list_params.append(l[k]) + lease_list.append(lease_list_params) - output = tabulate.tabulate(lease_list, headers) + output = tabulate.tabulate(lease_list, lease_display_fields.values()) print(output) @@ -63,11 +159,14 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() - group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") - group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCPv6 statistics") + group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") - parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") - parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") + parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") args = parser.parse_args() @@ -77,10 +176,24 @@ if __name__ == '__main__': print("DHCPv6 service is not configured") sys.exit(0) + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if os.system('systemctl -q is-active isc-dhcpv6-server.service') != 0: + print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") + if args.leases: - leases = get_leases(lease_file, state='active') - show_leases(leases) + leases = get_leases(lease_file, args.state, args.pool, args.sort) + + if args.json: + print(json.dumps(leases, indent=4)) + else: + show_leases(leases) elif args.statistics: print("DHCPv6 statistics option is not available") + elif args.allowed == 'pool': + print(' '.join(c.list_effective_nodes("service dhcpv6-server shared-network-name"))) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) else: - print("Invalid option") + parser.print_help() diff --git a/src/op_mode/version.py b/src/op_mode/version.py index ce3b3b54f..5aff0f767 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -51,7 +51,8 @@ version_output_tmpl = """ Version: VyOS {{version}} Built by: {{built_by}} Built on: {{built_on}} -Build ID: {{build_id}} +Build UUID: {{build_uuid}} +Build Commit ID: {{build_git}} Architecture: {{system_arch}} Boot via: {{boot_via}} diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server new file mode 100755 index 000000000..e11eb6d52 --- /dev/null +++ b/src/services/vyos-http-api-server @@ -0,0 +1,237 @@ +#!/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/>. +# +# + +import os +import sys +import grp +import json +import traceback +import threading + +import vyos.config + +import bottle + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.config import VyOSError + + +DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' + +CFG_GROUP = 'vyattacfg' + +app = bottle.default_app() + +# Giant lock! +lock = threading.Lock() + +def load_server_config(): + with open(DEFAULT_CONFIG_FILE) as f: + config = json.load(f) + return config + +def check_auth(key_list, key): + id = None + for k in key_list: + if k['key'] == key: + id = k['id'] + return id + +def error(code, msg): + bottle.response.status = code + resp = {"success": False, "error": msg, "data": None} + return json.dumps(resp) + +def success(data): + resp = {"success": True, "data": data, "error": None} + return json.dumps(resp) + +@app.route('/configure', method='POST') +def configure(): + session = app.config['vyos_session'] + config = app.config['vyos_config'] + api_keys = app.config['vyos_keys'] + + key = bottle.request.forms.get("key") + id = check_auth(api_keys, key) + if not id: + return error(401, "Valid API key is required") + + strict_field = bottle.request.forms.get("strict") + if strict_field == "true": + strict = True + else: + strict = False + + commands = bottle.request.forms.get("data") + if not commands: + return error(400, "Non-empty data field is required") + else: + try: + commands = json.loads(commands) + except Exception as e: + return error(400, "Failed to parse JSON: {0}".format(e)) + + # Allow users to pass just one command + if not isinstance(commands, list): + commands = [commands] + + # We don't want multiple people/apps to be able to commit at once, + # or modify the shared session while someone else is doing the same, + # so the lock is really global + lock.acquire() + + status = 200 + error_msg = None + try: + for c in commands: + # What we've got may not even be a dict + if not isinstance(c, dict): + raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c))) + + # Missing op or path is a show stopper + if not ('op' in c): + raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c))) + if not ('path' in c): + raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c))) + + # Missing value is fine, substitute for empty string + if 'value' in c: + value = c['value'] + else: + value = "" + + op = c['op'] + path = c['path'] + + if not path: + raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c))) + + # Type checking + if not isinstance(path, list): + raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c))) + + if not isinstance(value, str): + raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c))) + + # Account for the case when value field is present and set to null + if not value: + value = "" + + # For vyos.configsessios calls that have no separate value arguments, + # and for type checking too + try: + cfg_path = " ".join(path + [value]).strip() + except TypeError: + raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c))) + + if op == 'set': + # XXX: it would be nice to do a strict check for "path already exists", + # but there's probably no way to do that + session.set(path, value=value) + elif op == 'delete': + if strict and not config.exists(cfg_path): + raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path)) + session.delete(path, value=value) + elif op == 'comment': + session.comment(path, value=value) + else: + raise ConfigSessionError("\"{0}\" is not a valid operation".format(op)) + # end for + session.commit() + print("Configuration modified via HTTP API using key \"{0}\"".format(id)) + except ConfigSessionError as e: + session.discard() + status = 400 + if app.config['vyos_debug']: + print(traceback.format_exc(), file=sys.stderr) + error_msg = str(e) + except Exception as e: + session.discard() + print(traceback.format_exc(), file=sys.stderr) + status = 500 + + # Don't give the details away to the outer world + error_msg = "An internal error occured. Check the logs for details." + finally: + lock.release() + + if status != 200: + return error(status, error_msg) + else: + return success(None) + +@app.route('/retrieve', method='POST') +def get_value(): + config = app.config['vyos_config'] + + api_keys = app.config['vyos_keys'] + + key = bottle.request.forms.get("key") + id = check_auth(api_keys, key) + if not id: + return error(401, "Valid API key is required") + + command = bottle.request.forms.get("data") + command = json.loads(command) + + op = command['op'] + path = " ".join(command['path']) + + try: + if op == 'returnValue': + res = config.return_value(path) + elif op == 'returnValues': + res = config.return_values(path) + elif op == 'exists': + res = config.exists(path) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except VyOSError as e: + return error(400, str(e)) + except Exception as e: + print(traceback.format_exc(), file=sys.stderr) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + +if __name__ == '__main__': + # systemd's user and group options don't work, do it by hand here, + # else no one else will be able to commit + cfg_group = grp.getgrnam(CFG_GROUP) + os.setgid(cfg_group.gr_gid) + + # Need to set file permissions to 775 too so that every vyattacfg group member + # has write access to the running config + os.umask(0o002) + + try: + server_config = load_server_config() + except Exception as e: + print("Failed to load the HTTP API server config: {0}".format(e)) + + session = ConfigSession(os.getpid()) + env = session.get_session_env() + config = vyos.config.Config(session_env=env) + + app.config['vyos_session'] = session + app.config['vyos_config'] = config + app.config['vyos_keys'] = server_config['api_keys'] + app.config['vyos_debug'] = server_config['debug'] + + bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True) diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service new file mode 100644 index 000000000..4fa68b4ff --- /dev/null +++ b/src/systemd/vyos-http-api.service @@ -0,0 +1,24 @@ +[Unit] +Description=VyOS HTTP API service +After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service +Requires=vyos-router.service + +[Service] +ExecStartPre=/usr/libexec/vyos/init/vyos-config +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server +Type=idle +KillMode=process + +SyslogIdentifier=vyos-http-api +SyslogFacility=daemon + +Restart=on-failure + +# Does't work but leave it here +User=root +Group=vyattacfg + +[Install] +# Installing in a earlier target leaves ExecStartPre waiting +WantedBy=getty.target + diff --git a/src/utils/vyos-config-file-query b/src/utils/vyos-config-file-query new file mode 100755 index 000000000..a10c7e9b3 --- /dev/null +++ b/src/utils/vyos-config-file-query @@ -0,0 +1,100 @@ +#!/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/>. +# + +import re +import os +import sys +import json +import argparse + +import vyos.configtree + + +arg_parser = argparse.ArgumentParser() +arg_parser.add_argument('-p', '--path', type=str, + help="VyOS config node, e.g. \"system config-management commit-revisions\"", required=True) +arg_parser.add_argument('-f', '--file', type=str, help="VyOS config file, e.g. /config/config.boot", required=True) + +arg_parser.add_argument('-s', '--separator', type=str, default=' ', help="Value separator for the plain format") +arg_parser.add_argument('-j', '--json', action='store_true') + +op_group = arg_parser.add_mutually_exclusive_group(required=True) +op_group.add_argument('--return-value', action='store_true', help="Return a single node value") +op_group.add_argument('--return-values', action='store_true', help="Return all values of a multi-value node") +op_group.add_argument('--list-nodes', action='store_true', help="List children of a node") +op_group.add_argument('--exists', action='store_true', help="Check if a node exists") + +args = arg_parser.parse_args() + + +try: + with open(args.file, 'r') as f: + config_file = f.read() +except OSError as e: + print("Could not read the config file: {0}".format(e)) + sys.exit(1) + +try: + config = vyos.configtree.ConfigTree(config_file) +except Exception as e: + print(e) + sys.exit(1) + + +path = re.split(r'\s+', args.path) +values = None + +if args.exists: + if config.exists(path): + sys.exit(0) + else: + sys.exit(1) +elif args.return_value: + try: + values = [config.return_value(path)] + except vyos.configtree.ConfigTreeError as e: + print(e) + sys.exit(1) +elif args.return_values: + try: + values = config.return_values(path) + except vyos.configtree.ConfigTreeError as e: + print(e) + sys.exit(1) +elif args.list_nodes: + values = config.list_nodes(path) + if not values: + values = [] +else: + # Can't happen + print("Operation required") + sys.exit(1) + + +if values: + if args.json: + print(json.dumps(values)) + else: + if len(values) == 1: + print(values[0]) + else: + # XXX: assuming values never contain quotes + values = list(map(lambda s: "\'{0}\'".format(s), values)) + values_str = args.separator.join(values) + print(values_str) + +sys.exit(0) diff --git a/src/validators/cidr b/src/validators/cidr new file mode 100755 index 000000000..815aa8ba1 --- /dev/null +++ b/src/validators/cidr @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-cidr $1 |