From ef7f5ca2fd2c0113875dbd9143342e925cf00621 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 22 Nov 2021 15:29:46 -0600 Subject: graphql: T3993: refactor directive and mutation definitions --- src/services/api/graphql/README.graphql | 5 +- src/services/api/graphql/graphql/directives.py | 33 +++++----- src/services/api/graphql/graphql/mutations.py | 77 +++++++--------------- src/services/api/graphql/recipes/config_file.py | 16 ----- .../api/graphql/recipes/create_dhcp_server.py | 13 ---- .../graphql/recipes/create_interface_ethernet.py | 13 ---- src/services/api/graphql/recipes/recipe.py | 68 ------------------- src/services/api/graphql/recipes/session.py | 68 +++++++++++++++++++ 8 files changed, 111 insertions(+), 182 deletions(-) delete mode 100644 src/services/api/graphql/recipes/config_file.py delete mode 100644 src/services/api/graphql/recipes/create_dhcp_server.py delete mode 100644 src/services/api/graphql/recipes/create_interface_ethernet.py delete mode 100644 src/services/api/graphql/recipes/recipe.py create mode 100644 src/services/api/graphql/recipes/session.py diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index 6e9e16c6b..29f58f709 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -101,11 +101,8 @@ services │   │   └── schema.graphql │   ├── README.graphql │   ├── recipes -│   │   ├── config_file.py -│   │   ├── create_dhcp_server.py -│   │   ├── create_interface_ethernet.py │   │   ├── __init__.py -│   │   ├── recipe.py +│   │   ├── session.py │   │   └── templates │   │   ├── create_dhcp_server.tmpl │   │   └── create_interface_ethernet.tmpl diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index a7706610c..f5cd88acd 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,34 +1,37 @@ from ariadne import SchemaDirectiveVisitor, ObjectType from . mutations import make_configure_resolver, make_config_file_resolver -class ConfigureDirective(SchemaDirectiveVisitor): - """ - Class providing implementation of 'configure' 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 name = name.replace('Result', '', 1) - func = make_configure_resolver(name) + func = make_resolver(name) field.resolve = func return field -class ConfigFileDirective(SchemaDirectiveVisitor): + +class ConfigureDirective(VyosDirective): """ - Class providing implementation of 'configfile' directive in schema. + Class providing implementation of 'configure' directive in schema. """ def visit_field_definition(self, field, object_type): - name = f'{field.type}' - # field.type contains the return value of the mutation; trim value - # to produce canonical name - name = name.replace('Result', '', 1) + super().visit_field_definition(field, object_type, + make_resolver=make_configure_resolver) - func = make_config_file_resolver(name) - field.resolve = func - return field +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 02d5be0ef..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_configure_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,9 +20,9 @@ def make_configure_resolver(mutation_name): functools.wraps. :raise Exception: - encapsulating ConfigErrors, or internal errors + raising ConfigErrors, or internal errors """ - class_name = mutation_name + 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)' @@ -40,10 +41,17 @@ def make_configure_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,53 +65,16 @@ def make_configure_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): - op = '' if 'Save' in mutation_name: - op = 'save' + class_name = mutation_name.replace('Save', '', 1) + return make_resolver(mutation_name, class_name, 'save') elif 'Load' in mutation_name: - op = 'load' - - class_name = mutation_name.replace('Save', '', 1).replace('Load', '', 1) - 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)' - - @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'] - session = state.settings['app'].state.vyos_session - - mod = import_module(f'api.graphql.recipes.{func_base_name}') - klass = getattr(mod, class_name) - k = klass(session, data) - if op == 'save': - k.save() - elif op == 'load': - k.load() - else: - return { - "success": False, - "errors": ["Input must be saveConfigFile | loadConfigFile"] - } - - return { - "success": True, - "data": data - } - except Exception as error: - return { - "success": False, - "errors": [str(error)] - } - - return func_impl + 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/recipes/config_file.py b/src/services/api/graphql/recipes/config_file.py deleted file mode 100644 index 850e5326e..000000000 --- a/src/services/api/graphql/recipes/config_file.py +++ /dev/null @@ -1,16 +0,0 @@ - -from . recipe import Recipe - -class ConfigFile(Recipe): - def __init__(self, session, command_file): - super().__init__(session, command_file) - - # Define any custom processing of parameters here by overriding - # save/load: - # - # def save(self): - # self.data = transform_data(self.data) - # super().save() - # def load(self): - # self.data = transform_data(self.data) - # super().load() diff --git a/src/services/api/graphql/recipes/create_dhcp_server.py b/src/services/api/graphql/recipes/create_dhcp_server.py deleted file mode 100644 index ac6f15209..000000000 --- a/src/services/api/graphql/recipes/create_dhcp_server.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class CreateDhcpServer(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/create_interface_ethernet.py b/src/services/api/graphql/recipes/create_interface_ethernet.py deleted file mode 100644 index aafb4d55c..000000000 --- a/src/services/api/graphql/recipes/create_interface_ethernet.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class CreateInterfaceEthernet(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/recipe.py b/src/services/api/graphql/recipes/recipe.py deleted file mode 100644 index 91d8bd67a..000000000 --- a/src/services/api/graphql/recipes/recipe.py +++ /dev/null @@ -1,68 +0,0 @@ -from ariadne import convert_camel_case_to_snake -import vyos.defaults -from vyos.template import render - -class Recipe(object): - def __init__(self, session, data): - self._session = session - 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 - func_base_name = self._name - - tmpl_file = f'{func_base_name}.tmpl' - cmd_file = f'/tmp/{func_base_name}.cmds' - tmpl_dir = vyos.defaults.directories['api_templates'] - - try: - render(cmd_file, tmpl_file, data, location=tmpl_dir) - commands = [] - with open(cmd_file) as f: - lines = f.readlines() - for line in lines: - commands.append(line.split()) - for cmd in commands: - if cmd[0] == 'set': - session.set(cmd[1:]) - elif cmd[0] == 'delete': - session.delete(cmd[1:]) - else: - raise ValueError('Operation must be "set" or "delete"') - session.commit() - except Exception as error: - raise error - - 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/session.py b/src/services/api/graphql/recipes/session.py new file mode 100644 index 000000000..aa3932ab9 --- /dev/null +++ b/src/services/api/graphql/recipes/session.py @@ -0,0 +1,68 @@ +from ariadne import convert_camel_case_to_snake +import vyos.defaults +from vyos.template import render + +class Session(object): + def __init__(self, session, data): + self._session = session + 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 + func_base_name = self._name + + tmpl_file = f'{func_base_name}.tmpl' + cmd_file = f'/tmp/{func_base_name}.cmds' + tmpl_dir = vyos.defaults.directories['api_templates'] + + try: + render(cmd_file, tmpl_file, data, location=tmpl_dir) + commands = [] + with open(cmd_file) as f: + lines = f.readlines() + for line in lines: + commands.append(line.split()) + for cmd in commands: + if cmd[0] == 'set': + session.set(cmd[1:]) + elif cmd[0] == 'delete': + session.delete(cmd[1:]) + else: + raise ValueError('Operation must be "set" or "delete"') + session.commit() + except Exception as error: + raise error + + 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 -- cgit v1.2.3