summaryrefslogtreecommitdiff
path: root/src/services/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/services/api')
-rw-r--r--src/services/api/graphql/__init__.py (renamed from src/services/api/graphql/recipes/__init__.py)0
-rw-r--r--src/services/api/graphql/bindings.py16
-rw-r--r--src/services/api/graphql/generate/composite_function.py11
-rw-r--r--src/services/api/graphql/generate/config_session_function.py28
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_composite.py165
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_config_session.py165
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_op_mode.py230
-rw-r--r--src/services/api/graphql/graphql/auth_token_mutation.py49
-rw-r--r--src/services/api/graphql/graphql/directives.py48
-rw-r--r--src/services/api/graphql/graphql/errors.py8
-rw-r--r--src/services/api/graphql/graphql/mutations.py95
-rw-r--r--src/services/api/graphql/graphql/queries.py87
-rw-r--r--src/services/api/graphql/graphql/schema/auth_token.graphql19
-rw-r--r--src/services/api/graphql/graphql/schema/config_file.graphql27
-rw-r--r--src/services/api/graphql/graphql/schema/dhcp_server.graphql35
-rw-r--r--src/services/api/graphql/graphql/schema/firewall_group.graphql95
-rw-r--r--src/services/api/graphql/graphql/schema/image.graphql29
-rw-r--r--src/services/api/graphql/graphql/schema/interface_ethernet.graphql18
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql32
-rw-r--r--src/services/api/graphql/graphql/schema/show.graphql14
-rw-r--r--src/services/api/graphql/graphql/schema/show_config.graphql21
-rw-r--r--src/services/api/graphql/libs/key_auth.py18
-rw-r--r--src/services/api/graphql/libs/op_mode.py101
-rw-r--r--src/services/api/graphql/libs/token_auth.py71
-rw-r--r--src/services/api/graphql/session/__init__.py0
-rwxr-xr-xsrc/services/api/graphql/session/composite/system_status.py38
-rw-r--r--src/services/api/graphql/session/errors/op_mode_errors.py15
-rw-r--r--src/services/api/graphql/session/override/remove_firewall_address_group_members.py (renamed from src/services/api/graphql/recipes/remove_firewall_address_group_members.py)0
-rw-r--r--src/services/api/graphql/session/session.py (renamed from src/services/api/graphql/recipes/session.py)117
-rw-r--r--src/services/api/graphql/session/templates/create_dhcp_server.tmpl (renamed from src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl)0
-rw-r--r--src/services/api/graphql/session/templates/create_firewall_address_group.tmpl (renamed from src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl)0
-rw-r--r--src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl (renamed from src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl)0
-rw-r--r--src/services/api/graphql/session/templates/create_interface_ethernet.tmpl (renamed from src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl)0
-rw-r--r--src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl (renamed from src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl)0
-rw-r--r--src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl (renamed from src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl)0
-rw-r--r--src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl (renamed from src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl)0
-rw-r--r--src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl (renamed from src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl)0
37 files changed, 1183 insertions, 369 deletions
diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/__init__.py
index e69de29bb..e69de29bb 100644
--- a/src/services/api/graphql/recipes/__init__.py
+++ b/src/services/api/graphql/__init__.py
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
index 84d719fda..aa1ba0eb0 100644
--- a/src/services/api/graphql/bindings.py
+++ b/src/services/api/graphql/bindings.py
@@ -17,13 +17,27 @@ import vyos.defaults
from . graphql.queries import query
from . graphql.mutations import mutation
from . graphql.directives import directives_dict
+from . graphql.errors import op_mode_error
+from . graphql.auth_token_mutation import auth_token_mutation
+from . generate.schema_from_op_mode import generate_op_mode_definitions
+from . generate.schema_from_config_session import generate_config_session_definitions
+from . generate.schema_from_composite import generate_composite_definitions
+from . libs.token_auth import init_secret
+from . import state
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()
+ generate_config_session_definitions()
+ generate_composite_definitions()
+
+ if state.settings['app'].state.vyos_auth_type == 'token':
+ init_secret()
+
type_defs = load_schema_from_path(api_schema_dir)
- schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict)
+ schema = make_executable_schema(type_defs, query, op_mode_error, mutation, auth_token_mutation, snake_case_fallback_resolvers, directives=directives_dict)
return schema
diff --git a/src/services/api/graphql/generate/composite_function.py b/src/services/api/graphql/generate/composite_function.py
new file mode 100644
index 000000000..bc9d80fbb
--- /dev/null
+++ b/src/services/api/graphql/generate/composite_function.py
@@ -0,0 +1,11 @@
+# typing information for composite functions: those that invoke several
+# elementary requests, and return the result as a single dict
+import typing
+
+def system_status():
+ pass
+
+queries = {'system_status': system_status}
+
+mutations = {}
+
diff --git a/src/services/api/graphql/generate/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py
new file mode 100644
index 000000000..fc0dd7a87
--- /dev/null
+++ b/src/services/api/graphql/generate/config_session_function.py
@@ -0,0 +1,28 @@
+# typing information for native configsession functions; used to generate
+# schema definition files
+import typing
+
+def show_config(path: list[str], configFormat: typing.Optional[str]):
+ pass
+
+def show(path: list[str]):
+ pass
+
+queries = {'show_config': show_config,
+ 'show': show}
+
+def save_config_file(fileName: typing.Optional[str]):
+ pass
+def load_config_file(fileName: str):
+ pass
+def add_system_image(location: str):
+ pass
+def delete_system_image(name: str):
+ pass
+
+mutations = {'save_config_file': save_config_file,
+ 'load_config_file': load_config_file,
+ 'add_system_image': add_system_image,
+ 'delete_system_image': delete_system_image}
+
+
diff --git a/src/services/api/graphql/generate/schema_from_composite.py b/src/services/api/graphql/generate/schema_from_composite.py
new file mode 100755
index 000000000..61a08cb2f
--- /dev/null
+++ b/src/services/api/graphql/generate/schema_from_composite.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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/>.
+#
+#
+# A utility to generate GraphQL schema defintions from typing information of
+# composite functions comprising several requests.
+
+import os
+import sys
+import json
+from inspect import signature, getmembers, isfunction, isclass, getmro
+from jinja2 import Template
+
+from vyos.defaults import directories
+if __package__ is None or __package__ == '':
+ sys.path.append("/usr/libexec/vyos/services/api")
+ from graphql.libs.op_mode import snake_to_pascal_case, map_type_name
+ from composite_function import queries, mutations
+ from vyos.config import Config
+ from vyos.configdict import dict_merge
+ from vyos.xml import defaults
+else:
+ from .. libs.op_mode import snake_to_pascal_case, map_type_name
+ from . composite_function import queries, mutations
+ from .. import state
+
+SCHEMA_PATH = directories['api_schema']
+
+if __package__ is None or __package__ == '':
+ # allow running stand-alone
+ conf = Config()
+ base = ['service', 'https', 'api']
+ graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+ if 'graphql' not in graphql_dict:
+ exit("graphql is not configured")
+
+ graphql_dict = dict_merge(defaults(base), graphql_dict)
+ auth_type = graphql_dict['graphql']['authentication']['type']
+else:
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+schema_data: dict = {'auth_type': auth_type,
+ 'schema_name': '',
+ 'schema_fields': []}
+
+query_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositequery
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @compositequery
+{%- endif %}
+}
+"""
+
+mutation_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositemutation
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @compositemutation
+{%- endif %}
+}
+"""
+
+def create_schema(func_name: str, func: callable, template: str) -> str:
+ sig = signature(func)
+
+ field_dict = {}
+ for k in sig.parameters:
+ field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation)
+
+ schema_fields = []
+ for k,v in field_dict.items():
+ schema_fields.append(k+': '+v)
+
+ schema_data['schema_name'] = snake_to_pascal_case(func_name)
+ schema_data['schema_fields'] = schema_fields
+
+ j2_template = Template(template)
+ res = j2_template.render(schema_data)
+
+ return res
+
+def generate_composite_definitions():
+ results = []
+ for name,func in queries.items():
+ res = create_schema(name, func, query_template)
+ results.append(res)
+
+ for name,func in mutations.items():
+ res = create_schema(name, func, mutation_template)
+ results.append(res)
+
+ out = '\n'.join(results)
+ with open(f'{SCHEMA_PATH}/composite.graphql', 'w') as f:
+ f.write(out)
+
+if __name__ == '__main__':
+ generate_composite_definitions()
diff --git a/src/services/api/graphql/generate/schema_from_config_session.py b/src/services/api/graphql/generate/schema_from_config_session.py
new file mode 100755
index 000000000..49bf2440e
--- /dev/null
+++ b/src/services/api/graphql/generate/schema_from_config_session.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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/>.
+#
+#
+# A utility to generate GraphQL schema defintions from typing information of
+# (wrappers of) native configsession functions.
+
+import os
+import sys
+import json
+from inspect import signature, getmembers, isfunction, isclass, getmro
+from jinja2 import Template
+
+from vyos.defaults import directories
+if __package__ is None or __package__ == '':
+ sys.path.append("/usr/libexec/vyos/services/api")
+ from graphql.libs.op_mode import snake_to_pascal_case, map_type_name
+ from config_session_function import queries, mutations
+ from vyos.config import Config
+ from vyos.configdict import dict_merge
+ from vyos.xml import defaults
+else:
+ from .. libs.op_mode import snake_to_pascal_case, map_type_name
+ from . config_session_function import queries, mutations
+ from .. import state
+
+SCHEMA_PATH = directories['api_schema']
+
+if __package__ is None or __package__ == '':
+ # allow running stand-alone
+ conf = Config()
+ base = ['service', 'https', 'api']
+ graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+ if 'graphql' not in graphql_dict:
+ exit("graphql is not configured")
+
+ graphql_dict = dict_merge(defaults(base), graphql_dict)
+ auth_type = graphql_dict['graphql']['authentication']['type']
+else:
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+schema_data: dict = {'auth_type': auth_type,
+ 'schema_name': '',
+ 'schema_fields': []}
+
+query_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionquery
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @configsessionquery
+{%- endif %}
+}
+"""
+
+mutation_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionmutation
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @configsessionmutation
+{%- endif %}
+}
+"""
+
+def create_schema(func_name: str, func: callable, template: str) -> str:
+ sig = signature(func)
+
+ field_dict = {}
+ for k in sig.parameters:
+ field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation)
+
+ schema_fields = []
+ for k,v in field_dict.items():
+ schema_fields.append(k+': '+v)
+
+ schema_data['schema_name'] = snake_to_pascal_case(func_name)
+ schema_data['schema_fields'] = schema_fields
+
+ j2_template = Template(template)
+ res = j2_template.render(schema_data)
+
+ return res
+
+def generate_config_session_definitions():
+ results = []
+ for name,func in queries.items():
+ res = create_schema(name, func, query_template)
+ results.append(res)
+
+ for name,func in mutations.items():
+ res = create_schema(name, func, mutation_template)
+ results.append(res)
+
+ out = '\n'.join(results)
+ with open(f'{SCHEMA_PATH}/configsession.graphql', 'w') as f:
+ f.write(out)
+
+if __name__ == '__main__':
+ generate_config_session_definitions()
diff --git a/src/services/api/graphql/generate/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py
new file mode 100755
index 000000000..fc63b0100
--- /dev/null
+++ b/src/services/api/graphql/generate/schema_from_op_mode.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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/>.
+#
+#
+# A utility to generate GraphQL schema defintions from standardized op-mode
+# scripts.
+
+import os
+import sys
+import json
+from inspect import signature, getmembers, isfunction, isclass, getmro
+from jinja2 import Template
+
+from vyos.defaults import directories
+from vyos.util import load_as_module
+if __package__ is None or __package__ == '':
+ sys.path.append("/usr/libexec/vyos/services/api")
+ from graphql.libs.op_mode import is_op_mode_function_name, is_show_function_name
+ from graphql.libs.op_mode import snake_to_pascal_case, map_type_name
+ from vyos.config import Config
+ from vyos.configdict import dict_merge
+ from vyos.xml import defaults
+else:
+ from .. libs.op_mode import is_op_mode_function_name, is_show_function_name
+ from .. libs.op_mode import snake_to_pascal_case, map_type_name
+ from .. import state
+
+OP_MODE_PATH = directories['op_mode']
+SCHEMA_PATH = directories['api_schema']
+DATA_DIR = directories['data']
+
+op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json')
+op_mode_error_schema = 'op_mode_error.graphql'
+
+if __package__ is None or __package__ == '':
+ # allow running stand-alone
+ conf = Config()
+ base = ['service', 'https', 'api']
+ graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+ if 'graphql' not in graphql_dict:
+ exit("graphql is not configured")
+
+ graphql_dict = dict_merge(defaults(base), graphql_dict)
+ auth_type = graphql_dict['graphql']['authentication']['type']
+else:
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+schema_data: dict = {'auth_type': auth_type,
+ 'schema_name': '',
+ 'schema_fields': []}
+
+query_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ op_mode_error: OpModeError
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @genopquery
+{%- endif %}
+}
+"""
+
+mutation_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ op_mode_error: OpModeError
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @genopquery
+{%- endif %}
+}
+"""
+
+error_template = """
+interface OpModeError {
+ name: String!
+ message: String!
+ vyos_code: Int!
+}
+{% for name in error_names %}
+type {{ name }} implements OpModeError {
+ name: String!
+ message: String!
+ vyos_code: Int!
+}
+{%- endfor %}
+"""
+
+def create_schema(func_name: str, base_name: str, func: callable) -> str:
+ sig = signature(func)
+
+ field_dict = {}
+ for k in sig.parameters:
+ field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation)
+
+ # It is assumed that if one is generating a schema for a 'show_*'
+ # function, that 'get_raw_data' is present and 'raw' is desired.
+ if 'raw' in list(field_dict):
+ del field_dict['raw']
+
+ schema_fields = []
+ for k,v in field_dict.items():
+ schema_fields.append(k+': '+v)
+
+ schema_data['schema_name'] = snake_to_pascal_case(func_name + '_' + base_name)
+ schema_data['schema_fields'] = schema_fields
+
+ if is_show_function_name(func_name):
+ j2_template = Template(query_template)
+ else:
+ j2_template = Template(mutation_template)
+
+ res = j2_template.render(schema_data)
+
+ return res
+
+def create_error_schema():
+ from vyos import opmode
+
+ e = Exception
+ err_types = getmembers(opmode, isclass)
+ err_types = [k for k in err_types if issubclass(k[1], e)]
+ # drop base class, to be replaced by interface type. Find the class
+ # programmatically, in case the base class name changes.
+ for i in range(len(err_types)):
+ if err_types[i][1] in getmro(err_types[i-1][1]):
+ del err_types[i]
+ break
+ err_names = [k[0] for k in err_types]
+ error_data = {'error_names': err_names}
+ j2_template = Template(error_template)
+ res = j2_template.render(error_data)
+
+ return res
+
+def generate_op_mode_definitions():
+ out = create_error_schema()
+ with open(f'{SCHEMA_PATH}/{op_mode_error_schema}', 'w') as f:
+ f.write(out)
+
+ with open(op_mode_include_file) as f:
+ op_mode_files = json.load(f)
+
+ for file in op_mode_files:
+ basename = os.path.splitext(file)[0].replace('-', '_')
+ module = load_as_module(basename, os.path.join(OP_MODE_PATH, file))
+
+ funcs = getmembers(module, isfunction)
+ funcs = list(filter(lambda ft: is_op_mode_function_name(ft[0]), funcs))
+
+ funcs_dict = {}
+ for (name, thunk) in funcs:
+ funcs_dict[name] = thunk
+
+ results = []
+ for name,func in funcs_dict.items():
+ res = create_schema(name, basename, func)
+ results.append(res)
+
+ out = '\n'.join(results)
+ with open(f'{SCHEMA_PATH}/{basename}.graphql', 'w') as f:
+ f.write(out)
+
+if __name__ == '__main__':
+ generate_op_mode_definitions()
diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py
new file mode 100644
index 000000000..21ac40094
--- /dev/null
+++ b/src/services/api/graphql/graphql/auth_token_mutation.py
@@ -0,0 +1,49 @@
+# Copyright 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# 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 jwt
+import datetime
+from typing import Any, Dict
+from ariadne import ObjectType, UnionType
+from graphql import GraphQLResolveInfo
+
+from .. libs.token_auth import generate_token
+from .. import state
+
+auth_token_mutation = ObjectType("Mutation")
+
+@auth_token_mutation.field('AuthToken')
+def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict):
+ # non-nullable fields
+ user = data['username']
+ passwd = data['password']
+
+ secret = state.settings['secret']
+ exp_interval = int(state.settings['app'].state.vyos_token_exp)
+ expiration = (datetime.datetime.now(tz=datetime.timezone.utc) +
+ datetime.timedelta(seconds=exp_interval))
+
+ res = generate_token(user, passwd, secret, expiration)
+ if res:
+ data['result'] = res
+ return {
+ "success": True,
+ "data": data
+ }
+
+ return {
+ "success": False,
+ "errors": ['token generation failed']
+ }
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index 0a9298f55..a7919854a 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -31,49 +31,57 @@ class VyosDirective(SchemaDirectiveVisitor):
field.resolve = func
return field
+class ConfigSessionQueryDirective(VyosDirective):
+ """
+ Class providing implementation of 'configsessionquery' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_config_session_query_resolver)
-class ConfigureDirective(VyosDirective):
+class ConfigSessionMutationDirective(VyosDirective):
"""
- Class providing implementation of 'configure' directive in schema.
+ Class providing implementation of 'configsessionmutation' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
- make_resolver=make_configure_resolver)
+ make_resolver=make_config_session_mutation_resolver)
-class ShowConfigDirective(VyosDirective):
+class GenOpQueryDirective(VyosDirective):
"""
- Class providing implementation of 'show' directive in schema.
+ 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_show_config_resolver)
+ make_resolver=make_gen_op_query_resolver)
-class ConfigFileDirective(VyosDirective):
+class GenOpMutationDirective(VyosDirective):
"""
- Class providing implementation of 'configfile' directive in schema.
+ 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_config_file_resolver)
+ make_resolver=make_gen_op_mutation_resolver)
-class ShowDirective(VyosDirective):
+class CompositeQueryDirective(VyosDirective):
"""
- Class providing implementation of 'show' directive in schema.
+ Class providing implementation of 'system_status' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
- make_resolver=make_show_resolver)
+ make_resolver=make_composite_query_resolver)
-class ImageDirective(VyosDirective):
+class CompositeMutationDirective(VyosDirective):
"""
- Class providing implementation of 'image' directive in schema.
+ Class providing implementation of 'system_status' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
- make_resolver=make_image_resolver)
+ make_resolver=make_composite_mutation_resolver)
-directives_dict = {"configure": ConfigureDirective,
- "showconfig": ShowConfigDirective,
- "configfile": ConfigFileDirective,
- "show": ShowDirective,
- "image": ImageDirective}
+directives_dict = {"configsessionquery": ConfigSessionQueryDirective,
+ "configsessionmutation": ConfigSessionMutationDirective,
+ "genopquery": GenOpQueryDirective,
+ "genopmutation": GenOpMutationDirective,
+ "compositequery": CompositeQueryDirective,
+ "compositemutation": CompositeMutationDirective}
diff --git a/src/services/api/graphql/graphql/errors.py b/src/services/api/graphql/graphql/errors.py
new file mode 100644
index 000000000..1066300e0
--- /dev/null
+++ b/src/services/api/graphql/graphql/errors.py
@@ -0,0 +1,8 @@
+
+from ariadne import InterfaceType
+
+op_mode_error = InterfaceType("OpModeError")
+
+@op_mode_error.type_resolver
+def resolve_op_mode_error(obj, *_):
+ return obj['name']
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 0c3eb702a..87ea59c43 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
@@ -14,13 +14,16 @@
# along with this library. If not, see <http://www.gnu.org/licenses/>.
from importlib import import_module
-from typing import Any, Dict
+from typing import Any, Dict, Optional
from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
-from api.graphql.recipes.session import Session
+from .. libs import key_auth
+from api.graphql.session.session import Session
+from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
+from vyos.opmode import Error as OpModeError
mutation = ObjectType("Mutation")
@@ -39,29 +42,62 @@ def make_mutation_resolver(mutation_name, class_name, session_func):
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)'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)'
@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']
- }
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+ if auth_type == 'key':
+ data = kwargs['data']
+ key = data['key']
+
+ auth = key_auth.auth_required(key)
+ if auth is None:
+ return {
+ "success": False,
+ "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']
+
+ elif auth_type == 'token':
+ data = kwargs['data']
+ if data is None:
+ data = {}
+ info = kwargs['info']
+ user = info.context.get('user')
+ if user is None:
+ error = info.context.get('error')
+ if error is not None:
+ return {
+ "success": False,
+ "errors": [error]
+ }
+ return {
+ "success": False,
+ "errors": ['not authenticated']
+ }
+ else:
+ # AtrributeError will have already been raised if no
+ # vyos_auth_type; validation and defaultValue ensure it is
+ # one of the previous cases, so this is never reached.
+ pass
- data = kwargs['data']
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
try:
- mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ mod = import_module(f'api.graphql.session.override.{func_base_name}')
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)
@@ -72,28 +108,31 @@ def make_mutation_resolver(mutation_name, class_name, session_func):
"success": True,
"data": data
}
+ except OpModeError as e:
+ typename = type(e).__name__
+ msg = str(e)
+ return {
+ "success": False,
+ "errore": ['op_mode_error'],
+ "op_mode_error": {"name": f"{typename}",
+ "message": msg if msg else op_mode_err_msg.get(typename, "Unknown"),
+ "vyos_code": op_mode_err_code.get(typename, 9999)}
+ }
except Exception as error:
return {
"success": False,
- "errors": [str(error)]
+ "errors": [repr(error)]
}
return func_impl
-def make_prefix_resolver(mutation_name, prefix=[]):
- for pre in prefix:
- Pre = pre.capitalize()
- if Pre in mutation_name:
- class_name = mutation_name.replace(Pre, '', 1)
- return make_mutation_resolver(mutation_name, class_name, pre)
- raise Exception
-
-def make_configure_resolver(mutation_name):
- class_name = mutation_name
- return make_mutation_resolver(mutation_name, class_name, 'configure')
+def make_config_session_mutation_resolver(mutation_name):
+ return make_mutation_resolver(mutation_name, mutation_name,
+ convert_camel_case_to_snake(mutation_name))
-def make_config_file_resolver(mutation_name):
- return make_prefix_resolver(mutation_name, prefix=['save', 'load'])
+def make_gen_op_mutation_resolver(mutation_name):
+ return make_mutation_resolver(mutation_name, mutation_name, 'gen_op_mutation')
-def make_image_resolver(mutation_name):
- return make_prefix_resolver(mutation_name, prefix=['add', 'delete'])
+def make_composite_mutation_resolver(mutation_name):
+ return make_mutation_resolver(mutation_name, mutation_name,
+ convert_camel_case_to_snake(mutation_name))
diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py
index e1868091e..1ad586428 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
@@ -14,13 +14,16 @@
# along with this library. If not, see <http://www.gnu.org/licenses/>.
from importlib import import_module
-from typing import Any, Dict
+from typing import Any, Dict, Optional
from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
-from api.graphql.recipes.session import Session
+from .. libs import key_auth
+from api.graphql.session.session import Session
+from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
+from vyos.opmode import Error as OpModeError
query = ObjectType("Query")
@@ -39,29 +42,62 @@ def make_query_resolver(query_name, class_name, session_func):
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)'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)'
@query.field(query_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']
- }
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+ if auth_type == 'key':
+ data = kwargs['data']
+ key = data['key']
+
+ auth = key_auth.auth_required(key)
+ if auth is None:
+ return {
+ "success": False,
+ "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']
+
+ elif auth_type == 'token':
+ data = kwargs['data']
+ if data is None:
+ data = {}
+ info = kwargs['info']
+ user = info.context.get('user')
+ if user is None:
+ error = info.context.get('error')
+ if error is not None:
+ return {
+ "success": False,
+ "errors": [error]
+ }
+ return {
+ "success": False,
+ "errors": ['not authenticated']
+ }
+ else:
+ # AtrributeError will have already been raised if no
+ # vyos_auth_type; validation and defaultValue ensure it is
+ # one of the previous cases, so this is never reached.
+ pass
- data = kwargs['data']
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
try:
- mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ mod = import_module(f'api.graphql.session.override.{func_base_name}')
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)
@@ -72,18 +108,31 @@ def make_query_resolver(query_name, class_name, session_func):
"success": True,
"data": data
}
+ except OpModeError as e:
+ typename = type(e).__name__
+ msg = str(e)
+ return {
+ "success": False,
+ "errors": ['op_mode_error'],
+ "op_mode_error": {"name": f"{typename}",
+ "message": msg if msg else op_mode_err_msg.get(typename, "Unknown"),
+ "vyos_code": op_mode_err_code.get(typename, 9999)}
+ }
except Exception as error:
return {
"success": False,
- "errors": [str(error)]
+ "errors": [repr(error)]
}
return func_impl
-def make_show_config_resolver(query_name):
- class_name = query_name
- return make_query_resolver(query_name, class_name, 'show_config')
+def make_config_session_query_resolver(query_name):
+ return make_query_resolver(query_name, query_name,
+ convert_camel_case_to_snake(query_name))
+
+def make_gen_op_query_resolver(query_name):
+ return make_query_resolver(query_name, query_name, 'gen_op_query')
-def make_show_resolver(query_name):
- class_name = query_name
- return make_query_resolver(query_name, class_name, 'show')
+def make_composite_query_resolver(query_name):
+ return make_query_resolver(query_name, query_name,
+ convert_camel_case_to_snake(query_name))
diff --git a/src/services/api/graphql/graphql/schema/auth_token.graphql b/src/services/api/graphql/graphql/schema/auth_token.graphql
new file mode 100644
index 000000000..af53a293a
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/auth_token.graphql
@@ -0,0 +1,19 @@
+
+input AuthTokenInput {
+ username: String!
+ password: String!
+}
+
+type AuthToken {
+ result: Generic
+}
+
+type AuthTokenResult {
+ data: AuthToken
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ AuthToken(data: AuthTokenInput) : AuthTokenResult
+}
diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql
deleted file mode 100644
index 31ab26b9e..000000000
--- a/src/services/api/graphql/graphql/schema/config_file.graphql
+++ /dev/null
@@ -1,27 +0,0 @@
-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/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
deleted file mode 100644
index 25f091bfa..000000000
--- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql
+++ /dev/null
@@ -1,35 +0,0 @@
-input DhcpServerConfigInput {
- sharedNetworkName: String
- subnet: String
- defaultRouter: String
- nameServer: String
- domainName: String
- lease: Int
- range: Int
- start: String
- stop: String
- dnsForwardingAllowFrom: String
- dnsForwardingCacheSize: Int
- dnsForwardingListenAddress: String
-}
-
-type DhcpServerConfig {
- sharedNetworkName: String
- subnet: String
- defaultRouter: String
- nameServer: String
- domainName: String
- lease: Int
- range: Int
- start: String
- stop: String
- dnsForwardingAllowFrom: String
- dnsForwardingCacheSize: Int
- dnsForwardingListenAddress: String
-}
-
-type CreateDhcpServerResult {
- data: DhcpServerConfig
- success: Boolean!
- errors: [String]
-}
diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql
deleted file mode 100644
index d89904b9e..000000000
--- a/src/services/api/graphql/graphql/schema/firewall_group.graphql
+++ /dev/null
@@ -1,95 +0,0 @@
-input CreateFirewallAddressGroupInput {
- name: String!
- address: [String]
-}
-
-type CreateFirewallAddressGroup {
- name: String!
- address: [String]
-}
-
-type CreateFirewallAddressGroupResult {
- data: CreateFirewallAddressGroup
- success: Boolean!
- errors: [String]
-}
-
-input UpdateFirewallAddressGroupMembersInput {
- name: String!
- address: [String!]!
-}
-
-type UpdateFirewallAddressGroupMembers {
- name: String!
- address: [String!]!
-}
-
-type UpdateFirewallAddressGroupMembersResult {
- data: UpdateFirewallAddressGroupMembers
- success: Boolean!
- errors: [String]
-}
-
-input RemoveFirewallAddressGroupMembersInput {
- name: String!
- address: [String!]!
-}
-
-type RemoveFirewallAddressGroupMembers {
- name: String!
- address: [String!]!
-}
-
-type RemoveFirewallAddressGroupMembersResult {
- data: RemoveFirewallAddressGroupMembers
- success: Boolean!
- errors: [String]
-}
-
-input CreateFirewallAddressIpv6GroupInput {
- name: String!
- address: [String]
-}
-
-type CreateFirewallAddressIpv6Group {
- name: String!
- address: [String]
-}
-
-type CreateFirewallAddressIpv6GroupResult {
- data: CreateFirewallAddressIpv6Group
- success: Boolean!
- errors: [String]
-}
-
-input UpdateFirewallAddressIpv6GroupMembersInput {
- name: String!
- address: [String!]!
-}
-
-type UpdateFirewallAddressIpv6GroupMembers {
- name: String!
- address: [String!]!
-}
-
-type UpdateFirewallAddressIpv6GroupMembersResult {
- data: UpdateFirewallAddressIpv6GroupMembers
- success: Boolean!
- errors: [String]
-}
-
-input RemoveFirewallAddressIpv6GroupMembersInput {
- name: String!
- address: [String!]!
-}
-
-type RemoveFirewallAddressIpv6GroupMembers {
- name: String!
- address: [String!]!
-}
-
-type RemoveFirewallAddressIpv6GroupMembersResult {
- data: RemoveFirewallAddressIpv6GroupMembers
- success: Boolean!
- errors: [String]
-}
diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql
deleted file mode 100644
index 7d1b4f9d0..000000000
--- a/src/services/api/graphql/graphql/schema/image.graphql
+++ /dev/null
@@ -1,29 +0,0 @@
-input AddSystemImageInput {
- location: String!
-}
-
-type AddSystemImage {
- location: String
- result: String
-}
-
-type AddSystemImageResult {
- data: AddSystemImage
- success: Boolean!
- errors: [String]
-}
-
-input DeleteSystemImageInput {
- name: String!
-}
-
-type DeleteSystemImage {
- name: String
- result: String
-}
-
-type DeleteSystemImageResult {
- data: DeleteSystemImage
- success: Boolean!
- errors: [String]
-}
diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
deleted file mode 100644
index 32438b315..000000000
--- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-input InterfaceEthernetConfigInput {
- interface: String
- address: String
- replace: Boolean = true
- description: String
-}
-
-type InterfaceEthernetConfig {
- interface: String
- address: String
- description: String
-}
-
-type CreateInterfaceEthernetResult {
- data: InterfaceEthernetConfig
- 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 952e46f34..62b0d30bb 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -3,28 +3,14 @@ schema {
mutation: Mutation
}
-directive @configure on FIELD_DEFINITION
-directive @configfile on FIELD_DEFINITION
-directive @show on FIELD_DEFINITION
-directive @showconfig on FIELD_DEFINITION
-directive @image on FIELD_DEFINITION
+directive @compositequery on FIELD_DEFINITION
+directive @compositemutation on FIELD_DEFINITION
+directive @configsessionquery on FIELD_DEFINITION
+directive @configsessionmutation on FIELD_DEFINITION
+directive @genopquery on FIELD_DEFINITION
+directive @genopmutation on FIELD_DEFINITION
-type Query {
- Show(data: ShowInput) : ShowResult @show
- ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig
-}
+scalar Generic
-type Mutation {
- CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure
- CreateInterfaceEthernet(data: InterfaceEthernetConfigInput) : CreateInterfaceEthernetResult @configure
- CreateFirewallAddressGroup(data: CreateFirewallAddressGroupInput) : CreateFirewallAddressGroupResult @configure
- UpdateFirewallAddressGroupMembers(data: UpdateFirewallAddressGroupMembersInput) : UpdateFirewallAddressGroupMembersResult @configure
- RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure
- CreateFirewallAddressIpv6Group(data: CreateFirewallAddressIpv6GroupInput) : CreateFirewallAddressIpv6GroupResult @configure
- UpdateFirewallAddressIpv6GroupMembers(data: UpdateFirewallAddressIpv6GroupMembersInput) : UpdateFirewallAddressIpv6GroupMembersResult @configure
- RemoveFirewallAddressIpv6GroupMembers(data: RemoveFirewallAddressIpv6GroupMembersInput) : RemoveFirewallAddressIpv6GroupMembersResult @configure
- SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile
- LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile
- AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @image
- DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @image
-}
+type Query
+type Mutation
diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql
deleted file mode 100644
index c7709e48b..000000000
--- a/src/services/api/graphql/graphql/schema/show.graphql
+++ /dev/null
@@ -1,14 +0,0 @@
-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
deleted file mode 100644
index 34afd2aa9..000000000
--- a/src/services/api/graphql/graphql/schema/show_config.graphql
+++ /dev/null
@@ -1,21 +0,0 @@
-"""
-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/libs/key_auth.py b/src/services/api/graphql/libs/key_auth.py
new file mode 100644
index 000000000..2db0f7d48
--- /dev/null
+++ b/src/services/api/graphql/libs/key_auth.py
@@ -0,0 +1,18 @@
+
+from .. import state
+
+def check_auth(key_list, key):
+ if not key_list:
+ return None
+ key_id = None
+ for k in key_list:
+ if k['key'] == key:
+ key_id = k['id']
+ return key_id
+
+def auth_required(key):
+ api_keys = None
+ api_keys = state.settings['app'].state.vyos_keys
+ key_id = check_auth(api_keys, key)
+ state.settings['app'].state.vyos_id = key_id
+ return key_id
diff --git a/src/services/api/graphql/libs/op_mode.py b/src/services/api/graphql/libs/op_mode.py
new file mode 100644
index 000000000..6939ed5d6
--- /dev/null
+++ b/src/services/api/graphql/libs/op_mode.py
@@ -0,0 +1,101 @@
+# Copyright 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# 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 re
+import typing
+import importlib.util
+from typing import Union
+from humps import decamelize
+
+from vyos.defaults import directories
+from vyos.util import load_as_module
+from vyos.opmode import _normalize_field_names
+
+def load_op_mode_as_module(name: str):
+ path = os.path.join(directories['op_mode'], name)
+ name = os.path.splitext(name)[0].replace('-', '_')
+ return load_as_module(name, path)
+
+def is_op_mode_function_name(name):
+ if re.match(r"^(show|clear|reset|restart)", name):
+ return True
+ return False
+
+def is_show_function_name(name):
+ if re.match(r"^show", name):
+ return True
+ return False
+
+def _nth_split(delim: str, n: int, s: str):
+ groups = s.split(delim)
+ l = len(groups)
+ if n > l-1 or n < 1:
+ return (s, '')
+ return (delim.join(groups[:n]), delim.join(groups[n:]))
+
+def _nth_rsplit(delim: str, n: int, s: str):
+ groups = s.split(delim)
+ l = len(groups)
+ if n > l-1 or n < 1:
+ return (s, '')
+ return (delim.join(groups[:l-n]), delim.join(groups[l-n:]))
+
+# Since we have mangled possible hyphens in the file name while constructing
+# the snake case of the query/mutation name, we will need to recover the
+# file name by searching with mangling:
+def _filter_on_mangled(test):
+ def func(elem):
+ mangle = os.path.splitext(elem)[0].replace('-', '_')
+ return test == mangle
+ return func
+
+# Find longest name in concatenated string that matches the basename of an
+# op-mode script. Should one prefer to concatenate in the reverse order
+# (script_name + '_' + function_name), use _nth_rsplit.
+def split_compound_op_mode_name(name: str, files: list):
+ for i in range(1, name.count('_') + 1):
+ pair = _nth_split('_', i, name)
+ f = list(filter(_filter_on_mangled(pair[1]), files))
+ if f:
+ pair = (pair[0], f[0])
+ return pair
+ return (name, '')
+
+def snake_to_pascal_case(name: str) -> str:
+ res = ''.join(map(str.title, name.split('_')))
+ return res
+
+def map_type_name(type_name: type, optional: bool = False) -> str:
+ if type_name == str:
+ return 'String!' if not optional else 'String = null'
+ if type_name == int:
+ return 'Int!' if not optional else 'Int = null'
+ if type_name == bool:
+ return 'Boolean!' if not optional else 'Boolean = false'
+ if typing.get_origin(type_name) == list:
+ if not optional:
+ return f'[{map_type_name(typing.get_args(type_name)[0])}]!'
+ return f'[{map_type_name(typing.get_args(type_name)[0])}]'
+ # typing.Optional is typing.Union[_, NoneType]
+ if (typing.get_origin(type_name) is typing.Union and
+ typing.get_args(type_name)[1] == type(None)):
+ return f'{map_type_name(typing.get_args(type_name)[0], optional=True)}'
+
+ # scalar 'Generic' is defined in schema.graphql
+ return 'Generic'
+
+def normalize_output(result: Union[dict, list]) -> Union[dict, list]:
+ return _normalize_field_names(decamelize(result))
diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py
new file mode 100644
index 000000000..2100eba7f
--- /dev/null
+++ b/src/services/api/graphql/libs/token_auth.py
@@ -0,0 +1,71 @@
+import jwt
+import uuid
+import pam
+from secrets import token_hex
+
+from .. import state
+
+def _check_passwd_pam(username: str, passwd: str) -> bool:
+ if pam.authenticate(username, passwd):
+ return True
+ return False
+
+def init_secret():
+ length = int(state.settings['app'].state.vyos_secret_len)
+ secret = token_hex(length)
+ state.settings['secret'] = secret
+
+def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict:
+ if user is None or passwd is None:
+ return {}
+ if _check_passwd_pam(user, passwd):
+ app = state.settings['app']
+ try:
+ users = app.state.vyos_token_users
+ except AttributeError:
+ app.state.vyos_token_users = {}
+ users = app.state.vyos_token_users
+ user_id = uuid.uuid1().hex
+ payload_data = {'iss': user, 'sub': user_id, 'exp': exp}
+ secret = state.settings.get('secret')
+ if secret is None:
+ return {
+ "success": False,
+ "errors": ['failed secret generation']
+ }
+ token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256")
+
+ users |= {user_id: user}
+ return {'token': token}
+
+def get_user_context(request):
+ context = {}
+ context['request'] = request
+ context['user'] = None
+ if 'Authorization' in request.headers:
+ auth = request.headers['Authorization']
+ scheme, token = auth.split()
+ if scheme.lower() != 'bearer':
+ return context
+
+ try:
+ secret = state.settings.get('secret')
+ payload = jwt.decode(token, secret, algorithms=["HS256"])
+ user_id: str = payload.get('sub')
+ if user_id is None:
+ return context
+ except jwt.exceptions.ExpiredSignatureError:
+ context['error'] = 'expired token'
+ return context
+ except jwt.PyJWTError:
+ return context
+ try:
+ users = state.settings['app'].state.vyos_token_users
+ except AttributeError:
+ return context
+
+ user = users.get(user_id)
+ if user is not None:
+ context['user'] = user
+
+ return context
diff --git a/src/services/api/graphql/session/__init__.py b/src/services/api/graphql/session/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/session/__init__.py
diff --git a/src/services/api/graphql/session/composite/system_status.py b/src/services/api/graphql/session/composite/system_status.py
new file mode 100755
index 000000000..d809f32e3
--- /dev/null
+++ b/src/services/api/graphql/session/composite/system_status.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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 os
+import sys
+import json
+import importlib.util
+
+from vyos.defaults import directories
+
+from api.graphql.libs.op_mode import load_op_mode_as_module
+
+def get_system_version() -> dict:
+ show_version = load_op_mode_as_module('version.py')
+ return show_version.show(raw=True, funny=False)
+
+def get_system_uptime() -> dict:
+ show_uptime = load_op_mode_as_module('uptime.py')
+ return show_uptime._get_raw_data()
+
+def get_system_ram_usage() -> dict:
+ show_ram = load_op_mode_as_module('memory.py')
+ return show_ram.show(raw=True)
diff --git a/src/services/api/graphql/session/errors/op_mode_errors.py b/src/services/api/graphql/session/errors/op_mode_errors.py
new file mode 100644
index 000000000..7bc1d1d81
--- /dev/null
+++ b/src/services/api/graphql/session/errors/op_mode_errors.py
@@ -0,0 +1,15 @@
+
+
+op_mode_err_msg = {
+ "UnconfiguredSubsystem": "subsystem is not configured or not running",
+ "DataUnavailable": "data currently unavailable",
+ "PermissionDenied": "client does not have permission",
+ "IncorrectValue": "argument value is incorrect"
+}
+
+op_mode_err_code = {
+ "UnconfiguredSubsystem": 2000,
+ "DataUnavailable": 2001,
+ "PermissionDenied": 1003,
+ "IncorrectValue": 1002
+}
diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py
index b91932e14..b91932e14 100644
--- a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
+++ b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py
diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/session/session.py
index 1f844ff70..0b77b1433 100644
--- a/src/services/api/graphql/recipes/session.py
+++ b/src/services/api/graphql/session/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,14 +13,21 @@
# 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 vyos.opmode import Error as OpModeError
+
+from api.graphql.libs.op_mode import load_op_mode_as_module, split_compound_op_mode_name
+from api.graphql.libs.op_mode import normalize_output
+
+op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json')
class Session:
"""
@@ -33,39 +40,11 @@ class Session:
self._data = data
self._name = convert_camel_case_to_snake(type(self).__name__)
- 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 delete_path_if_childless(self, path):
- session = self._session
- config = Config(session.get_session_env())
- if not config.list_nodes(path):
- session.delete(path)
- session.commit()
+ with open(op_mode_include_file) as f:
+ self._op_mode_list = json.loads(f.read())
+ except Exception:
+ self._op_mode_list = None
def show_config(self):
session = self._session
@@ -75,14 +54,14 @@ class Session:
try:
out = session.show_config(data['path'])
if data.get('config_format', '') == 'json':
- config_tree = vyos.configtree.ConfigTree(out)
+ config_tree = ConfigTree(out)
out = json.loads(config_tree.to_json())
except Exception as error:
raise error
return out
- def save(self):
+ def save_config_file(self):
session = self._session
data = self._data
if 'file_name' not in data or not data['file_name']:
@@ -93,7 +72,7 @@ class Session:
except Exception as error:
raise error
- def load(self):
+ def load_config_file(self):
session = self._session
data = self._data
@@ -115,7 +94,7 @@ class Session:
return out
- def add(self):
+ def add_system_image(self):
session = self._session
data = self._data
@@ -126,7 +105,7 @@ class Session:
return res
- def delete(self):
+ def delete_system_image(self):
session = self._session
data = self._data
@@ -136,3 +115,63 @@ class Session:
raise error
return res
+
+ def system_status(self):
+ import api.graphql.session.composite.system_status as system_status
+
+ session = self._session
+ data = self._data
+
+ status = {}
+ status['host_name'] = session.show(['host', 'name']).strip()
+ status['version'] = system_status.get_system_version()
+ status['uptime'] = system_status.get_system_uptime()
+ 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, scriptname) = split_compound_op_mode_name(name, op_mode_list)
+ if scriptname == '':
+ raise FileNotFoundError(f"No op-mode file named in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{scriptname}')
+ func = getattr(mod, func_name)
+ try:
+ res = func(True, **data)
+ except OpModeError as e:
+ raise e
+
+ res = normalize_output(res)
+
+ 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, scriptname) = split_compound_op_mode_name(name, op_mode_list)
+ if scriptname == '':
+ raise FileNotFoundError(f"No op-mode file named in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{scriptname}')
+ func = getattr(mod, func_name)
+ try:
+ res = func(**data)
+ except OpModeError as e:
+ raise e
+
+ return res
diff --git a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl
index 70de43183..70de43183 100644
--- a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl
+++ b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl
diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl
index a890d0086..a890d0086 100644
--- a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl
+++ b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl
diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl
index e9b660722..e9b660722 100644
--- a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl
+++ b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl
diff --git a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl
index d9d7ed691..d9d7ed691 100644
--- a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl
+++ b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl
diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl
index 458f3e5fc..458f3e5fc 100644
--- a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl
+++ b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl
diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl
index 0efa0b226..0efa0b226 100644
--- a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl
+++ b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl
diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl
index f56c61231..f56c61231 100644
--- a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl
+++ b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl
diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl
index f98a5517c..f98a5517c 100644
--- a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl
+++ b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl