diff options
| author | John Estabrook <jestabro@vyos.io> | 2021-12-12 17:30:56 -0600 | 
|---|---|---|
| committer | John Estabrook <jestabro@vyos.io> | 2021-12-12 17:34:14 -0600 | 
| commit | 30311db5a00c78872c9ad9b29e7081e0d81a5362 (patch) | |
| tree | 883b454c0d18d870d804232d3b6d74bb6664b268 | |
| parent | c3471fe9d4cf0aab46feae94618925a95bcd5411 (diff) | |
| download | vyos-1x-30311db5a00c78872c9ad9b29e7081e0d81a5362.tar.gz vyos-1x-30311db5a00c78872c9ad9b29e7081e0d81a5362.zip | |
graphql: T3993: distinguish queries and mutations; update README.graphql
| -rw-r--r-- | src/services/api/graphql/README.graphql | 55 | ||||
| -rw-r--r-- | src/services/api/graphql/bindings.py | 18 | ||||
| -rw-r--r-- | src/services/api/graphql/graphql/directives.py | 16 | ||||
| -rw-r--r-- | src/services/api/graphql/graphql/mutations.py | 52 | ||||
| -rw-r--r-- | src/services/api/graphql/graphql/queries.py | 89 | ||||
| -rw-r--r-- | src/services/api/graphql/graphql/schema/schema.graphql | 11 | ||||
| -rw-r--r-- | src/services/api/graphql/recipes/remove_firewall_address_group_members.py | 14 | ||||
| -rw-r--r-- | src/services/api/graphql/recipes/session.py | 15 | 
8 files changed, 230 insertions, 40 deletions
| diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index a3c30b005..6aa834329 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' @@ -58,8 +63,8 @@ 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 the same 'endpoint' (meaning the form of the request and -resolver; the actual enpoint for all GraphQL requests is +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. @@ -75,7 +80,7 @@ mutation {  Op-mode 'show' commands may be requested by path, e.g.: -mutation { +query {    Show (data: {path: ["interfaces", "ethernet", "detail"]}) {      success      errors @@ -88,16 +93,52 @@ mutation {  N.B. to see the output the 'data' field 'result' must be present in the  request. -The GraphQL playground will be found at: +Mutations to manipulate firewall address groups: -https://{{ host_address }}/graphql +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 +  } +} -An equivalent curl command to the first example above would be: +mutation { +  RemoveFirewallAddressGroupMembers (data: {name: "ADDR-GRP", +                                            address: "192.168.0.1"}) { +    success +    errors +  } +} + +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 + + +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 diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 1fbe13d0c..84d719fda 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -1,4 +1,20 @@ +# 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 @@ -8,6 +24,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=directives_dict) +    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 10bc522db..0a9298f55 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,4 +1,20 @@ +# 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): diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 8e5aab56d..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 @@ -10,7 +24,7 @@ from api.graphql.recipes.session import Session  mutation = ObjectType("Mutation") -def make_resolver(mutation_name, class_name, session_func): +def make_mutation_resolver(mutation_name, class_name, session_func):      """Dynamically generate a resolver for the mutation named in the      schema by 'mutation_name'. @@ -66,34 +80,20 @@ def make_resolver(mutation_name, class_name, session_func):      return func_impl -def make_configure_resolver(mutation_name): -    class_name = mutation_name -    return make_resolver(mutation_name, class_name, 'configure') +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_show_config_resolver(mutation_name): +def make_configure_resolver(mutation_name):      class_name = mutation_name -    return make_resolver(mutation_name, class_name, 'show_config') +    return make_mutation_resolver(mutation_name, class_name, 'configure')  def make_config_file_resolver(mutation_name): -    if 'Save' in mutation_name: -        class_name = mutation_name.replace('Save', '', 1) -        return make_resolver(mutation_name, class_name, 'save') -    elif 'Load' in mutation_name: -        class_name = mutation_name.replace('Load', '', 1) -        return make_resolver(mutation_name, class_name, 'load') -    else: -        raise Exception - -def make_show_resolver(mutation_name): -    class_name = mutation_name -    return make_resolver(mutation_name, class_name, 'show') +    return make_prefix_resolver(mutation_name, prefix=['save', 'load'])  def make_image_resolver(mutation_name): -    if 'Add' in mutation_name: -        class_name = mutation_name.replace('Add', '', 1) -        return make_resolver(mutation_name, class_name, 'add') -    elif 'Delete' in mutation_name: -        class_name = mutation_name.replace('Delete', '', 1) -        return make_resolver(mutation_name, class_name, 'delete') -    else: -        raise Exception +    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/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index c6899bee6..ce58b991a 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -3,16 +3,17 @@ schema {      mutation: Mutation  } -type Query { -    _dummy: String -} -  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 { +    Show(data: ShowInput) : ShowResult @show +    ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig +} +  type Mutation {      CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure      CreateInterfaceEthernet(data: InterfaceEthernetConfigInput) : CreateInterfaceEthernetResult @configure @@ -21,8 +22,6 @@ type Mutation {      RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure      SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile      LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile -    Show(data: ShowInput) : ShowResult @show -    ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig      AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @image      DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @image  } 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 index cde30c27a..b91932e14 100644 --- a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py +++ b/src/services/api/graphql/recipes/remove_firewall_address_group_members.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 . session import Session diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py index 5ece78ee6..1f844ff70 100644 --- a/src/services/api/graphql/recipes/session.py +++ b/src/services/api/graphql/recipes/session.py @@ -1,3 +1,18 @@ +# 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 | 
