diff options
Diffstat (limited to 'src')
18 files changed, 509 insertions, 129 deletions
| diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 82956b219..c1cfc1dcb 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -242,21 +242,20 @@ def apply(vrf):          if tmp == 0:              cmd('nft delete table inet vrf_zones') -    # add configuration to FRR -    frr_cfg = frr.FRRConfig() -    frr_cfg.load_configuration(frr_daemon) -    frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '') -    frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config']) -    frr_cfg.commit_configuration(frr_daemon) - -    # If FRR config is blank, rerun the blank commit x times due to frr-reload -    # behavior/bug not properly clearing out on one commit. -    if vrf['new_frr_config'] == '': -        for a in range(5): -            frr_cfg.commit_configuration(frr_daemon) - -    # Save configuration to /run/frr/config/frr.conf -    frr.save_configuration() +    #   T3694: Somehow we hit a priority inversion here as we need to remove the +    #   VRF assigned VNI before we can remove a BGP bound VRF instance. Maybe +    #   move this to an individual helper script that set's up the VNI for the +    #   given VRF after any routing protocol. +    # +    #   # add configuration to FRR +    #   frr_cfg = frr.FRRConfig() +    #   frr_cfg.load_configuration(frr_daemon) +    #   frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '') +    #   frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config']) +    #   frr_cfg.commit_configuration(frr_daemon) +    # +    #   # Save configuration to /run/frr/config/frr.conf +    #   frr.save_configuration()      return None diff --git a/src/migration-scripts/quagga/7-to-8 b/src/migration-scripts/quagga/7-to-8 index 38507bd3d..15c44924f 100755 --- a/src/migration-scripts/quagga/7-to-8 +++ b/src/migration-scripts/quagga/7-to-8 @@ -14,76 +14,14 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# - T2450: drop interface-route and interface-route6 from "protocols static" +# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths" +#   under the IPv4 address-family tree. Reason is we currently have no way in +#   configuring this for IPv6 address-family. This mimics the FRR configuration.  from sys import argv  from sys import exit -  from vyos.configtree import ConfigTree -def migrate_interface_route(config, base, path, route_route6): -    """ Generic migration function which can be called on every instance of -    interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes. - -    What we do? -      - Drop 'interface-route' or 'interface-route6' and migrate the route unter the -        'route' or 'route6' tag node. -    """ -    if config.exists(base + path): -        for route in config.list_nodes(base + path): -            interface = config.list_nodes(base + path + [route, 'next-hop-interface']) - -            tmp = base + path + [route, 'next-hop-interface'] -            for interface in config.list_nodes(tmp): -                new_base = base + [route_route6, route, 'interface'] -                config.set(new_base) -                config.set_tag(base + [route_route6]) -                config.set_tag(new_base) -                config.copy(tmp + [interface], new_base + [interface]) - -        config.delete(base + path) - -def migrate_route(config, base, path, route_route6): -    """ Generic migration function which can be called on every instance of -    route, beeing it ipv4, ipv6 or even nested under the static table nodes. - -    What we do? -      - for consistency reasons rename next-hop-interface to interface -      - for consistency reasons rename next-hop-vrf to vrf -    """ -    if config.exists(base + path): -        for route in config.list_nodes(base + path): -            next_hop = base + path + [route, 'next-hop'] -            if config.exists(next_hop): -                for gateway in config.list_nodes(next_hop): -                    # IPv4 routes calls it next-hop-interface, rename this to -                    # interface instead so it's consitent with IPv6 -                    interface_path = next_hop + [gateway, 'next-hop-interface'] -                    if config.exists(interface_path): -                        config.rename(interface_path, 'interface') - -                    # When VRFs got introduced, I (c-po) named it next-hop-vrf, -                    # we can also call it vrf which is simply shorter. -                    vrf_path = next_hop + [gateway, 'next-hop-vrf'] -                    if config.exists(vrf_path): -                        config.rename(vrf_path, 'vrf') - -            next_hop = base + path + [route, 'interface'] -            if config.exists(next_hop): -                for interface in config.list_nodes(next_hop): -                    # IPv4 routes calls it next-hop-interface, rename this to -                    # interface instead so it's consitent with IPv6 -                    interface_path = next_hop + [interface, 'next-hop-interface'] -                    if config.exists(interface_path): -                        config.rename(interface_path, 'interface') - -                    # When VRFs got introduced, I (c-po) named it next-hop-vrf, -                    # we can also call it vrf which is simply shorter. -                    vrf_path = next_hop + [interface, 'next-hop-vrf'] -                    if config.exists(vrf_path): -                        config.rename(vrf_path, 'vrf') - -  if (len(argv) < 2):      print("Must specify file name!")      exit(1) @@ -93,41 +31,27 @@ file_name = argv[1]  with open(file_name, 'r') as f:      config_file = f.read() -base = ['protocols', 'static'] - +base = ['protocols', 'bgp']  config = ConfigTree(config_file) +  if not config.exists(base):      # Nothing to do      exit(0) -# Migrate interface-route into route -migrate_interface_route(config, base, ['interface-route'], 'route') - -# Migrate interface-route6 into route6 -migrate_interface_route(config, base, ['interface-route6'], 'route6') - -# Cleanup nodes inside route -migrate_route(config, base, ['route'], 'route') - -# Cleanup nodes inside route6 -migrate_route(config, base, ['route6'], 'route6') - -# -# PBR table cleanup -table_path = base + ['table'] -if config.exists(table_path): -    for table in config.list_nodes(table_path): -        # Migrate interface-route into route -        migrate_interface_route(config, table_path + [table], ['interface-route'], 'route') - -        # Migrate interface-route6 into route6 -        migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6') - -        # Cleanup nodes inside route -        migrate_route(config, table_path + [table], ['route'], 'route') - -        # Cleanup nodes inside route6 -        migrate_route(config, table_path + [table], ['route6'], 'route6') +# Check if BGP is actually configured and obtain the ASN +asn_list = config.list_nodes(base) +if asn_list: +    # There's always just one BGP node, if any +    bgp_base = base + [asn_list[0]] + +    maximum_paths = bgp_base + ['maximum-paths'] +    if config.exists(maximum_paths): +        for bgp_type in ['ebgp', 'ibgp']: +            if config.exists(maximum_paths + [bgp_type]): +                new_base =  bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths'] +                config.set(new_base) +                config.copy(maximum_paths + [bgp_type], new_base + [bgp_type]) +        config.delete(maximum_paths)  try:      with open(file_name, 'w') as f: diff --git a/src/migration-scripts/quagga/8-to-9 b/src/migration-scripts/quagga/8-to-9 index 15c44924f..38507bd3d 100755 --- a/src/migration-scripts/quagga/8-to-9 +++ b/src/migration-scripts/quagga/8-to-9 @@ -14,14 +14,76 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths" -#   under the IPv4 address-family tree. Reason is we currently have no way in -#   configuring this for IPv6 address-family. This mimics the FRR configuration. +# - T2450: drop interface-route and interface-route6 from "protocols static"  from sys import argv  from sys import exit +  from vyos.configtree import ConfigTree +def migrate_interface_route(config, base, path, route_route6): +    """ Generic migration function which can be called on every instance of +    interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes. + +    What we do? +      - Drop 'interface-route' or 'interface-route6' and migrate the route unter the +        'route' or 'route6' tag node. +    """ +    if config.exists(base + path): +        for route in config.list_nodes(base + path): +            interface = config.list_nodes(base + path + [route, 'next-hop-interface']) + +            tmp = base + path + [route, 'next-hop-interface'] +            for interface in config.list_nodes(tmp): +                new_base = base + [route_route6, route, 'interface'] +                config.set(new_base) +                config.set_tag(base + [route_route6]) +                config.set_tag(new_base) +                config.copy(tmp + [interface], new_base + [interface]) + +        config.delete(base + path) + +def migrate_route(config, base, path, route_route6): +    """ Generic migration function which can be called on every instance of +    route, beeing it ipv4, ipv6 or even nested under the static table nodes. + +    What we do? +      - for consistency reasons rename next-hop-interface to interface +      - for consistency reasons rename next-hop-vrf to vrf +    """ +    if config.exists(base + path): +        for route in config.list_nodes(base + path): +            next_hop = base + path + [route, 'next-hop'] +            if config.exists(next_hop): +                for gateway in config.list_nodes(next_hop): +                    # IPv4 routes calls it next-hop-interface, rename this to +                    # interface instead so it's consitent with IPv6 +                    interface_path = next_hop + [gateway, 'next-hop-interface'] +                    if config.exists(interface_path): +                        config.rename(interface_path, 'interface') + +                    # When VRFs got introduced, I (c-po) named it next-hop-vrf, +                    # we can also call it vrf which is simply shorter. +                    vrf_path = next_hop + [gateway, 'next-hop-vrf'] +                    if config.exists(vrf_path): +                        config.rename(vrf_path, 'vrf') + +            next_hop = base + path + [route, 'interface'] +            if config.exists(next_hop): +                for interface in config.list_nodes(next_hop): +                    # IPv4 routes calls it next-hop-interface, rename this to +                    # interface instead so it's consitent with IPv6 +                    interface_path = next_hop + [interface, 'next-hop-interface'] +                    if config.exists(interface_path): +                        config.rename(interface_path, 'interface') + +                    # When VRFs got introduced, I (c-po) named it next-hop-vrf, +                    # we can also call it vrf which is simply shorter. +                    vrf_path = next_hop + [interface, 'next-hop-vrf'] +                    if config.exists(vrf_path): +                        config.rename(vrf_path, 'vrf') + +  if (len(argv) < 2):      print("Must specify file name!")      exit(1) @@ -31,27 +93,41 @@ file_name = argv[1]  with open(file_name, 'r') as f:      config_file = f.read() -base = ['protocols', 'bgp'] -config = ConfigTree(config_file) +base = ['protocols', 'static'] +config = ConfigTree(config_file)  if not config.exists(base):      # Nothing to do      exit(0) -# Check if BGP is actually configured and obtain the ASN -asn_list = config.list_nodes(base) -if asn_list: -    # There's always just one BGP node, if any -    bgp_base = base + [asn_list[0]] - -    maximum_paths = bgp_base + ['maximum-paths'] -    if config.exists(maximum_paths): -        for bgp_type in ['ebgp', 'ibgp']: -            if config.exists(maximum_paths + [bgp_type]): -                new_base =  bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths'] -                config.set(new_base) -                config.copy(maximum_paths + [bgp_type], new_base + [bgp_type]) -        config.delete(maximum_paths) +# Migrate interface-route into route +migrate_interface_route(config, base, ['interface-route'], 'route') + +# Migrate interface-route6 into route6 +migrate_interface_route(config, base, ['interface-route6'], 'route6') + +# Cleanup nodes inside route +migrate_route(config, base, ['route'], 'route') + +# Cleanup nodes inside route6 +migrate_route(config, base, ['route6'], 'route6') + +# +# PBR table cleanup +table_path = base + ['table'] +if config.exists(table_path): +    for table in config.list_nodes(table_path): +        # Migrate interface-route into route +        migrate_interface_route(config, table_path + [table], ['interface-route'], 'route') + +        # Migrate interface-route6 into route6 +        migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6') + +        # Cleanup nodes inside route +        migrate_route(config, table_path + [table], ['route'], 'route') + +        # Cleanup nodes inside route6 +        migrate_route(config, table_path + [table], ['route6'], 'route6')  try:      with open(file_name, 'w') as f: 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 diff --git a/src/services/api/graphql/graphql/__init__.py b/src/services/api/graphql/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/services/api/graphql/graphql/__init__.py diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py new file mode 100644 index 000000000..651421c35 --- /dev/null +++ b/src/services/api/graphql/graphql/directives.py @@ -0,0 +1,17 @@ +from ariadne import SchemaDirectiveVisitor, ObjectType +from . mutations import make_resolver + +class DataDirective(SchemaDirectiveVisitor): +    """ +    Class providing implementation of 'generate' 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_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 new file mode 100644 index 000000000..98c665c9a --- /dev/null +++ b/src/services/api/graphql/graphql/mutations.py @@ -0,0 +1,60 @@ + +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 + +mutation = ObjectType("Mutation") + +def make_resolver(mutation_name): +    """Dynamically generate a resolver for the mutation named in the +    schema by 'mutation_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: +        encapsulating 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}' +    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) +            k.configure() + +            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/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql new file mode 100644 index 000000000..a7ee75d40 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -0,0 +1,35 @@ +input dhcpServerConfigInput { +    sharedNetworkName: String +    subnet: String +    defaultRouter: String +    dnsServer: String +    domainName: String +    lease: Int +    range: Int +    start: String +    stop: String +    dnsForwardingAllowFrom: String +    dnsForwardingCacheSize: Int +    dnsForwardingListenAddress: String +} + +type dhcpServerConfig { +    sharedNetworkName: String +    subnet: String +    defaultRouter: String +    dnsServer: String +    domainName: String +    lease: Int +    range: Int +    start: String +    stop: String +    dnsForwardingAllowFrom: String +    dnsForwardingCacheSize: Int +    dnsForwardingListenAddress: String +} + +type createDhcpServerResult { +    data: dhcpServerConfig +    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 new file mode 100644 index 000000000..fdcf97bad --- /dev/null +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -0,0 +1,18 @@ +input interfaceEthernetConfigInput { +    interface: String +    address: String +    replace: Boolean = true +    description: String +} + +type interfaceEthernetConfig { +    interface: String +    address: String +    description: String +} + +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 new file mode 100644 index 000000000..8a5e17962 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -0,0 +1,15 @@ +schema { +    query: Query +    mutation: Mutation +} + +type Query { +    _dummy: String +} + +directive @generate on FIELD_DEFINITION + +type Mutation { +    createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate +    createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate +} diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/services/api/graphql/recipes/__init__.py diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py new file mode 100644 index 000000000..3edb3028e --- /dev/null +++ b/src/services/api/graphql/recipes/dhcp_server.py @@ -0,0 +1,13 @@ + +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 new file mode 100644 index 000000000..f88f5924f --- /dev/null +++ b/src/services/api/graphql/recipes/interface_ethernet.py @@ -0,0 +1,13 @@ + +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 new file mode 100644 index 000000000..8fbb9e0bf --- /dev/null +++ b/src/services/api/graphql/recipes/recipe.py @@ -0,0 +1,49 @@ +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/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl new file mode 100644 index 000000000..629ce83c1 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl @@ -0,0 +1,9 @@ +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 }} 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 }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }} +set service dns forwarding allow-from {{ dns_forwarding_allow_from }} +set service dns forwarding cache-size {{ dns_forwarding_cache_size }} +set service dns forwarding listen-address {{ dns_forwarding_listen_address }} diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl new file mode 100644 index 000000000..d9d7ed691 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl @@ -0,0 +1,5 @@ +{% if replace %} +delete interfaces ethernet {{ interface }} address +{% endif %} +set interfaces ethernet {{ interface }} address {{ address }} +set interfaces ethernet {{ interface }} description {{ description }} diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py new file mode 100644 index 000000000..63db9f4ef --- /dev/null +++ b/src/services/api/graphql/state.py @@ -0,0 +1,4 @@ + +def init(): +    global settings +    settings = {} diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cbf321dc8..cb4ce4072 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -36,10 +36,16 @@ from starlette.datastructures import FormData, MutableHeaders  from starlette.formparsers import FormParser, MultiPartParser  from multipart.multipart import parse_options_header +from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers +from ariadne.asgi import GraphQL +  import vyos.config +import vyos.defaults  from vyos.configsession import ConfigSession, ConfigSessionError +import api.graphql.state +  DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'  CFG_GROUP = 'vyattacfg' @@ -603,6 +609,25 @@ def show_op(data: ShowModel):      return success(res) +### +# GraphQL integration +### + +api.graphql.state.init() + +from api.graphql.graphql.mutations import mutation +from api.graphql.graphql.directives import DataDirective + +api_schema_dir = vyos.defaults.directories['api_schema'] + +type_defs = load_schema_from_path(api_schema_dir) + +schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) + +app.add_route('/graphql', GraphQL(schema, debug=True)) + +### +  if __name__ == '__main__':      # systemd's user and group options don't work, do it by hand here,      # else no one else will be able to commit @@ -626,6 +651,8 @@ if __name__ == '__main__':      app.state.vyos_debug = True if server_config['debug'] == 'true' else False      app.state.vyos_strict = True if server_config['strict'] == 'true' else False +    api.graphql.state.settings['app'] = app +      try:          uvicorn.run(app, host=server_config["listen_address"],                           port=int(server_config["port"]), | 
