From 9756b870d503eab00714b21c66bf30de47878f6e Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 5 Aug 2021 11:25:31 -0500 Subject: http-api: T2768: add README.graphql (cherry picked from commit 5b69aad5bfe1fd1dfc51afb1d4b6323028009deb) --- src/services/api/graphql/README.graphql | 116 ++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/services/api/graphql/README.graphql (limited to 'src/services/api/graphql/README.graphql') diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql new file mode 100644 index 000000000..a04138010 --- /dev/null +++ b/src/services/api/graphql/README.graphql @@ -0,0 +1,116 @@ + +Example using GraphQL mutations to configure a DHCP server: + +This assumes that the http-api is running: + +'set service https api' + +One can configure an address on an interface, and configure the DHCP server +to run with that address as default router by requesting these 'mutations' +in the GraphQL playground: + +mutation { + createInterfaceEthernet (data: {interface: "eth1", + address: "192.168.0.1/24", + description: "BOB"}) { + success + errors + data { + address + } + } +} + +mutation { + createDhcpServer(data: {sharedNetworkName: "BOB", + subnet: "192.168.0.0/24", + defaultRouter: "192.168.0.1", + dnsServer: "192.168.0.1", + domainName: "vyos.net", + lease: 86400, + range: 0, + start: "192.168.0.9", + stop: "192.168.0.254", + dnsForwardingAllowFrom: "192.168.0.0/24", + dnsForwardingCacheSize: 0, + dnsForwardingListenAddress: "192.168.0.1"}) { + success + errors + data { + defaultRouter + } + } +} + +The GraphQL playground will be found at: + +https://{{ host_address }}/graphql + +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. + +What's here: + +services +├── api +│   └── graphql +│   ├── graphql +│   │   ├── directives.py +│   │   ├── __init__.py +│   │   ├── mutations.py +│   │   └── schema +│   │   ├── dhcp_server.graphql +│   │   ├── interface_ethernet.graphql +│   │   └── schema.graphql +│   ├── recipes +│   │   ├── dhcp_server.py +│   │   ├── __init__.py +│   │   ├── interface_ethernet.py +│   │   ├── recipe.py +│   │   └── templates +│   │   ├── dhcp_server.tmpl +│   │   └── interface_ethernet.tmpl +│   └── state.py +├── vyos-configd +├── vyos-hostsd +└── vyos-http-api-server + +The GraphQL library that we are using, Ariadne, advertises itself as a +'schema-first' implementation: define the schema; define resolvers +(handlers) for declared Query and Mutation types (Subscription types are not +currently used). + +In the current approach to a high-level API, we consider the +Jinja2-templated collection of configuration mode 'set'/'delete' commands as +the Ur-data; the GraphQL schema is produced from those files, located in +'api/graphql/recipes/templates'. + +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. + +What needs to be done: + +• automate generation of schema and wrappers from templated configuration +commands + +• investigate whether the subclassing provided by the named wrappers in +'api/graphql/recipes' is sufficient for use cases which need to modify data + +• encapsulate the manipulation of 'canonical names' which transforms the +prefixed camel-case schema names to various snake-case file/function names + +• consider mechanism for migration of templates: offline vs. on-the-fly + +• define the naming convention for those schema fields that refer to +configuration mode parameters: e.g. how much of the path is needed as prefix +to uniquely define the term -- cgit v1.2.3 From 66ff05703ed260c744290fb604dbf31a0dfcd1da Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 7 Nov 2021 14:47:40 -0600 Subject: http-api: T2768: update dhcp-server example for migration 5-to-6 (cherry picked from commit dc9a2821d063a96681d6cb1d962618829b71937d) --- src/services/api/graphql/README.graphql | 2 +- src/services/api/graphql/graphql/schema/dhcp_server.graphql | 4 ++-- src/services/api/graphql/recipes/templates/dhcp_server.tmpl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src/services/api/graphql/README.graphql') diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index a04138010..580c0eb7f 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -25,7 +25,7 @@ mutation { 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, diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql index a7ee75d40..9f741a0a5 100644 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -2,7 +2,7 @@ input dhcpServerConfigInput { sharedNetworkName: String subnet: String defaultRouter: String - dnsServer: String + nameServer: String domainName: String lease: Int range: Int @@ -17,7 +17,7 @@ type dhcpServerConfig { sharedNetworkName: String subnet: String defaultRouter: String - dnsServer: String + nameServer: String domainName: String lease: Int range: Int diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl index 629ce83c1..70de43183 100644 --- a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl +++ b/src/services/api/graphql/recipes/templates/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 }} -- cgit v1.2.3 From 419f81a0c39740de0ff61ce25325ebea76c4a395 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 14 Nov 2021 19:03:25 -0600 Subject: graphql: T3993: add config file save/load (cherry picked from commit 8915a19f7761253b7bdf6ca847069539ee33851d) --- src/services/api/graphql/README.graphql | 24 +++++++++++ src/services/api/graphql/bindings.py | 4 +- src/services/api/graphql/graphql/directives.py | 17 +++++++- src/services/api/graphql/graphql/mutations.py | 49 ++++++++++++++++++++++ .../api/graphql/graphql/schema/config_file.graphql | 27 ++++++++++++ .../api/graphql/graphql/schema/schema.graphql | 3 ++ src/services/api/graphql/recipes/config_file.py | 16 +++++++ src/services/api/graphql/recipes/recipe.py | 19 +++++++++ 8 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/services/api/graphql/graphql/schema/config_file.graphql create mode 100644 src/services/api/graphql/recipes/config_file.py (limited to 'src/services/api/graphql/README.graphql') diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index 580c0eb7f..c91b70782 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -42,6 +42,30 @@ mutation { } } +mutation { + saveConfigFile(data: {fileName: "/config/config.boot"}) { + success + errors + data { + fileName + } + } +} + +N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to save to +/config/config.boot; to save to an alternative path, specify fileName. + +mutation { + loadConfigFile(data: {fileName: "/home/vyos/config.boot"}) { + success + errors + data { + fileName + } + } +} + + The GraphQL playground will be found at: https://{{ host_address }}/graphql diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 1403841b4..c123f68d8 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -1,6 +1,6 @@ import vyos.defaults from . graphql.mutations import mutation -from . graphql.directives import DataDirective +from . graphql.directives import DataDirective, ConfigFileDirective from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers @@ -9,6 +9,6 @@ def generate_schema(): type_defs = load_schema_from_path(api_schema_dir) - schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) + schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective, "configfile": ConfigFileDirective}) return schema diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 651421c35..85d514de4 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,5 +1,5 @@ from ariadne import SchemaDirectiveVisitor, ObjectType -from . mutations import make_resolver +from . mutations import make_resolver, make_config_file_resolver class DataDirective(SchemaDirectiveVisitor): """ @@ -15,3 +15,18 @@ class DataDirective(SchemaDirectiveVisitor): func = make_resolver(name) field.resolve = func return field + +class ConfigFileDirective(SchemaDirectiveVisitor): + """ + Class providing implementation of 'configfile' directive in schema. + + """ + def visit_field_definition(self, field, object_type): + name = f'{field.type}' + # field.type contains the return value of the mutation; trim value + # to produce canonical name + name = name.replace('Result', '', 1) + + func = make_config_file_resolver(name) + field.resolve = func + return field diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 98c665c9a..2eb0a0b4a 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -57,4 +57,53 @@ def make_resolver(mutation_name): return func_impl +def make_config_file_resolver(mutation_name): + op = '' + if 'save' in mutation_name: + op = 'save' + elif 'load' in mutation_name: + op = 'load' + class_name = mutation_name.replace('save', '', 1).replace('load', '', 1) + func_base_name = convert_camel_case_to_snake(class_name) + resolver_name = f'resolve_{func_base_name}' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' + + @mutation.field(mutation_name) + @convert_kwargs_to_snake_case + @with_signature(func_sig, func_name=resolver_name) + async def func_impl(*args, **kwargs): + try: + if 'data' not in kwargs: + return { + "success": False, + "errors": ['missing data'] + } + + data = kwargs['data'] + session = state.settings['app'].state.vyos_session + + mod = import_module(f'api.graphql.recipes.{func_base_name}') + klass = getattr(mod, class_name) + k = klass(session, data) + if op == 'save': + k.save() + elif op == 'load': + k.load() + else: + return { + "success": False, + "errors": ["Input must be saveConfigFile | loadConfigFile"] + } + + return { + "success": True, + "data": data + } + except Exception as error: + return { + "success": False, + "errors": [str(error)] + } + + return func_impl diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql new file mode 100644 index 000000000..3096cf743 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/config_file.graphql @@ -0,0 +1,27 @@ +input saveConfigFileInput { + fileName: String +} + +type saveConfigFile { + fileName: String +} + +type saveConfigFileResult { + data: saveConfigFile + success: Boolean! + errors: [String] +} + +input loadConfigFileInput { + fileName: String! +} + +type loadConfigFile { + fileName: String! +} + +type loadConfigFileResult { + data: loadConfigFile + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 8a5e17962..70fe0d726 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -8,8 +8,11 @@ type Query { } directive @generate on FIELD_DEFINITION +directive @configfile on FIELD_DEFINITION type Mutation { createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate + saveConfigFile(data: saveConfigFileInput) : saveConfigFileResult @configfile + loadConfigFile(data: loadConfigFileInput) : loadConfigFileResult @configfile } diff --git a/src/services/api/graphql/recipes/config_file.py b/src/services/api/graphql/recipes/config_file.py new file mode 100644 index 000000000..850e5326e --- /dev/null +++ b/src/services/api/graphql/recipes/config_file.py @@ -0,0 +1,16 @@ + +from . recipe import Recipe + +class ConfigFile(Recipe): + def __init__(self, session, command_file): + super().__init__(session, command_file) + + # Define any custom processing of parameters here by overriding + # save/load: + # + # def save(self): + # self.data = transform_data(self.data) + # super().save() + # def load(self): + # self.data = transform_data(self.data) + # super().load() diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py index 8fbb9e0bf..91d8bd67a 100644 --- a/src/services/api/graphql/recipes/recipe.py +++ b/src/services/api/graphql/recipes/recipe.py @@ -46,4 +46,23 @@ class Recipe(object): except Exception as error: raise error + def save(self): + session = self._session + data = self.data + if 'file_name' not in data or not data['file_name']: + data['file_name'] = '/config/config.boot' + try: + session.save_config(data['file_name']) + except Exception as error: + raise error + + def load(self): + session = self._session + data = self.data + + try: + session.load_config(data['file_name']) + session.commit() + except Exception as error: + raise error -- cgit v1.2.3