summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/op-mode-standardized.json7
-rw-r--r--op-mode-definitions/nat.xml.in2
-rw-r--r--op-mode-definitions/show-bridge.xml.in6
-rw-r--r--op-mode-definitions/show-vrf.xml.in2
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py5
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py2
-rwxr-xr-xsrc/op_mode/bridge.py98
-rwxr-xr-xsrc/op_mode/nat.py26
-rwxr-xr-xsrc/op_mode/show_vrf.py66
-rwxr-xr-xsrc/op_mode/vrf.py20
-rw-r--r--src/services/api/graphql/bindings.py3
-rw-r--r--src/services/api/graphql/graphql/directives.py20
-rw-r--r--src/services/api/graphql/graphql/mutations.py12
-rw-r--r--src/services/api/graphql/graphql/queries.py12
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql2
-rwxr-xr-xsrc/services/api/graphql/recipes/queries/system_status.py15
-rw-r--r--src/services/api/graphql/recipes/session.py61
-rwxr-xr-xsrc/services/api/graphql/utils/schema_from_op_mode.py161
-rw-r--r--src/services/api/graphql/utils/util.py55
19 files changed, 480 insertions, 95 deletions
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json
new file mode 100644
index 000000000..4dccbba7f
--- /dev/null
+++ b/data/op-mode-standardized.json
@@ -0,0 +1,7 @@
+[
+"cpu.py",
+"memory.py",
+"neighbor.py",
+"route.py",
+"version.py"
+]
diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in
index 84e999995..b0ec8989f 100644
--- a/op-mode-definitions/nat.xml.in
+++ b/op-mode-definitions/nat.xml.in
@@ -22,7 +22,7 @@
<properties>
<help>Show statistics for configured source NAT rules</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_nat_statistics.py --source</command>
+ <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source</command>
</node>
<node name="translations">
<properties>
diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in
index 8b857888b..dd2a28931 100644
--- a/op-mode-definitions/show-bridge.xml.in
+++ b/op-mode-definitions/show-bridge.xml.in
@@ -11,7 +11,7 @@
<properties>
<help>View the VLAN filter settings of the bridge</help>
</properties>
- <command>bridge -c vlan show</command>
+ <command>${vyos_op_scripts_dir}/bridge.py show_vlan</command>
</leafNode>
</children>
</node>
@@ -34,13 +34,13 @@
<properties>
<help>Displays the multicast group database for the bridge</help>
</properties>
- <command>bridge -c mdb show dev $3</command>
+ <command>${vyos_op_scripts_dir}/bridge.py show_mdb --interface=$3</command>
</leafNode>
<leafNode name="fdb">
<properties>
<help>Show the forwarding database of the bridge</help>
</properties>
- <command>bridge -c fdb show br $3</command>
+ <command>${vyos_op_scripts_dir}/bridge.py show_fdb --interface=$3</command>
</leafNode>
</children>
</tagNode>
diff --git a/op-mode-definitions/show-vrf.xml.in b/op-mode-definitions/show-vrf.xml.in
index d8d5284d7..0e0370445 100644
--- a/op-mode-definitions/show-vrf.xml.in
+++ b/op-mode-definitions/show-vrf.xml.in
@@ -15,7 +15,7 @@
<path>vrf name</path>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command>
+ <command>${vyos_op_scripts_dir}/vrf.py show --name="$3"</command>
<children>
<leafNode name="processes">
<properties>
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index bf0f6840d..af2d0588d 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -118,6 +118,11 @@ def verify(vxlan):
# in use.
vxlan_overhead += 20
+ # If source_address is not used - check IPv6 'remote' list
+ elif 'remote' in vxlan:
+ if any(is_ipv6(a) for a in vxlan['remote']):
+ vxlan_overhead += 20
+
lower_mtu = Interface(vxlan['source_interface']).get_mtu()
if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):
raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index e275ace84..97b3a6396 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -76,7 +76,7 @@ def get_config(config=None):
# We need to know the amount of other WWAN interfaces as ModemManager needs
# to be started or stopped.
conf.set_level(base)
- _, wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=True)
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
index 4ab127156..411aa06d1 100755
--- a/src/op_mode/bridge.py
+++ b/src/op_mode/bridge.py
@@ -17,6 +17,7 @@
import jmespath
import json
import sys
+import typing
from sys import exit
from tabulate import tabulate
@@ -43,6 +44,33 @@ def _get_raw_data_summary():
return data_dict
+def _get_raw_data_vlan():
+ """
+ :returns dict
+ """
+ json_data = cmd('sudo bridge --json --compressvlans vlan show')
+ data_dict = json.loads(json_data)
+ return data_dict
+
+
+def _get_raw_data_fdb(bridge):
+ """Get MAC-address for the bridge brX
+ :returns list
+ """
+ json_data = cmd(f'sudo bridge --json fdb show br {bridge}')
+ data_dict = json.loads(json_data)
+ return data_dict
+
+
+def _get_raw_data_mdb(bridge):
+ """Get MAC-address multicast gorup for the bridge brX
+ :return list
+ """
+ json_data = cmd(f'bridge --json mdb show br {bridge}')
+ data_dict = json.loads(json_data)
+ return data_dict
+
+
def _get_bridge_members(bridge: str) -> list:
"""
Get list of interface bridge members
@@ -86,6 +114,52 @@ def _get_formatted_output_summary(data):
return output
+def _get_formatted_output_vlan(data):
+ data_entries = []
+ for entry in data:
+ interface = entry.get('ifname')
+ vlans = entry.get('vlans')
+ for vlan_entry in vlans:
+ vlan = vlan_entry.get('vlan')
+ if vlan_entry.get('vlanEnd'):
+ vlan_end = vlan_entry.get('vlanEnd')
+ vlan = f'{vlan}-{vlan_end}'
+ flags = ', '.join(vlan_entry.get('flags')).lower()
+ data_entries.append([interface, vlan, flags])
+
+ headers = ["Interface", "Vlan", "Flags"]
+ output = tabulate(data_entries, headers)
+ return output
+
+
+def _get_formatted_output_fdb(data):
+ data_entries = []
+ for entry in data:
+ interface = entry.get('ifname')
+ mac = entry.get('mac')
+ state = entry.get('state')
+ flags = ','.join(entry['flags'])
+ data_entries.append([interface, mac, state, flags])
+
+ headers = ["Interface", "Mac address", "State", "Flags"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def _get_formatted_output_mdb(data):
+ data_entries = []
+ for entry in data:
+ for mdb_entry in entry['mdb']:
+ interface = mdb_entry.get('port')
+ group = mdb_entry.get('grp')
+ state = mdb_entry.get('state')
+ flags = ','.join(mdb_entry.get('flags'))
+ data_entries.append([interface, group, state, flags])
+ headers = ["Interface", "Group", "State", "Flags"]
+ output = tabulate(data_entries, headers)
+ return output
+
+
def show(raw: bool):
bridge_data = _get_raw_data_summary()
if raw:
@@ -94,6 +168,30 @@ def show(raw: bool):
return _get_formatted_output_summary(bridge_data)
+def show_vlan(raw: bool):
+ bridge_vlan = _get_raw_data_vlan()
+ if raw:
+ return bridge_vlan
+ else:
+ return _get_formatted_output_vlan(bridge_vlan)
+
+
+def show_fdb(raw: bool, interface: str):
+ fdb_data = _get_raw_data_fdb(interface)
+ if raw:
+ return fdb_data
+ else:
+ return _get_formatted_output_fdb(fdb_data)
+
+
+def show_mdb(raw: bool, interface: str):
+ mdb_data = _get_raw_data_mdb(interface)
+ if raw:
+ return mdb_data
+ else:
+ return _get_formatted_output_mdb(mdb_data)
+
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 666c72c7c..4b54ecf31 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -147,6 +147,24 @@ port {port}'''
return output
+def _get_formatted_output_statistics(data, direction):
+ data_entries = []
+ for rule in data:
+ if 'comment' in rule['rule']:
+ comment = rule.get('rule').get('comment')
+ rule_number = comment.split('-')[-1]
+ rule_number = rule_number.split(' ')[0]
+ if 'expr' in rule['rule']:
+ interface = rule.get('rule').get('expr')[0].get('match').get('right') \
+ if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any'
+ packets = jmespath.search('rule.expr[*].counter.packets | [0]', rule)
+ _bytes = jmespath.search('rule.expr[*].counter.bytes | [0]', rule)
+ data_entries.append([rule_number, packets, _bytes, interface])
+ headers = ["Rule", "Packets", "Bytes", "Interface"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
def show_rules(raw: bool, direction: str):
nat_rules = _get_raw_data_rules(direction)
if raw:
@@ -155,6 +173,14 @@ def show_rules(raw: bool, direction: str):
return _get_formatted_output_rules(nat_rules, direction)
+def show_statistics(raw: bool, direction: str):
+ nat_statistics = _get_raw_data_rules(direction)
+ if raw:
+ return nat_statistics
+ else:
+ return _get_formatted_output_statistics(nat_statistics, direction)
+
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py
deleted file mode 100755
index 3c7a90205..000000000
--- a/src/op_mode/show_vrf.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020 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 argparse
-import jinja2
-from json import loads
-
-from vyos.util import cmd
-
-vrf_out_tmpl = """VRF name state mac address flags interfaces
--------- ----- ----------- ----- ----------
-{%- for v in vrf %}
-{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}}
-{%- endfor %}
-
-"""
-
-def list_vrfs():
- command = 'ip -j -br link show type vrf'
- answer = loads(cmd(command))
- return [_ for _ in answer if _]
-
-def list_vrf_members(vrf):
- command = f'ip -j -br link show master {vrf}'
- answer = loads(cmd(command))
- return [_ for _ in answer if _]
-
-parser = argparse.ArgumentParser()
-group = parser.add_mutually_exclusive_group()
-group.add_argument("-e", "--extensive", action="store_true",
- help="provide detailed vrf informatio")
-parser.add_argument('interface', metavar='I', type=str, nargs='?',
- help='interface to display')
-
-args = parser.parse_args()
-
-if args.extensive:
- data = { 'vrf': [] }
- for vrf in list_vrfs():
- name = vrf['ifname']
- if args.interface and name != args.interface:
- continue
-
- vrf['members'] = []
- for member in list_vrf_members(name):
- vrf['members'].append(member['ifname'])
- data['vrf'].append(vrf)
-
- tmpl = jinja2.Template(vrf_out_tmpl)
- print(tmpl.render(data))
-
-else:
- print(" ".join([vrf['ifname'] for vrf in list_vrfs()]))
diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py
index 63d9b5ee5..f86516786 100755
--- a/src/op_mode/vrf.py
+++ b/src/op_mode/vrf.py
@@ -16,6 +16,7 @@
import json
import sys
+import typing
from tabulate import tabulate
from vyos.util import cmd
@@ -23,12 +24,23 @@ from vyos.util import cmd
import vyos.opmode
-def _get_raw_data():
+def _get_raw_data(name=None):
"""
- :return: list
+ If vrf name is not set - get all VRFs
+ If vrf name is set - get only this name data
+ If vrf name set and not found - return []
"""
output = cmd('sudo ip --json --brief link show type vrf')
data = json.loads(output)
+ if not data:
+ return []
+ if name:
+ is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False
+ if is_vrf_exists:
+ output = cmd(f'sudo ip --json --brief link show dev {name}')
+ data = json.loads(output)
+ return data
+ return []
return data
@@ -62,8 +74,8 @@ def _get_formatted_output(raw_data):
return output
-def show(raw: bool):
- vrf_data = _get_raw_data()
+def show(raw: bool, name: typing.Optional[str]):
+ vrf_data = _get_raw_data(name=name)
if raw:
return vrf_data
else:
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
index 84d719fda..049d59de7 100644
--- a/src/services/api/graphql/bindings.py
+++ b/src/services/api/graphql/bindings.py
@@ -17,11 +17,14 @@ import vyos.defaults
from . graphql.queries import query
from . graphql.mutations import mutation
from . graphql.directives import directives_dict
+from . utils.schema_from_op_mode import generate_op_mode_definitions
from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
def generate_schema():
api_schema_dir = vyos.defaults.directories['api_schema']
+ generate_op_mode_definitions()
+
type_defs = load_schema_from_path(api_schema_dir)
schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict)
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index 551d28831..d8ceefae6 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -80,9 +80,27 @@ class ImageDirective(VyosDirective):
super().visit_field_definition(field, object_type,
make_resolver=make_image_resolver)
+class GenOpQueryDirective(VyosDirective):
+ """
+ Class providing implementation of 'genopquery' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_gen_op_query_resolver)
+
+class GenOpMutationDirective(VyosDirective):
+ """
+ Class providing implementation of 'genopmutation' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_gen_op_mutation_resolver)
+
directives_dict = {"configure": ConfigureDirective,
"showconfig": ShowConfigDirective,
"systemstatus": SystemStatusDirective,
"configfile": ConfigFileDirective,
"show": ShowDirective,
- "image": ImageDirective}
+ "image": ImageDirective,
+ "genopquery": GenOpQueryDirective,
+ "genopmutation": GenOpMutationDirective}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 93e046319..3e89fb239 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -63,6 +63,10 @@ def make_mutation_resolver(mutation_name, class_name, session_func):
"errors": ['invalid API key']
}
+ # We are finished with the 'key' entry, and may remove so as to
+ # pass the rest of data (if any) to function.
+ del data['key']
+
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
@@ -71,7 +75,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func):
klass = getattr(mod, class_name)
except ImportError:
# otherwise, dynamically generate subclass to invoke subclass
- # name based templates
+ # name based functions
klass = type(class_name, (Session,), {})
k = klass(session, data)
method = getattr(k, session_func)
@@ -107,3 +111,7 @@ def make_config_file_resolver(mutation_name):
def make_image_resolver(mutation_name):
return make_prefix_resolver(mutation_name, prefix=['add', 'delete'])
+
+def make_gen_op_mutation_resolver(mutation_name):
+ class_name = mutation_name
+ return make_mutation_resolver(mutation_name, class_name, 'gen_op_mutation')
diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py
index eeaa9e19c..f6544709e 100644
--- a/src/services/api/graphql/graphql/queries.py
+++ b/src/services/api/graphql/graphql/queries.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -63,6 +63,10 @@ def make_query_resolver(query_name, class_name, session_func):
"errors": ['invalid API key']
}
+ # We are finished with the 'key' entry, and may remove so as to
+ # pass the rest of data (if any) to function.
+ del data['key']
+
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
@@ -71,7 +75,7 @@ def make_query_resolver(query_name, class_name, session_func):
klass = getattr(mod, class_name)
except ImportError:
# otherwise, dynamically generate subclass to invoke subclass
- # name based templates
+ # name based functions
klass = type(class_name, (Session,), {})
k = klass(session, data)
method = getattr(k, session_func)
@@ -101,3 +105,7 @@ def make_system_status_resolver(query_name):
def make_show_resolver(query_name):
class_name = query_name
return make_query_resolver(query_name, class_name, 'show')
+
+def make_gen_op_query_resolver(query_name):
+ class_name = query_name
+ return make_query_resolver(query_name, class_name, 'gen_op_query')
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
index 8ae71f632..624be2620 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -9,6 +9,8 @@ directive @show on FIELD_DEFINITION
directive @showconfig on FIELD_DEFINITION
directive @systemstatus on FIELD_DEFINITION
directive @image on FIELD_DEFINITION
+directive @genopquery on FIELD_DEFINITION
+directive @genopmutation on FIELD_DEFINITION
scalar Generic
diff --git a/src/services/api/graphql/recipes/queries/system_status.py b/src/services/api/graphql/recipes/queries/system_status.py
index 00c137443..8dadcc9f3 100755
--- a/src/services/api/graphql/recipes/queries/system_status.py
+++ b/src/services/api/graphql/recipes/queries/system_status.py
@@ -23,23 +23,16 @@ import importlib.util
from vyos.defaults import directories
-OP_PATH = directories['op_mode']
-
-def load_as_module(name: str):
- path = os.path.join(OP_PATH, name)
- spec = importlib.util.spec_from_file_location(name, path)
- mod = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(mod)
- return mod
+from api.graphql.utils.util import load_op_mode_as_module
def get_system_version() -> dict:
- show_version = load_as_module('version.py')
+ 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_as_module('show_uptime.py')
+ show_uptime = load_op_mode_as_module('show_uptime.py')
return show_uptime.get_raw_data()
def get_system_ram_usage() -> dict:
- show_ram = load_as_module('memory.py')
+ show_ram = load_op_mode_as_module('memory.py')
return show_ram.show(raw=True)
diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py
index c436de08a..6b580af01 100644
--- a/src/services/api/graphql/recipes/session.py
+++ b/src/services/api/graphql/recipes/session.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -13,15 +13,20 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this library. If not, see <http://www.gnu.org/licenses/>.
+import os
import json
from ariadne import convert_camel_case_to_snake
-import vyos.defaults
from vyos.config import Config
from vyos.configtree import ConfigTree
+from vyos.defaults import directories
from vyos.template import render
+from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name
+
+op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json')
+
class Session:
"""
Wrapper for calling configsession functions based on GraphQL requests.
@@ -33,6 +38,12 @@ class Session:
self._data = data
self._name = convert_camel_case_to_snake(type(self).__name__)
+ try:
+ with open(op_mode_include_file) as f:
+ self._op_mode_list = f.read()
+ except Exception:
+ self._op_mode_list = None
+
def configure(self):
session = self._session
data = self._data
@@ -40,7 +51,7 @@ class Session:
tmpl_file = f'{func_base_name}.tmpl'
cmd_file = f'/tmp/{func_base_name}.cmds'
- tmpl_dir = vyos.defaults.directories['api_templates']
+ tmpl_dir = directories['api_templates']
try:
render(cmd_file, tmpl_file, data, location=tmpl_dir)
@@ -150,3 +161,47 @@ class Session:
status['ram'] = system_status.get_system_ram_usage()
return status
+
+ def gen_op_query(self):
+ session = self._session
+ data = self._data
+ name = self._name
+ op_mode_list = self._op_mode_list
+
+ # handle the case that the op-mode file contains underscores:
+ if op_mode_list is None:
+ raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
+ (func_name, basename) = split_compound_op_mode_name(name, op_mode_list)
+ if basename == '':
+ raise FileNotFoundError(f"No op-mode file basename in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{basename}.py')
+ func = getattr(mod, func_name)
+ if len(list(data)) > 0:
+ res = func(True, **data)
+ else:
+ res = func(True)
+
+ return res
+
+ def gen_op_mutation(self):
+ session = self._session
+ data = self._data
+ name = self._name
+ op_mode_list = self._op_mode_list
+
+ # handle the case that the op-mode file name contains underscores:
+ if op_mode_list is None:
+ raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
+ (func_name, basename) = split_compound_op_mode_name(name, op_mode_list)
+ if basename == '':
+ raise FileNotFoundError(f"No op-mode file basename in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{basename}.py')
+ func = getattr(mod, func_name)
+ if len(list(data)) > 0:
+ res = func(**data)
+ else:
+ res = func()
+
+ return res
diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py
new file mode 100755
index 000000000..cdde5f187
--- /dev/null
+++ b/src/services/api/graphql/utils/schema_from_op_mode.py
@@ -0,0 +1,161 @@
+#!/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
+from jinja2 import Template
+
+from vyos.defaults import directories
+from . util import load_as_module, is_op_mode_function_name, is_show_function_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')
+
+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 @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 }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation
+}
+"""
+
+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)
+
+ # 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 generate_op_mode_definitions():
+ 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]
+ 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
new file mode 100644
index 000000000..e3dea31bf
--- /dev/null
+++ b/src/services/api/graphql/utils/util.py
@@ -0,0 +1,55 @@
+# 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 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]
+ 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_rsplit(delim: str, n: int, s: str):
+ groups = s.split(delim)
+ l = len(groups)
+ if n > l-1:
+ return ('', s)
+ return (delim.join(groups[:l-n]), delim.join(groups[l-n:]))
+
+def split_compound_op_mode_name(name: str, files: list):
+ for i in range(1, name.count('_') + 1):
+ pair = _nth_rsplit('_', i, name)
+ if pair[1] in files:
+ return pair
+ return (name, '')