diff options
| -rw-r--r-- | data/templates/https/nginx.default.j2 | 2 | ||||
| -rw-r--r-- | python/vyos/configsession.py | 14 | ||||
| -rw-r--r-- | python/vyos/opmode.py | 5 | ||||
| -rw-r--r-- | src/op_mode/mtr.py | 2 | ||||
| -rw-r--r-- | src/op_mode/mtr_execute.py | 217 | ||||
| -rw-r--r-- | src/services/api/rest/models.py | 14 | ||||
| -rw-r--r-- | src/services/api/rest/routers.py | 24 | 
7 files changed, 275 insertions, 3 deletions
| diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2 index 1dde66ebf..51da46946 100644 --- a/data/templates/https/nginx.default.j2 +++ b/data/templates/https/nginx.default.j2 @@ -48,7 +48,7 @@ server {      ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';      # proxy settings for HTTP API, if enabled; 503, if not -    location ~ ^/(retrieve|configure|config-file|image|import-pki|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) { +    location ~ ^/(retrieve|configure|config-file|image|import-pki|container-image|generate|show|reboot|reset|poweroff|traceroute|docs|openapi.json|redoc|graphql) {  {% if api is vyos_defined %}          proxy_pass http://unix:/run/api.sock;          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 7d51b94e4..6bfca5200 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -48,6 +48,16 @@ REBOOT = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reboot']  POWEROFF = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'poweroff']  OP_CMD_ADD = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'add']  OP_CMD_DELETE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'delete'] +TRACEROUTE = [ +    '/usr/libexec/vyos/op_mode/mtr_execute.py', +    'mtr', +    '--for-api', +    '--report-mode', +    '--report-cycles', +    '1', +    '--json', +    '--host', +]  # Default "commit via" string  APP = "vyos-http-api" @@ -285,3 +295,7 @@ class ConfigSession(object):      def show_container_image(self):          out = self.__run_command(SHOW + ['container', 'image'])          return out + +    def traceroute(self, host): +        out = self.__run_command(TRACEROUTE + [host]) +        return out diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 066c8058f..83e25fb50 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -89,7 +89,10 @@ class InternalError(Error):  def _is_op_mode_function_name(name): -    if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew|release|execute)", name): +    if re.match( +        r'^(show|clear|reset|restart|add|update|delete|generate|set|renew|release|execute|mtr)', +        name, +    ):          return True      else:          return False diff --git a/src/op_mode/mtr.py b/src/op_mode/mtr.py index baf9672a1..eea80c4da 100644 --- a/src/op_mode/mtr.py +++ b/src/op_mode/mtr.py @@ -23,7 +23,7 @@ from vyos.utils.network import vrf_list  from vyos.utils.process import call  options = { -    'report': { +    'report-mode': {          'mtr': '{command} --report',          'type': 'noarg',          'help': 'This option puts mtr into report mode. When in this mode, mtr will run for the number of cycles specified by the -c option, and then print statistics and exit.' diff --git a/src/op_mode/mtr_execute.py b/src/op_mode/mtr_execute.py new file mode 100644 index 000000000..2585a7ee4 --- /dev/null +++ b/src/op_mode/mtr_execute.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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/>. + +import ipaddress +import socket +import sys +import typing + +from json import loads + +from vyos.utils.network import interface_list +from vyos.utils.network import vrf_list +from vyos.utils.process import cmd +from vyos.utils.process import call + +import vyos.opmode + +ArgProtocol = typing.Literal['tcp', 'udp', 'sctp'] +noargs_list = [ +    'report_mode', +    'json', +    'report_wide', +    'split', +    'raw', +    'no_dns', +    'aslookup', +] + + +def vrf_list_default(): +    return vrf_list() + ['default'] + + +options = { +    'report_mode': { +        'mtr': '{command} --report', +    }, +    'protocol': { +        'mtr': '{command} --{value}', +    }, +    'json': { +        'mtr': '{command} --json', +    }, +    'report_wide': { +        'mtr': '{command} --report-wide', +    }, +    'raw': { +        'mtr': '{command} --raw', +    }, +    'split': { +        'mtr': '{command} --split', +    }, +    'no_dns': { +        'mtr': '{command} --no-dns', +    }, +    'show_ips': { +        'mtr': '{command} --show-ips {value}', +    }, +    'ipinfo': { +        'mtr': '{command} --ipinfo {value}', +    }, +    'aslookup': { +        'mtr': '{command} --aslookup', +    }, +    'interval': { +        'mtr': '{command} --interval {value}', +    }, +    'report_cycles': { +        'mtr': '{command} --report-cycles {value}', +    }, +    'psize': { +        'mtr': '{command} --psize {value}', +    }, +    'bitpattern': { +        'mtr': '{command} --bitpattern {value}', +    }, +    'gracetime': { +        'mtr': '{command} --gracetime {value}', +    }, +    'tos': { +        'mtr': '{command} --tos {value}', +    }, +    'mpls': { +        'mtr': '{command} --mpls {value}', +    }, +    'interface': { +        'mtr': '{command} --interface {value}', +        'helpfunction': interface_list, +    }, +    'address': { +        'mtr': '{command} --address {value}', +    }, +    'first_ttl': { +        'mtr': '{command} --first-ttl {value}', +    }, +    'max_ttl': { +        'mtr': '{command} --max-ttl {value}', +    }, +    'max_unknown': { +        'mtr': '{command} --max-unknown {value}', +    }, +    'port': { +        'mtr': '{command} --port {value}', +    }, +    'localport': { +        'mtr': '{command} --localport {value}', +    }, +    'timeout': { +        'mtr': '{command} --timeout {value}', +    }, +    'mark': { +        'mtr': '{command} --mark {value}', +    }, +    'vrf': { +        'mtr': 'sudo ip vrf exec {value} {command}', +        'helpfunction': vrf_list_default, +        'dflt': 'default', +    }, +} + +mtr_command = { +    4: '/bin/mtr -4', +    6: '/bin/mtr -6', +} + + +def mtr( +    host: str, +    for_api: typing.Optional[bool], +    report_mode: typing.Optional[bool], +    protocol: typing.Optional[ArgProtocol], +    report_wide: typing.Optional[bool], +    raw: typing.Optional[bool], +    json: typing.Optional[bool], +    split: typing.Optional[bool], +    no_dns: typing.Optional[bool], +    show_ips: typing.Optional[str], +    ipinfo: typing.Optional[str], +    aslookup: typing.Optional[bool], +    interval: typing.Optional[str], +    report_cycles: typing.Optional[str], +    psize: typing.Optional[str], +    bitpattern: typing.Optional[str], +    gracetime: typing.Optional[str], +    tos: typing.Optional[str], +    mpl: typing.Optional[bool], +    interface: typing.Optional[str], +    address: typing.Optional[str], +    first_ttl: typing.Optional[str], +    max_ttl: typing.Optional[str], +    max_unknown: typing.Optional[str], +    port: typing.Optional[str], +    localport: typing.Optional[str], +    timeout: typing.Optional[str], +    mark: typing.Optional[str], +    vrf: typing.Optional[str], +): +    args = locals() +    for name, option in options.items(): +        if 'dflt' in option and not args[name]: +            args[name] = option['dflt'] + +    try: +        ip = socket.gethostbyname(host) +    except UnicodeError: +        raise vyos.opmode.InternalError(f'Unknown host: {host}') +    except socket.gaierror: +        ip = host + +    try: +        version = ipaddress.ip_address(ip).version +    except ValueError: +        raise vyos.opmode.InternalError(f'Unknown host: {host}') + +    command = mtr_command[version] + +    for key, val in args.items(): +        if key in options and val: +            if 'helpfunction' in options[key]: +                allowed_values = options[key]['helpfunction']() +                if val not in allowed_values: +                    raise vyos.opmode.InternalError( +                        f'Invalid argument for option {key} - {val}' +                    ) +            value = '' if key in noargs_list else val +            command = options[key]['mtr'].format(command=command, value=val) + +    if json: +        output = cmd(f'{command} {host}') +        if for_api: +            output = loads(output) +        print(output) +    else: +        call(f'{command} --curses --displaymode 0 {host}') + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/services/api/rest/models.py b/src/services/api/rest/models.py index 23ae9be9d..27d9fb5ee 100644 --- a/src/services/api/rest/models.py +++ b/src/services/api/rest/models.py @@ -279,6 +279,20 @@ class PoweroffModel(ApiModel):          } +class TracerouteModel(ApiModel): +    op: StrictStr +    host: StrictStr + +    class Config: +        schema_extra = { +            'example': { +                'key': 'id_key', +                'op': 'traceroute', +                'host': 'host', +            } +        } + +  class Success(BaseModel):      success: bool      data: Union[str, bool, Dict] diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py index da981d5bf..47d06b7e9 100644 --- a/src/services/api/rest/routers.py +++ b/src/services/api/rest/routers.py @@ -68,6 +68,7 @@ from .models import RebootModel  from .models import ResetModel  from .models import ImportPkiModel  from .models import PoweroffModel +from .models import TracerouteModel  if TYPE_CHECKING: @@ -209,6 +210,7 @@ class MultipartRequest(Request):                          '/container-image',                          '/image',                          '/configure-section', +                        '/traceroute',                      ):                          if 'path' not in c:                              self.form_err = ( @@ -742,6 +744,28 @@ def poweroff_op(data: PoweroffModel):      return success(res) +@router.post('/traceroute') +def traceroute_op(data: TracerouteModel): +    state = SessionState() +    session = state.session + +    op = data.op +    host = data.host + +    try: +        if op == 'traceroute': +            res = session.traceroute(host) +        else: +            return error(400, f"'{op}' is not a valid operation") +    except ConfigSessionError as e: +        return error(400, str(e)) +    except Exception: +        LOG.critical(traceback.format_exc()) +        return error(500, 'An internal error occurred. Check the logs for details.') + +    return success(res) + +  def rest_init(app: 'FastAPI'):      if all(r in app.routes for r in router.routes):          return | 
