diff options
Diffstat (limited to 'src')
31 files changed, 412 insertions, 171 deletions
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 1e76147dd..3b8fae710 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -634,10 +634,10 @@ def generate(openvpn): def apply(openvpn): interface = openvpn['ifname'] - call(f'systemctl stop openvpn@{interface}.service') # Do some cleanup when OpenVPN is disabled/deleted if 'deleted' in openvpn or 'disable' in openvpn: + call(f'systemctl stop openvpn@{interface}.service') for cleanup_file in glob(f'/run/openvpn/{interface}.*'): if os.path.isfile(cleanup_file): os.unlink(cleanup_file) @@ -649,7 +649,7 @@ def apply(openvpn): # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - call(f'systemctl start openvpn@{interface}.service') + call(f'systemctl reload-or-restart openvpn@{interface}.service') o = VTunIf(**openvpn) o.update(openvpn) diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index faa5eb628..f013e5411 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -25,7 +25,9 @@ from vyos.configverify import verify_interface_exists from vyos.configverify import verify_vrf from vyos.ifconfig import WWANIf from vyos.util import cmd +from vyos.util import call from vyos.util import dict_search +from vyos.util import DEVNULL from vyos import ConfigError from vyos import airbag airbag.enable() @@ -88,7 +90,7 @@ def apply(wwan): options += ',user={user},password={password}'.format(**wwan['authentication']) command = f'{base_cmd} --simple-connect="{options}"' - cmd(command) + call(command, stdout=DEVNULL) w.update(wwan) return None diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 2a420b193..e1852f2ce 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,13 +20,17 @@ from sys import exit from vyos.config import Config from vyos.configverify import verify_vrf -from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random +from vyos.snmpv3_hashgen import plaintext_to_md5 +from vyos.snmpv3_hashgen import plaintext_to_sha1 +from vyos.snmpv3_hashgen import random from vyos.template import render from vyos.template import is_ipv4 -from vyos.util import call, chmod_755 +from vyos.util import call +from vyos.util import chmod_755 from vyos.validate import is_addr_assigned from vyos.version import get_version_data -from vyos import ConfigError, airbag +from vyos import ConfigError +from vyos import airbag airbag.enable() config_file_client = r'/etc/snmp/snmp.conf' @@ -410,19 +414,20 @@ def verify(snmp): port = listen[1] protocol = snmp['protocol'] + tmp = None if is_ipv4(addr): # example: udp:127.0.0.1:161 - listen = f'{protocol}:{addr}:{port}' + tmp = f'{protocol}:{addr}:{port}' elif snmp['ipv6_enabled']: # example: udp6:[::1]:161 - listen = f'{protocol}6:[{addr}]:{port}' + tmp = f'{protocol}6:[{addr}]:{port}' # We only wan't to configure addresses that exist on the system. # Hint the user if they don't exist if is_addr_assigned(addr): - snmp['listen_on'].append(listen) + if tmp: snmp['listen_on'].append(tmp) else: - print('WARNING: SNMP listen address {0} not configured!'.format(addr)) + print(f'WARNING: SNMP listen address {addr} not configured!') verify_vrf(snmp) diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index e9d6a339c..2220d7b66 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -37,7 +37,7 @@ PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' default_config_data = { - 'issue': 'Welcome to VyOS - \\n \\l\n', + 'issue': 'Welcome to VyOS - \\n \\l\n\n', 'issue_net': 'Welcome to VyOS\n', 'motd': motd } diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 919083ac4..38c0c4463 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,7 +18,6 @@ import os from sys import exit from json import loads -from tempfile import NamedTemporaryFile from vyos.config import Config from vyos.configdict import node_changed @@ -31,10 +30,12 @@ from vyos.util import get_interface_config from vyos.util import popen from vyos.util import run from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() -config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' +config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' +nft_vrf_config = '/tmp/nftables-vrf-zones' def list_rules(): command = 'ip -j -4 rule show' @@ -128,8 +129,8 @@ def verify(vrf): def generate(vrf): render(config_file, 'vrf/vrf.conf.tmpl', vrf) # Render nftables zones config - vrf['nft_vrf_zones'] = NamedTemporaryFile().name - render(vrf['nft_vrf_zones'], 'firewall/nftables-vrf-zones.tmpl', vrf) + + render(nft_vrf_config, 'firewall/nftables-vrf-zones.tmpl', vrf) return None @@ -165,8 +166,9 @@ def apply(vrf): _, err = popen('nft list table inet vrf_zones') # If not, create a table if err: - cmd(f'nft -f {vrf["nft_vrf_zones"]}') - os.unlink(vrf['nft_vrf_zones']) + if os.path.exists(nft_vrf_config): + cmd(f'nft -f {nft_vrf_config}') + os.unlink(nft_vrf_config) for name, config in vrf['name'].items(): table = config['table'] diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py index 87ee8f2d1..50d60f0dc 100755 --- a/src/conf_mode/vrf_vni.py +++ b/src/conf_mode/vrf_vni.py @@ -32,32 +32,23 @@ def get_config(config=None): else: conf = Config() - # This script only works with a passed VRF name - if len(argv) < 1: - raise NotImplementedError - vrf = argv[1] + base = ['vrf'] + vrf = conf.get_config_dict(base, get_first_key=True) + return vrf - # "assemble" dict - easier here then use a full blown get_config_dict() - # on a single leafNode - vni = { 'vrf' : vrf } - tmp = conf.return_value(['vrf', 'name', vrf, 'vni']) - if tmp: vni.update({ 'vni' : tmp }) - - return vni - -def verify(vni): +def verify(vrf): return None -def generate(vni): - vni['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vni) +def generate(vrf): + vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vrf) return None -def apply(vni): +def apply(vrf): # add configuration to FRR frr_cfg = frr.FRRConfig() frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '') - frr_cfg.add_before(r'(interface .*|line vty)', vni['new_frr_config']) + frr_cfg.modify_section(f'^vrf .+$', '') + frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) # Save configuration to /run/frr/config/frr.conf diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan new file mode 100644 index 000000000..28190776f --- /dev/null +++ b/src/etc/cron.d/check-wwan @@ -0,0 +1 @@ +*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py diff --git a/src/etc/systemd/system/openvpn@.service.d/10-override.conf b/src/etc/systemd/system/openvpn@.service.d/10-override.conf index 03fe6b587..775a2d7ba 100644 --- a/src/etc/systemd/system/openvpn@.service.d/10-override.conf +++ b/src/etc/systemd/system/openvpn@.service.d/10-override.conf @@ -7,6 +7,7 @@ WorkingDirectory= WorkingDirectory=/run/openvpn ExecStart= ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +ExecReload=/bin/kill -HUP $MAINPID User=openvpn Group=openvpn AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py new file mode 100755 index 000000000..c6e6c54b7 --- /dev/null +++ b/src/helpers/vyos-check-wwan.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +from vyos.configquery import VbashOpRun +from vyos.configquery import ConfigTreeQuery + +from vyos.util import is_wwan_connected +from vyos.util import call + +conf = ConfigTreeQuery() +dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'), + get_first_key=True) + +for interface, interface_config in dict.items(): + if not is_wwan_connected(interface): + if 'disable' in interface_config: + # do not restart this interface as it's disabled by the user + continue + + #op = VbashOpRun() + #op.run(['connect', 'interface', interface]) + call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py') + +exit(0) diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name index 13fb9e31f..e21d8c9ff 100755 --- a/src/helpers/vyos_net_name +++ b/src/helpers/vyos_net_name @@ -144,7 +144,19 @@ def get_configfile_interfaces() -> dict: logging.critical(f"OSError {e}") exit(1) - config = ConfigTree(config_file) + try: + config = ConfigTree(config_file) + except Exception: + logging.debug(f"updating component version string syntax") + try: + # this will update the component version string in place, for + # updates 1.2 --> 1.3/1.4 + os.system(f'/usr/libexec/vyos/run-config-migration.py {config_path} --virtual --set-vintage=vyos') + with open(config_path) as f: + config_file = f.read() + config = ConfigTree(config_file) + except Exception as e: + logging.critical(f"ConfigTree error: {e}") base = ['interfaces', 'ethernet'] if config.exists(base): diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index a773aa28e..ffc574362 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -17,21 +17,19 @@ import os import argparse -from sys import exit from psutil import process_iter -from time import strftime, localtime, time from vyos.util import call +from vyos.util import DEVNULL +from vyos.util import is_wwan_connected -def check_interface(interface): +def check_ppp_interface(interface): if not os.path.isfile(f'/etc/ppp/peers/{interface}'): - print(f'Interface {interface}: invalid!') + print(f'Interface {interface} does not exist!') exit(1) def check_ppp_running(interface): - """ - Check if ppp process is running in the interface in question - """ + """ Check if PPP process is running in the interface in question """ for p in process_iter(): if "pppd" in p.name(): if interface in p.cmdline(): @@ -40,32 +38,46 @@ def check_ppp_running(interface): return False def connect(interface): - """ - Connect PPP interface - """ - check_interface(interface) + """ Connect dialer interface """ - # Check if interface is already dialed - if os.path.isdir(f'/sys/class/net/{interface}'): - print(f'Interface {interface}: already connected!') - elif check_ppp_running(interface): - print(f'Interface {interface}: connection is beeing established!') + if interface.startswith('ppp'): + check_ppp_interface(interface) + # Check if interface is already dialed + if os.path.isdir(f'/sys/class/net/{interface}'): + print(f'Interface {interface}: already connected!') + elif check_ppp_running(interface): + print(f'Interface {interface}: connection is beeing established!') + else: + print(f'Interface {interface}: connecting...') + call(f'systemctl restart ppp@{interface}.service') + elif interface.startswith('wwan'): + if is_wwan_connected(interface): + print(f'Interface {interface}: already connected!') + else: + call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py') else: - print(f'Interface {interface}: connecting...') - call(f'systemctl restart ppp@{interface}.service') + print(f'Unknown interface {interface}, can not connect. Aborting!') def disconnect(interface): - """ - Disconnect PPP interface - """ - check_interface(interface) + """ Disconnect dialer interface """ - # Check if interface is already down - if not check_ppp_running(interface): - print(f'Interface {interface}: connection is already down') + if interface.startswith('ppp'): + check_ppp_interface(interface) + + # Check if interface is already down + if not check_ppp_running(interface): + print(f'Interface {interface}: connection is already down') + else: + print(f'Interface {interface}: disconnecting...') + call(f'systemctl stop ppp@{interface}.service') + elif interface.startswith('wwan'): + if not is_wwan_connected(interface): + print(f'Interface {interface}: connection is already down') + else: + modem = interface.lstrip('wwan') + call(f'mmcli --modem {modem} --simple-disconnect', stdout=DEVNULL) else: - print(f'Interface {interface}: disconnecting...') - call(f'systemctl stop ppp@{interface}.service') + print(f'Unknown interface {interface}, can not disconnect. Aborting!') def main(): parser = argparse.ArgumentParser() diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 3d50eb938..eac068274 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -94,10 +94,8 @@ def split_text(text, used=0): used: number of characted already used in the screen """ no_tty = call('tty -s') - if no_tty: - return text.split() - returned = cmd('stty size') + returned = cmd('stty size') if not no_tty else '' if len(returned) == 2: rows, columns = [int(_) for _ in returned] else: diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index 580c0eb7f..29f58f709 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -10,7 +10,7 @@ to run with that address as default router by requesting these 'mutations' in the GraphQL playground: mutation { - createInterfaceEthernet (data: {interface: "eth1", + CreateInterfaceEthernet (data: {interface: "eth1", address: "192.168.0.1/24", description: "BOB"}) { success @@ -22,7 +22,7 @@ mutation { } mutation { - createDhcpServer(data: {sharedNetworkName: "BOB", + CreateDhcpServer(data: {sharedNetworkName: "BOB", subnet: "192.168.0.0/24", defaultRouter: "192.168.0.1", nameServer: "192.168.0.1", @@ -42,6 +42,38 @@ mutation { } } +To save the configuration, use the following mutation: + +mutation { + SaveConfigFile(data: {fileName: "/config/config.boot"}) { + success + errors + data { + fileName + } + } +} + +N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to +save to /config/config.boot; to save to an alternative path, specify +fileName. + +Similarly, using the same 'endpoint' (meaning the form of the request and +resolver; the actual enpoint for all GraphQL requests is +https://hostname/graphql), one can load an arbitrary config file from a +path. + +mutation { + LoadConfigFile(data: {fileName: "/home/vyos/config.boot"}) { + success + errors + data { + fileName + } + } +} + + The GraphQL playground will be found at: https://{{ host_address }}/graphql @@ -57,22 +89,23 @@ What's here: services ├── api │ └── graphql +│ ├── bindings.py │ ├── graphql │ │ ├── directives.py │ │ ├── __init__.py │ │ ├── mutations.py │ │ └── schema +│ │ ├── config_file.graphql │ │ ├── dhcp_server.graphql │ │ ├── interface_ethernet.graphql │ │ └── schema.graphql +│ ├── README.graphql │ ├── recipes -│ │ ├── dhcp_server.py │ │ ├── __init__.py -│ │ ├── interface_ethernet.py -│ │ ├── recipe.py +│ │ ├── session.py │ │ └── templates -│ │ ├── dhcp_server.tmpl -│ │ └── interface_ethernet.tmpl +│ │ ├── create_dhcp_server.tmpl +│ │ └── create_interface_ethernet.tmpl │ └── state.py ├── vyos-configd ├── vyos-hostsd @@ -90,13 +123,14 @@ the Ur-data; the GraphQL schema is produced from those files, located in Resolvers for the schema Mutation fields are dynamically generated using a 'directive' added to the respective schema field. The directive, -'@generate', is handled by the class 'DataDirective' in -'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in -'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate -wrapper in 'api/graphql/recipes', with base class doing the (overridable) -configuration steps of calling all defined 'set'/'delete' commands. - -Integrating the above with vyos-http-api-server is ~10 lines of code. +'@configure', is handled by the class 'ConfigureDirective' in +'api/graphql/graphql/directives.py', which calls the +'make_configure_resolver' function in 'api/graphql/graphql/mutations.py'; +the produced resolver calls the appropriate wrapper in +'api/graphql/recipes', with base class doing the (overridable) configuration +steps of calling all defined 'set'/'delete' commands. + +Integrating the above with vyos-http-api-server is 4 lines of code. What needs to be done: diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py new file mode 100644 index 000000000..1fbe13d0c --- /dev/null +++ b/src/services/api/graphql/bindings.py @@ -0,0 +1,13 @@ +import vyos.defaults +from . graphql.mutations import mutation +from . graphql.directives import directives_dict +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'] + + type_defs = load_schema_from_path(api_schema_dir) + + schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives=directives_dict) + + return schema diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 651421c35..f5cd88acd 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,12 +1,11 @@ from ariadne import SchemaDirectiveVisitor, ObjectType -from . mutations import make_resolver +from . mutations import make_configure_resolver, make_config_file_resolver -class DataDirective(SchemaDirectiveVisitor): - """ - Class providing implementation of 'generate' directive in schema. +def non(arg): + pass - """ - def visit_field_definition(self, field, object_type): +class VyosDirective(SchemaDirectiveVisitor): + def visit_field_definition(self, field, object_type, make_resolver=non): name = f'{field.type}' # field.type contains the return value of the mutation; trim value # to produce canonical name @@ -15,3 +14,24 @@ class DataDirective(SchemaDirectiveVisitor): func = make_resolver(name) field.resolve = func return field + + +class ConfigureDirective(VyosDirective): + """ + Class providing implementation of 'configure' directive in schema. + + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_configure_resolver) + +class ConfigFileDirective(VyosDirective): + """ + Class providing implementation of 'configfile' directive in schema. + + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_config_file_resolver) + +directives_dict = {"configure": ConfigureDirective, "configfile": ConfigFileDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 98c665c9a..8a28b13d7 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -6,10 +6,11 @@ from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state +from api.graphql.recipes.session import Session mutation = ObjectType("Mutation") -def make_resolver(mutation_name): +def make_resolver(mutation_name, class_name, session_func): """Dynamically generate a resolver for the mutation named in the schema by 'mutation_name'. @@ -19,11 +20,11 @@ def make_resolver(mutation_name): functools.wraps. :raise Exception: - encapsulating ConfigErrors, or internal errors + raising ConfigErrors, or internal errors """ - class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1) + func_base_name = convert_camel_case_to_snake(class_name) - resolver_name = f'resolve_create_{func_base_name}' + resolver_name = f'resolve_{func_base_name}' func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' @mutation.field(mutation_name) @@ -40,10 +41,17 @@ def make_resolver(mutation_name): data = kwargs['data'] session = state.settings['app'].state.vyos_session - mod = import_module(f'api.graphql.recipes.{func_base_name}') - klass = getattr(mod, class_name) + # one may override the session functions with a local subclass + try: + mod = import_module(f'api.graphql.recipes.{func_base_name}') + klass = getattr(mod, class_name) + except ImportError: + # otherwise, dynamically generate subclass to invoke subclass + # name based templates + klass = type(class_name, (Session,), {}) k = klass(session, data) - k.configure() + method = getattr(k, session_func) + method() return { "success": True, @@ -57,4 +65,16 @@ def make_resolver(mutation_name): return func_impl +def make_configure_resolver(mutation_name): + class_name = mutation_name + return make_resolver(mutation_name, class_name, 'configure') +def make_config_file_resolver(mutation_name): + if 'Save' in mutation_name: + class_name = mutation_name.replace('Save', '', 1) + return make_resolver(mutation_name, class_name, 'save') + elif 'Load' in mutation_name: + class_name = mutation_name.replace('Load', '', 1) + return make_resolver(mutation_name, class_name, 'load') + else: + raise Exception diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql new file mode 100644 index 000000000..31ab26b9e --- /dev/null +++ b/src/services/api/graphql/graphql/schema/config_file.graphql @@ -0,0 +1,27 @@ +input SaveConfigFileInput { + fileName: String +} + +type SaveConfigFile { + fileName: String +} + +type SaveConfigFileResult { + data: SaveConfigFile + success: Boolean! + errors: [String] +} + +input LoadConfigFileInput { + fileName: String! +} + +type LoadConfigFile { + fileName: String! +} + +type LoadConfigFileResult { + data: LoadConfigFile + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql index 9f741a0a5..25f091bfa 100644 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -1,4 +1,4 @@ -input dhcpServerConfigInput { +input DhcpServerConfigInput { sharedNetworkName: String subnet: String defaultRouter: String @@ -13,7 +13,7 @@ input dhcpServerConfigInput { dnsForwardingListenAddress: String } -type dhcpServerConfig { +type DhcpServerConfig { sharedNetworkName: String subnet: String defaultRouter: String @@ -28,8 +28,8 @@ type dhcpServerConfig { dnsForwardingListenAddress: String } -type createDhcpServerResult { - data: dhcpServerConfig +type CreateDhcpServerResult { + data: DhcpServerConfig success: Boolean! errors: [String] } diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql new file mode 100644 index 000000000..efe7de632 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql @@ -0,0 +1,47 @@ +input CreateFirewallAddressGroupInput { + name: String! + address: [String] +} + +type CreateFirewallAddressGroup { + name: String! + address: [String] +} + +type CreateFirewallAddressGroupResult { + data: CreateFirewallAddressGroup + success: Boolean! + errors: [String] +} + +input UpdateFirewallAddressGroupMembersInput { + name: String! + address: [String!]! +} + +type UpdateFirewallAddressGroupMembers { + name: String! + address: [String!]! +} + +type UpdateFirewallAddressGroupMembersResult { + data: UpdateFirewallAddressGroupMembers + success: Boolean! + errors: [String] +} + +input RemoveFirewallAddressGroupMembersInput { + name: String! + address: [String!]! +} + +type RemoveFirewallAddressGroupMembers { + name: String! + address: [String!]! +} + +type RemoveFirewallAddressGroupMembersResult { + data: RemoveFirewallAddressGroupMembers + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql index fdcf97bad..32438b315 100644 --- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -1,18 +1,18 @@ -input interfaceEthernetConfigInput { +input InterfaceEthernetConfigInput { interface: String address: String replace: Boolean = true description: String } -type interfaceEthernetConfig { +type InterfaceEthernetConfig { interface: String address: String description: String } -type createInterfaceEthernetResult { - data: interfaceEthernetConfig +type CreateInterfaceEthernetResult { + data: InterfaceEthernetConfig success: Boolean! errors: [String] } diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 8a5e17962..9e97a0d60 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -7,9 +7,15 @@ type Query { _dummy: String } -directive @generate on FIELD_DEFINITION +directive @configure on FIELD_DEFINITION +directive @configfile on FIELD_DEFINITION type Mutation { - createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate - createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate + CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure + CreateInterfaceEthernet(data: InterfaceEthernetConfigInput) : CreateInterfaceEthernetResult @configure + CreateFirewallAddressGroup(data: CreateFirewallAddressGroupInput) : CreateFirewallAddressGroupResult @configure + UpdateFirewallAddressGroupMembers(data: UpdateFirewallAddressGroupMembersInput) : UpdateFirewallAddressGroupMembersResult @configure + RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure + SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile + LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile } diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py deleted file mode 100644 index 3edb3028e..000000000 --- a/src/services/api/graphql/recipes/dhcp_server.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class DhcpServer(Recipe): - def __init__(self, session, command_file): - super().__init__(session, command_file) - - # Define any custom processing of parameters here by overriding - # configure: - # - # def configure(self): - # self.data = transform_data(self.data) - # super().configure() diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py deleted file mode 100644 index f88f5924f..000000000 --- a/src/services/api/graphql/recipes/interface_ethernet.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class InterfaceEthernet(Recipe): - def __init__(self, session, command_file): - super().__init__(session, command_file) - - # Define any custom processing of parameters here by overriding - # configure: - # - # def configure(self): - # self.data = transform_data(self.data) - # super().configure() diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py new file mode 100644 index 000000000..cde30c27a --- /dev/null +++ b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py @@ -0,0 +1,21 @@ + +from . session import Session + +class RemoveFirewallAddressGroupMembers(Session): + def __init__(self, session, data): + super().__init__(session, data) + + # Define any custom processing of parameters here by overriding + # configure: + # + # def configure(self): + # self._data = transform_data(self._data) + # super().configure() + # self.clean_up() + + def configure(self): + super().configure() + + group_name = self._data['name'] + path = ['firewall', 'group', 'address-group', group_name] + self.delete_path_if_childless(path) diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/session.py index 8fbb9e0bf..b96cc1753 100644 --- a/src/services/api/graphql/recipes/recipe.py +++ b/src/services/api/graphql/recipes/session.py @@ -1,27 +1,17 @@ from ariadne import convert_camel_case_to_snake import vyos.defaults +from vyos.config import Config from vyos.template import render -class Recipe(object): +class Session(object): def __init__(self, session, data): self._session = session - self.data = data + self._data = data self._name = convert_camel_case_to_snake(type(self).__name__) - @property - def data(self): - return self.__data - - @data.setter - def data(self, data): - if isinstance(data, dict): - self.__data = data - else: - raise ValueError("data must be of type dict") - def configure(self): session = self._session - data = self.data + data = self._data func_base_name = self._name tmpl_file = f'{func_base_name}.tmpl' @@ -46,4 +36,30 @@ class Recipe(object): except Exception as error: raise error + def delete_path_if_childless(self, path): + session = self._session + config = Config(session.get_session_env()) + if not config.list_nodes(path): + session.delete(path) + session.commit() + + def save(self): + session = self._session + data = self._data + if 'file_name' not in data or not data['file_name']: + data['file_name'] = '/config/config.boot' + + try: + session.save_config(data['file_name']) + except Exception as error: + raise error + + def load(self): + session = self._session + data = self._data + try: + session.load_config(data['file_name']) + session.commit() + except Exception as error: + raise error diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl index 70de43183..70de43183 100644 --- a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl +++ b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl new file mode 100644 index 000000000..a890d0086 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl @@ -0,0 +1,4 @@ +set firewall group address-group {{ name }} +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl index d9d7ed691..d9d7ed691 100644 --- a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl +++ b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl new file mode 100644 index 000000000..458f3e5fc --- /dev/null +++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +delete firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl new file mode 100644 index 000000000..f56c61231 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cb4ce4072..aa7ac6708 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,16 +32,13 @@ from fastapi.responses import HTMLResponse from fastapi.exceptions import RequestValidationError from fastapi.routing import APIRoute from pydantic import BaseModel, StrictStr, validator -from starlette.datastructures import FormData, MutableHeaders +from starlette.datastructures import FormData from starlette.formparsers import FormParser, MultiPartParser from multipart.multipart import parse_options_header -from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers from ariadne.asgi import GraphQL import vyos.config -import vyos.defaults - from vyos.configsession import ConfigSession, ConfigSessionError import api.graphql.state @@ -69,11 +66,11 @@ def load_server_config(): return config def check_auth(key_list, key): - id = None + key_id = None for k in key_list: if k['key'] == key: - id = k['id'] - return id + key_id = k['id'] + return key_id def error(code, msg): resp = {"success": False, "error": msg, "data": None} @@ -223,10 +220,10 @@ responses = { def auth_required(data: ApiModel): key = data.key api_keys = app.state.vyos_keys - id = check_auth(api_keys, key) - if not id: + key_id = check_auth(api_keys, key) + if not key_id: raise HTTPException(status_code=401, detail="Valid API key is required") - app.state.vyos_id = id + app.state.vyos_id = key_id # override Request and APIRoute classes in order to convert form request to json; # do all explicit validation here, for backwards compatability of error messages; @@ -613,16 +610,11 @@ def show_op(data: ShowModel): # GraphQL integration ### -api.graphql.state.init() - -from api.graphql.graphql.mutations import mutation -from api.graphql.graphql.directives import DataDirective +from api.graphql.bindings import generate_schema -api_schema_dir = vyos.defaults.directories['api_schema'] - -type_defs = load_schema_from_path(api_schema_dir) +api.graphql.state.init() -schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) +schema = generate_schema() app.add_route('/graphql', GraphQL(schema, debug=True)) @@ -640,16 +632,16 @@ if __name__ == '__main__': try: server_config = load_server_config() - except Exception as e: - logger.critical("Failed to load the HTTP API server config: {0}".format(e)) + except Exception as err: + logger.critical(f"Failed to load the HTTP API server config: {err}") - session = ConfigSession(os.getpid()) + config_session = ConfigSession(os.getpid()) - app.state.vyos_session = session + app.state.vyos_session = config_session app.state.vyos_keys = server_config['api_keys'] - app.state.vyos_debug = True if server_config['debug'] == 'true' else False - app.state.vyos_strict = True if server_config['strict'] == 'true' else False + app.state.vyos_debug = bool(server_config['debug'] == 'true') + app.state.vyos_strict = bool(server_config['strict'] == 'true') api.graphql.state.settings['app'] = app @@ -657,6 +649,6 @@ if __name__ == '__main__': uvicorn.run(app, host=server_config["listen_address"], port=int(server_config["port"]), proxy_headers=True) - except OSError as e: - logger.critical(f"OSError {e}") + except OSError as err: + logger.critical(f"OSError {err}") sys.exit(1) |