diff options
-rw-r--r-- | data/templates/tftp-server/default.tmpl | 5 | ||||
-rw-r--r-- | interface-definitions/include/listen-address-vrf.xml.i | 25 | ||||
-rw-r--r-- | interface-definitions/tftp-server.xml.in | 2 | ||||
-rw-r--r-- | op-mode-definitions/show-configuration.xml.in | 15 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_tftp-server.py | 39 | ||||
-rwxr-xr-x | src/conf_mode/tftp_server.py | 9 | ||||
-rwxr-xr-x | src/op_mode/show_configuration_json.py | 36 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/directives.py | 25 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/mutations.py | 11 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/schema/schema.graphql | 4 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/schema/show.graphql | 14 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/schema/show_config.graphql | 21 | ||||
-rw-r--r-- | src/services/api/graphql/recipes/session.py | 31 | ||||
-rw-r--r-- | src/systemd/tftpd@.service | 2 | ||||
-rwxr-xr-x | src/utils/vyos-hostsd-client | 3 |
15 files changed, 232 insertions, 10 deletions
diff --git a/data/templates/tftp-server/default.tmpl b/data/templates/tftp-server/default.tmpl index 6b2d6a903..a7edf60ad 100644 --- a/data/templates/tftp-server/default.tmpl +++ b/data/templates/tftp-server/default.tmpl @@ -1,2 +1,7 @@ ### Autogenerated by tftp_server.py ### DAEMON_ARGS="--listen --user tftp --address {{ listen_address }} {{ "--create --umask 000" if allow_upload is defined }} --secure {{ directory }}" +{% if vrf is defined %} +VRF_ARGS="ip vrf exec {{ vrf }}" +{% else %} +VRF_ARGS="" +{% endif %} diff --git a/interface-definitions/include/listen-address-vrf.xml.i b/interface-definitions/include/listen-address-vrf.xml.i new file mode 100644 index 000000000..7ec9eace4 --- /dev/null +++ b/interface-definitions/include/listen-address-vrf.xml.i @@ -0,0 +1,25 @@ +<!-- include start from listen-address-vrf.xml.i --> +<tagNode name="listen-address"> + <properties> + <help>Local IP addresses for service to listen on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to listen for incoming connections</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + #include <include/interface/vrf.xml.i> + </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/tftp-server.xml.in b/interface-definitions/tftp-server.xml.in index 037c097ca..4963eab3c 100644 --- a/interface-definitions/tftp-server.xml.in +++ b/interface-definitions/tftp-server.xml.in @@ -24,7 +24,7 @@ <leafNode name="port"> <defaultValue>69</defaultValue> </leafNode> - #include <include/listen-address.xml.i> + #include <include/listen-address-vrf.xml.i> </children> </node> </children> diff --git a/op-mode-definitions/show-configuration.xml.in b/op-mode-definitions/show-configuration.xml.in index 318942ab0..5a2fdedfa 100644 --- a/op-mode-definitions/show-configuration.xml.in +++ b/op-mode-definitions/show-configuration.xml.in @@ -30,6 +30,21 @@ <!-- no admin check --> <command>${vyos_op_scripts_dir}/show_configuration_files.sh</command> </node> + <node name="json"> + <properties> + <help>Show running configuration in JSON format</help> + </properties> + <!-- no admin check --> + <command>${vyos_op_scripts_dir}/show_configuration_json.py</command> + <children> + <node name="pretty"> + <properties> + <help>Show running configuration in readable JSON format</help> + </properties> + <command>${vyos_op_scripts_dir}/show_configuration_json.py --pretty</command> + </node> + </children> + </node> </children> </node> </children> diff --git a/smoketest/scripts/cli/test_service_tftp-server.py b/smoketest/scripts/cli/test_service_tftp-server.py index 1a1bf0cdf..b57c33f26 100755 --- a/smoketest/scripts/cli/test_service_tftp-server.py +++ b/smoketest/scripts/cli/test_service_tftp-server.py @@ -20,6 +20,7 @@ from psutil import process_iter from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.util import cmd from vyos.util import read_file from vyos.util import process_named_running from vyos.template import is_ipv6 @@ -29,6 +30,7 @@ base_path = ['service', 'tftp-server'] dummy_if_path = ['interfaces', 'dummy', 'dum69'] address_ipv4 = '192.0.2.1' address_ipv6 = '2001:db8::1' +vrf = 'mgmt' class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): def setUp(self): @@ -97,5 +99,42 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): count += 1 self.assertEqual(count, len(address)) + def test_03_tftpd_vrf(self): + directory = '/tmp' + port = '69' # default port + + self.cli_set(base_path + ['allow-upload']) + self.cli_set(base_path + ['directory', directory]) + self.cli_set(base_path + ['listen-address', address_ipv4, 'vrf', vrf]) + + # VRF does yet not exist - an error must be thrown + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(['vrf', 'name', vrf, 'table', '1338']) + self.cli_set(dummy_if_path + ['vrf', vrf]) + + # commit changes + self.cli_commit() + + config = read_file('/etc/default/tftpd0') + # verify listen IP address + self.assertIn(f'{address_ipv4}:{port} -4', config) + # verify directory + self.assertIn(directory, config) + # verify upload + self.assertIn('--create --umask 000', config) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + # Check for process in VRF + tmp = cmd(f'ip vrf pids {vrf}') + self.assertIn(PROCESS_NAME, tmp) + + # delete VRF + self.cli_delete(dummy_if_path + ['vrf']) + self.cli_delete(['vrf', 'name', vrf]) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index 2409eec1f..ef726670c 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -24,6 +24,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_vrf from vyos.template import render from vyos.template import is_ipv4 from vyos.util import call @@ -65,10 +66,11 @@ def verify(tftpd): if 'listen_address' not in tftpd: raise ConfigError('TFTP server listen address must be configured!') - for address in tftpd['listen_address']: + for address, address_config in tftpd['listen_address'].items(): if not is_addr_assigned(address): print(f'WARNING: TFTP server listen address "{address}" not ' \ 'assigned to any interface!') + verify_vrf(address_config) return None @@ -83,7 +85,7 @@ def generate(tftpd): return None idx = 0 - for address in tftpd['listen_address']: + for address, address_config in tftpd['listen_address'].items(): config = deepcopy(tftpd) port = tftpd['port'] if is_ipv4(address): @@ -91,6 +93,9 @@ def generate(tftpd): else: config['listen_address'] = f'[{address}]:{port} -6' + if 'vrf' in address_config: + config['vrf'] = address_config['vrf'] + file = config_file + str(idx) render(file, 'tftp-server/default.tmpl', config) idx = idx + 1 diff --git a/src/op_mode/show_configuration_json.py b/src/op_mode/show_configuration_json.py new file mode 100755 index 000000000..fdece533b --- /dev/null +++ b/src/op_mode/show_configuration_json.py @@ -0,0 +1,36 @@ +#!/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/>. + +import argparse +import json + +from vyos.configquery import ConfigTreeQuery + + +config = ConfigTreeQuery() +c = config.get_config_dict() + +parser = argparse.ArgumentParser() +parser.add_argument("-p", "--pretty", action="store_true", help="Show pretty configuration in JSON format") + + +if __name__ == '__main__': + args = parser.parse_args() + + if args.pretty: + print(json.dumps(c, indent=4)) + else: + print(json.dumps(c)) diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index f5cd88acd..4bc31c6b5 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,5 +1,5 @@ from ariadne import SchemaDirectiveVisitor, ObjectType -from . mutations import make_configure_resolver, make_config_file_resolver +from . mutations import * def non(arg): pass @@ -19,19 +19,36 @@ class VyosDirective(SchemaDirectiveVisitor): 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 ShowConfigDirective(VyosDirective): + """ + Class providing implementation of 'show' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_show_config_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} +class ShowDirective(VyosDirective): + """ + Class providing implementation of 'show' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_show_resolver) + +directives_dict = {"configure": ConfigureDirective, + "showconfig": ShowConfigDirective, + "configfile": ConfigFileDirective, + "show": ShowDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 8a28b13d7..0ba2cd4bb 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -51,7 +51,8 @@ def make_resolver(mutation_name, class_name, session_func): klass = type(class_name, (Session,), {}) k = klass(session, data) method = getattr(k, session_func) - method() + result = method() + data['result'] = result return { "success": True, @@ -69,6 +70,10 @@ def make_configure_resolver(mutation_name): class_name = mutation_name return make_resolver(mutation_name, class_name, 'configure') +def make_show_config_resolver(mutation_name): + class_name = mutation_name + return make_resolver(mutation_name, class_name, 'show_config') + def make_config_file_resolver(mutation_name): if 'Save' in mutation_name: class_name = mutation_name.replace('Save', '', 1) @@ -78,3 +83,7 @@ def make_config_file_resolver(mutation_name): return make_resolver(mutation_name, class_name, 'load') else: raise Exception + +def make_show_resolver(mutation_name): + class_name = mutation_name + return make_resolver(mutation_name, class_name, 'show') diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 9e97a0d60..375b88cc5 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -9,6 +9,8 @@ type Query { directive @configure on FIELD_DEFINITION directive @configfile on FIELD_DEFINITION +directive @show on FIELD_DEFINITION +directive @showconfig on FIELD_DEFINITION type Mutation { CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure @@ -18,4 +20,6 @@ type Mutation { RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile + Show(data: ShowInput) : ShowResult @show + ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig } diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql new file mode 100644 index 000000000..c7709e48b --- /dev/null +++ b/src/services/api/graphql/graphql/schema/show.graphql @@ -0,0 +1,14 @@ +input ShowInput { + path: [String!]! +} + +type Show { + path: [String] + result: String +} + +type ShowResult { + data: Show + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql new file mode 100644 index 000000000..34afd2aa9 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/show_config.graphql @@ -0,0 +1,21 @@ +""" +Use 'scalar Generic' for show config output, to avoid attempts to +JSON-serialize in case of JSON output. +""" +scalar Generic + +input ShowConfigInput { + path: [String!]! + configFormat: String +} + +type ShowConfig { + path: [String] + result: Generic +} + +type ShowConfigResult { + data: ShowConfig + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py index b96cc1753..f8c072b39 100644 --- a/src/services/api/graphql/recipes/session.py +++ b/src/services/api/graphql/recipes/session.py @@ -1,6 +1,10 @@ +import json + from ariadne import convert_camel_case_to_snake + import vyos.defaults from vyos.config import Config +from vyos.configtree import ConfigTree from vyos.template import render class Session(object): @@ -43,6 +47,21 @@ class Session(object): session.delete(path) session.commit() + def show_config(self): + session = self._session + data = self._data + out = '' + + try: + out = session.show_config(data['path']) + if data.get('config_format', '') == 'json': + config_tree = vyos.configtree.ConfigTree(out) + out = json.loads(config_tree.to_json()) + except Exception as error: + raise error + + return out + def save(self): session = self._session data = self._data @@ -63,3 +82,15 @@ class Session(object): session.commit() except Exception as error: raise error + + def show(self): + session = self._session + data = self._data + out = '' + + try: + out = session.show(data['path']) + except Exception as error: + raise error + + return out diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service index 266bc0962..a674bf598 100644 --- a/src/systemd/tftpd@.service +++ b/src/systemd/tftpd@.service @@ -7,7 +7,7 @@ RequiresMountsFor=/run Type=forking #NotifyAccess=main EnvironmentFile=-/etc/default/tftpd%I -ExecStart=/usr/sbin/in.tftpd "$DAEMON_ARGS" +ExecStart=/bin/sh -c "${VRF_ARGS} /usr/sbin/in.tftpd ${DAEMON_ARGS}" Restart=on-failure [Install] diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client index d4d38315a..a0515951a 100755 --- a/src/utils/vyos-hostsd-client +++ b/src/utils/vyos-hostsd-client @@ -129,7 +129,8 @@ try: params = h.split(",") if len(params) < 2: raise ValueError("Malformed host entry") - entry['address'] = params[1] + # Address needs to be a list because of changes made in T2683 + entry['address'] = [params[1]] entry['aliases'] = params[2:] data[params[0]] = entry client.add_hosts({args.tag: data}) |