Skip to content

Commit

Permalink
T6695: Machine-readable operational mode support for traceroute
Browse files Browse the repository at this point in the history
  • Loading branch information
natali-rs1985 committed Oct 11, 2024
1 parent 7d42643 commit 60cd669
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 3 deletions.
2 changes: 1 addition & 1 deletion data/templates/https/nginx.default.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions python/vyos/configsession.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
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"
Expand Down Expand Up @@ -285,3 +295,7 @@ def delete_container_image(self, name):
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
5 changes: 4 additions & 1 deletion python/vyos/opmode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/op_mode/mtr.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
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.'
Expand Down
217 changes: 217 additions & 0 deletions src/op_mode/mtr_execute.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions src/services/api/rest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,20 @@ class Config:
}


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]
Expand Down
24 changes: 24 additions & 0 deletions src/services/api/rest/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from .models import ResetModel
from .models import ImportPkiModel
from .models import PoweroffModel
from .models import TracerouteModel


if TYPE_CHECKING:
Expand Down Expand Up @@ -209,6 +210,7 @@ async def body(self) -> bytes:
'/container-image',
'/image',
'/configure-section',
'/traceroute',
):
if 'path' not in c:
self.form_err = (
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 60cd669

Please sign in to comment.