diff options
33 files changed, 488 insertions, 434 deletions
| diff --git a/data/templates/conntrackd/conntrackd.conf.j2 b/data/templates/conntrackd/conntrackd.conf.j2 index 66024869d..808a77759 100644 --- a/data/templates/conntrackd/conntrackd.conf.j2 +++ b/data/templates/conntrackd/conntrackd.conf.j2 @@ -9,7 +9,9 @@ Sync {  {%     if iface_config.peer is vyos_defined %}      UDP {  {%         if listen_address is vyos_defined %} -        IPv4_address {{ listen_address }} +{%             for address in listen_address %} +        IPv4_address {{ address }} +{%             endfor %}  {%         endif %}          IPv4_Destination_Address {{ iface_config.peer }}          Port {{ iface_config.port if iface_config.port is vyos_defined else '3780' }} diff --git a/data/templates/firewall/nftables-zone.j2 b/data/templates/firewall/nftables-zone.j2 index 919881e19..17ef5101d 100644 --- a/data/templates/firewall/nftables-zone.j2 +++ b/data/templates/firewall/nftables-zone.j2 @@ -39,18 +39,22 @@  {%     if zone_conf.local_zone is vyos_defined %}      chain VZONE_{{ zone_name }}_IN {          iifname lo counter return -{%         for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %} +{%         if zone_conf.from is vyos_defined %} +{%             for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %}          iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }}          iifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%         endfor %} +{%             endfor %} +{%         endif %}          {{ zone_conf | nft_default_rule('zone_' + zone_name) }}      }      chain VZONE_{{ zone_name }}_OUT {          oifname lo counter return -{%         for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall[fw_name] is vyos_defined %} +{%         if zone_conf.from_local is vyos_defined %} +{%             for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall[fw_name] is vyos_defined %}          oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }}          oifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%         endfor %} +{%             endfor %} +{%         endif %}          {{ zone_conf | nft_default_rule('zone_' + zone_name) }}      }  {%     else %} @@ -59,12 +63,14 @@  {%         if zone_conf.intra_zone_filtering is vyos_defined %}          iifname { {{ zone_conf.interface | join(",") }} } counter return  {%         endif %} -{%         for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %} -{%             if zone[from_zone].local_zone is not defined %} +{%         if zone_conf.from is vyos_defined %} +{%             for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %} +{%                 if zone[from_zone].local_zone is not defined %}          iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }}          iifname { {{ zone[from_zone].interface | join(",") }} } counter return -{%             endif %} -{%         endfor %} +{%                 endif %} +{%             endfor %} +{%         endif %}          {{ zone_conf | nft_default_rule('zone_' + zone_name) }}      }  {%     endif %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index 9d609f73f..a0f0b8c11 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -204,13 +204,13 @@ table ip6 vyos_filter {  {% if state_policy is vyos_defined %}      chain VYOS_STATE_POLICY6 {  {%     if state_policy.established is vyos_defined %} -        {{ state_policy.established | nft_state_policy('established', ipv6=True) }} +        {{ state_policy.established | nft_state_policy('established') }}  {%     endif %}  {%     if state_policy.invalid is vyos_defined %} -        {{ state_policy.invalid | nft_state_policy('invalid', ipv6=True) }} +        {{ state_policy.invalid | nft_state_policy('invalid') }}  {%     endif %}  {%     if state_policy.related is vyos_defined %} -        {{ state_policy.related | nft_state_policy('related', ipv6=True) }} +        {{ state_policy.related | nft_state_policy('related') }}  {%     endif %}          return      } diff --git a/data/templates/ssh/sshd_config.j2 b/data/templates/ssh/sshd_config.j2 index e7dbca581..79b07478b 100644 --- a/data/templates/ssh/sshd_config.j2 +++ b/data/templates/ssh/sshd_config.j2 @@ -96,3 +96,7 @@ DenyGroups {{ access_control.deny.group | join(' ') }}  # sshd(8) will send a message through the encrypted channel to request a response from the client  ClientAliveInterval {{ client_keepalive_interval }}  {% endif %} + +{% if rekey.data is vyos_defined  %} +RekeyLimit {{ rekey.data }}M {{ rekey.time + 'M' if rekey.time is vyos_defined }} +{% endif %} diff --git a/debian/vyos-1x.preinst b/debian/vyos-1x.preinst index 71750b3a1..213a23d9e 100644 --- a/debian/vyos-1x.preinst +++ b/debian/vyos-1x.preinst @@ -2,3 +2,4 @@ dpkg-divert --package vyos-1x --add --rename /etc/securetty  dpkg-divert --package vyos-1x --add --rename /etc/security/capability.conf  dpkg-divert --package vyos-1x --add --rename /lib/systemd/system/lcdproc.service  dpkg-divert --package vyos-1x --add --rename /etc/logrotate.d/conntrackd +dpkg-divert --package vyos-1x --add --rename /usr/share/pam-configs/radius diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 773e86f00..673461036 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -711,6 +711,7 @@              </properties>              <children>                #include <include/firewall/action-accept-drop-reject.xml.i> +              #include <include/firewall/log.xml.i>                #include <include/firewall/rule-log-level.xml.i>              </children>            </node> @@ -720,6 +721,7 @@              </properties>              <children>                #include <include/firewall/action-accept-drop-reject.xml.i> +              #include <include/firewall/log.xml.i>                #include <include/firewall/rule-log-level.xml.i>              </children>            </node> @@ -729,6 +731,7 @@              </properties>              <children>                #include <include/firewall/action-accept-drop-reject.xml.i> +              #include <include/firewall/log.xml.i>                #include <include/firewall/rule-log-level.xml.i>              </children>            </node> diff --git a/interface-definitions/include/policy/route-common-rule-ipv6.xml.i b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i index cfeba1a6c..662206336 100644 --- a/interface-definitions/include/policy/route-common-rule-ipv6.xml.i +++ b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i @@ -198,6 +198,10 @@            <validator name="numeric" argument="--range 1-200"/>            <regex>(main)</regex>          </constraint> +        <completionHelp> +          <list>main</list> +          <path>protocols static table</path> +        </completionHelp>        </properties>      </leafNode>      <leafNode name="tcp-mss"> diff --git a/interface-definitions/include/policy/route-common-rule.xml.i b/interface-definitions/include/policy/route-common-rule.xml.i index 5a17dbc95..35fccca50 100644 --- a/interface-definitions/include/policy/route-common-rule.xml.i +++ b/interface-definitions/include/policy/route-common-rule.xml.i @@ -198,6 +198,10 @@            <validator name="numeric" argument="--range 1-200"/>            <regex>(main)</regex>          </constraint> +        <completionHelp> +          <list>main</list> +          <path>protocols static table</path> +        </completionHelp>        </properties>      </leafNode>      <leafNode name="tcp-mss"> diff --git a/interface-definitions/include/qos/limiter-actions.xml.i b/interface-definitions/include/qos/limiter-actions.xml.i new file mode 100644 index 000000000..a993423aa --- /dev/null +++ b/interface-definitions/include/qos/limiter-actions.xml.i @@ -0,0 +1,66 @@ +<!-- include start from qos/limiter-actions.xml.i --> +<leafNode name="exceed-action"> +  <properties> +    <help>Default action for packets exceeding the limiter (default: drop)</help> +    <completionHelp> +      <list>continue drop ok reclassify pipe</list> +    </completionHelp> +    <valueHelp> +      <format>continue</format> +      <description>Don't do anything, just continue with the next action in line</description> +    </valueHelp> +    <valueHelp> +      <format>drop</format> +      <description>Drop the packet immediately</description> +    </valueHelp> +    <valueHelp> +      <format>ok</format> +      <description>Accept the packet</description> +    </valueHelp> +    <valueHelp> +      <format>reclassify</format> +      <description>Treat the packet as non-matching to the filter this action is attached to and continue with the next filter in line (if any)</description> +    </valueHelp> +    <valueHelp> +      <format>pipe</format> +      <description>Pass the packet to the next action in line</description> +    </valueHelp> +    <constraint> +      <regex>(continue|drop|ok|reclassify|pipe)</regex> +    </constraint> +  </properties> +  <defaultValue>drop</defaultValue> +</leafNode> +<leafNode name="notexceed-action"> +  <properties> +    <help>Default action for packets not exceeding the limiter (default: ok)</help> +    <completionHelp> +      <list>continue drop ok reclassify pipe</list> +    </completionHelp> +    <valueHelp> +      <format>continue</format> +      <description>Don't do anything, just continue with the next action in line</description> +    </valueHelp> +    <valueHelp> +      <format>drop</format> +      <description>Drop the packet immediately</description> +    </valueHelp> +    <valueHelp> +      <format>ok</format> +      <description>Accept the packet</description> +    </valueHelp> +    <valueHelp> +      <format>reclassify</format> +      <description>Treat the packet as non-matching to the filter this action is attached to and continue with the next filter in line (if any)</description> +    </valueHelp> +    <valueHelp> +      <format>pipe</format> +      <description>Pass the packet to the next action in line</description> +    </valueHelp> +    <constraint> +      <regex>(continue|drop|ok|reclassify|pipe)</regex> +    </constraint> +  </properties> +  <defaultValue>ok</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in index e8f575a1e..e2dbcbeef 100644 --- a/interface-definitions/qos.xml.in +++ b/interface-definitions/qos.xml.in @@ -188,6 +188,7 @@                    #include <include/qos/burst.xml.i>                    #include <include/generic-description.xml.i>                    #include <include/qos/match.xml.i> +                  #include <include/qos/limiter-actions.xml.i>                    <leafNode name="priority">                      <properties>                        <help>Priority for rule evaluation</help> @@ -211,6 +212,7 @@                  <children>                    #include <include/qos/bandwidth.xml.i>                    #include <include/qos/burst.xml.i> +                  #include <include/qos/limiter-actions.xml.i>                  </children>                </node>                #include <include/generic-description.xml.i> diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in index 126183162..f3c731fe5 100644 --- a/interface-definitions/ssh.xml.in +++ b/interface-definitions/ssh.xml.in @@ -206,6 +206,37 @@              </properties>              <defaultValue>22</defaultValue>            </leafNode> +          <node name="rekey"> +            <properties> +              <help>SSH session rekey limit</help> +            </properties> +            <children> +              <leafNode name="data"> +                <properties> +                  <help>Threshold data in megabytes</help> +                  <valueHelp> +                    <format>u32:1-65535</format> +                    <description>Megabytes</description> +                  </valueHelp> +                  <constraint> +                    <validator name="numeric" argument="--range 1-65535"/> +                  </constraint> +                </properties> +              </leafNode> +              <leafNode name="time"> +                <properties> +                  <help>Threshold time in minutes</help> +                  <valueHelp> +                    <format>u32:1-65535</format> +                    <description>Minutes</description> +                  </valueHelp> +                  <constraint> +                    <validator name="numeric" argument="--range 1-65535"/> +                  </constraint> +                </properties> +              </leafNode> +            </children> +          </node>            <leafNode name="client-keepalive-interval">              <properties>                <help>Enable transmission of keepalives from server to client</help> diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i index d2804e3b3..7dbc4fde5 100644 --- a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i +++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i @@ -153,7 +153,7 @@    <properties>      <help>Show BGP information for specified neighbor</help>      <completionHelp> -      <script>vtysh -c 'show bgp summary' | awk '{print $1'} | grep -e '^[0-9a-f]'</script> +      <script>vtysh -c "$(IFS=$' '; echo "${COMP_WORDS[@]:0:${#COMP_WORDS[@]}-2} summary")" |  awk '/^[0-9a-f]/ {print $1}'</script>      </completionHelp>    </properties>    <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> diff --git a/python/vyos/template.py b/python/vyos/template.py index 0870a0523..2a4135f9e 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -566,12 +566,17 @@ def nft_default_rule(fw_conf, fw_name, ipv6=False):      return " ".join(output)  @register_filter('nft_state_policy') -def nft_state_policy(conf, state, ipv6=False): +def nft_state_policy(conf, state):      out = [f'ct state {state}'] -    if 'log' in conf: -        log_level = conf['log'] -        out.append(f'log level {log_level}') +    if 'log' in conf and 'enable' in conf['log']: +        log_state = state[:3].upper() +        log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper() +        out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"') + +        if 'log_level' in conf: +            log_level = conf['log_level'] +            out.append(f'level {log_level}')      out.append('counter') diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 4bf9c9b73..93bb761c1 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -408,13 +408,16 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):          self.cli_commit()          # Verify all changes +          frrconfig = self.getFRRconfig('router ospf') -        self.assertIn(f' segment-routing on', frrconfig)          self.assertIn(f' segment-routing global-block {global_block_low} {global_block_high} local-block {local_block_low} {local_block_high}', frrconfig)          self.assertIn(f' segment-routing node-msd {maximum_stack_size}', frrconfig)          self.assertIn(f' segment-routing prefix {prefix_one} index {prefix_one_value} explicit-null', frrconfig)          self.assertIn(f' segment-routing prefix {prefix_two} index {prefix_two_value} no-php-flag', frrconfig) +        self.skipTest('https://github.com/FRRouting/frr/issues/12007') +        self.assertIn(f' segment-routing on', frrconfig) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 2bbd7142a..8746cc701 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -73,6 +73,9 @@ def verify(ssh):      if not ssh:          return None +    if 'rekey' in ssh and 'data' not in ssh['rekey']: +        raise ConfigError(f'Rekey data is required!') +      verify_vrf(ssh)      return None diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py index b27aa6060..fff537936 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -48,6 +48,14 @@ def _get_raw_data(family):      Return: dictionary      """      xml = _get_xml_data(family) +    if len(xml) == 0: +        output = {'conntrack': +            { +                'error': True, +                'reason': 'entries not found' +            } +        } +        return output      return _xml_to_dict(xml) @@ -72,7 +80,8 @@ def get_formatted_output(dict_data):      :return: formatted output      """      data_entries = [] -    #dict_data = _get_raw_data(family) +    if 'error' in dict_data['conntrack']: +        return 'Entries not found'      for entry in dict_data['conntrack']['flow']:          orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {}          reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {} diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index d8ceefae6..d75d72582 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -31,54 +31,21 @@ class VyosDirective(SchemaDirectiveVisitor):          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 SystemStatusDirective(VyosDirective): -    """ -    Class providing implementation of 'system_status' directive in schema. -    """ -    def visit_field_definition(self, field, object_type): -        super().visit_field_definition(field, object_type, -                                       make_resolver=make_system_status_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 ConfigSessionQueryDirective(VyosDirective):      """ -    Class providing implementation of 'show' directive in schema. +    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_show_resolver) +                                       make_resolver=make_config_session_query_resolver) -class ImageDirective(VyosDirective): +class ConfigSessionMutationDirective(VyosDirective):      """ -    Class providing implementation of 'image' directive in schema. +    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_image_resolver) +                                       make_resolver=make_config_session_mutation_resolver)  class GenOpQueryDirective(VyosDirective):      """ @@ -96,11 +63,16 @@ class GenOpMutationDirective(VyosDirective):          super().visit_field_definition(field, object_type,                                         make_resolver=make_gen_op_mutation_resolver) -directives_dict = {"configure": ConfigureDirective, -                   "showconfig": ShowConfigDirective, -                   "systemstatus": SystemStatusDirective, -                   "configfile": ConfigFileDirective, -                   "show": ShowDirective, -                   "image": ImageDirective, +class SystemStatusDirective(VyosDirective): +    """ +    Class providing implementation of 'system_status' directive in schema. +    """ +    def visit_field_definition(self, field, object_type): +        super().visit_field_definition(field, object_type, +                                       make_resolver=make_system_status_resolver) + +directives_dict = {"configsessionquery": ConfigSessionQueryDirective, +                   "configsessionmutation": ConfigSessionMutationDirective,                     "genopquery": GenOpQueryDirective, -                   "genopmutation": GenOpMutationDirective} +                   "genopmutation": GenOpMutationDirective, +                   "systemstatus": SystemStatusDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 5ccc9b0b6..f7d285a77 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -106,24 +106,9 @@ def make_mutation_resolver(mutation_name, class_name, session_func):      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']) +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') +    return make_mutation_resolver(mutation_name, mutation_name, 'gen_op_mutation') diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index b46914dcc..5f3a7d005 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -106,18 +106,12 @@ def make_query_resolver(query_name, class_name, session_func):      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_system_status_resolver(query_name): -    class_name = query_name -    return make_query_resolver(query_name, class_name, 'system_status') - -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') + +def make_system_status_resolver(query_name): +    return make_query_resolver(query_name, query_name, 'system_status') 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/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql deleted file mode 100644 index 345c349ac..000000000 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ /dev/null @@ -1,36 +0,0 @@ -input DhcpServerConfigInput { -    key: String! -    sharedNetworkName: String -    subnet: String -    defaultRouter: String -    nameServer: 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 -    nameServer: 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/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql deleted file mode 100644 index 9454d2997..000000000 --- a/src/services/api/graphql/graphql/schema/firewall_group.graphql +++ /dev/null @@ -1,101 +0,0 @@ -input CreateFirewallAddressGroupInput { -    key: String! -    name: String! -    address: [String] -} - -type CreateFirewallAddressGroup { -    name: String! -    address: [String] -} - -type CreateFirewallAddressGroupResult { -    data: CreateFirewallAddressGroup -    success: Boolean! -    errors: [String] -} - -input UpdateFirewallAddressGroupMembersInput { -    key: String! -    name: String! -    address: [String!]! -} - -type UpdateFirewallAddressGroupMembers { -    name: String! -    address: [String!]! -} - -type UpdateFirewallAddressGroupMembersResult { -    data: UpdateFirewallAddressGroupMembers -    success: Boolean! -    errors: [String] -} - -input RemoveFirewallAddressGroupMembersInput { -    key: String! -    name: String! -    address: [String!]! -} - -type RemoveFirewallAddressGroupMembers { -    name: String! -    address: [String!]! -} - -type RemoveFirewallAddressGroupMembersResult { -    data: RemoveFirewallAddressGroupMembers -    success: Boolean! -    errors: [String] -} - -input CreateFirewallAddressIpv6GroupInput { -    key: String! -    name: String! -    address: [String] -} - -type CreateFirewallAddressIpv6Group { -    name: String! -    address: [String] -} - -type CreateFirewallAddressIpv6GroupResult { -    data: CreateFirewallAddressIpv6Group -    success: Boolean! -    errors: [String] -} - -input UpdateFirewallAddressIpv6GroupMembersInput { -    key: String! -    name: String! -    address: [String!]! -} - -type UpdateFirewallAddressIpv6GroupMembers { -    name: String! -    address: [String!]! -} - -type UpdateFirewallAddressIpv6GroupMembersResult { -    data: UpdateFirewallAddressIpv6GroupMembers -    success: Boolean! -    errors: [String] -} - -input RemoveFirewallAddressIpv6GroupMembersInput { -    key: String! -    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 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/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql deleted file mode 100644 index 8a17d919f..000000000 --- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql +++ /dev/null @@ -1,19 +0,0 @@ -input InterfaceEthernetConfigInput { -    key: String! -    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 index 624be2620..2acecade4 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -3,34 +3,16 @@ schema {      mutation: Mutation  } -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  } -type Mutation { -    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 -} +type Mutation 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..f990e63d0 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -45,40 +45,6 @@ class Session:          except Exception:              self._op_mode_list = None -    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 = 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 @@ -94,7 +60,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 +71,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 +93,7 @@ class Session:          return out -    def add(self): +    def add_system_image(self):          session = self._session          data = self._data @@ -138,7 +104,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' | 
