From fa8dcff5d2e18ded5310d3f86ea0dc1bf2795af8 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
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(-)

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 <maintainers@vyos.io>
+# Copyright 2021-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
@@ -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 <maintainers@vyos.io>
+# Copyright 2021-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
@@ -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 <maintainers@vyos.io>
+# Copyright 2021-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
@@ -13,15 +13,20 @@
 # 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 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