Skip to content

Commit

Permalink
common code extended with task decorator require_exec and generaliz…
Browse files Browse the repository at this point in the history
…ation of default targets
  • Loading branch information
jesteria committed Mar 8, 2023
1 parent b71f59f commit 616e105
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 41 deletions.
2 changes: 2 additions & 0 deletions src/netrics/measurement/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
)

from .dns import AddressLookups # noqa: F401

from .executable import require_exec # noqa: F401
2 changes: 1 addition & 1 deletion src/netrics/measurement/common/connectivity/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def main():
"""
schema = ConfSchema({
Optional('destinations', default=default.PING_DESTINATIONS):
Optional('destinations', default=default.DESTINATIONS):
task.schema.DestinationList(),

Optional('attempts', default=command.DEFAULT_ATTEMPTS):
Expand Down
6 changes: 3 additions & 3 deletions src/netrics/measurement/common/connectivity/default.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
PING_DESTINATIONS = ('google.com',
'facebook.com',
'nytimes.com')
DESTINATIONS = ('google.com',
'facebook.com',
'nytimes.com')
43 changes: 43 additions & 0 deletions src/netrics/measurement/common/executable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Common helpers requiring system executables"""
import functools
import shutil

import netrics.task


class ExecTask:
"""Wrapped callable requiring a named system executable."""

def __init__(self, name, func):
self._executable_name_ = name

# assign func's __module__, __name__, etc.
# (but DON'T update __dict__)
#
# (also assigns __wrapped__)
functools.update_wrapper(self, func, updated=())

def __repr__(self):
return repr(self.__wrapped__)

def __call__(self, *args, **kwargs):
# ensure executable on PATH
executable_path = shutil.which(self._executable_name_)

if executable_path is None:
netrics.task.log.critical(f"{self._executable_name_} executable not found")
return netrics.task.status.file_missing

return self.__wrapped__(executable_path, *args, **kwargs)


class require_exec:
"""Decorator constructor to wrap a callable such that it first
checks that the named system executable is accessible via `PATH`.
"""
def __init__(self, name):
self.executable_name = name

def __call__(self, func):
return ExecTask(self.executable_name, func)
18 changes: 8 additions & 10 deletions src/netrics/measurement/lml.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Measure latency to the "last mile" host via scamper."""
import json
import random
import shutil
import statistics
import subprocess
from ipaddress import ip_address
Expand All @@ -11,7 +10,11 @@

from netrics import task

from .common import AddressLookups, require_net
from .common import (
AddressLookups,
require_exec,
require_net,
)


#
Expand Down Expand Up @@ -45,8 +48,9 @@


@task.param.require(PARAMS)
@require_exec('scamper')
@require_net
def main(params):
def main(scamper, params):
"""Measure latency to the "last mile" host via scamper.
The local network, and then internet hosts (as configured in global
Expand All @@ -67,12 +71,6 @@ def main(params):
parsed and written out according to configuration.
"""
# ensure scamper on PATH
scamper_path = shutil.which('scamper')
if scamper_path is None:
task.log.critical("scamper executable not found")
return task.status.file_missing

# resolve destination(s) given by domain to IP
address_lookups = AddressLookups(params.destinations)

Expand All @@ -97,7 +95,7 @@ def main(params):
try:
process = subprocess.run(
(
scamper_path,
scamper,
'-O', 'json',
'-c', trace_cmd,
'-i', target_ip,
Expand Down
49 changes: 23 additions & 26 deletions src/netrics/measurement/lml_traceroute.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Measure latency to the "last mile" host via traceroute & ping."""
import random
import re
import shutil
import subprocess
import typing
from ipaddress import ip_address
Expand All @@ -10,7 +9,11 @@

from netrics import task

from .common import output, require_net
from .common import (
output,
require_exec,
require_net,
)


#
Expand Down Expand Up @@ -39,8 +42,9 @@


@task.param.require(PARAMS)
@require_exec('traceroute')
@require_net
def main(params):
def main(traceroute, params):
"""Measure latency to the "last mile" host via traceroute and ping.
The local network, and then internet hosts (as configured in global
Expand All @@ -62,13 +66,6 @@ def main(params):
written as well.
"""
# ensure traceroute on PATH
# (ping is used by require_net)
traceroute_path = shutil.which('traceroute')
if traceroute_path is None:
task.log.critical("traceroute executable not found")
return task.status.file_missing

# randomize target from configured destination(s)
target_hosts = list(params.destinations)
random.shuffle(target_hosts)
Expand All @@ -77,9 +74,9 @@ def main(params):
for target_host in target_hosts:
# trace target
try:
traceroute = subprocess.run(
traceroute_process = subprocess.run(
(
traceroute_path,
traceroute,
target_host,
),
capture_output=True,
Expand All @@ -99,37 +96,37 @@ def main(params):
# extract "last mile" host from trace
try:
last_mile = LastMileResult.extract(target_host,
traceroute.stdout,
traceroute.stderr)
traceroute_process.stdout,
traceroute_process.stderr)
except TracerouteAddressError as exc:
task.log.error(
dest=target_host,
stdout=traceroute.stdout,
stderr=traceroute.stderr,
stdout=traceroute_process.stdout,
stderr=traceroute_process.stderr,
line=exc.line,
msg='failed to parse traceroute hop ip address from output line',
)
continue
except TracerouteParseError as exc:
task.log.error(
dest=target_host,
stdout=traceroute.stdout,
stderr=traceroute.stderr,
stdout=traceroute_process.stdout,
stderr=traceroute_process.stderr,
line=exc.line,
msg='unexpected traceroute output line or parse failure',
)
continue
except TracerouteOutputError:
task.log.error(
dest=target_host,
stdout=traceroute.stdout,
stderr=traceroute.stderr,
stdout=traceroute_process.stdout,
stderr=traceroute_process.stderr,
msg='failed to extract last mile ip from traceroute output',
)
continue

# ping last mile host
ping = subprocess.run(
ping_process = subprocess.run(
(
'ping',
'-c', params.count,
Expand All @@ -141,18 +138,18 @@ def main(params):
text=True,
)

if ping.returncode > 1:
if ping_process.returncode > 1:
task.log.critical(
dest=last_mile.ip_address,
status=f'Error ({ping.returncode})',
stdout=ping.stdout,
stderr=ping.stderr,
status=f'Error ({ping_process.returncode})',
stdout=ping_process.stdout,
stderr=ping_process.stderr,
msg='last mile ping failure',
)
return task.status.no_host

# parse ping results
ping_stats = output.parse_ping(ping.stdout)
ping_stats = output.parse_ping(ping_process.stdout)

break # we're done!
else:
Expand Down
2 changes: 1 addition & 1 deletion src/netrics/measurement/ping.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
# destinations: (ping): list of hosts
# OR mapping of hosts to their labels (for results)
Optional('destinations',
default=default.PING_DESTINATIONS): task.schema.DestinationCollection(),
default=default.DESTINATIONS): task.schema.DestinationCollection(),

# count: (ping): natural number
Optional('count', default='10'): task.schema.NaturalStr('count'),
Expand Down

0 comments on commit 616e105

Please sign in to comment.