Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F119063609
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
45 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index d8ceefae6..62579d326 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -1,106 +1,120 @@
# Copyright 2021 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/>.
from ariadne import SchemaDirectiveVisitor, ObjectType
from . queries import *
from . mutations import *
def non(arg):
pass
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_resolver(name)
field.resolve = func
return field
class ConfigureDirective(VyosDirective):
"""
Class providing implementation of 'configure' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
make_resolver=make_configure_resolver)
class ShowConfigDirective(VyosDirective):
"""
Class providing implementation of 'show' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
make_resolver=make_show_config_resolver)
class SystemStatusDirective(VyosDirective):
"""
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_system_status_resolver)
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)
class ShowDirective(VyosDirective):
"""
Class providing implementation of 'show' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
make_resolver=make_show_resolver)
class ImageDirective(VyosDirective):
"""
Class providing implementation of 'image' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
make_resolver=make_image_resolver)
+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 ConfigSessionMutationDirective(VyosDirective):
+ """
+ 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_config_session_mutation_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,
+ "configsessionquery": ConfigSessionQueryDirective,
+ "configsessionmutation": ConfigSessionMutationDirective,
"genopquery": GenOpQueryDirective,
"genopmutation": GenOpMutationDirective}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 5ccc9b0b6..84ee03eed 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -1,129 +1,133 @@
# 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
# 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/>.
from importlib import import_module
from typing import Any, Dict
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 .. 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")
def make_mutation_resolver(mutation_name, class_name, session_func):
"""Dynamically generate a resolver for the mutation named in the
schema by 'mutation_name'.
Dynamic generation is provided using the package 'makefun' (via the
decorator 'with_signature'), which provides signature-preserving
function wrappers; it provides several improvements over, say,
functools.wraps.
:raise Exception:
raising ConfigErrors, or internal errors
"""
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']
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']
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
try:
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 functions
klass = type(class_name, (Session,), {})
k = klass(session, data)
method = getattr(k, session_func)
result = method()
data['result'] = result
return {
"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": [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_file_resolver(mutation_name):
return make_prefix_resolver(mutation_name, prefix=['save', 'load'])
def make_image_resolver(mutation_name):
return make_prefix_resolver(mutation_name, prefix=['add', 'delete'])
+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_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 b46914dcc..126a0d3c4 100644
--- a/src/services/api/graphql/graphql/queries.py
+++ b/src/services/api/graphql/graphql/queries.py
@@ -1,123 +1,126 @@
# 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
# 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/>.
from importlib import import_module
from typing import Any, Dict
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 .. 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")
def make_query_resolver(query_name, class_name, session_func):
"""Dynamically generate a resolver for the query named in the
schema by 'query_name'.
Dynamic generation is provided using the package 'makefun' (via the
decorator 'with_signature'), which provides signature-preserving
function wrappers; it provides several improvements over, say,
functools.wraps.
:raise Exception:
raising ConfigErrors, or internal errors
"""
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)'
@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']
}
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']
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
try:
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 functions
klass = type(class_name, (Session,), {})
k = klass(session, data)
method = getattr(k, session_func)
result = method()
data['result'] = result
return {
"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": [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_system_status_resolver(query_name):
class_name = query_name
return make_query_resolver(query_name, class_name, 'system_status')
def make_show_resolver(query_name):
class_name = query_name
return make_query_resolver(query_name, class_name, 'show')
+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):
- class_name = query_name
- return make_query_resolver(query_name, class_name, 'gen_op_query')
+ return make_query_resolver(query_name, query_name, 'gen_op_query')
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 a7263114b..000000000
--- a/src/services/api/graphql/graphql/schema/config_file.graphql
+++ /dev/null
@@ -1,29 +0,0 @@
-input SaveConfigFileInput {
- key: String!
- fileName: String
-}
-
-type SaveConfigFile {
- fileName: String
-}
-
-type SaveConfigFileResult {
- data: SaveConfigFile
- success: Boolean!
- errors: [String]
-}
-
-input LoadConfigFileInput {
- key: String!
- fileName: String!
-}
-
-type LoadConfigFile {
- fileName: String!
-}
-
-type LoadConfigFileResult {
- data: LoadConfigFile
- success: Boolean!
- errors: [String]
-}
diff --git a/src/services/api/graphql/graphql/schema/configsession.graphql b/src/services/api/graphql/graphql/schema/configsession.graphql
new file mode 100644
index 000000000..b1deac4b3
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/configsession.graphql
@@ -0,0 +1,115 @@
+
+input ShowConfigInput {
+ key: String!
+ path: [String!]!
+ configFormat: String = null
+}
+
+type ShowConfig {
+ result: Generic
+}
+
+type ShowConfigResult {
+ data: ShowConfig
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+ ShowConfig(data: ShowConfigInput) : ShowConfigResult @configsessionquery
+}
+
+input ShowInput {
+ key: String!
+ path: [String!]!
+}
+
+type Show {
+ result: Generic
+}
+
+type ShowResult {
+ data: Show
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+ Show(data: ShowInput) : ShowResult @configsessionquery
+}
+
+input SaveConfigFileInput {
+ key: String!
+ fileName: String = null
+}
+
+type SaveConfigFile {
+ result: Generic
+}
+
+type SaveConfigFileResult {
+ data: SaveConfigFile
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configsessionmutation
+}
+
+input LoadConfigFileInput {
+ key: String!
+ fileName: String!
+}
+
+type LoadConfigFile {
+ result: Generic
+}
+
+type LoadConfigFileResult {
+ data: LoadConfigFile
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configsessionmutation
+}
+
+input AddSystemImageInput {
+ key: String!
+ location: String!
+}
+
+type AddSystemImage {
+ result: Generic
+}
+
+type AddSystemImageResult {
+ data: AddSystemImage
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @configsessionmutation
+}
+
+input DeleteSystemImageInput {
+ key: String!
+ name: String!
+}
+
+type DeleteSystemImage {
+ result: Generic
+}
+
+type DeleteSystemImageResult {
+ data: DeleteSystemImage
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @configsessionmutation
+}
\ No newline at end of file
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 485033875..000000000
--- a/src/services/api/graphql/graphql/schema/image.graphql
+++ /dev/null
@@ -1,31 +0,0 @@
-input AddSystemImageInput {
- key: String!
- location: String!
-}
-
-type AddSystemImage {
- location: String
- result: String
-}
-
-type AddSystemImageResult {
- data: AddSystemImage
- success: Boolean!
- errors: [String]
-}
-
-input DeleteSystemImageInput {
- key: String!
- 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/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
index 624be2620..fff7c43b0 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -1,36 +1,28 @@
schema {
query: Query
mutation: Mutation
}
directive @configure on FIELD_DEFINITION
-directive @configfile on FIELD_DEFINITION
-directive @show on FIELD_DEFINITION
-directive @showconfig on FIELD_DEFINITION
directive @systemstatus on FIELD_DEFINITION
-directive @image on FIELD_DEFINITION
+directive @configsessionquery on FIELD_DEFINITION
+directive @configsessionmutation on FIELD_DEFINITION
directive @genopquery on FIELD_DEFINITION
directive @genopmutation on FIELD_DEFINITION
scalar Generic
type Query {
- Show(data: ShowInput) : ShowResult @show
- ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig
SystemStatus(data: SystemStatusInput) : SystemStatusResult @systemstatus
}
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
}
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 278ed536b..000000000
--- a/src/services/api/graphql/graphql/schema/show.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-input ShowInput {
- key: String!
- 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 5a1fe43da..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.
-"""
-
-input ShowConfigInput {
- key: String!
- 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/session/session.py b/src/services/api/graphql/session/session.py
index f7510841e..61dd3ed1b 100644
--- a/src/services/api/graphql/session/session.py
+++ b/src/services/api/graphql/session/session.py
@@ -1,208 +1,208 @@
# 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
# 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 json
from ariadne import convert_camel_case_to_snake
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.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.
Non-nullable fields in the respective schema allow avoiding a key check
in 'data'.
"""
def __init__(self, session, data):
self._session = 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 = json.loads(f.read())
except Exception:
self._op_mode_list = None
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 = 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()
def show_config(self):
session = self._session
data = self._data
out = ''
try:
out = session.show_config(data['path'])
if data.get('config_format', '') == 'json':
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']:
data['file_name'] = '/config/config.boot'
try:
session.save_config(data['file_name'])
except Exception as error:
raise error
- def load(self):
+ def load_config_file(self):
session = self._session
data = self._data
try:
session.load_config(data['file_name'])
session.commit()
except Exception as error:
raise error
def show(self):
session = self._session
data = self._data
out = ''
try:
out = session.show(data['path'])
except Exception as error:
raise error
return out
- def add(self):
+ def add_system_image(self):
session = self._session
data = self._data
try:
res = session.install_image(data['location'])
except Exception as error:
raise error
return res
- def delete(self):
+ def delete_system_image(self):
session = self._session
data = self._data
try:
res = session.remove_image(data['name'])
except Exception as error:
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
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/utils/config_session_function.py b/src/services/api/graphql/utils/config_session_function.py
new file mode 100644
index 000000000..fc0dd7a87
--- /dev/null
+++ b/src/services/api/graphql/utils/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/utils/schema_from_config_session.py b/src/services/api/graphql/utils/schema_from_config_session.py
new file mode 100755
index 000000000..ea78aaf88
--- /dev/null
+++ b/src/services/api/graphql/utils/schema_from_config_session.py
@@ -0,0 +1,119 @@
+#!/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 json
+from inspect import signature, getmembers, isfunction, isclass, getmro
+from jinja2 import Template
+
+if __package__ is None or __package__ == '':
+ from util import snake_to_pascal_case, map_type_name
+else:
+ from . util import snake_to_pascal_case, map_type_name
+
+# this will be run locally before the build
+SCHEMA_PATH = '../graphql/schema'
+
+schema_data: dict = {'schema_name': '',
+ 'schema_fields': []}
+
+query_template = """
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionquery
+}
+"""
+
+mutation_template = """
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionmutation
+}
+"""
+
+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():
+ from config_session_function import queries, mutations
+
+ 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/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py
index 379d15250..57d63628b 100755
--- a/src/services/api/graphql/utils/schema_from_op_mode.py
+++ b/src/services/api/graphql/utils/schema_from_op_mode.py
@@ -1,205 +1,183 @@
#!/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 json
-import typing
from inspect import signature, getmembers, isfunction, isclass, getmro
from jinja2 import Template
from vyos.defaults import directories
if __package__ is None or __package__ == '':
from util import load_as_module, is_op_mode_function_name, is_show_function_name
+ from util import snake_to_pascal_case, map_type_name
else:
from . util import load_as_module, is_op_mode_function_name, is_show_function_name
+ from . util import snake_to_pascal_case, map_type_name
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'
schema_data: dict = {'schema_name': '',
'schema_fields': []}
query_template = """
input {{ schema_name }}Input {
key: String!
{%- for field_entry in schema_fields %}
{{ field_entry }}
{%- endfor %}
}
type {{ schema_name }} {
result: Generic
}
type {{ schema_name }}Result {
data: {{ schema_name }}
op_mode_error: OpModeError
success: Boolean!
errors: [String]
}
extend type Query {
{{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery
}
"""
mutation_template = """
input {{ schema_name }}Input {
key: String!
{%- for field_entry in schema_fields %}
{{ field_entry }}
{%- endfor %}
}
type {{ schema_name }} {
result: Generic
}
type {{ schema_name }}Result {
data: {{ schema_name }}
op_mode_error: OpModeError
success: Boolean!
errors: [String]
}
extend type Mutation {
{{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation
}
"""
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 _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 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)
+ 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_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/utils/util.py b/src/services/api/graphql/utils/util.py
index 073126853..da2bcdb5b 100644
--- a/src/services/api/graphql/utils/util.py
+++ b/src/services/api/graphql/utils/util.py
@@ -1,76 +1,100 @@
# 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 vyos.defaults import directories
def load_as_module(name: str, path: str):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
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'
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 6, 8:54 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4653615
Default Alt Text
(45 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment