From fa8dcff5d2e18ded5310d3f86ea0dc1bf2795af8 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 24 Jul 2022 15:36:18 -0500 Subject: graphql: T4554: add resolver support for op-mode scripts --- src/services/api/graphql/bindings.py | 3 ++ src/services/api/graphql/graphql/directives.py | 20 ++++++- src/services/api/graphql/graphql/mutations.py | 12 ++++- src/services/api/graphql/graphql/queries.py | 12 ++++- .../api/graphql/graphql/schema/schema.graphql | 2 + src/services/api/graphql/recipes/session.py | 61 ++++++++++++++++++++-- src/services/api/graphql/utils/util.py | 14 +++++ 7 files changed, 116 insertions(+), 8 deletions(-) (limited to 'src/services/api/graphql') diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 84d719fda..049d59de7 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -17,11 +17,14 @@ import vyos.defaults from . graphql.queries import query from . graphql.mutations import mutation from . graphql.directives import directives_dict +from . utils.schema_from_op_mode import generate_op_mode_definitions 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() + type_defs = load_schema_from_path(api_schema_dir) schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict) diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 551d28831..d8ceefae6 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -80,9 +80,27 @@ class ImageDirective(VyosDirective): super().visit_field_definition(field, object_type, make_resolver=make_image_resolver) +class GenOpQueryDirective(VyosDirective): + """ + Class providing implementation of 'genopquery' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_gen_op_query_resolver) + +class GenOpMutationDirective(VyosDirective): + """ + Class providing implementation of 'genopmutation' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_gen_op_mutation_resolver) + directives_dict = {"configure": ConfigureDirective, "showconfig": ShowConfigDirective, "systemstatus": SystemStatusDirective, "configfile": ConfigFileDirective, "show": ShowDirective, - "image": ImageDirective} + "image": ImageDirective, + "genopquery": GenOpQueryDirective, + "genopmutation": GenOpMutationDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 93e046319..3e89fb239 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors +# Copyright 2021-2022 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -63,6 +63,10 @@ def make_mutation_resolver(mutation_name, class_name, session_func): "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'] + session = state.settings['app'].state.vyos_session # one may override the session functions with a local subclass @@ -71,7 +75,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func): klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass - # name based templates + # name based functions klass = type(class_name, (Session,), {}) k = klass(session, data) method = getattr(k, session_func) @@ -107,3 +111,7 @@ def make_config_file_resolver(mutation_name): def make_image_resolver(mutation_name): return make_prefix_resolver(mutation_name, prefix=['add', 'delete']) + +def make_gen_op_mutation_resolver(mutation_name): + class_name = mutation_name + return make_mutation_resolver(mutation_name, class_name, 'gen_op_mutation') diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index eeaa9e19c..f6544709e 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors +# Copyright 2021-2022 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -63,6 +63,10 @@ def make_query_resolver(query_name, class_name, session_func): "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'] + session = state.settings['app'].state.vyos_session # one may override the session functions with a local subclass @@ -71,7 +75,7 @@ def make_query_resolver(query_name, class_name, session_func): klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass - # name based templates + # name based functions klass = type(class_name, (Session,), {}) k = klass(session, data) method = getattr(k, session_func) @@ -101,3 +105,7 @@ def make_system_status_resolver(query_name): def make_show_resolver(query_name): class_name = query_name return make_query_resolver(query_name, class_name, 'show') + +def make_gen_op_query_resolver(query_name): + class_name = query_name + return make_query_resolver(query_name, class_name, 'gen_op_query') diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 8ae71f632..624be2620 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -9,6 +9,8 @@ directive @show on FIELD_DEFINITION directive @showconfig on FIELD_DEFINITION directive @systemstatus on FIELD_DEFINITION directive @image on FIELD_DEFINITION +directive @genopquery on FIELD_DEFINITION +directive @genopmutation on FIELD_DEFINITION scalar Generic diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py index c436de08a..6b580af01 100644 --- a/src/services/api/graphql/recipes/session.py +++ b/src/services/api/graphql/recipes/session.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors +# Copyright 2021-2022 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -13,15 +13,20 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . +import os 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.defaults import directories from vyos.template import render +from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name + +op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') + class Session: """ Wrapper for calling configsession functions based on GraphQL requests. @@ -33,6 +38,12 @@ class Session: self._data = data self._name = convert_camel_case_to_snake(type(self).__name__) + try: + with open(op_mode_include_file) as f: + self._op_mode_list = f.read() + except Exception: + self._op_mode_list = None + def configure(self): session = self._session data = self._data @@ -40,7 +51,7 @@ class Session: tmpl_file = f'{func_base_name}.tmpl' cmd_file = f'/tmp/{func_base_name}.cmds' - tmpl_dir = vyos.defaults.directories['api_templates'] + tmpl_dir = directories['api_templates'] try: render(cmd_file, tmpl_file, data, location=tmpl_dir) @@ -150,3 +161,47 @@ class Session: status['ram'] = system_status.get_system_ram_usage() return status + + def gen_op_query(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, basename) = split_compound_op_mode_name(name, op_mode_list) + if basename == '': + raise FileNotFoundError(f"No op-mode file basename in string '{name}'") + + mod = load_op_mode_as_module(f'{basename}.py') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(True, **data) + else: + res = func(True) + + return res + + def gen_op_mutation(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file name contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, basename) = split_compound_op_mode_name(name, op_mode_list) + if basename == '': + raise FileNotFoundError(f"No op-mode file basename in string '{name}'") + + mod = load_op_mode_as_module(f'{basename}.py') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(**data) + else: + res = func() + + return res diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/utils/util.py index bf30f673a..e3dea31bf 100644 --- a/src/services/api/graphql/utils/util.py +++ b/src/services/api/graphql/utils/util.py @@ -39,3 +39,17 @@ def is_show_function_name(name): if re.match(r"^show", name): return True return False + +def _nth_rsplit(delim: str, n: int, s: str): + groups = s.split(delim) + l = len(groups) + if n > l-1: + return ('', s) + return (delim.join(groups[:l-n]), delim.join(groups[l-n:])) + +def split_compound_op_mode_name(name: str, files: list): + for i in range(1, name.count('_') + 1): + pair = _nth_rsplit('_', i, name) + if pair[1] in files: + return pair + return (name, '') -- cgit v1.2.3