diff options
| author | John Estabrook <jestabro@vyos.io> | 2022-10-06 15:37:19 -0500 | 
|---|---|---|
| committer | John Estabrook <jestabro@vyos.io> | 2022-10-07 15:00:41 -0500 | 
| commit | 5f81ced8d57d3e315252abc58e777ff946ccf494 (patch) | |
| tree | 39e3cd828df579fbe11e756b824b3c0456437420 /src | |
| parent | bb4901773df9682b67081dda5baf0cb39c742d1e (diff) | |
| download | vyos-1x-5f81ced8d57d3e315252abc58e777ff946ccf494.tar.gz vyos-1x-5f81ced8d57d3e315252abc58e777ff946ccf494.zip  | |
graphql: T4738: generate schema defs for configsession methods
Diffstat (limited to 'src')
14 files changed, 323 insertions, 142 deletions
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index d8ceefae6..62579d326 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -80,6 +80,22 @@ class ImageDirective(VyosDirective):          super().visit_field_definition(field, object_type,                                         make_resolver=make_image_resolver) +class ConfigSessionQueryDirective(VyosDirective): +    """ +    Class providing implementation of 'configsessionquery' directive in schema. +    """ +    def visit_field_definition(self, field, object_type): +        super().visit_field_definition(field, object_type, +                                       make_resolver=make_config_session_query_resolver) + +class ConfigSessionMutationDirective(VyosDirective): +    """ +    Class providing implementation of 'configsessionmutation' directive in schema. +    """ +    def visit_field_definition(self, field, object_type): +        super().visit_field_definition(field, object_type, +                                       make_resolver=make_config_session_mutation_resolver) +  class GenOpQueryDirective(VyosDirective):      """      Class providing implementation of 'genopquery' directive in schema. @@ -97,10 +113,8 @@ class GenOpMutationDirective(VyosDirective):                                         make_resolver=make_gen_op_mutation_resolver)  directives_dict = {"configure": ConfigureDirective, -                   "showconfig": ShowConfigDirective,                     "systemstatus": SystemStatusDirective, -                   "configfile": ConfigFileDirective, -                   "show": ShowDirective, -                   "image": ImageDirective, +                   "configsessionquery": ConfigSessionQueryDirective, +                   "configsessionmutation": ConfigSessionMutationDirective,                     "genopquery": GenOpQueryDirective,                     "genopmutation": GenOpMutationDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 5ccc9b0b6..84ee03eed 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -124,6 +124,10 @@ def make_config_file_resolver(mutation_name):  def make_image_resolver(mutation_name):      return make_prefix_resolver(mutation_name, prefix=['add', 'delete']) +def make_config_session_mutation_resolver(mutation_name): +    return make_mutation_resolver(mutation_name, mutation_name, +                                  convert_camel_case_to_snake(mutation_name)) +  def make_gen_op_mutation_resolver(mutation_name):      class_name = mutation_name      return make_mutation_resolver(mutation_name, class_name, 'gen_op_mutation') diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index b46914dcc..126a0d3c4 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -118,6 +118,9 @@ def make_show_resolver(query_name):      class_name = query_name      return make_query_resolver(query_name, class_name, 'show') +def make_config_session_query_resolver(query_name): +    return make_query_resolver(query_name, query_name, +                               convert_camel_case_to_snake(query_name)) +  def make_gen_op_query_resolver(query_name): -    class_name = query_name -    return make_query_resolver(query_name, class_name, 'gen_op_query') +    return make_query_resolver(query_name, query_name, 'gen_op_query') diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql deleted file mode 100644 index a7263114b..000000000 --- a/src/services/api/graphql/graphql/schema/config_file.graphql +++ /dev/null @@ -1,29 +0,0 @@ -input SaveConfigFileInput { -    key: String! -    fileName: String -} - -type SaveConfigFile { -    fileName: String -} - -type SaveConfigFileResult { -    data: SaveConfigFile -    success: Boolean! -    errors: [String] -} - -input LoadConfigFileInput { -    key: String! -    fileName: String! -} - -type LoadConfigFile { -    fileName: String! -} - -type LoadConfigFileResult { -    data: LoadConfigFile -    success: Boolean! -    errors: [String] -} diff --git a/src/services/api/graphql/graphql/schema/configsession.graphql b/src/services/api/graphql/graphql/schema/configsession.graphql new file mode 100644 index 000000000..b1deac4b3 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/configsession.graphql @@ -0,0 +1,115 @@ + +input ShowConfigInput { +    key: String! +    path: [String!]! +    configFormat: String = null +} + +type ShowConfig { +    result: Generic +} + +type ShowConfigResult { +    data: ShowConfig +    success: Boolean! +    errors: [String] +} + +extend type Query { +    ShowConfig(data: ShowConfigInput) : ShowConfigResult @configsessionquery +} + +input ShowInput { +    key: String! +    path: [String!]! +} + +type Show { +    result: Generic +} + +type ShowResult { +    data: Show +    success: Boolean! +    errors: [String] +} + +extend type Query { +    Show(data: ShowInput) : ShowResult @configsessionquery +} + +input SaveConfigFileInput { +    key: String! +    fileName: String = null +} + +type SaveConfigFile { +    result: Generic +} + +type SaveConfigFileResult { +    data: SaveConfigFile +    success: Boolean! +    errors: [String] +} + +extend type Mutation { +    SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configsessionmutation +} + +input LoadConfigFileInput { +    key: String! +    fileName: String! +} + +type LoadConfigFile { +    result: Generic +} + +type LoadConfigFileResult { +    data: LoadConfigFile +    success: Boolean! +    errors: [String] +} + +extend type Mutation { +    LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configsessionmutation +} + +input AddSystemImageInput { +    key: String! +    location: String! +} + +type AddSystemImage { +    result: Generic +} + +type AddSystemImageResult { +    data: AddSystemImage +    success: Boolean! +    errors: [String] +} + +extend type Mutation { +    AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @configsessionmutation +} + +input DeleteSystemImageInput { +    key: String! +    name: String! +} + +type DeleteSystemImage { +    result: Generic +} + +type DeleteSystemImageResult { +    data: DeleteSystemImage +    success: Boolean! +    errors: [String] +} + +extend type Mutation { +    DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @configsessionmutation +}
\ No newline at end of file diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql deleted file mode 100644 index 485033875..000000000 --- a/src/services/api/graphql/graphql/schema/image.graphql +++ /dev/null @@ -1,31 +0,0 @@ -input AddSystemImageInput { -    key: String! -    location: String! -} - -type AddSystemImage { -    location: String -    result: String -} - -type AddSystemImageResult { -    data: AddSystemImage -    success: Boolean! -    errors: [String] -} - -input DeleteSystemImageInput { -    key: String! -    name: String! -} - -type DeleteSystemImage { -    name: String -    result: String -} - -type DeleteSystemImageResult { -    data: DeleteSystemImage -    success: Boolean! -    errors: [String] -} diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 624be2620..fff7c43b0 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -4,19 +4,15 @@ schema {  }  directive @configure on FIELD_DEFINITION -directive @configfile on FIELD_DEFINITION -directive @show on FIELD_DEFINITION -directive @showconfig on FIELD_DEFINITION  directive @systemstatus on FIELD_DEFINITION -directive @image on FIELD_DEFINITION +directive @configsessionquery on FIELD_DEFINITION +directive @configsessionmutation on FIELD_DEFINITION  directive @genopquery on FIELD_DEFINITION  directive @genopmutation on FIELD_DEFINITION  scalar Generic  type Query { -    Show(data: ShowInput) : ShowResult @show -    ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig      SystemStatus(data: SystemStatusInput) : SystemStatusResult @systemstatus  } @@ -29,8 +25,4 @@ type Mutation {      CreateFirewallAddressIpv6Group(data: CreateFirewallAddressIpv6GroupInput) : CreateFirewallAddressIpv6GroupResult @configure      UpdateFirewallAddressIpv6GroupMembers(data: UpdateFirewallAddressIpv6GroupMembersInput) : UpdateFirewallAddressIpv6GroupMembersResult @configure      RemoveFirewallAddressIpv6GroupMembers(data: RemoveFirewallAddressIpv6GroupMembersInput) : RemoveFirewallAddressIpv6GroupMembersResult @configure -    SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile -    LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile -    AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @image -    DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @image  } diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql deleted file mode 100644 index 278ed536b..000000000 --- a/src/services/api/graphql/graphql/schema/show.graphql +++ /dev/null @@ -1,15 +0,0 @@ -input ShowInput { -    key: String! -    path: [String!]! -} - -type Show { -    path: [String] -    result: String -} - -type ShowResult { -    data: Show -    success: Boolean! -    errors: [String] -} diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql deleted file mode 100644 index 5a1fe43da..000000000 --- a/src/services/api/graphql/graphql/schema/show_config.graphql +++ /dev/null @@ -1,21 +0,0 @@ -""" -Use 'scalar Generic' for show config output, to avoid attempts to -JSON-serialize in case of JSON output. -""" - -input ShowConfigInput { -    key: String! -    path: [String!]! -    configFormat: String -} - -type ShowConfig { -    path: [String] -    result: Generic -} - -type ShowConfigResult { -    data: ShowConfig -    success: Boolean! -    errors: [String] -} diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index f7510841e..61dd3ed1b 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -94,7 +94,7 @@ class Session:          return out -    def save(self): +    def save_config_file(self):          session = self._session          data = self._data          if 'file_name' not in data or not data['file_name']: @@ -105,7 +105,7 @@ class Session:          except Exception as error:              raise error -    def load(self): +    def load_config_file(self):          session = self._session          data = self._data @@ -127,7 +127,7 @@ class Session:          return out -    def add(self): +    def add_system_image(self):          session = self._session          data = self._data @@ -138,7 +138,7 @@ class Session:          return res -    def delete(self): +    def delete_system_image(self):          session = self._session          data = self._data diff --git a/src/services/api/graphql/utils/config_session_function.py b/src/services/api/graphql/utils/config_session_function.py new file mode 100644 index 000000000..fc0dd7a87 --- /dev/null +++ b/src/services/api/graphql/utils/config_session_function.py @@ -0,0 +1,28 @@ +# typing information for native configsession functions; used to generate +# schema definition files +import typing + +def show_config(path: list[str], configFormat: typing.Optional[str]): +    pass + +def show(path: list[str]): +    pass + +queries = {'show_config': show_config, +           'show': show} + +def save_config_file(fileName: typing.Optional[str]): +    pass +def load_config_file(fileName: str): +    pass +def add_system_image(location: str): +    pass +def delete_system_image(name: str): +    pass + +mutations = {'save_config_file': save_config_file, +             'load_config_file': load_config_file, +             'add_system_image': add_system_image, +             'delete_system_image': delete_system_image} + + diff --git a/src/services/api/graphql/utils/schema_from_config_session.py b/src/services/api/graphql/utils/schema_from_config_session.py new file mode 100755 index 000000000..ea78aaf88 --- /dev/null +++ b/src/services/api/graphql/utils/schema_from_config_session.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +# +# +# A utility to generate GraphQL schema defintions from typing information of +# (wrappers of) native configsession functions. + +import os +import json +from inspect import signature, getmembers, isfunction, isclass, getmro +from jinja2 import Template + +if __package__ is None or __package__ == '': +    from util import snake_to_pascal_case, map_type_name +else: +    from . util import snake_to_pascal_case, map_type_name + +# this will be run locally before the build +SCHEMA_PATH = '../graphql/schema' + +schema_data: dict = {'schema_name': '', +                     'schema_fields': []} + +query_template  = """ +input {{ schema_name }}Input { +    key: String! +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} + +type {{ schema_name }} { +    result: Generic +} + +type {{ schema_name }}Result { +    data: {{ schema_name }} +    success: Boolean! +    errors: [String] +} + +extend type Query { +    {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionquery +} +""" + +mutation_template  = """ +input {{ schema_name }}Input { +    key: String! +    {%- for field_entry in schema_fields %} +    {{ field_entry }} +    {%- endfor %} +} + +type {{ schema_name }} { +    result: Generic +} + +type {{ schema_name }}Result { +    data: {{ schema_name }} +    success: Boolean! +    errors: [String] +} + +extend type Mutation { +    {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionmutation +} +""" + +def create_schema(func_name: str, func: callable, template: str) -> str: +    sig = signature(func) + +    field_dict = {} +    for k in sig.parameters: +        field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation) + +    schema_fields = [] +    for k,v in field_dict.items(): +        schema_fields.append(k+': '+v) + +    schema_data['schema_name'] = snake_to_pascal_case(func_name) +    schema_data['schema_fields'] = schema_fields + +    j2_template = Template(template) +    res = j2_template.render(schema_data) + +    return res + +def generate_config_session_definitions(): +    from config_session_function import queries, mutations + +    results = [] +    for name,func in queries.items(): +        res = create_schema(name, func, query_template) +        results.append(res) + +    for name,func in mutations.items(): +        res = create_schema(name, func, mutation_template) +        results.append(res) + +    out = '\n'.join(results) +    with open(f'{SCHEMA_PATH}/configsession.graphql', 'w') as f: +        f.write(out) + +if __name__ == '__main__': +    generate_config_session_definitions() diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py index 379d15250..57d63628b 100755 --- a/src/services/api/graphql/utils/schema_from_op_mode.py +++ b/src/services/api/graphql/utils/schema_from_op_mode.py @@ -20,15 +20,16 @@  import os  import json -import typing  from inspect import signature, getmembers, isfunction, isclass, getmro  from jinja2 import Template  from vyos.defaults import directories  if __package__ is None or __package__ == '':      from util import load_as_module, is_op_mode_function_name, is_show_function_name +    from util import snake_to_pascal_case, map_type_name  else:      from . util import load_as_module, is_op_mode_function_name, is_show_function_name +    from . util import snake_to_pascal_case, map_type_name  OP_MODE_PATH = directories['op_mode']  SCHEMA_PATH = directories['api_schema'] @@ -103,35 +104,12 @@ type {{ name }} implements OpModeError {  {%- endfor %}  """ -def _snake_to_pascal_case(name: str) -> str: -    res = ''.join(map(str.title, name.split('_'))) -    return res - -def _map_type_name(type_name: type, optional: bool = False) -> str: -    if type_name == str: -        return 'String!' if not optional else 'String = null' -    if type_name == int: -        return 'Int!' if not optional else 'Int = null' -    if type_name == bool: -        return 'Boolean!' if not optional else 'Boolean = false' -    if typing.get_origin(type_name) == list: -        if not optional: -            return f'[{_map_type_name(typing.get_args(type_name)[0])}]!' -        return f'[{_map_type_name(typing.get_args(type_name)[0])}]' -    # typing.Optional is typing.Union[_, NoneType] -    if (typing.get_origin(type_name) is typing.Union and -            typing.get_args(type_name)[1] == type(None)): -        return f'{_map_type_name(typing.get_args(type_name)[0], optional=True)}' - -    # scalar 'Generic' is defined in schema.graphql -    return 'Generic' -  def create_schema(func_name: str, base_name: str, func: callable) -> str:      sig = signature(func)      field_dict = {}      for k in sig.parameters: -        field_dict[sig.parameters[k].name] = _map_type_name(sig.parameters[k].annotation) +        field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation)      # It is assumed that if one is generating a schema for a 'show_*'      # function, that 'get_raw_data' is present and 'raw' is desired. @@ -142,7 +120,7 @@ def create_schema(func_name: str, base_name: str, func: callable) -> str:      for k,v in field_dict.items():          schema_fields.append(k+': '+v) -    schema_data['schema_name'] = _snake_to_pascal_case(func_name + '_' + base_name) +    schema_data['schema_name'] = snake_to_pascal_case(func_name + '_' + base_name)      schema_data['schema_fields'] = schema_fields      if is_show_function_name(func_name): diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/utils/util.py index 073126853..da2bcdb5b 100644 --- a/src/services/api/graphql/utils/util.py +++ b/src/services/api/graphql/utils/util.py @@ -15,6 +15,7 @@  import os  import re +import typing  import importlib.util  from vyos.defaults import directories @@ -74,3 +75,26 @@ def split_compound_op_mode_name(name: str, files: list):              pair = (pair[0], f[0])              return pair      return (name, '') + +def snake_to_pascal_case(name: str) -> str: +    res = ''.join(map(str.title, name.split('_'))) +    return res + +def map_type_name(type_name: type, optional: bool = False) -> str: +    if type_name == str: +        return 'String!' if not optional else 'String = null' +    if type_name == int: +        return 'Int!' if not optional else 'Int = null' +    if type_name == bool: +        return 'Boolean!' if not optional else 'Boolean = false' +    if typing.get_origin(type_name) == list: +        if not optional: +            return f'[{map_type_name(typing.get_args(type_name)[0])}]!' +        return f'[{map_type_name(typing.get_args(type_name)[0])}]' +    # typing.Optional is typing.Union[_, NoneType] +    if (typing.get_origin(type_name) is typing.Union and +            typing.get_args(type_name)[1] == type(None)): +        return f'{map_type_name(typing.get_args(type_name)[0], optional=True)}' + +    # scalar 'Generic' is defined in schema.graphql +    return 'Generic'  | 
