summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNataliia Solomko <natalirs1985@gmail.com>2024-10-11 13:24:43 +0300
committerNataliia Solomko <natalirs1985@gmail.com>2024-10-11 13:37:45 +0300
commit60cd669d2cee2a17d4e6ab6fce9101069d311e23 (patch)
treeeecb5f26c5367d4b88276a035a1d0f01140ba64e
parent7d4264365e487d37115cff0633b25e4b2012a126 (diff)
downloadvyos-1x-60cd669d2cee2a17d4e6ab6fce9101069d311e23.tar.gz
vyos-1x-60cd669d2cee2a17d4e6ab6fce9101069d311e23.zip
T6695: Machine-readable operational mode support for traceroute
-rw-r--r--data/templates/https/nginx.default.j22
-rw-r--r--python/vyos/configsession.py14
-rw-r--r--python/vyos/opmode.py5
-rw-r--r--src/op_mode/mtr.py2
-rw-r--r--src/op_mode/mtr_execute.py217
-rw-r--r--src/services/api/rest/models.py14
-rw-r--r--src/services/api/rest/routers.py24
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