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
@@ -0,0 +1,171 @@
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
+ from typing import Optional, Type
28
+
29
+ from pydantic import BaseModel
30
+
31
+ from nodescraper.cli.constants import META_VAR_MAP
32
+ from nodescraper.cli.inputargtypes import bool_arg, dict_arg
33
+ from nodescraper.interfaces.plugin import PluginInterface
34
+ from nodescraper.models import DataModel
35
+ from nodescraper.typeutils import TypeUtils
36
+
37
+
38
+ class DynamicParserBuilder:
39
+ """Dynamically build an argparse parser based on function type annotations or pydantic model types"""
40
+
41
+ def __init__(self, parser: argparse.ArgumentParser, plugin_class: Type[PluginInterface]):
42
+ self.parser = parser
43
+ self.plugin_class = plugin_class
44
+
45
+ def build_plugin_parser(self) -> dict:
46
+ """Add parser argument based on arguments in a plugin run function signature"""
47
+ skip_args = ["self", "preserve_connection", "max_event_priority_level"]
48
+ type_map = TypeUtils.get_func_arg_types(self.plugin_class.run, self.plugin_class)
49
+
50
+ model_type_map = {}
51
+
52
+ for arg, arg_data in type_map.items():
53
+ if arg in skip_args:
54
+ continue
55
+
56
+ type_class_map = {
57
+ type_class.type_class: type_class for type_class in arg_data.type_classes
58
+ }
59
+
60
+ # skip args where generic type has been set to None
61
+ if type(None) in type_class_map:
62
+ continue
63
+
64
+ model_arg = self.get_model_arg(type_class_map)
65
+
66
+ # only add cli args for top level model args
67
+ if model_arg:
68
+ model_args = self.build_model_arg_parser(model_arg, arg_data.required)
69
+ for model_arg in model_args:
70
+ model_type_map[model_arg] = arg
71
+ else:
72
+ self.add_argument(type_class_map, arg.replace("_", "-"), arg_data.required)
73
+
74
+ return model_type_map
75
+
76
+ @classmethod
77
+ def get_model_arg(cls, type_class_map: dict) -> Optional[Type[BaseModel]]:
78
+ """Get the first type which is a pydantic model from a type class map
79
+
80
+ Args:
81
+ type_class_map (dict): mapping of type classes
82
+
83
+ Returns:
84
+ Optional[Type[BaseModel]]: pydantic model type
85
+ """
86
+ return next(
87
+ (
88
+ type_class
89
+ for type_class in type_class_map
90
+ if (
91
+ isinstance(type_class, type)
92
+ and issubclass(type_class, BaseModel)
93
+ and not issubclass(type_class, DataModel)
94
+ )
95
+ ),
96
+ None,
97
+ )
98
+
99
+ def add_argument(
100
+ self,
101
+ type_class_map: dict,
102
+ arg_name: str,
103
+ required: bool,
104
+ ) -> None:
105
+ """Add an argument to a parser with an appropriate type
106
+
107
+ Args:
108
+ type_class_map (dict): type classes for the arg
109
+ arg_name (str): argument name
110
+ required (bool): whether or not the arg is required
111
+ """
112
+ if list in type_class_map:
113
+ type_class = type_class_map[list]
114
+ self.parser.add_argument(
115
+ f"--{arg_name}",
116
+ nargs="*",
117
+ type=type_class.inner_type if type_class.inner_type else str,
118
+ required=required,
119
+ metavar=META_VAR_MAP.get(type_class.inner_type, "STRING"),
120
+ )
121
+ elif bool in type_class_map:
122
+ self.parser.add_argument(
123
+ f"--{arg_name}",
124
+ type=bool_arg,
125
+ required=required,
126
+ choices=[True, False],
127
+ )
128
+ elif float in type_class_map:
129
+ self.parser.add_argument(
130
+ f"--{arg_name}", type=float, required=required, metavar=META_VAR_MAP[float]
131
+ )
132
+ elif int in type_class_map:
133
+ self.parser.add_argument(
134
+ f"--{arg_name}", type=int, required=required, metavar=META_VAR_MAP[int]
135
+ )
136
+ elif str in type_class_map:
137
+ self.parser.add_argument(
138
+ f"--{arg_name}", type=str, required=required, metavar=META_VAR_MAP[str]
139
+ )
140
+ elif dict in type_class_map or self.get_model_arg(type_class_map):
141
+ self.parser.add_argument(
142
+ f"--{arg_name}", type=dict_arg, required=required, metavar=META_VAR_MAP[dict]
143
+ )
144
+ else:
145
+ self.parser.add_argument(
146
+ f"--{arg_name}", type=str, required=required, metavar=META_VAR_MAP[str]
147
+ )
148
+
149
+ def build_model_arg_parser(self, model: type[BaseModel], required: bool) -> list[str]:
150
+ """Add args to a parser based on attributes of a pydantic model
151
+
152
+ Args:
153
+ model (type[BaseModel]): input model
154
+ required (bool): whether the args from the model are required
155
+
156
+ Returns:
157
+ list[str]: list of model attributes that were added as args to the parser
158
+ """
159
+ type_map = TypeUtils.get_model_types(model)
160
+
161
+ for attr, attr_data in type_map.items():
162
+ type_class_map = {
163
+ type_class.type_class: type_class for type_class in attr_data.type_classes
164
+ }
165
+
166
+ if type(None) in type_class_map and len(attr_data.type_classes) == 1:
167
+ continue
168
+
169
+ self.add_argument(type_class_map, attr.replace("_", "-"), required)
170
+
171
+ return list(type_map.keys())
@@ -0,0 +1,517 @@
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 csv
28
+ import glob
29
+ import json
30
+ import logging
31
+ import os
32
+ import sys
33
+ from pathlib import Path
34
+ from typing import Optional, Tuple
35
+
36
+ from pydantic import BaseModel
37
+
38
+ from nodescraper.cli.inputargtypes import ModelArgHandler
39
+ from nodescraper.configbuilder import ConfigBuilder
40
+ from nodescraper.configregistry import ConfigRegistry
41
+ from nodescraper.enums import ExecutionStatus, SystemInteractionLevel, SystemLocation
42
+ from nodescraper.models import PluginConfig, PluginResult, SystemInfo, TaskResult
43
+ from nodescraper.pluginexecutor import PluginExecutor
44
+ from nodescraper.pluginregistry import PluginRegistry
45
+ from nodescraper.resultcollators.tablesummary import TableSummary
46
+
47
+
48
+ def get_system_info(args: argparse.Namespace) -> SystemInfo:
49
+ """build system info object using args
50
+
51
+ Args:
52
+ args (argparse.Namespace): parsed args
53
+
54
+ Raises:
55
+ argparse.ArgumentTypeError: if system location arg is invalid
56
+
57
+ Returns:
58
+ SystemInfo: system info instance
59
+ """
60
+
61
+ if args.system_config:
62
+ system_info = args.system_config
63
+ else:
64
+ system_info = SystemInfo()
65
+
66
+ if args.sys_name:
67
+ system_info.name = args.sys_name
68
+
69
+ if args.sys_sku:
70
+ system_info.sku = args.sys_sku
71
+
72
+ if args.sys_platform:
73
+ system_info.platform = args.sys_platform
74
+
75
+ if args.sys_location:
76
+ try:
77
+ location = getattr(SystemLocation, args.sys_location)
78
+ except Exception as e:
79
+ raise argparse.ArgumentTypeError("Invalid input for system location") from e
80
+
81
+ system_info.location = location
82
+
83
+ return system_info
84
+
85
+
86
+ def get_plugin_configs(
87
+ plugin_config_input: list[str],
88
+ system_interaction_level: SystemInteractionLevel,
89
+ built_in_configs: dict[str, PluginConfig],
90
+ parsed_plugin_args: dict[str, argparse.Namespace],
91
+ plugin_subparser_map: dict[str, tuple[argparse.ArgumentParser, dict]],
92
+ ) -> list[PluginConfig]:
93
+ """Build list of plugin configs based on input args
94
+
95
+ Args:
96
+ plugin_config_input (list[str]): list of plugin config inputs, can be paths to JSON files or built-in config names
97
+ system_interaction_level (SystemInteractionLevel): system interaction level, used to determine the type of actions that plugins can perform
98
+ built_in_configs (dict[str, PluginConfig]): built-in plugin configs, mapping from config name to PluginConfig instance
99
+ parsed_plugin_args (dict[str, argparse.Namespace]): parsed plugin arguments, mapping from plugin name to parsed args
100
+ plugin_subparser_map (dict[str, tuple[argparse.ArgumentParser, dict]]): plugin subparser map, mapping from plugin name to tuple of parser and model type map
101
+
102
+ Raises:
103
+ argparse.ArgumentTypeError: if system interaction level is invalid
104
+ argparse.ArgumentTypeError: if no plugin config found for a given input
105
+
106
+ Returns:
107
+ list[PluginConfig]: list of PluginConfig instances based on input args
108
+ """
109
+ try:
110
+ system_interaction_level = getattr(SystemInteractionLevel, system_interaction_level)
111
+ except Exception as e:
112
+ raise argparse.ArgumentTypeError("Invalid input for system interaction level") from e
113
+
114
+ base_config = PluginConfig(result_collators={str(TableSummary.__name__): {}})
115
+
116
+ base_config.global_args["system_interaction_level"] = system_interaction_level
117
+
118
+ plugin_configs = [base_config]
119
+
120
+ if plugin_config_input:
121
+ for config in plugin_config_input:
122
+ if os.path.exists(config):
123
+ plugin_configs.append(ModelArgHandler(PluginConfig).process_file_arg(config))
124
+ elif config in built_in_configs:
125
+ plugin_configs.append(built_in_configs[config])
126
+ else:
127
+ raise argparse.ArgumentTypeError(f"No plugin config found for: {config}")
128
+
129
+ if parsed_plugin_args:
130
+ plugin_input_config = PluginConfig()
131
+
132
+ for plugin, plugin_args in parsed_plugin_args.items():
133
+ config = {}
134
+ model_type_map = plugin_subparser_map[plugin][1]
135
+ for arg, val in vars(plugin_args).items():
136
+ if val is None:
137
+ continue
138
+ if arg in model_type_map:
139
+ model = model_type_map[arg]
140
+ if model in config:
141
+ config[model][arg] = val
142
+ else:
143
+ config[model] = {arg: val}
144
+ else:
145
+ config[arg] = val
146
+ plugin_input_config.plugins[plugin] = config
147
+
148
+ plugin_configs.append(plugin_input_config)
149
+
150
+ return plugin_configs
151
+
152
+
153
+ def build_config(
154
+ config_reg: ConfigRegistry,
155
+ plugin_reg: PluginRegistry,
156
+ logger: logging.Logger,
157
+ plugins: Optional[list[str]] = None,
158
+ built_in_configs: Optional[list[str]] = None,
159
+ ) -> PluginConfig:
160
+ """build a plugin config
161
+
162
+ Args:
163
+ config_reg (ConfigRegistry): config registry instance
164
+ plugin_reg (PluginRegistry): plugin registry instance
165
+ logger (logging.Logger): logger instance
166
+ plugins (Optional[list[str]], optional): list of plugin names to include. Defaults to None.
167
+ built_in_configs (Optional[list[str]], optional): list of built in config names to include. Defaults to None.
168
+
169
+ Returns:
170
+ PluginConfig: plugin config obf
171
+ """
172
+ configs = []
173
+ if plugins:
174
+ logger.info("Building config for plugins: %s", plugins)
175
+ config_builder = ConfigBuilder(plugin_registry=plugin_reg)
176
+ configs.append(config_builder.gen_config(plugins))
177
+
178
+ if built_in_configs:
179
+ logger.info("Retrieving built in configs: %s", built_in_configs)
180
+ for config in built_in_configs:
181
+ if config not in config_reg.configs:
182
+ logger.warning("No built in config found for name: %s", config)
183
+ else:
184
+ configs.append(config_reg.configs[config])
185
+
186
+ config = PluginExecutor.merge_configs(configs)
187
+ return config
188
+
189
+
190
+ def parse_describe(
191
+ parsed_args: argparse.Namespace,
192
+ plugin_reg: PluginRegistry,
193
+ config_reg: ConfigRegistry,
194
+ logger: logging.Logger,
195
+ ):
196
+ """parse 'describe' cmd line argument
197
+
198
+ Args:
199
+ parsed_args (argparse.Namespace): parsed cmd line arguments
200
+ plugin_reg (PluginRegistry): plugin registry instance
201
+ config_reg (ConfigRegistry): config registry instance
202
+ logger (logging.Logger): logger instance
203
+ """
204
+ if not parsed_args.name:
205
+ if parsed_args.type == "config":
206
+ print("Available built-in configs:") # noqa: T201
207
+ for name in config_reg.configs:
208
+ print(f" {name}") # noqa: T201
209
+ elif parsed_args.type == "plugin":
210
+ print("Available plugins:") # noqa: T201
211
+ for name in plugin_reg.plugins:
212
+ print(f" {name}") # noqa: T201
213
+ print(f"\nUsage: describe {parsed_args.type} <name>") # noqa: T201
214
+ sys.exit(0)
215
+
216
+ if parsed_args.type == "config":
217
+ if parsed_args.name not in config_reg.configs:
218
+ logger.error("No config found for name: %s", parsed_args.name)
219
+ sys.exit(1)
220
+ config_model = config_reg.configs[parsed_args.name]
221
+ print(f"Config Name: {parsed_args.name}") # noqa: T201
222
+ print(f"Description: {getattr(config_model, 'desc', '')}") # noqa: T201
223
+ print("Plugins:") # noqa: T201
224
+ for plugin in getattr(config_model, "plugins", []):
225
+ print(f"\t{plugin}") # noqa: T201
226
+
227
+ elif parsed_args.type == "plugin":
228
+ if parsed_args.name not in plugin_reg.plugins:
229
+ logger.error("No plugin found for name: %s", parsed_args.name)
230
+ sys.exit(1)
231
+ plugin_class = plugin_reg.plugins[parsed_args.name]
232
+ print(f"Plugin Name: {parsed_args.name}") # noqa: T201
233
+ print(f"Description: {getattr(plugin_class, '__doc__', '')}") # noqa: T201
234
+
235
+ sys.exit(0)
236
+
237
+
238
+ def parse_gen_plugin_config(
239
+ parsed_args: argparse.Namespace,
240
+ plugin_reg: PluginRegistry,
241
+ config_reg: ConfigRegistry,
242
+ logger: logging.Logger,
243
+ ):
244
+ """parse 'gen_plugin_config' cmd line argument
245
+
246
+ Args:
247
+ parsed_args (argparse.Namespace): parsed cmd line arguments
248
+ plugin_reg (PluginRegistry): plugin registry instance
249
+ config_reg (ConfigRegistry): config registry instance
250
+ logger (logging.Logger): logger instance
251
+ """
252
+ try:
253
+ config = build_config(
254
+ config_reg, plugin_reg, logger, parsed_args.plugins, parsed_args.built_in_configs
255
+ )
256
+
257
+ config.name = parsed_args.config_name.split(".")[0]
258
+ config.desc = "Auto generated config"
259
+ output_path = os.path.join(parsed_args.output_path, parsed_args.config_name)
260
+ with open(output_path, "w", encoding="utf-8") as out_file:
261
+ out_file.write(config.model_dump_json(indent=2))
262
+
263
+ logger.info("Config saved to: %s", output_path)
264
+ sys.exit(0)
265
+ except Exception:
266
+ logger.exception("Exception when building config")
267
+ sys.exit(1)
268
+
269
+
270
+ def log_system_info(log_path: Optional[str], system_info: SystemInfo, logger: logging.Logger):
271
+ """dump system info object to json log
272
+
273
+ Args:
274
+ log_path (str): path to log folder
275
+ system_info (SystemInfo): system object instance
276
+ """
277
+ if log_path:
278
+ try:
279
+ with open(
280
+ os.path.join(log_path, "system_info.json"), "w", encoding="utf-8"
281
+ ) as log_file:
282
+ json.dump(
283
+ system_info.model_dump(mode="json", exclude_none=True),
284
+ log_file,
285
+ indent=2,
286
+ )
287
+ except Exception as exp:
288
+ logger.error(exp)
289
+
290
+
291
+ def extract_analyzer_args_from_model(
292
+ plugin_cls: type, data_model: BaseModel, logger: logging.Logger
293
+ ) -> Optional[BaseModel]:
294
+ """Extract analyzer args from a plugin and a data model.
295
+
296
+ Args:
297
+ plugin_cls (type): The plugin class from registry.
298
+ data_model (BaseModel): System data model.
299
+ logger (logging.Logger): logger.
300
+
301
+ Returns:
302
+ Optional[BaseModel]: Instance of analyzer args model or None if unavailable.
303
+ """
304
+ if not hasattr(plugin_cls, "ANALYZER_ARGS") or not plugin_cls.ANALYZER_ARGS:
305
+ logger.warning(
306
+ "Plugin: %s does not support reference config creation. No analyzer args defined.",
307
+ getattr(plugin_cls, "__name__", str(plugin_cls)),
308
+ )
309
+ return None
310
+
311
+ try:
312
+ return plugin_cls.ANALYZER_ARGS.build_from_model(data_model)
313
+ except NotImplementedError as e:
314
+ logger.info("%s: %s", plugin_cls.__name__, str(e))
315
+ return None
316
+
317
+
318
+ def generate_reference_config(
319
+ results: list[PluginResult], plugin_reg: PluginRegistry, logger: logging.Logger
320
+ ) -> PluginConfig:
321
+ """Generate reference config from plugin results
322
+
323
+ Args:
324
+ results (list[PluginResult]): list of plugin results
325
+ plugin_reg (PluginRegistry): registry containing all registered plugins
326
+ logger (logging.Logger): logger
327
+
328
+ Returns:
329
+ PluginConfig: holds model that defines final reference config
330
+ """
331
+ plugin_config = PluginConfig()
332
+ plugins = {}
333
+ for obj in results:
334
+ if obj.result_data.collection_result.status != ExecutionStatus.OK:
335
+ logger.warning(
336
+ "Plugin: %s result status is %s, skipping",
337
+ obj.source,
338
+ obj.result_data.collection_result.status,
339
+ )
340
+ continue
341
+
342
+ data_model = obj.result_data.system_data
343
+ if data_model is None:
344
+ logger.warning("Plugin: %s data model not found: %s, skipping", obj.source)
345
+ continue
346
+
347
+ plugin = plugin_reg.plugins.get(obj.source)
348
+
349
+ if obj.source not in plugins:
350
+ plugins[obj.source] = {}
351
+
352
+ a_args = extract_analyzer_args_from_model(plugin, data_model, logger)
353
+ if a_args:
354
+ plugins[obj.source]["analysis_args"] = a_args.model_dump(exclude_none=True)
355
+
356
+ plugin_config.plugins = plugins
357
+
358
+ return plugin_config
359
+
360
+
361
+ def generate_reference_config_from_logs(
362
+ path: str, plugin_reg: PluginRegistry, logger: logging.Logger
363
+ ) -> PluginConfig:
364
+ """Parse previous log files and generate plugin config with populated analyzer args
365
+
366
+ Args:
367
+ path (str): path to log files
368
+ plugin_reg (PluginRegistry): plugin registry instance
369
+ logger (logging.Logger): logger instance
370
+
371
+ Returns:
372
+ PluginConfig: instance of plugin config
373
+ """
374
+ found = find_datamodel_and_result(path)
375
+ plugin_config = PluginConfig()
376
+ plugins = {}
377
+ for dm, res in found:
378
+ result_path = Path(res)
379
+ res_payload = json.loads(result_path.read_text(encoding="utf-8"))
380
+ task_res = TaskResult(**res_payload)
381
+ dm_path = Path(dm)
382
+ dm_payload = json.loads(dm_path.read_text(encoding="utf-8"))
383
+ plugin = plugin_reg.plugins.get(task_res.parent)
384
+ if not plugin:
385
+ logger.warning(
386
+ "Plugin %s not found in the plugin registry: %s.",
387
+ task_res.parent,
388
+ )
389
+ continue
390
+
391
+ data_model = plugin.DATA_MODEL.model_validate(dm_payload)
392
+
393
+ args = extract_analyzer_args_from_model(plugin, data_model, logger)
394
+ if not args:
395
+ continue
396
+
397
+ plugins[task_res.parent] = {"analysis_args": args.model_dump(exclude_none=True)}
398
+
399
+ plugin_config.plugins = plugins
400
+ return plugin_config
401
+
402
+
403
+ def find_datamodel_and_result(base_path: str) -> list[Tuple[str, str]]:
404
+ """Get datamodel and result files
405
+
406
+ Args:
407
+ base_path (str): location of previous run logs
408
+
409
+ Returns:
410
+ list[Tuple[str, str]]: tuple of datamodel and result json files
411
+ """
412
+ tuple_list: list[Tuple[str, str, str]] = []
413
+ for root, _, files in os.walk(base_path):
414
+ if "collector" in os.path.basename(root).lower():
415
+ datamodel_path = None
416
+ result_path = None
417
+
418
+ for fname in files:
419
+ low = fname.lower()
420
+ if low.endswith("datamodel.json"):
421
+ datamodel_path = os.path.join(root, fname)
422
+ elif low == "result.json":
423
+ result_path = os.path.join(root, fname)
424
+
425
+ if datamodel_path and result_path:
426
+ tuple_list.append((datamodel_path, result_path))
427
+
428
+ return tuple_list
429
+
430
+
431
+ def dump_results_to_csv(
432
+ results: list[PluginResult],
433
+ nodename: str,
434
+ log_path: str,
435
+ timestamp: str,
436
+ logger: logging.Logger,
437
+ ):
438
+ """dump node-scraper summary results to csv file
439
+
440
+ Args:
441
+ results (list[PluginResult]): list of PluginResults
442
+ nodename (str): node where results come from
443
+ log_path (str): path to results
444
+ timestamp (str): time when results were taken
445
+ logger (logging.Logger): instance of logger
446
+ """
447
+ fieldnames = ["nodename", "plugin", "status", "timestamp", "message"]
448
+ filename = log_path + "/nodescraper.csv"
449
+ all_rows = []
450
+ for res in results:
451
+ row = {
452
+ "nodename": nodename,
453
+ "plugin": res.source,
454
+ "status": res.status.name,
455
+ "timestamp": timestamp,
456
+ "message": res.message,
457
+ }
458
+ all_rows.append(row)
459
+
460
+ dump_to_csv(all_rows, filename, fieldnames, logger)
461
+
462
+
463
+ def dump_to_csv(all_rows: list, filename: str, fieldnames: list[str], logger: logging.Logger):
464
+ """dump data to csv
465
+
466
+ Args:
467
+ all_rows (list): rows to be written
468
+ filename (str): name of file to write to
469
+ fieldnames (list[str]): header for csv file
470
+ logger (logging.Logger): isntance of logger
471
+ """
472
+ try:
473
+ with open(filename, "w", newline="") as f:
474
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
475
+ writer.writeheader()
476
+ for row in all_rows:
477
+ writer.writerow(row)
478
+ except Exception as exp:
479
+ logger.error("Could not dump data to csv file: %s", exp)
480
+ logger.info("Data written to csv file: %s", filename)
481
+
482
+
483
+ def generate_summary(search_path: str, output_path: Optional[str], logger: logging.Logger):
484
+ """Concatenate csv files into 1 summary csv file
485
+
486
+ Args:
487
+ search_path (str): Path for previous runs
488
+ output_path (Optional[str]): Path for new summary csv file
489
+ logger (logging.Logger): instance of logger
490
+ """
491
+
492
+ fieldnames = ["nodename", "plugin", "status", "timestamp", "message"]
493
+ all_rows = []
494
+
495
+ pattern = os.path.join(search_path, "**", "nodescraper.csv")
496
+ matched_files = glob.glob(pattern, recursive=True)
497
+
498
+ if not matched_files:
499
+ logger.error(f"No nodescraper.csv files found under {search_path}")
500
+ return
501
+
502
+ for filepath in matched_files:
503
+ logger.info(f"Reading: {filepath}")
504
+ with open(filepath, newline="") as f:
505
+ reader = csv.DictReader(f)
506
+ for row in reader:
507
+ all_rows.append(row)
508
+
509
+ if not all_rows:
510
+ logger.error("No data rows found in matched CSV files.")
511
+ return
512
+
513
+ if not output_path:
514
+ output_path = os.getcwd()
515
+
516
+ output_path = os.path.join(output_path, "summary.csv")
517
+ dump_to_csv(all_rows, output_path, fieldnames, logger)