From 419f81a0c39740de0ff61ce25325ebea76c4a395 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Sun, 14 Nov 2021 19:03:25 -0600
Subject: graphql: T3993: add config file save/load

(cherry picked from commit 8915a19f7761253b7bdf6ca847069539ee33851d)
---
 src/services/api/graphql/README.graphql            | 24 +++++++++++
 src/services/api/graphql/bindings.py               |  4 +-
 src/services/api/graphql/graphql/directives.py     | 17 +++++++-
 src/services/api/graphql/graphql/mutations.py      | 49 ++++++++++++++++++++++
 .../api/graphql/graphql/schema/config_file.graphql | 27 ++++++++++++
 .../api/graphql/graphql/schema/schema.graphql      |  3 ++
 src/services/api/graphql/recipes/config_file.py    | 16 +++++++
 src/services/api/graphql/recipes/recipe.py         | 19 +++++++++
 8 files changed, 156 insertions(+), 3 deletions(-)
 create mode 100644 src/services/api/graphql/graphql/schema/config_file.graphql
 create mode 100644 src/services/api/graphql/recipes/config_file.py

(limited to 'src')

diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql
index 580c0eb7f..c91b70782 100644
--- a/src/services/api/graphql/README.graphql
+++ b/src/services/api/graphql/README.graphql
@@ -42,6 +42,30 @@ 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.
+
+mutation {
+  loadConfigFile(data: {fileName: "/home/vyos/config.boot"}) {
+    success
+    errors
+    data {
+      fileName
+    }
+  }
+}
+
+
 The GraphQL playground will be found at:
 
 https://{{ host_address }}/graphql
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
index 1403841b4..c123f68d8 100644
--- a/src/services/api/graphql/bindings.py
+++ b/src/services/api/graphql/bindings.py
@@ -1,6 +1,6 @@
 import vyos.defaults
 from . graphql.mutations import mutation
-from . graphql.directives import DataDirective
+from . graphql.directives import DataDirective, ConfigFileDirective
 
 from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
 
@@ -9,6 +9,6 @@ def generate_schema():
 
     type_defs = load_schema_from_path(api_schema_dir)
 
-    schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective})
+    schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective, "configfile": ConfigFileDirective})
 
     return schema
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index 651421c35..85d514de4 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_resolver
+from . mutations import make_resolver, make_config_file_resolver
 
 class DataDirective(SchemaDirectiveVisitor):
     """
@@ -15,3 +15,18 @@ class DataDirective(SchemaDirectiveVisitor):
         func = make_resolver(name)
         field.resolve = func
         return field
+
+class ConfigFileDirective(SchemaDirectiveVisitor):
+    """
+    Class providing implementation of 'configfile' 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)
+
+        func = make_config_file_resolver(name)
+        field.resolve = func
+        return field
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 98c665c9a..2eb0a0b4a 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -57,4 +57,53 @@ def make_resolver(mutation_name):
 
     return func_impl
 
+def make_config_file_resolver(mutation_name):
+    op = ''
+    if 'save' in mutation_name:
+        op = '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
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..3096cf743
--- /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/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
index 8a5e17962..70fe0d726 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -8,8 +8,11 @@ type Query {
 }
 
 directive @generate on FIELD_DEFINITION
+directive @configfile on FIELD_DEFINITION
 
 type Mutation {
     createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate
     createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate
+    saveConfigFile(data: saveConfigFileInput) : saveConfigFileResult @configfile
+    loadConfigFile(data: loadConfigFileInput) : loadConfigFileResult @configfile
 }
diff --git a/src/services/api/graphql/recipes/config_file.py b/src/services/api/graphql/recipes/config_file.py
new file mode 100644
index 000000000..850e5326e
--- /dev/null
+++ b/src/services/api/graphql/recipes/config_file.py
@@ -0,0 +1,16 @@
+
+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/recipe.py b/src/services/api/graphql/recipes/recipe.py
index 8fbb9e0bf..91d8bd67a 100644
--- a/src/services/api/graphql/recipes/recipe.py
+++ b/src/services/api/graphql/recipes/recipe.py
@@ -46,4 +46,23 @@ class Recipe(object):
         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