Skip to content

Commit d7d96c5

Browse files
author
Claire Wang
authored
Update report feature with new argument, add temp fix for ifcfg module (#324)
* add network checks and report Signed-off-by: claireyywang <[email protected]> * network shenanigens Signed-off-by: claireyywang <[email protected]> * network shenanigens Signed-off-by: claireyywang <[email protected]> * network shenanigens Signed-off-by: claireyywang <[email protected]> * add network check and report Signed-off-by: claireyywang <[email protected]> * update code format Signed-off-by: claireyywang <[email protected]> * revised code format Signed-off-by: claireyywang <[email protected]> * added rosdep key ifcfg-pip Signed-off-by: claireyywang <[email protected]> * revise code Signed-off-by: claireyywang <[email protected]> * working on report format Signed-off-by: claireyywang <[email protected]> * improving report Signed-off-by: claireyywang <[email protected]> * update platform report format Signed-off-by: claireyywang <[email protected]> * update network report format Signed-off-by: claireyywang <[email protected]> * add format print Signed-off-by: claireyywang <[email protected]> * add --report_failed feature Signed-off-by: claireyywang <[email protected]> * improving report format Signed-off-by: claireyywang <[email protected]> * temp fix ifcfg import module Signed-off-by: claireyywang <[email protected]> * update build dep Signed-off-by: claireyywang <[email protected]> * fix flake8 Signed-off-by: claireyywang <[email protected]> * fix flake8 Signed-off-by: claireyywang <[email protected]> * add abc and Report class Signed-off-by: claireyywang <[email protected]> * Implement ABC for each check and report and udpate format print Signed-off-by: claireyywang <[email protected]> * update ifcfg import error, fix code format Signed-off-by: claireyywang <[email protected]> * add newlines Signed-off-by: claireyywang <[email protected]> * update warning msgs Signed-off-by: claireyywang <[email protected]> * fix code format Signed-off-by: claireyywang <[email protected]> * update report of failed checks Signed-off-by: claireyywang <[email protected]> * update run_check Signed-off-by: claireyywang <[email protected]> * udpate generate_report Signed-off-by: claireyywang <[email protected]> * add sphinx style docstring and type annotations Signed-off-by: claireyywang <[email protected]> * add context manager for custom warning msg Signed-off-by: claireyywang <[email protected]> * fixed flakey issues Signed-off-by: claireyywang <[email protected]> * update Check and Report class error handling Signed-off-by: claireyywang <[email protected]> * fix report refed before assigned mistake Signed-off-by: claireyywang <[email protected]> * add failed entry point name Signed-off-by: claireyywang <[email protected]> * remove pass from try/except Signed-off-by: claireyywang <[email protected]> * add error handling for check/report Signed-off-by: claireyywang <[email protected]> * change ValueError to Exception Signed-off-by: claireyywang <[email protected]>
1 parent 09b30da commit d7d96c5

File tree

7 files changed

+335
-120
lines changed

7 files changed

+335
-120
lines changed

ros2doctor/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<license>Apache License 2.0</license>
99

1010
<depend>ros2cli</depend>
11-
11+
1212
<exec_depend>python3-rosdistro-modules</exec_depend>
1313

1414
<test_depend>ament_copyright</test_depend>

ros2doctor/ros2doctor/api/__init__.py

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,110 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import List
16+
from typing import Set
17+
from typing import Tuple
18+
1519
from pkg_resources import iter_entry_points
20+
from pkg_resources import UnknownExtra
21+
22+
from ros2doctor.api.format import doctor_warn
23+
24+
25+
class DoctorCheck:
26+
"""Abstract base class of ros2doctor check."""
27+
28+
def category(self) -> str:
29+
""":return: string linking checks and reports."""
30+
raise NotImplementedError
31+
32+
def check(self) -> bool:
33+
""":return: boolean indicating result of checks."""
34+
raise NotImplementedError
35+
36+
37+
class DoctorReport:
38+
"""Abstract base class of ros2doctor report."""
39+
40+
def category(self) -> str:
41+
""":return: string linking checks and reports."""
42+
raise NotImplementedError
1643

44+
def report(self) -> 'Report': # using str as wrapper for custom class Report
45+
""":return: Report object storing report content."""
46+
raise NotImplementedError
1747

18-
def run_checks():
19-
"""Run all checks when `ros2 doctor/wtf` is called."""
20-
all_results = []
21-
failed_names = []
22-
for check in iter_entry_points('ros2doctor.checks'):
23-
result = check.load()() # load() returns method
24-
all_results.append(result)
48+
49+
class Report:
50+
"""Stores report name and content."""
51+
52+
__slots__ = ['name', 'items']
53+
54+
def __init__(self, name: str):
55+
"""Initialize with report name."""
56+
self.name = name
57+
self.items = []
58+
59+
def add_to_report(self, item_name: str, item_info: str) -> None:
60+
"""Add report content to items list (list of string tuples)."""
61+
self.items.append((item_name, item_info))
62+
63+
64+
def run_checks() -> Tuple[Set[str], int, int]:
65+
"""
66+
Run all checks and return check results.
67+
68+
:return: 3-tuple (categories of failed checks, number of failed checks,
69+
total number of checks)
70+
"""
71+
failed_cats = set() # remove repeating elements
72+
fail = 0
73+
total = 0
74+
for check_entry_pt in iter_entry_points('ros2doctor.checks'):
75+
try:
76+
check_class = check_entry_pt.load()
77+
except (ImportError, UnknownExtra):
78+
doctor_warn('Check entry point %s fails to load.' % check_entry_pt.name)
79+
try:
80+
check_instance = check_class()
81+
except Exception:
82+
doctor_warn('Unable to instantiate check object from %s.' % check_entry_pt.name)
83+
try:
84+
check_category = check_instance.category()
85+
result = check_instance.check()
86+
except Exception:
87+
doctor_warn('Fail to call %s class functions.' % check_entry_pt.name)
2588
if result is False:
26-
failed_names.append(check.name)
27-
return all_results, failed_names
89+
fail += 1
90+
failed_cats.add(check_category)
91+
total += 1
92+
return failed_cats, fail, total
93+
2894

95+
def generate_reports(*, categories=None) -> List[Report]:
96+
"""
97+
Print all reports or reports of failed checks to terminal.
2998
30-
def generate_report():
31-
"""Print report to terminal when `-r/--report` is attached."""
32-
for report in iter_entry_points('ros2doctor.report'):
33-
report.load()() # load() returns method
99+
:return: list of Report objects
100+
"""
101+
reports = []
102+
for report_entry_pt in iter_entry_points('ros2doctor.report'):
103+
try:
104+
report_class = report_entry_pt.load()
105+
except (ImportError, UnknownExtra):
106+
doctor_warn('Report entry point %s fails to load.' % report_entry_pt.name)
107+
try:
108+
report_instance = report_class()
109+
except Exception:
110+
doctor_warn('Unable to instantiate report object from %s.' % report_entry_pt.name)
111+
try:
112+
report_category = report_instance.category()
113+
report = report_instance.report()
114+
except Exception:
115+
doctor_warn('Fail to call %s class functions.' % report_entry_pt.name)
116+
if categories:
117+
if report_category in categories:
118+
reports.append(report)
119+
else:
120+
reports.append(report)
121+
return reports

ros2doctor/ros2doctor/api/format.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,71 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import sys
16+
from typing import List
17+
from typing import Tuple
18+
import warnings
1519

16-
def print_term(k, v):
17-
"""Print term only if it exists."""
18-
if v:
19-
# TODO(clairewang): 20 padding needs to be dynamically set
20-
print('{:20}: {}'.format(k, v))
20+
21+
def format_print(report):
22+
"""
23+
Format print report content.
24+
25+
:param report: Report object with name and items list
26+
"""
27+
# temp fix for missing ifcfg
28+
if report is None:
29+
sys.stderr.write('No report found. Skip print...\n')
30+
return
31+
32+
print('\n ', report.name)
33+
padding_num = compute_padding(report.items)
34+
for item_name, item_content in report.items:
35+
print('{:{padding}}: {}'.format(item_name, item_content, padding=padding_num))
36+
37+
38+
def compute_padding(report_items: List[Tuple[str, str]]) -> int:
39+
"""
40+
Compute padding based on report content.
41+
42+
:param report_items: list of item name and item content tuple
43+
:return: padding number
44+
"""
45+
padding = 8
46+
check_items = list(zip(*report_items))[0] # get first elements of tuples
47+
max_len = len(max(check_items, key=len)) # find the longest string length
48+
if max_len >= padding:
49+
padding = max_len + 4 # padding number is longest string length + 4 spaces
50+
return padding
51+
52+
53+
def custom_warning_format(msg, cat, filename, linenum, file=None, line=None):
54+
return '%s: %s: %s: %s\n' % (filename, linenum, cat.__name__, msg)
55+
56+
57+
class CustomWarningFormat:
58+
"""Support custom warning format without modifying default format."""
59+
60+
def __enter__(self):
61+
self._default_format = warnings.formatwarning
62+
warnings.formatwarning = custom_warning_format
63+
64+
def __exit__(self, t, v, trb):
65+
"""
66+
Define exit action for context manager.
67+
68+
:param t: type
69+
:param v: value
70+
:param trb: traceback
71+
"""
72+
warnings.formatwarning = self._default_format
73+
74+
75+
def doctor_warn(msg: str) -> None:
76+
"""
77+
Use CustomWarningFormat to print customized warning message.
78+
79+
:param msg: warning message to be printed
80+
"""
81+
with CustomWarningFormat():
82+
warnings.warn(msg)

ros2doctor/ros2doctor/api/network.py

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,35 @@
1414

1515
import os
1616
import sys
17+
from typing import Tuple
18+
import warnings
1719

18-
import ifcfg
20+
from ros2doctor.api import DoctorCheck
21+
from ros2doctor.api import DoctorReport
22+
from ros2doctor.api import Report
23+
from ros2doctor.api.format import doctor_warn
1924

20-
from ros2doctor.api.format import print_term
25+
try:
26+
import ifcfg
27+
except ImportError:
28+
warnings.warn('Failed to import ifcfg. '
29+
'Use `python -m pip install ifcfg` to install needed package.',
30+
ImportWarning) # ImportWarning is suppressed by default
2131

2232

23-
def _is_unix_like_platform():
33+
def _is_unix_like_platform() -> bool:
2434
"""Return True if conforms to UNIX/POSIX-style APIs."""
2535
return os.name == 'posix'
2636

2737

28-
def check_network_config_helper():
38+
def _check_network_config_helper() -> Tuple[bool, bool, bool]:
2939
"""Check if loopback and multicast IP addresses are found."""
3040
has_loopback, has_non_loopback, has_multicast = False, False, False
41+
# temp fix for ifcfg package, maunually pass network check
42+
if 'ifcfg' not in sys.modules:
43+
doctor_warn('ifcfg not imported. Skip network check...')
44+
return True, True, True
45+
3146
for name, iface in ifcfg.interfaces().items():
3247
flags = iface.get('flags')
3348
if 'LOOPBACK' in flags:
@@ -39,22 +54,40 @@ def check_network_config_helper():
3954
return has_loopback, has_non_loopback, has_multicast
4055

4156

42-
def check_network_config():
43-
"""Conduct network checks and output error/warning messages."""
44-
has_loopback, has_non_loopback, has_multicast = check_network_config_helper()
45-
if not has_loopback:
46-
sys.stderr.write('ERROR: No loopback IP address is found.\n')
47-
if not has_non_loopback:
48-
sys.stderr.write('WARNING: Only loopback IP address is found.\n')
49-
if not has_multicast:
50-
sys.stderr.write('WARNING: No multicast IP address is found.\n')
51-
return has_loopback and has_non_loopback and has_multicast
57+
class NetworkCheck(DoctorCheck):
58+
"""Check network interface configuration for loopback and multicast."""
5259

60+
def category(self):
61+
return 'network'
5362

54-
def print_network():
55-
"""Print all system and ROS network information."""
56-
print('NETWORK CONFIGURATION')
57-
for name, iface in ifcfg.interfaces().items():
58-
for k, v in iface.items():
59-
print_term(k, v)
60-
print('\n')
63+
def check(self):
64+
"""Check network configuration."""
65+
has_loopback, has_non_loopback, has_multicast = _check_network_config_helper()
66+
if not has_loopback:
67+
doctor_warn('ERROR: No loopback IP address is found.')
68+
if not has_non_loopback:
69+
doctor_warn('Only loopback IP address is found.')
70+
if not has_multicast:
71+
doctor_warn('No multicast IP address is found.')
72+
return has_loopback and has_non_loopback and has_multicast
73+
74+
75+
class NetworkReport(DoctorReport):
76+
"""Report network configuration."""
77+
78+
def category(self):
79+
return 'network'
80+
81+
def report(self):
82+
"""Print system and ROS network information."""
83+
network_report = Report('NETWORK CONFIGURATION')
84+
# temp fix for ifcfg package, return none for report
85+
if 'ifcfg' not in sys.modules:
86+
doctor_warn('ERROR: ifcfg package not imported. Skipping network report...')
87+
return None
88+
89+
for name, iface in ifcfg.interfaces().items():
90+
for k, v in iface.items():
91+
if v:
92+
network_report.add_to_report(k, v)
93+
return network_report

0 commit comments

Comments
 (0)