diff options
Diffstat (limited to 'src')
43 files changed, 1204 insertions, 457 deletions
| diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index ac3dc536b..70d149f0d 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -40,20 +40,7 @@ airbag.enable()  config_containers_registry = '/etc/containers/registries.conf'  config_containers_storage = '/etc/containers/storage.conf' - -def _run_rerun(container_cmd): -    counter = 0 -    while True: -        if counter >= 10: -            break -        try: -            _cmd(container_cmd) -            break -        except: -            counter = counter +1 -            sleep(0.5) - -    return None +systemd_unit_path = '/run/systemd/system'  def _cmd(command):      if os.path.exists('/tmp/vyos.container.debug'): @@ -122,7 +109,7 @@ def verify(container):              # of image upgrade and deletion.              image = container_config['image']              if run(f'podman image exists {image}') != 0: -                Warning(f'Image "{image}" used in contianer "{name}" does not exist '\ +                Warning(f'Image "{image}" used in container "{name}" does not exist '\                          f'locally. Please use "add container image {image}" to add it '\                          f'to the system! Container "{name}" will not be started!') @@ -136,9 +123,6 @@ def verify(container):                      raise ConfigError(f'Container network "{network_name}" does not exist!')                  if 'address' in container_config['network'][network_name]: -                    if 'network' not in container_config: -                        raise ConfigError(f'Can not use "address" without "network" for container "{name}"!') -                      address = container_config['network'][network_name]['address']                      network = None                      if is_ipv4(address): @@ -220,6 +204,71 @@ def verify(container):      return None +def generate_run_arguments(name, container_config): +    image = container_config['image'] +    memory = container_config['memory'] +    restart = container_config['restart'] + +    # Add capability options. Should be in uppercase +    cap_add = '' +    if 'cap_add' in container_config: +        for c in container_config['cap_add']: +            c = c.upper() +            c = c.replace('-', '_') +            cap_add += f' --cap-add={c}' + +    # Add a host device to the container /dev/x:/dev/x +    device = '' +    if 'device' in container_config: +        for dev, dev_config in container_config['device'].items(): +            source_dev = dev_config['source'] +            dest_dev = dev_config['destination'] +            device += f' --device={source_dev}:{dest_dev}' + +    # Check/set environment options "-e foo=bar" +    env_opt = '' +    if 'environment' in container_config: +        for k, v in container_config['environment'].items(): +            env_opt += f" -e \"{k}={v['value']}\"" + +    # Publish ports +    port = '' +    if 'port' in container_config: +        protocol = '' +        for portmap in container_config['port']: +            if 'protocol' in container_config['port'][portmap]: +                protocol = container_config['port'][portmap]['protocol'] +                protocol = f'/{protocol}' +            else: +                protocol = '/tcp' +            sport = container_config['port'][portmap]['source'] +            dport = container_config['port'][portmap]['destination'] +            port += f' -p {sport}:{dport}{protocol}' + +    # Bind volume +    volume = '' +    if 'volume' in container_config: +        for vol, vol_config in container_config['volume'].items(): +            svol = vol_config['source'] +            dvol = vol_config['destination'] +            volume += f' -v {svol}:{dvol}' + +    container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \ +                         f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ +                         f'--name {name} {device} {port} {volume} {env_opt}' +     +    if 'allow_host_networks' in container_config: +        return f'{container_base_cmd} --net host {image}' + +    ip_param = '' +    networks = ",".join(container_config['network']) +    for network in container_config['network']: +        if 'address' in container_config['network'][network]: +            address = container_config['network'][network]['address'] +            ip_param = f'--ip {address}' + +    return f'{container_base_cmd} --net {networks} {ip_param} {image}' +  def generate(container):      # bail out early - looks like removal from running config      if not container: @@ -263,6 +312,15 @@ def generate(container):      render(config_containers_registry, 'container/registries.conf.j2', container)      render(config_containers_storage, 'container/storage.conf.j2', container) +    if 'name' in container: +        for name, container_config in container['name'].items(): +            if 'disable' in container_config: +                continue + +            file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') +            run_args = generate_run_arguments(name, container_config) +            render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args}) +      return None  def apply(container): @@ -270,8 +328,12 @@ def apply(container):      # Option "--force" allows to delete containers with any status      if 'container_remove' in container:          for name in container['container_remove']: -            call(f'podman stop --time 3 {name}') -            call(f'podman rm --force {name}') +            file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') +            call(f'systemctl stop vyos-container-{name}.service') +            if os.path.exists(file_path): +                os.unlink(file_path) + +    call('systemctl daemon-reload')      # Delete old networks if needed      if 'network_remove' in container: @@ -282,6 +344,7 @@ def apply(container):                  os.unlink(tmp)      # Add container +    disabled_new = False      if 'name' in container:          for name, container_config in container['name'].items():              image = container_config['image'] @@ -295,70 +358,17 @@ def apply(container):                  # check if there is a container by that name running                  tmp = _cmd('podman ps -a --format "{{.Names}}"')                  if name in tmp: -                    _cmd(f'podman stop --time 3 {name}') -                    _cmd(f'podman rm --force {name}') +                    file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') +                    call(f'systemctl stop vyos-container-{name}.service') +                    if os.path.exists(file_path): +                        disabled_new = True +                        os.unlink(file_path)                  continue -            memory = container_config['memory'] -            restart = container_config['restart'] - -            # Add capability options. Should be in uppercase -            cap_add = '' -            if 'cap_add' in container_config: -                for c in container_config['cap_add']: -                    c = c.upper() -                    c = c.replace('-', '_') -                    cap_add += f' --cap-add={c}' - -            # Add a host device to the container /dev/x:/dev/x -            device = '' -            if 'device' in container_config: -                for dev, dev_config in container_config['device'].items(): -                    source_dev = dev_config['source'] -                    dest_dev = dev_config['destination'] -                    device += f' --device={source_dev}:{dest_dev}' - -            # Check/set environment options "-e foo=bar" -            env_opt = '' -            if 'environment' in container_config: -                for k, v in container_config['environment'].items(): -                    env_opt += f" -e \"{k}={v['value']}\"" - -            # Publish ports -            port = '' -            if 'port' in container_config: -                protocol = '' -                for portmap in container_config['port']: -                    if 'protocol' in container_config['port'][portmap]: -                        protocol = container_config['port'][portmap]['protocol'] -                        protocol = f'/{protocol}' -                    else: -                        protocol = '/tcp' -                    sport = container_config['port'][portmap]['source'] -                    dport = container_config['port'][portmap]['destination'] -                    port += f' -p {sport}:{dport}{protocol}' - -            # Bind volume -            volume = '' -            if 'volume' in container_config: -                for vol, vol_config in container_config['volume'].items(): -                    svol = vol_config['source'] -                    dvol = vol_config['destination'] -                    volume += f' -v {svol}:{dvol}' - -            container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \ -                                 f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ -                                 f'--name {name} {device} {port} {volume} {env_opt}' -            if 'allow_host_networks' in container_config: -                _run_rerun(f'{container_base_cmd} --net host {image}') -            else: -                for network in container_config['network']: -                    ipparam = '' -                    if 'address' in container_config['network'][network]: -                        address = container_config['network'][network]['address'] -                        ipparam = f'--ip {address}' +            cmd(f'systemctl restart vyos-container-{name}.service') -                    _run_rerun(f'{container_base_cmd} --net {network} {ipparam} {image}') +    if disabled_new: +        call('systemctl daemon-reload')      return None diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index c196e272b..be80613c6 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -86,7 +86,7 @@ def get_config(config=None):      if 'api_keys' in api_dict:          keys_added = True -    if 'gql' in api_dict: +    if 'graphql' in api_dict:          api_dict = dict_merge(defaults(base), api_dict)      http_api.update(api_dict) diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 8d738f55e..762bad94f 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -87,6 +87,8 @@ def verify(wireguard):                                 'cannot be used for the interface!')      # run checks on individual configured WireGuard peer +    public_keys = [] +      for tmp in wireguard['peer']:          peer = wireguard['peer'][tmp] @@ -100,6 +102,11 @@ def verify(wireguard):              raise ConfigError('Both Wireguard port and address must be defined '                                f'for peer "{tmp}" if either one of them is set!') +        if peer['public_key'] in public_keys: +            raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"') + +        public_keys.append(peer['public_key']) +  def apply(wireguard):      tmp = WireGuardIf(wireguard['ifname'])      if 'deleted' in wireguard: diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 8b1a5a720..978c043e9 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -146,6 +146,10 @@ def verify(nat):              if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces():                  Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') +            if not dict_search('translation.address', config) and not dict_search('translation.port', config): +                if 'exclude' not in config: +                    raise ConfigError(f'{err_msg} translation requires address and/or port') +              addr = dict_search('translation.address', config)              if addr != None and addr != 'masquerade' and not is_ip_network(addr):                  for ip in addr.split('-'): @@ -166,6 +170,10 @@ def verify(nat):              elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():                  Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') +            if not dict_search('translation.address', config) and not dict_search('translation.port', config): +                if 'exclude' not in config: +                    raise ConfigError(f'{err_msg} translation requires address and/or port') +              # common rule verification              verify_rule(config, err_msg) @@ -204,6 +212,10 @@ def apply(nat):      cmd(f'nft -f {nftables_nat_config}')      cmd(f'nft -f {nftables_static_nat_conf}') +    if not nat or 'deleted' in nat: +        os.unlink(nftables_nat_config) +        os.unlink(nftables_static_nat_conf) +      return None  if __name__ == '__main__': diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 77a425f8b..cfefcfbe8 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -117,13 +117,26 @@ def get_config(config=None):                      ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values,                          ipsec['ike_group'][group]['proposal'][proposal]) -    if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']: +    # XXX: T2665: we can not safely rely on the defaults() when there are +    # tagNodes in place, it is better to blend in the defaults manually. +    if dict_search('remote_access.connection', ipsec):          default_values = defaults(base + ['remote-access', 'connection'])          for rw in ipsec['remote_access']['connection']:              ipsec['remote_access']['connection'][rw] = dict_merge(default_values,                ipsec['remote_access']['connection'][rw]) -    if 'remote_access' in ipsec and 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: +    # XXX: T2665: we can not safely rely on the defaults() when there are +    # tagNodes in place, it is better to blend in the defaults manually. +    if dict_search('remote_access.radius.server', ipsec): +        # Fist handle the "base" stuff like RADIUS timeout +        default_values = defaults(base + ['remote-access', 'radius']) +        if 'server' in default_values: +            del default_values['server'] +        ipsec['remote_access']['radius'] = dict_merge(default_values, +                                                      ipsec['remote_access']['radius']) + +        # Take care about individual RADIUS servers implemented as tagNodes - this +        # requires special treatment          default_values = defaults(base + ['remote-access', 'radius', 'server'])          for server in ipsec['remote_access']['radius']['server']:              ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values, diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py index 2aa687221..9614f0d28 100755 --- a/src/helpers/system-versions-foot.py +++ b/src/helpers/system-versions-foot.py @@ -1,6 +1,6 @@  #!/usr/bin/python3 -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019, 2022 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 @@ -16,24 +16,13 @@  # 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_component_version() - -component_string = formatversions.format_versions_string(sys_versions) - -os_version_string = vyos.version.get_version() +from vyos.component_version import write_system_footer  sys.stdout.write("\n\n")  if vyos.defaults.cfg_vintage == 'vyos': -    formatversions.write_vyos_versions_foot(None, component_string, -                                            os_version_string) +    write_system_footer(None, vintage='vyos')  elif vyos.defaults.cfg_vintage == 'vyatta': -    formatversions.write_vyatta_versions_foot(None, component_string, -                                              os_version_string) +    write_system_footer(None, vintage='vyatta')  else: -    formatversions.write_vyatta_versions_foot(None, component_string, -                                              os_version_string) +    write_system_footer(None, vintage='vyos') diff --git a/src/migration-scripts/https/3-to-4 b/src/migration-scripts/https/3-to-4 new file mode 100755 index 000000000..5ee528b31 --- /dev/null +++ b/src/migration-scripts/https/3-to-4 @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +# T4768 rename node 'gql' to 'graphql'. + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): +    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) + +old_base = ['service', 'https', 'api', 'gql'] +if not config.exists(old_base): +    # Nothing to do +    sys.exit(0) + +new_base = ['service', 'https', 'api', 'graphql'] +config.set(new_base) + +nodes = config.list_nodes(old_base) +for node in nodes: +    config.copy(old_base + [node], new_base + [node]) + +config.delete(old_base) + +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/bgp.py b/src/op_mode/bgp.py new file mode 100755 index 000000000..23001a9d7 --- /dev/null +++ b/src/op_mode/bgp.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. +# +# Purpose: +#    Displays bgp neighbors information. +#    Used by the "show bgp (vrf <tag>) ipv4|ipv6 neighbors" commands. + +import re +import sys +import typing + +import jmespath +from jinja2 import Template +from humps import decamelize + +from vyos.configquery import ConfigTreeQuery + +import vyos.opmode + + +frr_command_template = Template(""" +{% if family %} +    show bgp +        {{ 'vrf ' ~ vrf if vrf else '' }} +        {{ 'ipv6' if family == 'inet6' else 'ipv4'}} +        {{ 'neighbor ' ~ peer if peer else 'summary' }} +{% endif %} + +{% if raw %} +    json +{% endif %} +""") + + +def _verify(func): +    """Decorator checks if BGP config exists +    BGP configuration can be present under vrf <tag> +    If we do npt get arg 'peer' then it can be 'bgp summary' +    """ +    from functools import wraps + +    @wraps(func) +    def _wrapper(*args, **kwargs): +        config = ConfigTreeQuery() +        afi = 'ipv6' if kwargs.get('family') == 'inet6' else 'ipv4' +        global_vrfs = ['all', 'default'] +        peer = kwargs.get('peer') +        vrf = kwargs.get('vrf') +        unconf_message = f'BGP or neighbor is not configured' +        # Add option to check the specific neighbor if we have arg 'peer' +        peer_opt = f'neighbor {peer} address-family {afi}-unicast' if peer else '' +        vrf_opt = '' +        if vrf and vrf not in global_vrfs: +            vrf_opt = f'vrf name {vrf}' +        # Check if config does not exist +        if not config.exists(f'{vrf_opt} protocols bgp {peer_opt}'): +            raise vyos.opmode.UnconfiguredSubsystem(unconf_message) +        return func(*args, **kwargs) + +    return _wrapper + + +@_verify +def show_neighbors(raw: bool, +                   family: str, +                   peer: typing.Optional[str], +                   vrf: typing.Optional[str]): +    kwargs = dict(locals()) +    frr_command = frr_command_template.render(kwargs) +    frr_command = re.sub(r'\s+', ' ', frr_command) + +    from vyos.util import cmd +    output = cmd(f"vtysh -c '{frr_command}'") + +    if raw: +        from json import loads +        data = loads(output) +        # Get list of the peers +        peers = jmespath.search('*.peers | [0]', data) +        if peers: +            # Create new dict, delete old key 'peers' +            # add key 'peers' neighbors to the list +            list_peers = [] +            new_dict = jmespath.search('* | [0]', data) +            if 'peers' in new_dict: +                new_dict.pop('peers') + +                for neighbor, neighbor_options in peers.items(): +                    neighbor_options['neighbor'] = neighbor +                    list_peers.append(neighbor_options) +                new_dict['peers'] = list_peers +            return decamelize(new_dict) +        data = jmespath.search('* | [0]', data) +        return decamelize(data) + +    else: +        return output + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py new file mode 100755 index 000000000..07e9b7d6c --- /dev/null +++ b/src/op_mode/dhcp.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 +from ipaddress import ip_address +import typing + +from datetime import datetime +from sys import exit +from tabulate import tabulate +from isc_dhcp_leases import IscDhcpLeases + +from vyos.base import Warning +from vyos.configquery import ConfigTreeQuery + +from vyos.util import cmd +from vyos.util import dict_search +from vyos.util import is_systemd_service_running + +import vyos.opmode + + +config = ConfigTreeQuery() +pool_key = "shared-networkname" + + +def _in_pool(lease, pool): +    if pool_key in lease.sets: +        if lease.sets[pool_key] == pool: +            return True +    return False + + +def _utc_to_local(utc_dt): +    return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds()) + + +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 _find_list_of_dict_index(lst, key='ip', value='') -> int: +    """ +    Find the index entry of list of dict matching the dict value +    Exampe: +        % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}] +        % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2') +        % 1 +    """ +    idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None) +    return idx + + +def _get_raw_server_leases(family, pool=None) -> list: +    """ +    Get DHCP server leases +    :return list +    """ +    lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' +    data = [] +    leases = IscDhcpLeases(lease_file).get() +    if pool is not None: +        if config.exists(f'service dhcp-server shared-network-name {pool}'): +            leases = list(filter(lambda x: _in_pool(x, pool), leases)) +    for lease in leases: +        data_lease = {} +        data_lease['ip'] = lease.ip +        data_lease['state'] = lease.binding_state +        data_lease['pool'] = lease.sets.get('shared-networkname', '') +        data_lease['end'] = lease.end.timestamp() + +        if family == 'inet': +            data_lease['hardware'] = lease.ethernet +            data_lease['start'] = lease.start.timestamp() +            data_lease['hostname'] = lease.hostname + +        if family == 'inet6': +            data_lease['last_communication'] = lease.last_communication.timestamp() +            data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) +            lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} +            data_lease['type'] = lease_types_long[lease.type] + +        data_lease['remaining'] = lease.end - datetime.utcnow() + +        if data_lease['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_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] +        else: +            data_lease['remaining'] = '' + +        # Do not add old leases +        if data_lease['remaining'] != '': +            data.append(data_lease) + +        # deduplicate +        checked = [] +        for entry in data: +            addr = entry.get('ip') +            if addr not in checked: +                checked.append(addr) +            else: +                idx = _find_list_of_dict_index(data, key='ip', value=addr) +                data.pop(idx) + +    return data + + +def _get_formatted_server_leases(raw_data, family): +    data_entries = [] +    if family == 'inet': +        for lease in raw_data: +            ipaddr = lease.get('ip') +            hw_addr = lease.get('hardware') +            state = lease.get('state') +            start = lease.get('start') +            start =  _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') +            end = lease.get('end') +            end =  _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') +            remain = lease.get('remaining') +            pool = lease.get('pool') +            hostname = lease.get('hostname') +            data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname]) + +        headers = ['IP Address', 'Hardware address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool', +                   'Hostname'] + +    if family == 'inet6': +        for lease in raw_data: +            ipaddr = lease.get('ip') +            state = lease.get('state') +            start = lease.get('last_communication') +            start =  _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') +            end = lease.get('end') +            end =  _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') +            remain = lease.get('remaining') +            lease_type = lease.get('type') +            pool = lease.get('pool') +            host_identifier = lease.get('iaid_duid') +            data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier]) + +        headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool', +                   'IAID_DUID'] + +    output = tabulate(data_entries, headers, numalign='left') +    return output + + +def _get_dhcp_pools(family='inet') -> list: +    v = 'v6' if family == 'inet6' else '' +    pools = config.list_nodes(f'service dhcp{v}-server shared-network-name') +    return pools + + +def _get_pool_size(pool, family='inet'): +    v = 'v6' if family == 'inet6' else '' +    base = f'service dhcp{v}-server shared-network-name {pool}' +    size = 0 +    subnets = config.list_nodes(f'{base} subnet') +    for subnet in subnets: +        if family == 'inet6': +            ranges = config.list_nodes(f'{base} subnet {subnet} address-range start') +        else: +            ranges = config.list_nodes(f'{base} subnet {subnet} range') +        for range in ranges: +            if family == 'inet6': +                start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0] +                stop = config.value(f'{base} subnet {subnet} address-range start {start} stop') +            else: +                start = config.value(f'{base} subnet {subnet} range {range} start') +                stop = config.value(f'{base} subnet {subnet} range {range} stop') +            # Add +1 because both range boundaries are inclusive +            size += int(ip_address(stop)) - int(ip_address(start)) + 1 +    return size + + +def _get_raw_pool_statistics(family='inet', pool=None): +    if pool is None: +        pool = _get_dhcp_pools(family=family) +    else: +        pool = [pool] + +    v = 'v6' if family == 'inet6' else '' +    stats = [] +    for p in pool: +        subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet') +        size = _get_pool_size(family=family, pool=p) +        leases = len(_get_raw_server_leases(family=family, pool=p)) +        use_percentage = round(leases / size * 100) if size != 0 else 0 +        pool_stats = {'pool': p, 'size': size, 'leases': leases, +                      'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet} +        stats.append(pool_stats) +    return stats + + +def _get_formatted_pool_statistics(pool_data, family='inet'): +    data_entries = [] +    for entry in pool_data: +        pool = entry.get('pool') +        size = entry.get('size') +        leases = entry.get('leases') +        available = entry.get('available') +        use_percentage = entry.get('use_percentage') +        use_percentage = f'{use_percentage}%' +        data_entries.append([pool, size, leases, available, use_percentage]) + +    headers = ['Pool', 'Size','Leases', 'Available', 'Usage'] +    output = tabulate(data_entries, headers, numalign='left') +    return output + + +def _verify(func): +    """Decorator checks if DHCP(v6) config exists""" +    from functools import wraps + +    @wraps(func) +    def _wrapper(*args, **kwargs): +        config = ConfigTreeQuery() +        family = kwargs.get('family') +        v = 'v6' if family == 'inet6' else '' +        unconf_message = f'DHCP{v} server is not configured' +        # Check if config does not exist +        if not config.exists(f'service dhcp{v}-server'): +            raise vyos.opmode.UnconfiguredSubsystem(unconf_message) +        return func(*args, **kwargs) +    return _wrapper + + +@_verify +def show_pool_statistics(raw: bool, family: str, pool: typing.Optional[str]): +    pool_data = _get_raw_pool_statistics(family=family, pool=pool) +    if raw: +        return pool_data +    else: +        return _get_formatted_pool_statistics(pool_data, family=family) + + +@_verify +def show_server_leases(raw: bool, family: str): +    # if dhcp server is down, inactive leases may still be shown as active, so warn the user. +    if not is_systemd_service_running('isc-dhcp-server.service'): +        Warning('DHCP server is configured but not started. Data may be stale.') + +    leases = _get_raw_server_leases(family) +    if raw: +        return leases +    else: +        return _get_formatted_server_leases(leases, family) + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 7ec35d7bd..aaa0cec5a 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -43,7 +43,10 @@ def _alphanum_key(key):  def _get_vici_sas():      from vici import Session as vici_session -    session = vici_session() +    try: +        session = vici_session() +    except Exception: +        raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")      sas = list(session.list_sas())      return sas diff --git a/src/op_mode/log.py b/src/op_mode/log.py new file mode 100755 index 000000000..b0abd6191 --- /dev/null +++ b/src/op_mode/log.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 json +import re +import sys +import typing + +from jinja2 import Template + +from vyos.util import rc_cmd + +import vyos.opmode + +journalctl_command_template = Template(""" +--no-hostname +--quiet + +{% if boot %} +  --boot +{% endif %} + +{% if count %} +  --lines={{ count }} +{% endif %} + +{% if reverse %} +  --reverse +{% endif %} + +{% if since %} +  --since={{ since }} +{% endif %} + +{% if unit %} +  --unit={{ unit }} +{% endif %} + +{% if utc %} +  --utc +{% endif %} + +{% if raw %} +{# By default show 100 only lines for raw option if count does not set #} +{# Protection from parsing the full log by default #} +{%    if not boot %} +  --lines={{ '' ~ count if count else '100' }} +{%    endif %} +  --no-pager +  --output=json +{% endif %} +""") + + +def show(raw: bool, +         boot: typing.Optional[bool], +         count: typing.Optional[int], +         facility: typing.Optional[str], +         reverse: typing.Optional[bool], +         utc: typing.Optional[bool], +         unit: typing.Optional[str]): +    kwargs = dict(locals()) + +    journalctl_options = journalctl_command_template.render(kwargs) +    journalctl_options = re.sub(r'\s+', ' ', journalctl_options) +    rc, output = rc_cmd(f'journalctl {journalctl_options}') +    if raw: +        # Each 'journalctl --output json' line is a separate JSON object +        # So we should return list of dict +        return [json.loads(line) for line in output.split('\n')] +    return output + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/memory.py b/src/op_mode/memory.py index 178544be4..7666de646 100755 --- a/src/op_mode/memory.py +++ b/src/op_mode/memory.py @@ -20,7 +20,7 @@ import sys  import vyos.opmode -def _get_system_memory(): +def _get_raw_data():      from re import search as re_search      def find_value(keyword, mem_data): @@ -38,7 +38,7 @@ def _get_system_memory():      used = total - available -    res = { +    mem_data = {        "total":   total,        "free":    available,        "used":    used, @@ -46,24 +46,21 @@ def _get_system_memory():        "cached":  cached      } -    return res - -def _get_system_memory_human(): -    from vyos.util import bytes_to_human - -    mem = _get_system_memory() - -    for key in mem: +    for key in mem_data:          # The Linux kernel exposes memory values in kilobytes,          # so we need to normalize them -        mem[key] = bytes_to_human(mem[key], initial_exponent=10) +        mem_data[key] = mem_data[key] * 1024 -    return mem - -def _get_raw_data(): -    return _get_system_memory_human() +    return mem_data  def _get_formatted_output(mem): +    from vyos.util import bytes_to_human + +    # For human-readable outputs, we convert bytes to more convenient units +    # (100M, 1.3G...) +    for key in mem: +        mem[key] = bytes_to_human(mem[key]) +      out = "Total: {}\n".format(mem["total"])      out += "Free:  {}\n".format(mem["free"])      out += "Used:  {}".format(mem["used"]) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 845dbbb2c..f899eb3dc 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -22,12 +22,18 @@ import xmltodict  from sys import exit  from tabulate import tabulate +from vyos.configquery import ConfigTreeQuery +  from vyos.util import cmd  from vyos.util import dict_search  import vyos.opmode +base = 'nat' +unconf_message = 'NAT is not configured' + +  def _get_xml_translation(direction, family):      """      Get conntrack XML output --src-nat|--dst-nat @@ -277,6 +283,20 @@ def _get_formatted_translation(dict_data, nat_direction, family):      return output +def _verify(func): +    """Decorator checks if NAT config exists""" +    from functools import wraps + +    @wraps(func) +    def _wrapper(*args, **kwargs): +        config = ConfigTreeQuery() +        if not config.exists(base): +            raise vyos.opmode.UnconfiguredSubsystem(unconf_message) +        return func(*args, **kwargs) +    return _wrapper + + +@_verify  def show_rules(raw: bool, direction: str, family: str):      nat_rules = _get_raw_data_rules(direction, family)      if raw: @@ -285,6 +305,7 @@ def show_rules(raw: bool, direction: str, family: str):          return _get_formatted_output_rules(nat_rules, direction, family) +@_verify  def show_statistics(raw: bool, direction: str, family: str):      nat_statistics = _get_raw_data_rules(direction, family)      if raw: @@ -293,6 +314,7 @@ def show_statistics(raw: bool, direction: str, family: str):          return _get_formatted_output_statistics(nat_statistics, direction) +@_verify  def show_translations(raw: bool, direction: str, family: str):      family = 'ipv6' if family == 'inet6' else 'ipv4'      nat_translation = _get_raw_translation(direction, family) diff --git a/src/op_mode/route.py b/src/op_mode/route.py index e1eee5bbf..d11b00ba0 100755 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py @@ -83,7 +83,12 @@ def show(raw: bool,          if raw:              from json import loads -            return loads(output) +            d = loads(output) +            collect = [] +            for k,_ in d.items(): +                for l in d[k]: +                    collect.append(l) +            return collect          else:              return output diff --git a/src/op_mode/storage.py b/src/op_mode/storage.py index 75964c493..d16e271bd 100755 --- a/src/op_mode/storage.py +++ b/src/op_mode/storage.py @@ -20,6 +20,16 @@ import sys  import vyos.opmode  from vyos.util import cmd +# FIY: As of coreutils from Debian Buster and Bullseye, +# the outpt looks like this: +# +# $ df -h -t ext4 --output=source,size,used,avail,pcent +# Filesystem      Size  Used Avail Use% +# /dev/sda1        16G  7.6G  7.3G  51% +# +# Those field names are automatically normalized by vyos.opmode.run, +# so we don't touch them here, +# and only normalize values.  def _get_system_storage(only_persistent=False):      if not only_persistent: @@ -32,11 +42,19 @@ def _get_system_storage(only_persistent=False):      return res  def _get_raw_data(): +    from re import sub as re_sub +    from vyos.util import human_to_bytes +      out =  _get_system_storage(only_persistent=True)      lines = out.splitlines()      lists = [l.split() for l in lines]      res = {lists[0][i]: lists[1][i] for i in range(len(lists[0]))} +    res["Size"] = human_to_bytes(res["Size"]) +    res["Used"] = human_to_bytes(res["Used"]) +    res["Avail"] = human_to_bytes(res["Avail"]) +    res["Use%"] = re_sub(r'%', '', res["Use%"]) +      return res  def _get_formatted_output(): diff --git a/src/services/api/graphql/__init__.py b/src/services/api/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/services/api/graphql/__init__.py diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 0b1260912..aa1ba0eb0 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -18,16 +18,26 @@ from . graphql.queries import query  from . graphql.mutations import mutation  from . graphql.directives import directives_dict  from . graphql.errors import op_mode_error -from . utils.schema_from_op_mode import generate_op_mode_definitions +from . graphql.auth_token_mutation import auth_token_mutation +from . generate.schema_from_op_mode import generate_op_mode_definitions +from . generate.schema_from_config_session import generate_config_session_definitions +from . generate.schema_from_composite import generate_composite_definitions +from . libs.token_auth import init_secret +from . import state  from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers  def generate_schema():      api_schema_dir = vyos.defaults.directories['api_schema']      generate_op_mode_definitions() +    generate_config_session_definitions() +    generate_composite_definitions() + +    if state.settings['app'].state.vyos_auth_type == 'token': +        init_secret()      type_defs = load_schema_from_path(api_schema_dir) -    schema = make_executable_schema(type_defs, query, op_mode_error, mutation, snake_case_fallback_resolvers, directives=directives_dict) +    schema = make_executable_schema(type_defs, query, op_mode_error, mutation, auth_token_mutation, snake_case_fallback_resolvers, directives=directives_dict)      return schema diff --git a/src/services/api/graphql/utils/composite_function.py b/src/services/api/graphql/generate/composite_function.py index bc9d80fbb..bc9d80fbb 100644 --- a/src/services/api/graphql/utils/composite_function.py +++ b/src/services/api/graphql/generate/composite_function.py diff --git a/src/services/api/graphql/utils/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py index fc0dd7a87..fc0dd7a87 100644 --- a/src/services/api/graphql/utils/config_session_function.py +++ b/src/services/api/graphql/generate/config_session_function.py diff --git a/src/services/api/graphql/utils/schema_from_composite.py b/src/services/api/graphql/generate/schema_from_composite.py index f9983cd98..61a08cb2f 100755 --- a/src/services/api/graphql/utils/schema_from_composite.py +++ b/src/services/api/graphql/generate/schema_from_composite.py @@ -19,28 +19,60 @@  # composite functions comprising several requests.  import os +import sys  import json  from inspect import signature, getmembers, isfunction, isclass, getmro  from jinja2 import Template +from vyos.defaults import directories  if __package__ is None or __package__ == '': -    from util import snake_to_pascal_case, map_type_name +    sys.path.append("/usr/libexec/vyos/services/api") +    from graphql.libs.op_mode import snake_to_pascal_case, map_type_name +    from composite_function import queries, mutations +    from vyos.config import Config +    from vyos.configdict import dict_merge +    from vyos.xml import defaults  else: -    from . util import snake_to_pascal_case, map_type_name +    from .. libs.op_mode import snake_to_pascal_case, map_type_name +    from . composite_function import queries, mutations +    from .. import state + +SCHEMA_PATH = directories['api_schema'] -# this will be run locally before the build -SCHEMA_PATH = '../graphql/schema' +if __package__ is None or __package__ == '': +    # allow running stand-alone +    conf = Config() +    base = ['service', 'https', 'api'] +    graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), +                                          no_tag_node_value_mangle=True, +                                          get_first_key=True) +    if 'graphql' not in graphql_dict: +        exit("graphql is not configured") + +    graphql_dict = dict_merge(defaults(base), graphql_dict) +    auth_type = graphql_dict['graphql']['authentication']['type'] +else: +    auth_type = state.settings['app'].state.vyos_auth_type -schema_data: dict = {'schema_name': '', +schema_data: dict = {'auth_type': auth_type, +                     'schema_name': '',                       'schema_fields': []}  query_template  = """ +{%- if auth_type == 'key' %}  input {{ schema_name }}Input {      key: String!      {%- for field_entry in schema_fields %}      {{ field_entry }}      {%- endfor %}  } +{%- elif schema_fields %} +input {{ schema_name }}Input { +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} +{%- endif %}  type {{ schema_name }} {      result: Generic @@ -53,17 +85,29 @@ type {{ schema_name }}Result {  }  extend type Query { +{%- if auth_type == 'key' or schema_fields %}      {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositequery +{%- else %} +    {{ schema_name }} : {{ schema_name }}Result @compositequery +{%- endif %}  }  """  mutation_template  = """ +{%- if auth_type == 'key' %}  input {{ schema_name }}Input {      key: String!      {%- for field_entry in schema_fields %}      {{ field_entry }}      {%- endfor %}  } +{%- elif schema_fields %} +input {{ schema_name }}Input { +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} +{%- endif %}  type {{ schema_name }} {      result: Generic @@ -76,7 +120,11 @@ type {{ schema_name }}Result {  }  extend type Mutation { +{%- if auth_type == 'key' or schema_fields %}      {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositemutation +{%- else %} +    {{ schema_name }} : {{ schema_name }}Result @compositemutation +{%- endif %}  }  """ @@ -100,8 +148,6 @@ def create_schema(func_name: str, func: callable, template: str) -> str:      return res  def generate_composite_definitions(): -    from composite_function import queries, mutations -      results = []      for name,func in queries.items():          res = create_schema(name, func, query_template) diff --git a/src/services/api/graphql/utils/schema_from_config_session.py b/src/services/api/graphql/generate/schema_from_config_session.py index ea78aaf88..49bf2440e 100755 --- a/src/services/api/graphql/utils/schema_from_config_session.py +++ b/src/services/api/graphql/generate/schema_from_config_session.py @@ -19,28 +19,60 @@  # (wrappers of) native configsession functions.  import os +import sys  import json  from inspect import signature, getmembers, isfunction, isclass, getmro  from jinja2 import Template +from vyos.defaults import directories  if __package__ is None or __package__ == '': -    from util import snake_to_pascal_case, map_type_name +    sys.path.append("/usr/libexec/vyos/services/api") +    from graphql.libs.op_mode import snake_to_pascal_case, map_type_name +    from config_session_function import queries, mutations +    from vyos.config import Config +    from vyos.configdict import dict_merge +    from vyos.xml import defaults  else: -    from . util import snake_to_pascal_case, map_type_name +    from .. libs.op_mode import snake_to_pascal_case, map_type_name +    from . config_session_function import queries, mutations +    from .. import state + +SCHEMA_PATH = directories['api_schema'] -# this will be run locally before the build -SCHEMA_PATH = '../graphql/schema' +if __package__ is None or __package__ == '': +    # allow running stand-alone +    conf = Config() +    base = ['service', 'https', 'api'] +    graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), +                                          no_tag_node_value_mangle=True, +                                          get_first_key=True) +    if 'graphql' not in graphql_dict: +        exit("graphql is not configured") + +    graphql_dict = dict_merge(defaults(base), graphql_dict) +    auth_type = graphql_dict['graphql']['authentication']['type'] +else: +    auth_type = state.settings['app'].state.vyos_auth_type -schema_data: dict = {'schema_name': '', +schema_data: dict = {'auth_type': auth_type, +                     'schema_name': '',                       'schema_fields': []}  query_template  = """ +{%- if auth_type == 'key' %}  input {{ schema_name }}Input {      key: String!      {%- for field_entry in schema_fields %}      {{ field_entry }}      {%- endfor %}  } +{%- elif schema_fields %} +input {{ schema_name }}Input { +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} +{%- endif %}  type {{ schema_name }} {      result: Generic @@ -53,17 +85,29 @@ type {{ schema_name }}Result {  }  extend type Query { +{%- if auth_type == 'key' or schema_fields %}      {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionquery +{%- else %} +    {{ schema_name }} : {{ schema_name }}Result @configsessionquery +{%- endif %}  }  """  mutation_template  = """ +{%- if auth_type == 'key' %}  input {{ schema_name }}Input {      key: String!      {%- for field_entry in schema_fields %}      {{ field_entry }}      {%- endfor %}  } +{%- elif schema_fields %} +input {{ schema_name }}Input { +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} +{%- endif %}  type {{ schema_name }} {      result: Generic @@ -76,7 +120,11 @@ type {{ schema_name }}Result {  }  extend type Mutation { +{%- if auth_type == 'key' or schema_fields %}      {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionmutation +{%- else %} +    {{ schema_name }} : {{ schema_name }}Result @configsessionmutation +{%- endif %}  }  """ @@ -100,8 +148,6 @@ def create_schema(func_name: str, func: callable, template: str) -> str:      return res  def generate_config_session_definitions(): -    from config_session_function import queries, mutations -      results = []      for name,func in queries.items():          res = create_schema(name, func, query_template) diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py index 57d63628b..1fd198a37 100755 --- a/src/services/api/graphql/utils/schema_from_op_mode.py +++ b/src/services/api/graphql/generate/schema_from_op_mode.py @@ -19,17 +19,23 @@  # scripts.  import os +import sys  import json  from inspect import signature, getmembers, isfunction, isclass, getmro  from jinja2 import Template  from vyos.defaults import directories  if __package__ is None or __package__ == '': -    from util import load_as_module, is_op_mode_function_name, is_show_function_name -    from util import snake_to_pascal_case, map_type_name +    sys.path.append("/usr/libexec/vyos/services/api") +    from graphql.libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name +    from graphql.libs.op_mode import snake_to_pascal_case, map_type_name +    from vyos.config import Config +    from vyos.configdict import dict_merge +    from vyos.xml import defaults  else: -    from . util import load_as_module, is_op_mode_function_name, is_show_function_name -    from . util import snake_to_pascal_case, map_type_name +    from .. libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name +    from .. libs.op_mode import snake_to_pascal_case, map_type_name +    from .. import state  OP_MODE_PATH = directories['op_mode']  SCHEMA_PATH = directories['api_schema'] @@ -38,16 +44,40 @@ DATA_DIR = directories['data']  op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json')  op_mode_error_schema = 'op_mode_error.graphql' -schema_data: dict = {'schema_name': '', +if __package__ is None or __package__ == '': +    # allow running stand-alone +    conf = Config() +    base = ['service', 'https', 'api'] +    graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), +                                          no_tag_node_value_mangle=True, +                                          get_first_key=True) +    if 'graphql' not in graphql_dict: +        exit("graphql is not configured") + +    graphql_dict = dict_merge(defaults(base), graphql_dict) +    auth_type = graphql_dict['graphql']['authentication']['type'] +else: +    auth_type = state.settings['app'].state.vyos_auth_type + +schema_data: dict = {'auth_type': auth_type, +                     'schema_name': '',                       'schema_fields': []}  query_template  = """ +{%- if auth_type == 'key' %}  input {{ schema_name }}Input {      key: String!      {%- for field_entry in schema_fields %}      {{ field_entry }}      {%- endfor %}  } +{%- elif schema_fields %} +input {{ schema_name }}Input { +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} +{%- endif %}  type {{ schema_name }} {      result: Generic @@ -61,17 +91,29 @@ type {{ schema_name }}Result {  }  extend type Query { +{%- if auth_type == 'key' or schema_fields %}      {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery +{%- else %} +    {{ schema_name }} : {{ schema_name }}Result @genopquery +{%- endif %}  }  """  mutation_template  = """ +{%- if auth_type == 'key' %}  input {{ schema_name }}Input {      key: String!      {%- for field_entry in schema_fields %}      {{ field_entry }}      {%- endfor %}  } +{%- elif schema_fields %} +input {{ schema_name }}Input { +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} +{%- endif %}  type {{ schema_name }} {      result: Generic @@ -85,7 +127,11 @@ type {{ schema_name }}Result {  }  extend type Mutation { +{%- if auth_type == 'key' or schema_fields %}      {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation +{%- else %} +    {{ schema_name }} : {{ schema_name }}Result @genopquery +{%- endif %}  }  """ diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py new file mode 100644 index 000000000..21ac40094 --- /dev/null +++ b/src/services/api/graphql/graphql/auth_token_mutation.py @@ -0,0 +1,49 @@ +# Copyright 2022 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 jwt +import datetime +from typing import Any, Dict +from ariadne import ObjectType, UnionType +from graphql import GraphQLResolveInfo + +from .. libs.token_auth import generate_token +from .. import state + +auth_token_mutation = ObjectType("Mutation") + +@auth_token_mutation.field('AuthToken') +def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict): +    # non-nullable fields +    user = data['username'] +    passwd = data['password'] + +    secret = state.settings['secret'] +    exp_interval = int(state.settings['app'].state.vyos_token_exp) +    expiration = (datetime.datetime.now(tz=datetime.timezone.utc) + +                  datetime.timedelta(seconds=exp_interval)) + +    res = generate_token(user, passwd, secret, expiration) +    if res: +        data['result'] = res +        return { +            "success": True, +            "data": data +        } + +    return { +        "success": False, +        "errors": ['token generation failed'] +    } diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 32da0eeb7..2778feb69 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -20,7 +20,7 @@ from graphql import GraphQLResolveInfo  from makefun import with_signature  from .. import state -from .. import key_auth +from .. libs import key_auth  from api.graphql.session.session import Session  from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code  from vyos.opmode import Error as OpModeError @@ -42,32 +42,54 @@ def make_mutation_resolver(mutation_name, class_name, session_func):      func_base_name = convert_camel_case_to_snake(class_name)      resolver_name = f'resolve_{func_base_name}' -    func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' +    func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})'      @mutation.field(mutation_name)      @convert_kwargs_to_snake_case      @with_signature(func_sig, func_name=resolver_name)      async def func_impl(*args, **kwargs):          try: -            if 'data' not in kwargs: -                return { -                    "success": False, -                    "errors": ['missing data'] -                } - -            data = kwargs['data'] -            key = data['key'] - -            auth = key_auth.auth_required(key) -            if auth is None: -                return { -                     "success": False, -                     "errors": ['invalid API key'] -                } - -            # We are finished with the 'key' entry, and may remove so as to -            # pass the rest of data (if any) to function. -            del data['key'] +            auth_type = state.settings['app'].state.vyos_auth_type + +            if auth_type == 'key': +                data = kwargs['data'] +                key = data['key'] + +                auth = key_auth.auth_required(key) +                if auth is None: +                    return { +                         "success": False, +                         "errors": ['invalid API key'] +                    } + +                # We are finished with the 'key' entry, and may remove so as to +                # pass the rest of data (if any) to function. +                del data['key'] + +            elif auth_type == 'token': +                # there is a subtlety here: with the removal of the key entry, +                # some requests will now have empty input, hence no data arg, so +                # make it optional in the func_sig. However, it can not be None, +                # as the makefun package provides accurate TypeError exceptions; +                # hence set it to {}, but now it is a mutable default argument, +                # so clear the key 'result', which is added at the end of +                # this function. +                data = kwargs['data'] +                if 'result' in data: +                    del data['result'] + +                info = kwargs['info'] +                user = info.context.get('user') +                if user is None: +                    return { +                        "success": False, +                        "errors": ['not authenticated'] +                    } +            else: +                # AtrributeError will have already been raised if no +                # vyos_auth_type; validation and defaultValue ensure it is +                # one of the previous cases, so this is never reached. +                pass              session = state.settings['app'].state.vyos_session diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 791b0d3e0..9c8a4f064 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -20,7 +20,7 @@ from graphql import GraphQLResolveInfo  from makefun import with_signature  from .. import state -from .. import key_auth +from .. libs import key_auth  from api.graphql.session.session import Session  from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code  from vyos.opmode import Error as OpModeError @@ -42,32 +42,54 @@ def make_query_resolver(query_name, class_name, session_func):      func_base_name = convert_camel_case_to_snake(class_name)      resolver_name = f'resolve_{func_base_name}' -    func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' +    func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})'      @query.field(query_name)      @convert_kwargs_to_snake_case      @with_signature(func_sig, func_name=resolver_name)      async def func_impl(*args, **kwargs):          try: -            if 'data' not in kwargs: -                return { -                    "success": False, -                    "errors": ['missing data'] -                } - -            data = kwargs['data'] -            key = data['key'] - -            auth = key_auth.auth_required(key) -            if auth is None: -                return { -                     "success": False, -                     "errors": ['invalid API key'] -                } - -            # We are finished with the 'key' entry, and may remove so as to -            # pass the rest of data (if any) to function. -            del data['key'] +            auth_type = state.settings['app'].state.vyos_auth_type + +            if auth_type == 'key': +                data = kwargs['data'] +                key = data['key'] + +                auth = key_auth.auth_required(key) +                if auth is None: +                    return { +                         "success": False, +                         "errors": ['invalid API key'] +                    } + +                # We are finished with the 'key' entry, and may remove so as to +                # pass the rest of data (if any) to function. +                del data['key'] + +            elif auth_type == 'token': +                # there is a subtlety here: with the removal of the key entry, +                # some requests will now have empty input, hence no data arg, so +                # make it optional in the func_sig. However, it can not be None, +                # as the makefun package provides accurate TypeError exceptions; +                # hence set it to {}, but now it is a mutable default argument, +                # so clear the key 'result', which is added at the end of +                # this function. +                data = kwargs['data'] +                if 'result' in data: +                    del data['result'] + +                info = kwargs['info'] +                user = info.context.get('user') +                if user is None: +                    return { +                        "success": False, +                        "errors": ['not authenticated'] +                    } +            else: +                # AtrributeError will have already been raised if no +                # vyos_auth_type; validation and defaultValue ensure it is +                # one of the previous cases, so this is never reached. +                pass              session = state.settings['app'].state.vyos_session diff --git a/src/services/api/graphql/graphql/schema/auth_token.graphql b/src/services/api/graphql/graphql/schema/auth_token.graphql new file mode 100644 index 000000000..af53a293a --- /dev/null +++ b/src/services/api/graphql/graphql/schema/auth_token.graphql @@ -0,0 +1,19 @@ + +input AuthTokenInput { +    username: String! +    password: String! +} + +type AuthToken { +    result: Generic +} + +type AuthTokenResult { +    data: AuthToken +    success: Boolean! +    errors: [String] +} + +extend type Mutation { +    AuthToken(data: AuthTokenInput) : AuthTokenResult +} diff --git a/src/services/api/graphql/graphql/schema/composite.graphql b/src/services/api/graphql/graphql/schema/composite.graphql deleted file mode 100644 index 717fbd89d..000000000 --- a/src/services/api/graphql/graphql/schema/composite.graphql +++ /dev/null @@ -1,18 +0,0 @@ - -input SystemStatusInput { -    key: String! -} - -type SystemStatus { -    result: Generic -} - -type SystemStatusResult { -    data: SystemStatus -    success: Boolean! -    errors: [String] -} - -extend type Query { -    SystemStatus(data: SystemStatusInput) : SystemStatusResult @compositequery -}
\ No newline at end of file diff --git a/src/services/api/graphql/graphql/schema/configsession.graphql b/src/services/api/graphql/graphql/schema/configsession.graphql deleted file mode 100644 index b1deac4b3..000000000 --- a/src/services/api/graphql/graphql/schema/configsession.graphql +++ /dev/null @@ -1,115 +0,0 @@ - -input ShowConfigInput { -    key: String! -    path: [String!]! -    configFormat: String = null -} - -type ShowConfig { -    result: Generic -} - -type ShowConfigResult { -    data: ShowConfig -    success: Boolean! -    errors: [String] -} - -extend type Query { -    ShowConfig(data: ShowConfigInput) : ShowConfigResult @configsessionquery -} - -input ShowInput { -    key: String! -    path: [String!]! -} - -type Show { -    result: Generic -} - -type ShowResult { -    data: Show -    success: Boolean! -    errors: [String] -} - -extend type Query { -    Show(data: ShowInput) : ShowResult @configsessionquery -} - -input SaveConfigFileInput { -    key: String! -    fileName: String = null -} - -type SaveConfigFile { -    result: Generic -} - -type SaveConfigFileResult { -    data: SaveConfigFile -    success: Boolean! -    errors: [String] -} - -extend type Mutation { -    SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configsessionmutation -} - -input LoadConfigFileInput { -    key: String! -    fileName: String! -} - -type LoadConfigFile { -    result: Generic -} - -type LoadConfigFileResult { -    data: LoadConfigFile -    success: Boolean! -    errors: [String] -} - -extend type Mutation { -    LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configsessionmutation -} - -input AddSystemImageInput { -    key: String! -    location: String! -} - -type AddSystemImage { -    result: Generic -} - -type AddSystemImageResult { -    data: AddSystemImage -    success: Boolean! -    errors: [String] -} - -extend type Mutation { -    AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @configsessionmutation -} - -input DeleteSystemImageInput { -    key: String! -    name: String! -} - -type DeleteSystemImage { -    result: Generic -} - -type DeleteSystemImageResult { -    data: DeleteSystemImage -    success: Boolean! -    errors: [String] -} - -extend type Mutation { -    DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @configsessionmutation -}
\ No newline at end of file diff --git a/src/services/api/graphql/key_auth.py b/src/services/api/graphql/libs/key_auth.py index f756ed6d8..2db0f7d48 100644 --- a/src/services/api/graphql/key_auth.py +++ b/src/services/api/graphql/libs/key_auth.py @@ -1,5 +1,5 @@ -from . import state +from .. import state  def check_auth(key_list, key):      if not key_list: diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/libs/op_mode.py index da2bcdb5b..97a26520e 100644 --- a/src/services/api/graphql/utils/util.py +++ b/src/services/api/graphql/libs/op_mode.py @@ -17,8 +17,11 @@ import os  import re  import typing  import importlib.util +from typing import Union +from humps import decamelize  from vyos.defaults import directories +from vyos.opmode import _normalize_field_names  def load_as_module(name: str, path: str):      spec = importlib.util.spec_from_file_location(name, path) @@ -98,3 +101,6 @@ def map_type_name(type_name: type, optional: bool = False) -> str:      # scalar 'Generic' is defined in schema.graphql      return 'Generic' + +def normalize_output(result: Union[dict, list]) -> Union[dict, list]: +    return _normalize_field_names(decamelize(result)) diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py new file mode 100644 index 000000000..3ecd8b855 --- /dev/null +++ b/src/services/api/graphql/libs/token_auth.py @@ -0,0 +1,68 @@ +import jwt +import uuid +import pam +from secrets import token_hex + +from .. import state + +def _check_passwd_pam(username: str, passwd: str) -> bool: +    if pam.authenticate(username, passwd): +        return True +    return False + +def init_secret(): +    length = int(state.settings['app'].state.vyos_secret_len) +    secret = token_hex(length) +    state.settings['secret'] = secret + +def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict: +    if user is None or passwd is None: +        return {} +    if _check_passwd_pam(user, passwd): +        app = state.settings['app'] +        try: +            users = app.state.vyos_token_users +        except AttributeError: +            app.state.vyos_token_users = {} +            users = app.state.vyos_token_users +        user_id = uuid.uuid1().hex +        payload_data = {'iss': user, 'sub': user_id, 'exp': exp} +        secret = state.settings.get('secret') +        if secret is None: +            return { +                    "success": False, +                    "errors": ['failed secret generation'] +                   } +        token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256") + +        users |= {user_id: user} +        return {'token': token} + +def get_user_context(request): +    context = {} +    context['request'] = request +    context['user'] = None +    if 'Authorization' in request.headers: +        auth = request.headers['Authorization'] +        scheme, token = auth.split() +        if scheme.lower() != 'bearer': +            return context + +        try: +            secret = state.settings.get('secret') +            payload = jwt.decode(token, secret, algorithms=["HS256"]) +            user_id: str = payload.get('sub') +            if user_id is None: +                return context +        except jwt.PyJWTError: +            return context +        try: +            users = state.settings['app'].state.vyos_token_users +        except AttributeError: +            return context + +        user = users.get(user_id) +        if user is not None: +            context['user'] = user + +    return context diff --git a/src/services/api/graphql/session/composite/system_status.py b/src/services/api/graphql/session/composite/system_status.py index 3c1a3d45b..d809f32e3 100755 --- a/src/services/api/graphql/session/composite/system_status.py +++ b/src/services/api/graphql/session/composite/system_status.py @@ -23,7 +23,7 @@ import importlib.util  from vyos.defaults import directories -from api.graphql.utils.util import load_op_mode_as_module +from api.graphql.libs.op_mode import load_op_mode_as_module  def get_system_version() -> dict:      show_version = load_op_mode_as_module('version.py') diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index f990e63d0..0b77b1433 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -24,7 +24,8 @@ from vyos.defaults import directories  from vyos.template import render  from vyos.opmode import Error as OpModeError -from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name +from api.graphql.libs.op_mode import load_op_mode_as_module, split_compound_op_mode_name +from api.graphql.libs.op_mode import normalize_output  op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') @@ -149,6 +150,8 @@ class Session:          except OpModeError as e:              raise e +        res = normalize_output(res) +          return res      def gen_op_mutation(self): diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 4ace981ca..3c390d9dc 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -647,20 +647,21 @@ def reset_op(data: ResetModel):  ###  def graphql_init(fast_api_app): -    from api.graphql.bindings import generate_schema - +    from api.graphql.libs.token_auth import get_user_context      api.graphql.state.init()      api.graphql.state.settings['app'] = app +    # import after initializaion of state +    from api.graphql.bindings import generate_schema      schema = generate_schema()      in_spec = app.state.vyos_introspection      if app.state.vyos_origins:          origins = app.state.vyos_origins -        app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS"))) +        app.add_route('/graphql', CORSMiddleware(GraphQL(schema, context_value=get_user_context, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS")))      else: -        app.add_route('/graphql', GraphQL(schema, debug=True, introspection=in_spec)) +        app.add_route('/graphql', GraphQL(schema, context_value=get_user_context, debug=True, introspection=in_spec))  ### @@ -688,16 +689,21 @@ if __name__ == '__main__':      app.state.vyos_debug = server_config['debug']      app.state.vyos_strict = server_config['strict']      app.state.vyos_origins = server_config.get('cors', {}).get('allow_origin', []) -    if 'gql' in server_config: -        app.state.vyos_gql = True -        if isinstance(server_config['gql'], dict) and 'introspection' in server_config['gql']: -            app.state.vyos_introspection = True -        else: -            app.state.vyos_introspection = False +    if 'graphql' in server_config: +        app.state.vyos_graphql = True +        if isinstance(server_config['graphql'], dict): +            if 'introspection' in server_config['graphql']: +                app.state.vyos_introspection = True +            else: +                app.state.vyos_introspection = False +            # default value is merged in conf_mode http-api.py, if not set +            app.state.vyos_auth_type = server_config['graphql']['authentication']['type'] +            app.state.vyos_token_exp = server_config['graphql']['authentication']['expiration'] +            app.state.vyos_secret_len = server_config['graphql']['authentication']['secret_length']      else: -        app.state.vyos_gql = False +        app.state.vyos_graphql = False -    if app.state.vyos_gql: +    if app.state.vyos_graphql:          graphql_init(app)      try: diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index a0fccd1d0..864ee8419 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -67,13 +67,13 @@ class KeepalivedFifo:          # For VRRP configuration to be read, the commit must be finished          count = 1          while commit_in_progress(): -            if ( count <= 40 ): -                logger.debug(f'commit in progress try: {count}') +            if ( count <= 20 ): +                logger.debug(f'Attempt to load keepalived configuration aborted due to a commit in progress (attempt {count}/20)')              else: -                logger.error(f'commit still in progress after {count} continuing anyway') +                logger.error(f'Forced keepalived configuration loading despite a commit in progress ({count} wait time expired, not waiting further)')                  break              count += 1 -            time.sleep(0.5) +            time.sleep(1)          try:              base = ['high-availability', 'vrrp'] diff --git a/src/tests/test_op_mode.py b/src/tests/test_op_mode.py index 4786357c5..90963b3c5 100644 --- a/src/tests/test_op_mode.py +++ b/src/tests/test_op_mode.py @@ -37,8 +37,29 @@ class TestVyOSOpMode(TestCase):          with self.assertRaises(vyos.opmode.InternalError):              _normalize_field_names(data) -    def test_dict_fields_normalization(self): +    def test_dict_fields_normalization_simple_dict(self):          from vyos.opmode import _normalize_field_names -        data = {"foo bar": True, "bar-baz": False} +        data = {"foo bar": True, "Bar-Baz": False}          self.assertEqual(_normalize_field_names(data), {"foo_bar": True, "bar_baz": False}) + +    def test_dict_fields_normalization_nested_dict(self): +        from vyos.opmode import _normalize_field_names + +        data = {"foo bar": True, "bar-baz": {"baz-quux": {"quux-xyzzy": False}}} +        self.assertEqual(_normalize_field_names(data), +          {"foo_bar": True, "bar_baz": {"baz_quux": {"quux_xyzzy": False}}}) + +    def test_dict_fields_normalization_mixed(self): +        from vyos.opmode import _normalize_field_names + +        data = [{"foo bar": True, "bar-baz": [{"baz-quux": {"quux-xyzzy": [False]}}]}] +        self.assertEqual(_normalize_field_names(data), +          [{"foo_bar": True, "bar_baz": [{"baz_quux": {"quux_xyzzy": [False]}}]}]) + +    def test_dict_fields_normalization_primitive(self): +        from vyos.opmode import _normalize_field_names + +        data = [1, False, "foo"] +        self.assertEqual(_normalize_field_names(data), [1, False, "foo"]) + diff --git a/src/tests/test_util.py b/src/tests/test_util.py index 8ac9a500a..d8b2b7940 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.py @@ -26,3 +26,17 @@ class TestVyOSUtil(TestCase):      def test_sysctl_read(self):          self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1') + +    def test_camel_to_snake_case(self): +        self.assertEqual(camel_to_snake_case('ConnectionTimeout'), +                                             'connection_timeout') +        self.assertEqual(camel_to_snake_case('connectionTimeout'), +                                             'connection_timeout') +        self.assertEqual(camel_to_snake_case('TCPConnectionTimeout'), +                                             'tcp_connection_timeout') +        self.assertEqual(camel_to_snake_case('TCPPort'), +                                             'tcp_port') +        self.assertEqual(camel_to_snake_case('UseHTTPProxy'), +                                             'use_http_proxy') +        self.assertEqual(camel_to_snake_case('CustomerID'), +                                             'customer_id') diff --git a/src/validators/allowed-vlan b/src/validators/allowed-vlan deleted file mode 100755 index 11389390b..000000000 --- a/src/validators/allowed-vlan +++ /dev/null @@ -1,19 +0,0 @@ -#! /usr/bin/python3 - -import sys -import re - -if __name__ == '__main__': -    if len(sys.argv)>1: -        allowed_vlan = sys.argv[1] -        if re.search('[0-9]{1,4}-[0-9]{1,4}', allowed_vlan): -            for tmp in allowed_vlan.split('-'): -                if int(tmp) not in range(1, 4095): -                    sys.exit(1) -        else: -            if int(allowed_vlan) not in range(1, 4095): -                sys.exit(1) -    else: -        sys.exit(2) -     -    sys.exit(0) diff --git a/src/validators/dotted-decimal b/src/validators/dotted-decimal deleted file mode 100755 index 652110346..000000000 --- a/src/validators/dotted-decimal +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 sys - -area = sys.argv[1] - -res = re.match(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$', area) -if not res: -    print("\'{0}\' is not a valid dotted decimal value".format(area)) -    sys.exit(1) -else: -    components = res.groups() -    for n in range(0, 4): -        if (int(components[n]) > 255): -            print("Invalid component of a dotted decimal value: {0} exceeds 255".format(components[n])) -            sys.exit(1) - -sys.exit(0) diff --git a/src/validators/mac-address b/src/validators/mac-address index 7d020f387..bb859a603 100755 --- a/src/validators/mac-address +++ b/src/validators/mac-address @@ -1,27 +1,2 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2020 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 sys - -pattern = "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$" - -if __name__ == '__main__': -    if len(sys.argv) != 2: -        sys.exit(1) -    if not re.match(pattern, sys.argv[1]): -        sys.exit(1) -    sys.exit(0) +#!/usr/bin/env sh +${vyos_libexec_dir}/validate-value --regex "([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1" diff --git a/src/validators/mac-address-exclude b/src/validators/mac-address-exclude new file mode 100755 index 000000000..c44913023 --- /dev/null +++ b/src/validators/mac-address-exclude @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +${vyos_libexec_dir}/validate-value --regex "!([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1" diff --git a/src/validators/mac-address-firewall b/src/validators/mac-address-firewall deleted file mode 100755 index 70551f86d..000000000 --- a/src/validators/mac-address-firewall +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2022 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 sys - -pattern = "^!?([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$" - -if __name__ == '__main__': -    if len(sys.argv) != 2: -        sys.exit(1) -    if not re.match(pattern, sys.argv[1]): -        sys.exit(1) -    sys.exit(0) diff --git a/src/validators/tcp-flag b/src/validators/tcp-flag deleted file mode 100755 index 1496b904a..000000000 --- a/src/validators/tcp-flag +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/python3 - -import sys -import re - -if __name__ == '__main__': -    if len(sys.argv)>1: -        flag = sys.argv[1] -        if flag and flag[0] == '!': -            flag = flag[1:] -        if flag not in ['syn', 'ack', 'rst', 'fin', 'urg', 'psh', 'ecn', 'cwr']: -            print(f'Error: {flag} is not a valid TCP flag') -            sys.exit(1) -    else: -        sys.exit(2) - -    sys.exit(0) | 
