amd-node-scraper 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. amd_node_scraper-0.0.1.dist-info/LICENSE +21 -0
  2. amd_node_scraper-0.0.1.dist-info/METADATA +424 -0
  3. amd_node_scraper-0.0.1.dist-info/RECORD +197 -0
  4. amd_node_scraper-0.0.1.dist-info/WHEEL +5 -0
  5. amd_node_scraper-0.0.1.dist-info/entry_points.txt +2 -0
  6. amd_node_scraper-0.0.1.dist-info/top_level.txt +1 -0
  7. nodescraper/__init__.py +32 -0
  8. nodescraper/base/__init__.py +34 -0
  9. nodescraper/base/inbandcollectortask.py +118 -0
  10. nodescraper/base/inbanddataplugin.py +39 -0
  11. nodescraper/base/regexanalyzer.py +120 -0
  12. nodescraper/cli/__init__.py +29 -0
  13. nodescraper/cli/cli.py +511 -0
  14. nodescraper/cli/constants.py +27 -0
  15. nodescraper/cli/dynamicparserbuilder.py +171 -0
  16. nodescraper/cli/helper.py +517 -0
  17. nodescraper/cli/inputargtypes.py +129 -0
  18. nodescraper/configbuilder.py +123 -0
  19. nodescraper/configregistry.py +66 -0
  20. nodescraper/configs/node_status.json +19 -0
  21. nodescraper/connection/__init__.py +25 -0
  22. nodescraper/connection/inband/__init__.py +46 -0
  23. nodescraper/connection/inband/inband.py +171 -0
  24. nodescraper/connection/inband/inbandlocal.py +93 -0
  25. nodescraper/connection/inband/inbandmanager.py +151 -0
  26. nodescraper/connection/inband/inbandremote.py +173 -0
  27. nodescraper/connection/inband/sshparams.py +43 -0
  28. nodescraper/constants.py +26 -0
  29. nodescraper/enums/__init__.py +40 -0
  30. nodescraper/enums/eventcategory.py +89 -0
  31. nodescraper/enums/eventpriority.py +42 -0
  32. nodescraper/enums/executionstatus.py +44 -0
  33. nodescraper/enums/osfamily.py +34 -0
  34. nodescraper/enums/systeminteraction.py +41 -0
  35. nodescraper/enums/systemlocation.py +33 -0
  36. nodescraper/generictypes.py +36 -0
  37. nodescraper/interfaces/__init__.py +44 -0
  38. nodescraper/interfaces/connectionmanager.py +143 -0
  39. nodescraper/interfaces/dataanalyzertask.py +138 -0
  40. nodescraper/interfaces/datacollectortask.py +185 -0
  41. nodescraper/interfaces/dataplugin.py +356 -0
  42. nodescraper/interfaces/plugin.py +127 -0
  43. nodescraper/interfaces/resultcollator.py +56 -0
  44. nodescraper/interfaces/task.py +164 -0
  45. nodescraper/interfaces/taskresulthook.py +39 -0
  46. nodescraper/models/__init__.py +48 -0
  47. nodescraper/models/analyzerargs.py +93 -0
  48. nodescraper/models/collectorargs.py +30 -0
  49. nodescraper/models/connectionconfig.py +34 -0
  50. nodescraper/models/datamodel.py +171 -0
  51. nodescraper/models/datapluginresult.py +39 -0
  52. nodescraper/models/event.py +158 -0
  53. nodescraper/models/pluginconfig.py +38 -0
  54. nodescraper/models/pluginresult.py +39 -0
  55. nodescraper/models/systeminfo.py +44 -0
  56. nodescraper/models/taskresult.py +185 -0
  57. nodescraper/models/timerangeargs.py +38 -0
  58. nodescraper/pluginexecutor.py +274 -0
  59. nodescraper/pluginregistry.py +152 -0
  60. nodescraper/plugins/__init__.py +25 -0
  61. nodescraper/plugins/inband/__init__.py +25 -0
  62. nodescraper/plugins/inband/amdsmi/__init__.py +28 -0
  63. nodescraper/plugins/inband/amdsmi/amdsmi_analyzer.py +821 -0
  64. nodescraper/plugins/inband/amdsmi/amdsmi_collector.py +1313 -0
  65. nodescraper/plugins/inband/amdsmi/amdsmi_plugin.py +43 -0
  66. nodescraper/plugins/inband/amdsmi/amdsmidata.py +1002 -0
  67. nodescraper/plugins/inband/amdsmi/analyzer_args.py +50 -0
  68. nodescraper/plugins/inband/amdsmi/cper.py +65 -0
  69. nodescraper/plugins/inband/bios/__init__.py +29 -0
  70. nodescraper/plugins/inband/bios/analyzer_args.py +64 -0
  71. nodescraper/plugins/inband/bios/bios_analyzer.py +93 -0
  72. nodescraper/plugins/inband/bios/bios_collector.py +93 -0
  73. nodescraper/plugins/inband/bios/bios_plugin.py +43 -0
  74. nodescraper/plugins/inband/bios/biosdata.py +30 -0
  75. nodescraper/plugins/inband/cmdline/__init__.py +25 -0
  76. nodescraper/plugins/inband/cmdline/analyzer_args.py +80 -0
  77. nodescraper/plugins/inband/cmdline/cmdline_analyzer.py +113 -0
  78. nodescraper/plugins/inband/cmdline/cmdline_collector.py +77 -0
  79. nodescraper/plugins/inband/cmdline/cmdline_plugin.py +43 -0
  80. nodescraper/plugins/inband/cmdline/cmdlinedata.py +30 -0
  81. nodescraper/plugins/inband/device_enumeration/__init__.py +29 -0
  82. nodescraper/plugins/inband/device_enumeration/analyzer_args.py +73 -0
  83. nodescraper/plugins/inband/device_enumeration/device_enumeration_analyzer.py +81 -0
  84. nodescraper/plugins/inband/device_enumeration/device_enumeration_collector.py +176 -0
  85. nodescraper/plugins/inband/device_enumeration/device_enumeration_plugin.py +45 -0
  86. nodescraper/plugins/inband/device_enumeration/deviceenumdata.py +36 -0
  87. nodescraper/plugins/inband/dimm/__init__.py +25 -0
  88. nodescraper/plugins/inband/dimm/collector_args.py +31 -0
  89. nodescraper/plugins/inband/dimm/dimm_collector.py +151 -0
  90. nodescraper/plugins/inband/dimm/dimm_plugin.py +40 -0
  91. nodescraper/plugins/inband/dimm/dimmdata.py +30 -0
  92. nodescraper/plugins/inband/dkms/__init__.py +25 -0
  93. nodescraper/plugins/inband/dkms/analyzer_args.py +85 -0
  94. nodescraper/plugins/inband/dkms/dkms_analyzer.py +106 -0
  95. nodescraper/plugins/inband/dkms/dkms_collector.py +76 -0
  96. nodescraper/plugins/inband/dkms/dkms_plugin.py +43 -0
  97. nodescraper/plugins/inband/dkms/dkmsdata.py +33 -0
  98. nodescraper/plugins/inband/dmesg/__init__.py +28 -0
  99. nodescraper/plugins/inband/dmesg/analyzer_args.py +33 -0
  100. nodescraper/plugins/inband/dmesg/collector_args.py +39 -0
  101. nodescraper/plugins/inband/dmesg/dmesg_analyzer.py +503 -0
  102. nodescraper/plugins/inband/dmesg/dmesg_collector.py +164 -0
  103. nodescraper/plugins/inband/dmesg/dmesg_plugin.py +44 -0
  104. nodescraper/plugins/inband/dmesg/dmesgdata.py +116 -0
  105. nodescraper/plugins/inband/fabrics/__init__.py +28 -0
  106. nodescraper/plugins/inband/fabrics/fabrics_collector.py +726 -0
  107. nodescraper/plugins/inband/fabrics/fabrics_plugin.py +37 -0
  108. nodescraper/plugins/inband/fabrics/fabricsdata.py +140 -0
  109. nodescraper/plugins/inband/journal/__init__.py +28 -0
  110. nodescraper/plugins/inband/journal/collector_args.py +33 -0
  111. nodescraper/plugins/inband/journal/journal_collector.py +107 -0
  112. nodescraper/plugins/inband/journal/journal_plugin.py +40 -0
  113. nodescraper/plugins/inband/journal/journaldata.py +44 -0
  114. nodescraper/plugins/inband/kernel/__init__.py +25 -0
  115. nodescraper/plugins/inband/kernel/analyzer_args.py +64 -0
  116. nodescraper/plugins/inband/kernel/kernel_analyzer.py +91 -0
  117. nodescraper/plugins/inband/kernel/kernel_collector.py +129 -0
  118. nodescraper/plugins/inband/kernel/kernel_plugin.py +43 -0
  119. nodescraper/plugins/inband/kernel/kerneldata.py +32 -0
  120. nodescraper/plugins/inband/kernel_module/__init__.py +25 -0
  121. nodescraper/plugins/inband/kernel_module/analyzer_args.py +59 -0
  122. nodescraper/plugins/inband/kernel_module/kernel_module_analyzer.py +211 -0
  123. nodescraper/plugins/inband/kernel_module/kernel_module_collector.py +264 -0
  124. nodescraper/plugins/inband/kernel_module/kernel_module_data.py +60 -0
  125. nodescraper/plugins/inband/kernel_module/kernel_module_plugin.py +43 -0
  126. nodescraper/plugins/inband/memory/__init__.py +25 -0
  127. nodescraper/plugins/inband/memory/analyzer_args.py +45 -0
  128. nodescraper/plugins/inband/memory/memory_analyzer.py +98 -0
  129. nodescraper/plugins/inband/memory/memory_collector.py +330 -0
  130. nodescraper/plugins/inband/memory/memory_plugin.py +43 -0
  131. nodescraper/plugins/inband/memory/memorydata.py +90 -0
  132. nodescraper/plugins/inband/network/__init__.py +28 -0
  133. nodescraper/plugins/inband/network/network_collector.py +1828 -0
  134. nodescraper/plugins/inband/network/network_plugin.py +37 -0
  135. nodescraper/plugins/inband/network/networkdata.py +319 -0
  136. nodescraper/plugins/inband/nvme/__init__.py +28 -0
  137. nodescraper/plugins/inband/nvme/nvme_collector.py +167 -0
  138. nodescraper/plugins/inband/nvme/nvme_plugin.py +37 -0
  139. nodescraper/plugins/inband/nvme/nvmedata.py +45 -0
  140. nodescraper/plugins/inband/os/__init__.py +25 -0
  141. nodescraper/plugins/inband/os/analyzer_args.py +64 -0
  142. nodescraper/plugins/inband/os/os_analyzer.py +73 -0
  143. nodescraper/plugins/inband/os/os_collector.py +131 -0
  144. nodescraper/plugins/inband/os/os_plugin.py +43 -0
  145. nodescraper/plugins/inband/os/osdata.py +31 -0
  146. nodescraper/plugins/inband/package/__init__.py +25 -0
  147. nodescraper/plugins/inband/package/analyzer_args.py +48 -0
  148. nodescraper/plugins/inband/package/package_analyzer.py +253 -0
  149. nodescraper/plugins/inband/package/package_collector.py +273 -0
  150. nodescraper/plugins/inband/package/package_plugin.py +43 -0
  151. nodescraper/plugins/inband/package/packagedata.py +41 -0
  152. nodescraper/plugins/inband/pcie/__init__.py +29 -0
  153. nodescraper/plugins/inband/pcie/analyzer_args.py +63 -0
  154. nodescraper/plugins/inband/pcie/pcie_analyzer.py +1081 -0
  155. nodescraper/plugins/inband/pcie/pcie_collector.py +690 -0
  156. nodescraper/plugins/inband/pcie/pcie_data.py +2017 -0
  157. nodescraper/plugins/inband/pcie/pcie_plugin.py +43 -0
  158. nodescraper/plugins/inband/process/__init__.py +25 -0
  159. nodescraper/plugins/inband/process/analyzer_args.py +45 -0
  160. nodescraper/plugins/inband/process/collector_args.py +31 -0
  161. nodescraper/plugins/inband/process/process_analyzer.py +91 -0
  162. nodescraper/plugins/inband/process/process_collector.py +115 -0
  163. nodescraper/plugins/inband/process/process_plugin.py +46 -0
  164. nodescraper/plugins/inband/process/processdata.py +34 -0
  165. nodescraper/plugins/inband/rocm/__init__.py +25 -0
  166. nodescraper/plugins/inband/rocm/analyzer_args.py +66 -0
  167. nodescraper/plugins/inband/rocm/rocm_analyzer.py +100 -0
  168. nodescraper/plugins/inband/rocm/rocm_collector.py +205 -0
  169. nodescraper/plugins/inband/rocm/rocm_plugin.py +43 -0
  170. nodescraper/plugins/inband/rocm/rocmdata.py +62 -0
  171. nodescraper/plugins/inband/storage/__init__.py +25 -0
  172. nodescraper/plugins/inband/storage/analyzer_args.py +38 -0
  173. nodescraper/plugins/inband/storage/collector_args.py +31 -0
  174. nodescraper/plugins/inband/storage/storage_analyzer.py +152 -0
  175. nodescraper/plugins/inband/storage/storage_collector.py +110 -0
  176. nodescraper/plugins/inband/storage/storage_plugin.py +44 -0
  177. nodescraper/plugins/inband/storage/storagedata.py +70 -0
  178. nodescraper/plugins/inband/sysctl/__init__.py +29 -0
  179. nodescraper/plugins/inband/sysctl/analyzer_args.py +67 -0
  180. nodescraper/plugins/inband/sysctl/sysctl_analyzer.py +81 -0
  181. nodescraper/plugins/inband/sysctl/sysctl_collector.py +101 -0
  182. nodescraper/plugins/inband/sysctl/sysctl_plugin.py +43 -0
  183. nodescraper/plugins/inband/sysctl/sysctldata.py +42 -0
  184. nodescraper/plugins/inband/syslog/__init__.py +28 -0
  185. nodescraper/plugins/inband/syslog/syslog_collector.py +121 -0
  186. nodescraper/plugins/inband/syslog/syslog_plugin.py +37 -0
  187. nodescraper/plugins/inband/syslog/syslogdata.py +46 -0
  188. nodescraper/plugins/inband/uptime/__init__.py +25 -0
  189. nodescraper/plugins/inband/uptime/uptime_collector.py +88 -0
  190. nodescraper/plugins/inband/uptime/uptime_plugin.py +37 -0
  191. nodescraper/plugins/inband/uptime/uptimedata.py +31 -0
  192. nodescraper/resultcollators/__init__.py +25 -0
  193. nodescraper/resultcollators/tablesummary.py +159 -0
  194. nodescraper/taskresulthooks/__init__.py +28 -0
  195. nodescraper/taskresulthooks/filesystemloghook.py +88 -0
  196. nodescraper/typeutils.py +171 -0
  197. nodescraper/utils.py +412 -0
nodescraper/cli/cli.py ADDED
@@ -0,0 +1,511 @@
1
+ ###############################################################################
2
+ #
3
+ # MIT License
4
+ #
5
+ # Copyright (c) 2025 Advanced Micro Devices, Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+ #
25
+ ###############################################################################
26
+ import argparse
27
+ import datetime
28
+ import json
29
+ import logging
30
+ import os
31
+ import platform
32
+ import sys
33
+ from typing import Optional
34
+
35
+ import nodescraper
36
+ from nodescraper.cli.constants import DEFAULT_CONFIG, META_VAR_MAP
37
+ from nodescraper.cli.dynamicparserbuilder import DynamicParserBuilder
38
+ from nodescraper.cli.helper import (
39
+ dump_results_to_csv,
40
+ generate_reference_config,
41
+ generate_reference_config_from_logs,
42
+ generate_summary,
43
+ get_plugin_configs,
44
+ get_system_info,
45
+ log_system_info,
46
+ parse_describe,
47
+ parse_gen_plugin_config,
48
+ )
49
+ from nodescraper.cli.inputargtypes import ModelArgHandler, json_arg, log_path_arg
50
+ from nodescraper.configregistry import ConfigRegistry
51
+ from nodescraper.constants import DEFAULT_LOGGER
52
+ from nodescraper.enums import ExecutionStatus, SystemInteractionLevel, SystemLocation
53
+ from nodescraper.models import SystemInfo
54
+ from nodescraper.pluginexecutor import PluginExecutor
55
+ from nodescraper.pluginregistry import PluginRegistry
56
+
57
+
58
+ def build_parser(
59
+ plugin_reg: PluginRegistry,
60
+ config_reg: ConfigRegistry,
61
+ ) -> tuple[argparse.ArgumentParser, dict[str, tuple[argparse.ArgumentParser, dict]]]:
62
+ """Build an argument parser
63
+
64
+ Args:
65
+ plugin_reg (PluginRegistry): registry of plugins
66
+
67
+ Returns:
68
+ tuple[argparse.ArgumentParser, dict[str, tuple[argparse.ArgumentParser, dict]]]: tuple containing main
69
+ parser and subparsers for each plugin module
70
+ """
71
+ parser = argparse.ArgumentParser(
72
+ description="node scraper CLI",
73
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
74
+ )
75
+
76
+ parser.add_argument(
77
+ "--version",
78
+ action="version",
79
+ version=f"%(prog)s {nodescraper.__version__}",
80
+ )
81
+
82
+ parser.add_argument(
83
+ "--sys-name", default=platform.node(), help="System name", metavar=META_VAR_MAP[str]
84
+ )
85
+
86
+ parser.add_argument(
87
+ "--sys-location",
88
+ type=str.upper,
89
+ choices=[e.name for e in SystemLocation],
90
+ default="LOCAL",
91
+ help="Location of target system",
92
+ )
93
+
94
+ parser.add_argument(
95
+ "--sys-interaction-level",
96
+ type=str.upper,
97
+ choices=[e.name for e in SystemInteractionLevel],
98
+ default="INTERACTIVE",
99
+ help="Specify system interaction level, used to determine the type of actions that plugins can perform",
100
+ )
101
+
102
+ parser.add_argument(
103
+ "--sys-sku",
104
+ type=str.upper,
105
+ required=False,
106
+ help="Manually specify SKU of system",
107
+ metavar=META_VAR_MAP[str],
108
+ )
109
+
110
+ parser.add_argument(
111
+ "--sys-platform",
112
+ type=str,
113
+ required=False,
114
+ help="Specify system platform",
115
+ metavar=META_VAR_MAP[str],
116
+ )
117
+
118
+ parser.add_argument(
119
+ "--plugin-configs",
120
+ type=str,
121
+ nargs="*",
122
+ help=f"built-in config names or paths to plugin config JSONs.\nAvailable built-in configs: {', '.join(config_reg.configs.keys())}",
123
+ metavar=META_VAR_MAP[str],
124
+ )
125
+
126
+ parser.add_argument(
127
+ "--system-config",
128
+ type=ModelArgHandler(SystemInfo).process_file_arg,
129
+ required=False,
130
+ help="Path to system config json",
131
+ metavar=META_VAR_MAP[str],
132
+ )
133
+
134
+ parser.add_argument(
135
+ "--connection-config",
136
+ type=json_arg,
137
+ required=False,
138
+ help="Path to connection config json",
139
+ metavar=META_VAR_MAP[str],
140
+ )
141
+
142
+ parser.add_argument(
143
+ "--log-path",
144
+ default=".",
145
+ type=log_path_arg,
146
+ help="Specifies local path for node scraper logs, use 'None' to disable logging",
147
+ metavar=META_VAR_MAP[str],
148
+ )
149
+
150
+ parser.add_argument(
151
+ "--log-level",
152
+ default="INFO",
153
+ choices=logging._nameToLevel,
154
+ help="Change python log level",
155
+ )
156
+
157
+ parser.add_argument(
158
+ "--gen-reference-config",
159
+ dest="reference_config",
160
+ action="store_true",
161
+ help="Generate reference config from system. Writes to ./reference_config.json.",
162
+ )
163
+
164
+ parser.add_argument(
165
+ "--skip-sudo",
166
+ dest="skip_sudo",
167
+ action="store_true",
168
+ help="Skip plugins that require sudo permissions",
169
+ )
170
+
171
+ subparsers = parser.add_subparsers(dest="subcmd", help="Subcommands")
172
+
173
+ summary_parser = subparsers.add_parser(
174
+ "summary",
175
+ help="Generates summary csv file",
176
+ )
177
+
178
+ summary_parser.add_argument(
179
+ "--search-path",
180
+ dest="search_path",
181
+ type=log_path_arg,
182
+ help="Path to node-scraper previously generated results.",
183
+ )
184
+
185
+ summary_parser.add_argument(
186
+ "--output-path",
187
+ dest="output_path",
188
+ type=log_path_arg,
189
+ help="Specifies path for summary.csv.",
190
+ )
191
+
192
+ run_plugin_parser = subparsers.add_parser(
193
+ "run-plugins",
194
+ help="Run a series of plugins",
195
+ )
196
+
197
+ describe_parser = subparsers.add_parser(
198
+ "describe",
199
+ help="Display details on a built-in config or plugin",
200
+ )
201
+
202
+ describe_parser.add_argument(
203
+ "type",
204
+ choices=["config", "plugin"],
205
+ help="Type of object to describe (config or plugin)",
206
+ )
207
+
208
+ describe_parser.add_argument(
209
+ "name",
210
+ nargs="?",
211
+ help="Name of the config or plugin to describe",
212
+ )
213
+
214
+ config_builder_parser = subparsers.add_parser(
215
+ "gen-plugin-config",
216
+ help="Generate a config for a plugin or list of plugins",
217
+ )
218
+
219
+ config_builder_parser.add_argument(
220
+ "--gen-reference-config-from-logs",
221
+ dest="reference_config_from_logs",
222
+ type=log_path_arg,
223
+ help="Generate reference config from previous run logfiles. Writes to --output-path/reference_config.json if provided, otherwise ./reference_config.json.",
224
+ )
225
+
226
+ config_builder_parser.add_argument(
227
+ "--plugins",
228
+ nargs="*",
229
+ choices=list(plugin_reg.plugins.keys()),
230
+ help="Plugins to generate config for",
231
+ )
232
+
233
+ config_builder_parser.add_argument(
234
+ "--built-in-configs",
235
+ nargs="*",
236
+ choices=list(config_reg.configs.keys()),
237
+ help="Built in config names",
238
+ )
239
+
240
+ config_builder_parser.add_argument(
241
+ "--output-path",
242
+ default=os.getcwd(),
243
+ help="Directory to store config",
244
+ )
245
+
246
+ config_builder_parser.add_argument(
247
+ "--config-name",
248
+ default="plugin_config.json",
249
+ help="Name of config file",
250
+ )
251
+
252
+ plugin_subparsers = run_plugin_parser.add_subparsers(
253
+ dest="plugin_name", help="Available plugins"
254
+ )
255
+
256
+ plugin_subparser_map = {}
257
+ for plugin_name, plugin_class in plugin_reg.plugins.items():
258
+ plugin_subparser = plugin_subparsers.add_parser(
259
+ plugin_name,
260
+ help=f"Run {plugin_name} plugin",
261
+ )
262
+ try:
263
+ parser_builder = DynamicParserBuilder(plugin_subparser, plugin_class)
264
+ model_type_map = parser_builder.build_plugin_parser()
265
+ except Exception as e:
266
+ print(f"Exception building arg parsers for {plugin_name}: {str(e)}") # noqa: T201
267
+ continue
268
+ plugin_subparser_map[plugin_name] = (plugin_subparser, model_type_map)
269
+
270
+ return parser, plugin_subparser_map
271
+
272
+
273
+ def setup_logger(log_level: str = "INFO", log_path: Optional[str] = None) -> logging.Logger:
274
+ """set up root logger when using the CLI
275
+
276
+ Args:
277
+ log_level (str): log level to use
278
+ log_path (Optional[str]): optional path to filesystem log location
279
+
280
+ Returns:
281
+ logging.Logger: logger intstance
282
+ """
283
+ log_level = getattr(logging, log_level, "INFO")
284
+
285
+ handlers = [logging.StreamHandler(stream=sys.stdout)]
286
+
287
+ if log_path:
288
+ log_file_name = os.path.join(log_path, "nodescraper.log")
289
+ handlers.append(
290
+ logging.FileHandler(filename=log_file_name, mode="wt", encoding="utf-8"),
291
+ )
292
+
293
+ logging.basicConfig(
294
+ force=True,
295
+ level=log_level,
296
+ format="%(asctime)25s %(levelname)10s %(name)25s | %(message)s",
297
+ datefmt="%Y-%m-%d %H:%M:%S %Z",
298
+ handlers=handlers,
299
+ encoding="utf-8",
300
+ )
301
+ logging.root.setLevel(logging.INFO)
302
+ logging.getLogger("paramiko").setLevel(logging.ERROR)
303
+
304
+ logger = logging.getLogger(DEFAULT_LOGGER)
305
+
306
+ return logger
307
+
308
+
309
+ def process_args(
310
+ raw_arg_input: list[str], plugin_names: list[str]
311
+ ) -> tuple[list[str], dict[str, list[str]]]:
312
+ """separate top level args from plugin args
313
+
314
+ Args:
315
+ raw_arg_input (list[str]): list of all arg input
316
+ plugin_names (list[str]): list of plugin names
317
+
318
+ Returns:
319
+ tuple[list[str], dict[str, list[str]]]: tuple of top level args
320
+ and dict of plugin name to plugin args
321
+ """
322
+ top_level_args = raw_arg_input
323
+
324
+ try:
325
+ plugin_arg_index = raw_arg_input.index("run-plugins")
326
+ except ValueError:
327
+ plugin_arg_index = -1
328
+
329
+ plugin_arg_map = {}
330
+ invalid_plugins = []
331
+ if plugin_arg_index != -1 and plugin_arg_index != len(raw_arg_input) - 1:
332
+ top_level_args = raw_arg_input[: plugin_arg_index + 1]
333
+ plugin_args = raw_arg_input[plugin_arg_index + 1 :]
334
+
335
+ # handle help case
336
+ if plugin_args == ["-h"]:
337
+ top_level_args += plugin_args
338
+ else:
339
+ cur_plugin = None
340
+ for arg in plugin_args:
341
+ # Handle comma-separated plugin names (but not arguments)
342
+ if not arg.startswith("-") and "," in arg:
343
+ # Split comma-separated plugin names
344
+ for potential_plugin in arg.split(","):
345
+ potential_plugin = potential_plugin.strip()
346
+ if potential_plugin in plugin_names:
347
+ plugin_arg_map[potential_plugin] = []
348
+ cur_plugin = potential_plugin
349
+ elif potential_plugin:
350
+ # Track invalid plugin names to log event later
351
+ invalid_plugins.append(potential_plugin)
352
+ elif arg in plugin_names:
353
+ plugin_arg_map[arg] = []
354
+ cur_plugin = arg
355
+ elif cur_plugin:
356
+ plugin_arg_map[cur_plugin].append(arg)
357
+ elif not arg.startswith("-"):
358
+ # Track invalid plugin names to log event later
359
+ invalid_plugins.append(arg)
360
+ return (top_level_args, plugin_arg_map, invalid_plugins)
361
+
362
+
363
+ def main(arg_input: Optional[list[str]] = None):
364
+ """Main entry point for the CLI
365
+
366
+ Args:
367
+ arg_input (Optional[list[str]], optional): list of args to parse. Defaults to None.
368
+ """
369
+ if arg_input is None:
370
+ arg_input = sys.argv[1:]
371
+
372
+ plugin_reg = PluginRegistry()
373
+
374
+ config_reg = ConfigRegistry()
375
+ parser, plugin_subparser_map = build_parser(plugin_reg, config_reg)
376
+
377
+ try:
378
+ top_level_args, plugin_arg_map, invalid_plugins = process_args(
379
+ arg_input, list(plugin_subparser_map.keys())
380
+ )
381
+
382
+ parsed_args = parser.parse_args(top_level_args)
383
+ system_info = get_system_info(parsed_args)
384
+ sname = system_info.name.lower().replace("-", "_").replace(".", "_")
385
+ timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%I_%M_%S_%p")
386
+
387
+ if parsed_args.log_path and parsed_args.subcmd not in ["gen-plugin-config", "describe"]:
388
+ log_path = os.path.join(
389
+ parsed_args.log_path,
390
+ f"scraper_logs_{sname}_{timestamp}",
391
+ )
392
+ os.makedirs(log_path)
393
+ else:
394
+ log_path = None
395
+
396
+ logger = setup_logger(parsed_args.log_level, log_path)
397
+ if log_path:
398
+ logger.info("Log path: %s", log_path)
399
+
400
+ # Log warning if invalid plugin names were provided
401
+ if invalid_plugins:
402
+ logger.warning(
403
+ "Invalid plugin name(s) ignored: %s. Use 'describe plugin' to list available plugins.",
404
+ ", ".join(invalid_plugins),
405
+ )
406
+
407
+ if parsed_args.subcmd == "summary":
408
+ generate_summary(parsed_args.search_path, parsed_args.output_path, logger)
409
+ sys.exit(0)
410
+
411
+ if parsed_args.subcmd == "describe":
412
+ parse_describe(parsed_args, plugin_reg, config_reg, logger)
413
+
414
+ if parsed_args.subcmd == "gen-plugin-config":
415
+
416
+ if parsed_args.reference_config_from_logs:
417
+ ref_config = generate_reference_config_from_logs(
418
+ parsed_args.reference_config_from_logs, plugin_reg, logger
419
+ )
420
+ output_path = os.getcwd()
421
+ if parsed_args.output_path:
422
+ output_path = parsed_args.output_path
423
+ path = os.path.join(output_path, "reference_config.json")
424
+ try:
425
+ with open(path, "w") as f:
426
+ json.dump(
427
+ ref_config.model_dump(mode="json", exclude_none=True),
428
+ f,
429
+ indent=2,
430
+ )
431
+ logger.info("Reference config written to: %s", path)
432
+ except Exception as exp:
433
+ logger.error(exp)
434
+ sys.exit(0)
435
+
436
+ parse_gen_plugin_config(parsed_args, plugin_reg, config_reg, logger)
437
+
438
+ parsed_plugin_args = {}
439
+ for plugin, plugin_args in plugin_arg_map.items():
440
+ try:
441
+ parsed_plugin_args[plugin] = plugin_subparser_map[plugin][0].parse_args(plugin_args)
442
+ except Exception as e:
443
+ logger.error("%s exception parsing args for plugin: %s", str(e), plugin)
444
+
445
+ if not parsed_plugin_args and not parsed_args.plugin_configs:
446
+ logger.info(
447
+ "No plugins config args specified, running default config: %s", DEFAULT_CONFIG
448
+ )
449
+ plugin_configs = [DEFAULT_CONFIG]
450
+ else:
451
+ plugin_configs = parsed_args.plugin_configs or []
452
+
453
+ plugin_config_inst_list = get_plugin_configs(
454
+ plugin_config_input=plugin_configs,
455
+ system_interaction_level=parsed_args.sys_interaction_level,
456
+ built_in_configs=config_reg.configs,
457
+ parsed_plugin_args=parsed_plugin_args,
458
+ plugin_subparser_map=plugin_subparser_map,
459
+ )
460
+
461
+ if parsed_args.skip_sudo:
462
+ plugin_config_inst_list[-1].global_args.setdefault("collection_args", {})[
463
+ "skip_sudo"
464
+ ] = True
465
+
466
+ log_system_info(log_path, system_info, logger)
467
+ except Exception as e:
468
+ parser.error(str(e))
469
+
470
+ plugin_executor = PluginExecutor(
471
+ logger=logger,
472
+ plugin_configs=plugin_config_inst_list,
473
+ connections=parsed_args.connection_config,
474
+ system_info=system_info,
475
+ log_path=log_path,
476
+ plugin_registry=plugin_reg,
477
+ )
478
+
479
+ try:
480
+ results = plugin_executor.run_queue()
481
+
482
+ dump_results_to_csv(results, sname, log_path, timestamp, logger)
483
+
484
+ if parsed_args.reference_config:
485
+ ref_config = generate_reference_config(results, plugin_reg, logger)
486
+ if log_path:
487
+ path = os.path.join(log_path, "reference_config.json")
488
+ else:
489
+ path = os.path.join(os.getcwd(), "reference_config.json")
490
+ try:
491
+ with open(path, "w") as f:
492
+ json.dump(
493
+ ref_config.model_dump(mode="json", exclude_none=True),
494
+ f,
495
+ indent=2,
496
+ )
497
+ logger.info("Reference config written to: %s", path)
498
+ except Exception as exp:
499
+ logger.error(exp)
500
+
501
+ if any(result.status > ExecutionStatus.WARNING for result in results):
502
+ sys.exit(1)
503
+ else:
504
+ sys.exit(0)
505
+ except KeyboardInterrupt:
506
+ logger.info("Received Ctrl+C. Shutting down...")
507
+ sys.exit(130)
508
+
509
+
510
+ if __name__ == "__main__":
511
+ main()
@@ -0,0 +1,27 @@
1
+ ###############################################################################
2
+ #
3
+ # MIT License
4
+ #
5
+ # Copyright (c) 2025 Advanced Micro Devices, Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+ #
25
+ ###############################################################################
26
+ META_VAR_MAP = {int: "INT", bool: "BOOL", dict: "JSON_STRING", float: "FLOAT", str: "STRING"}
27
+ DEFAULT_CONFIG = "NodeStatus"