summaryrefslogtreecommitdiff
path: root/src/services/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/services/api')
-rw-r--r--src/services/api/graphql/README.graphql142
-rw-r--r--src/services/api/graphql/bindings.py29
-rw-r--r--src/services/api/graphql/graphql/directives.py74
-rw-r--r--src/services/api/graphql/graphql/mutations.py53
-rw-r--r--src/services/api/graphql/graphql/queries.py89
-rw-r--r--src/services/api/graphql/graphql/schema/config_file.graphql27
-rw-r--r--src/services/api/graphql/graphql/schema/dhcp_server.graphql12
-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.graphql8
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql25
-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/recipes/dhcp_server.py13
-rw-r--r--src/services/api/graphql/recipes/interface_ethernet.py13
-rw-r--r--src/services/api/graphql/recipes/recipe.py49
-rw-r--r--src/services/api/graphql/recipes/remove_firewall_address_group_members.py35
-rw-r--r--src/services/api/graphql/recipes/session.py138
-rw-r--r--src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl (renamed from src/services/api/graphql/recipes/templates/dhcp_server.tmpl)2
-rw-r--r--src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl4
-rw-r--r--src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl4
-rw-r--r--src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl (renamed from src/services/api/graphql/recipes/templates/interface_ethernet.tmpl)0
-rw-r--r--src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl3
-rw-r--r--src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl3
-rw-r--r--src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl3
-rw-r--r--src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl3
26 files changed, 764 insertions, 124 deletions
diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql
index a04138010..1133d79ed 100644
--- a/src/services/api/graphql/README.graphql
+++ b/src/services/api/graphql/README.graphql
@@ -1,7 +1,12 @@
+The following examples are in the form as entered in the GraphQL
+'playground', which is found at:
+
+https://{{ host_address }}/graphql
+
Example using GraphQL mutations to configure a DHCP server:
-This assumes that the http-api is running:
+All examples assume that the http-api is running:
'set service https api'
@@ -10,7 +15,7 @@ to run with that address as default router by requesting these 'mutations'
in the GraphQL playground:
mutation {
- createInterfaceEthernet (data: {interface: "eth1",
+ CreateInterfaceEthernet (data: {interface: "eth1",
address: "192.168.0.1/24",
description: "BOB"}) {
success
@@ -22,10 +27,10 @@ mutation {
}
mutation {
- createDhcpServer(data: {sharedNetworkName: "BOB",
+ CreateDhcpServer(data: {sharedNetworkName: "BOB",
subnet: "192.168.0.0/24",
defaultRouter: "192.168.0.1",
- dnsServer: "192.168.0.1",
+ nameServer: "192.168.0.1",
domainName: "vyos.net",
lease: 86400,
range: 0,
@@ -42,37 +47,133 @@ mutation {
}
}
-The GraphQL playground will be found at:
+To save the configuration, use the following mutation:
-https://{{ host_address }}/graphql
+mutation {
+ SaveConfigFile(data: {fileName: "/config/config.boot"}) {
+ success
+ errors
+ data {
+ fileName
+ }
+ }
+}
+
+N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to
+save to /config/config.boot; to save to an alternative path, specify
+fileName.
+
+Similarly, using an analogous 'endpoint' (meaning the form of the request
+and resolver; the actual enpoint for all GraphQL requests is
+https://hostname/graphql), one can load an arbitrary config file from a
+path.
+
+mutation {
+ LoadConfigFile(data: {fileName: "/home/vyos/config.boot"}) {
+ success
+ errors
+ data {
+ fileName
+ }
+ }
+}
+
+Op-mode 'show' commands may be requested by path, e.g.:
+
+query {
+ Show (data: {path: ["interfaces", "ethernet", "detail"]}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
+
+N.B. to see the output the 'data' field 'result' must be present in the
+request.
+
+Mutations to manipulate firewall address groups:
+
+mutation {
+ CreateFirewallAddressGroup (data: {name: "ADDR-GRP", address: "10.0.0.1"}) {
+ success
+ errors
+ }
+}
+
+mutation {
+ UpdateFirewallAddressGroupMembers (data: {name: "ADDR-GRP",
+ address: ["10.0.0.1-10.0.0.8", "192.168.0.1"]}) {
+ success
+ errors
+ }
+}
+
+mutation {
+ RemoveFirewallAddressGroupMembers (data: {name: "ADDR-GRP",
+ address: "192.168.0.1"}) {
+ success
+ errors
+ }
+}
-An equivalent curl command to the first example above would be:
+N.B. The schema for the above specify that 'address' be of the form 'list of
+strings' (SDL type [String!]! for UpdateFirewallAddressGroupMembers, where
+the ! indicates that the input is required; SDL type [String] in
+CreateFirewallAddressGroup, since a group may be created without any
+addresses). However, notice that a single string may be passed without being
+a member of a list, in which case the specification allows for 'input
+coercion':
+
+http://spec.graphql.org/October2021/#sec-Scalars.Input-Coercion
+
+Similarly, IPv6 versions of the above:
+
+CreateFirewallAddressIpv6Group
+UpdateFirewallAddressIpv6GroupMembers
+RemoveFirewallAddressIpv6GroupMembers
+
+
+Instead of using the GraphQL playground, an equivalent curl command to the
+first example above would be:
curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}'
Note that the 'mutation' term is prefaced by 'query' in the curl command.
+Curl equivalents may be read from within the GraphQL playground at the 'copy
+curl' button.
+
What's here:
services
├── api
│   └── graphql
+│   ├── bindings.py
│   ├── graphql
│   │   ├── directives.py
│   │   ├── __init__.py
│   │   ├── mutations.py
│   │   └── schema
+│   │   ├── config_file.graphql
│   │   ├── dhcp_server.graphql
+│   │   ├── firewall_group.graphql
│   │   ├── interface_ethernet.graphql
-│   │   └── schema.graphql
+│   │   ├── schema.graphql
+│   │   ├── show_config.graphql
+│   │   └── show.graphql
+│   ├── README.graphql
│   ├── recipes
-│   │   ├── dhcp_server.py
│   │   ├── __init__.py
-│   │   ├── interface_ethernet.py
-│   │   ├── recipe.py
+│   │   ├── remove_firewall_address_group_members.py
+│   │   ├── session.py
│   │   └── templates
-│   │   ├── dhcp_server.tmpl
-│   │   └── interface_ethernet.tmpl
+│   │   ├── create_dhcp_server.tmpl
+│   │   ├── create_firewall_address_group.tmpl
+│   │   ├── create_interface_ethernet.tmpl
+│   │   ├── remove_firewall_address_group_members.tmpl
+│   │   └── update_firewall_address_group_members.tmpl
│   └── state.py
├── vyos-configd
├── vyos-hostsd
@@ -90,13 +191,14 @@ the Ur-data; the GraphQL schema is produced from those files, located in
Resolvers for the schema Mutation fields are dynamically generated using a
'directive' added to the respective schema field. The directive,
-'@generate', is handled by the class 'DataDirective' in
-'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in
-'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate
-wrapper in 'api/graphql/recipes', with base class doing the (overridable)
-configuration steps of calling all defined 'set'/'delete' commands.
-
-Integrating the above with vyos-http-api-server is ~10 lines of code.
+'@configure', is handled by the class 'ConfigureDirective' in
+'api/graphql/graphql/directives.py', which calls the
+'make_configure_resolver' function in 'api/graphql/graphql/mutations.py';
+the produced resolver calls the appropriate wrapper in
+'api/graphql/recipes', with base class doing the (overridable) configuration
+steps of calling all defined 'set'/'delete' commands.
+
+Integrating the above with vyos-http-api-server is 4 lines of code.
What needs to be done:
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
new file mode 100644
index 000000000..84d719fda
--- /dev/null
+++ b/src/services/api/graphql/bindings.py
@@ -0,0 +1,29 @@
+# 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/>.
+
+import vyos.defaults
+from . graphql.queries import query
+from . graphql.mutations import mutation
+from . graphql.directives import directives_dict
+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']
+
+ type_defs = load_schema_from_path(api_schema_dir)
+
+ schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict)
+
+ return schema
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index 651421c35..0a9298f55 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -1,12 +1,27 @@
+# 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 . mutations import make_resolver
+from . queries import *
+from . mutations import *
-class DataDirective(SchemaDirectiveVisitor):
- """
- Class providing implementation of 'generate' directive in schema.
+def non(arg):
+ pass
- """
- def visit_field_definition(self, field, object_type):
+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
@@ -15,3 +30,50 @@ class DataDirective(SchemaDirectiveVisitor):
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 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)
+
+directives_dict = {"configure": ConfigureDirective,
+ "showconfig": ShowConfigDirective,
+ "configfile": ConfigFileDirective,
+ "show": ShowDirective,
+ "image": ImageDirective}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 98c665c9a..0c3eb702a 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -1,3 +1,17 @@
+# 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 importlib import import_module
from typing import Any, Dict
@@ -6,10 +20,11 @@ from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
+from api.graphql.recipes.session import Session
mutation = ObjectType("Mutation")
-def make_resolver(mutation_name):
+def make_mutation_resolver(mutation_name, class_name, session_func):
"""Dynamically generate a resolver for the mutation named in the
schema by 'mutation_name'.
@@ -19,11 +34,11 @@ def make_resolver(mutation_name):
functools.wraps.
:raise Exception:
- encapsulating ConfigErrors, or internal errors
+ raising ConfigErrors, or internal errors
"""
- class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1)
+
func_base_name = convert_camel_case_to_snake(class_name)
- resolver_name = f'resolve_create_{func_base_name}'
+ resolver_name = f'resolve_{func_base_name}'
func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
@mutation.field(mutation_name)
@@ -40,10 +55,18 @@ def make_resolver(mutation_name):
data = kwargs['data']
session = state.settings['app'].state.vyos_session
- mod = import_module(f'api.graphql.recipes.{func_base_name}')
- klass = getattr(mod, class_name)
+ # one may override the session functions with a local subclass
+ try:
+ mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ klass = getattr(mod, class_name)
+ except ImportError:
+ # otherwise, dynamically generate subclass to invoke subclass
+ # name based templates
+ klass = type(class_name, (Session,), {})
k = klass(session, data)
- k.configure()
+ method = getattr(k, session_func)
+ result = method()
+ data['result'] = result
return {
"success": True,
@@ -57,4 +80,20 @@ def make_resolver(mutation_name):
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'])
diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py
new file mode 100644
index 000000000..e1868091e
--- /dev/null
+++ b/src/services/api/graphql/graphql/queries.py
@@ -0,0 +1,89 @@
+# 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 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 api.graphql.recipes.session import Session
+
+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']
+ 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}')
+ klass = getattr(mod, class_name)
+ except ImportError:
+ # otherwise, dynamically generate subclass to invoke subclass
+ # name based templates
+ 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 Exception as error:
+ return {
+ "success": False,
+ "errors": [str(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_show_resolver(query_name):
+ class_name = query_name
+ return make_query_resolver(query_name, class_name, 'show')
diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql
new file mode 100644
index 000000000..31ab26b9e
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/config_file.graphql
@@ -0,0 +1,27 @@
+input SaveConfigFileInput {
+ fileName: String
+}
+
+type SaveConfigFile {
+ fileName: String
+}
+
+type SaveConfigFileResult {
+ data: SaveConfigFile
+ success: Boolean!
+ errors: [String]
+}
+
+input LoadConfigFileInput {
+ fileName: String!
+}
+
+type LoadConfigFile {
+ fileName: String!
+}
+
+type LoadConfigFileResult {
+ data: LoadConfigFile
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
index a7ee75d40..25f091bfa 100644
--- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql
+++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
@@ -1,8 +1,8 @@
-input dhcpServerConfigInput {
+input DhcpServerConfigInput {
sharedNetworkName: String
subnet: String
defaultRouter: String
- dnsServer: String
+ nameServer: String
domainName: String
lease: Int
range: Int
@@ -13,11 +13,11 @@ input dhcpServerConfigInput {
dnsForwardingListenAddress: String
}
-type dhcpServerConfig {
+type DhcpServerConfig {
sharedNetworkName: String
subnet: String
defaultRouter: String
- dnsServer: String
+ nameServer: String
domainName: String
lease: Int
range: Int
@@ -28,8 +28,8 @@ type dhcpServerConfig {
dnsForwardingListenAddress: String
}
-type createDhcpServerResult {
- data: dhcpServerConfig
+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
new file mode 100644
index 000000000..d89904b9e
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql
@@ -0,0 +1,95 @@
+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
new file mode 100644
index 000000000..7d1b4f9d0
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/image.graphql
@@ -0,0 +1,29 @@
+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
index fdcf97bad..32438b315 100644
--- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
+++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
@@ -1,18 +1,18 @@
-input interfaceEthernetConfigInput {
+input InterfaceEthernetConfigInput {
interface: String
address: String
replace: Boolean = true
description: String
}
-type interfaceEthernetConfig {
+type InterfaceEthernetConfig {
interface: String
address: String
description: String
}
-type createInterfaceEthernetResult {
- data: interfaceEthernetConfig
+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 8a5e17962..952e46f34 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -3,13 +3,28 @@ 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
+
type Query {
- _dummy: String
+ Show(data: ShowInput) : ShowResult @show
+ ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig
}
-directive @generate on FIELD_DEFINITION
-
type Mutation {
- createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate
- createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate
+ 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
new file mode 100644
index 000000000..c7709e48b
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/show.graphql
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 000000000..34afd2aa9
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/show_config.graphql
@@ -0,0 +1,21 @@
+"""
+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/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py
deleted file mode 100644
index 3edb3028e..000000000
--- a/src/services/api/graphql/recipes/dhcp_server.py
+++ /dev/null
@@ -1,13 +0,0 @@
-
-from . recipe import Recipe
-
-class DhcpServer(Recipe):
- def __init__(self, session, command_file):
- super().__init__(session, command_file)
-
- # Define any custom processing of parameters here by overriding
- # configure:
- #
- # def configure(self):
- # self.data = transform_data(self.data)
- # super().configure()
diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py
deleted file mode 100644
index f88f5924f..000000000
--- a/src/services/api/graphql/recipes/interface_ethernet.py
+++ /dev/null
@@ -1,13 +0,0 @@
-
-from . recipe import Recipe
-
-class InterfaceEthernet(Recipe):
- def __init__(self, session, command_file):
- super().__init__(session, command_file)
-
- # Define any custom processing of parameters here by overriding
- # configure:
- #
- # def configure(self):
- # self.data = transform_data(self.data)
- # super().configure()
diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py
deleted file mode 100644
index 8fbb9e0bf..000000000
--- a/src/services/api/graphql/recipes/recipe.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from ariadne import convert_camel_case_to_snake
-import vyos.defaults
-from vyos.template import render
-
-class Recipe(object):
- def __init__(self, session, data):
- self._session = session
- self.data = data
- self._name = convert_camel_case_to_snake(type(self).__name__)
-
- @property
- def data(self):
- return self.__data
-
- @data.setter
- def data(self, data):
- if isinstance(data, dict):
- self.__data = data
- else:
- raise ValueError("data must be of type dict")
-
- 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
-
-
diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
new file mode 100644
index 000000000..b91932e14
--- /dev/null
+++ b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
@@ -0,0 +1,35 @@
+# 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 . session import Session
+
+class RemoveFirewallAddressGroupMembers(Session):
+ def __init__(self, session, data):
+ super().__init__(session, data)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self._data = transform_data(self._data)
+ # super().configure()
+ # self.clean_up()
+
+ def configure(self):
+ super().configure()
+
+ group_name = self._data['name']
+ path = ['firewall', 'group', 'address-group', group_name]
+ self.delete_path_if_childless(path)
diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py
new file mode 100644
index 000000000..1f844ff70
--- /dev/null
+++ b/src/services/api/graphql/recipes/session.py
@@ -0,0 +1,138 @@
+# 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/>.
+
+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.template import render
+
+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__)
+
+ 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()
+
+ 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 = vyos.configtree.ConfigTree(out)
+ out = json.loads(config_tree.to_json())
+ except Exception as error:
+ raise error
+
+ return out
+
+ def save(self):
+ session = self._session
+ data = self._data
+ if 'file_name' not in data or not data['file_name']:
+ data['file_name'] = '/config/config.boot'
+
+ try:
+ session.save_config(data['file_name'])
+ except Exception as error:
+ raise error
+
+ def load(self):
+ session = self._session
+ data = self._data
+
+ try:
+ session.load_config(data['file_name'])
+ session.commit()
+ except Exception as error:
+ raise error
+
+ 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):
+ session = self._session
+ data = self._data
+
+ try:
+ res = session.install_image(data['location'])
+ except Exception as error:
+ raise error
+
+ return res
+
+ def delete(self):
+ session = self._session
+ data = self._data
+
+ try:
+ res = session.remove_image(data['name'])
+ except Exception as error:
+ raise error
+
+ return res
diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl
index 629ce83c1..70de43183 100644
--- a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
+++ b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl
@@ -1,5 +1,5 @@
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }}
-set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} name-server {{ name_server }}
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }}
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }}
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }}
diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl
new file mode 100644
index 000000000..a890d0086
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl
@@ -0,0 +1,4 @@
+set firewall group address-group {{ name }}
+{% for add in address %}
+set firewall group address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl
new file mode 100644
index 000000000..e9b660722
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl
@@ -0,0 +1,4 @@
+set firewall group ipv6-address-group {{ name }}
+{% for add in address %}
+set firewall group ipv6-address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl
index d9d7ed691..d9d7ed691 100644
--- a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
+++ b/src/services/api/graphql/recipes/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/recipes/templates/remove_firewall_address_group_members.tmpl
new file mode 100644
index 000000000..458f3e5fc
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+delete firewall group address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl
new file mode 100644
index 000000000..0efa0b226
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+delete firewall group ipv6-address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl
new file mode 100644
index 000000000..f56c61231
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+set firewall group address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl
new file mode 100644
index 000000000..f98a5517c
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+set firewall group ipv6-address-group {{ name }} address {{ add }}
+{% endfor %}