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.
- amd_node_scraper-0.0.1.dist-info/LICENSE +21 -0
- amd_node_scraper-0.0.1.dist-info/METADATA +424 -0
- amd_node_scraper-0.0.1.dist-info/RECORD +197 -0
- amd_node_scraper-0.0.1.dist-info/WHEEL +5 -0
- amd_node_scraper-0.0.1.dist-info/entry_points.txt +2 -0
- amd_node_scraper-0.0.1.dist-info/top_level.txt +1 -0
- nodescraper/__init__.py +32 -0
- nodescraper/base/__init__.py +34 -0
- nodescraper/base/inbandcollectortask.py +118 -0
- nodescraper/base/inbanddataplugin.py +39 -0
- nodescraper/base/regexanalyzer.py +120 -0
- nodescraper/cli/__init__.py +29 -0
- nodescraper/cli/cli.py +511 -0
- nodescraper/cli/constants.py +27 -0
- nodescraper/cli/dynamicparserbuilder.py +171 -0
- nodescraper/cli/helper.py +517 -0
- nodescraper/cli/inputargtypes.py +129 -0
- nodescraper/configbuilder.py +123 -0
- nodescraper/configregistry.py +66 -0
- nodescraper/configs/node_status.json +19 -0
- nodescraper/connection/__init__.py +25 -0
- nodescraper/connection/inband/__init__.py +46 -0
- nodescraper/connection/inband/inband.py +171 -0
- nodescraper/connection/inband/inbandlocal.py +93 -0
- nodescraper/connection/inband/inbandmanager.py +151 -0
- nodescraper/connection/inband/inbandremote.py +173 -0
- nodescraper/connection/inband/sshparams.py +43 -0
- nodescraper/constants.py +26 -0
- nodescraper/enums/__init__.py +40 -0
- nodescraper/enums/eventcategory.py +89 -0
- nodescraper/enums/eventpriority.py +42 -0
- nodescraper/enums/executionstatus.py +44 -0
- nodescraper/enums/osfamily.py +34 -0
- nodescraper/enums/systeminteraction.py +41 -0
- nodescraper/enums/systemlocation.py +33 -0
- nodescraper/generictypes.py +36 -0
- nodescraper/interfaces/__init__.py +44 -0
- nodescraper/interfaces/connectionmanager.py +143 -0
- nodescraper/interfaces/dataanalyzertask.py +138 -0
- nodescraper/interfaces/datacollectortask.py +185 -0
- nodescraper/interfaces/dataplugin.py +356 -0
- nodescraper/interfaces/plugin.py +127 -0
- nodescraper/interfaces/resultcollator.py +56 -0
- nodescraper/interfaces/task.py +164 -0
- nodescraper/interfaces/taskresulthook.py +39 -0
- nodescraper/models/__init__.py +48 -0
- nodescraper/models/analyzerargs.py +93 -0
- nodescraper/models/collectorargs.py +30 -0
- nodescraper/models/connectionconfig.py +34 -0
- nodescraper/models/datamodel.py +171 -0
- nodescraper/models/datapluginresult.py +39 -0
- nodescraper/models/event.py +158 -0
- nodescraper/models/pluginconfig.py +38 -0
- nodescraper/models/pluginresult.py +39 -0
- nodescraper/models/systeminfo.py +44 -0
- nodescraper/models/taskresult.py +185 -0
- nodescraper/models/timerangeargs.py +38 -0
- nodescraper/pluginexecutor.py +274 -0
- nodescraper/pluginregistry.py +152 -0
- nodescraper/plugins/__init__.py +25 -0
- nodescraper/plugins/inband/__init__.py +25 -0
- nodescraper/plugins/inband/amdsmi/__init__.py +28 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_analyzer.py +821 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_collector.py +1313 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_plugin.py +43 -0
- nodescraper/plugins/inband/amdsmi/amdsmidata.py +1002 -0
- nodescraper/plugins/inband/amdsmi/analyzer_args.py +50 -0
- nodescraper/plugins/inband/amdsmi/cper.py +65 -0
- nodescraper/plugins/inband/bios/__init__.py +29 -0
- nodescraper/plugins/inband/bios/analyzer_args.py +64 -0
- nodescraper/plugins/inband/bios/bios_analyzer.py +93 -0
- nodescraper/plugins/inband/bios/bios_collector.py +93 -0
- nodescraper/plugins/inband/bios/bios_plugin.py +43 -0
- nodescraper/plugins/inband/bios/biosdata.py +30 -0
- nodescraper/plugins/inband/cmdline/__init__.py +25 -0
- nodescraper/plugins/inband/cmdline/analyzer_args.py +80 -0
- nodescraper/plugins/inband/cmdline/cmdline_analyzer.py +113 -0
- nodescraper/plugins/inband/cmdline/cmdline_collector.py +77 -0
- nodescraper/plugins/inband/cmdline/cmdline_plugin.py +43 -0
- nodescraper/plugins/inband/cmdline/cmdlinedata.py +30 -0
- nodescraper/plugins/inband/device_enumeration/__init__.py +29 -0
- nodescraper/plugins/inband/device_enumeration/analyzer_args.py +73 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_analyzer.py +81 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_collector.py +176 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_plugin.py +45 -0
- nodescraper/plugins/inband/device_enumeration/deviceenumdata.py +36 -0
- nodescraper/plugins/inband/dimm/__init__.py +25 -0
- nodescraper/plugins/inband/dimm/collector_args.py +31 -0
- nodescraper/plugins/inband/dimm/dimm_collector.py +151 -0
- nodescraper/plugins/inband/dimm/dimm_plugin.py +40 -0
- nodescraper/plugins/inband/dimm/dimmdata.py +30 -0
- nodescraper/plugins/inband/dkms/__init__.py +25 -0
- nodescraper/plugins/inband/dkms/analyzer_args.py +85 -0
- nodescraper/plugins/inband/dkms/dkms_analyzer.py +106 -0
- nodescraper/plugins/inband/dkms/dkms_collector.py +76 -0
- nodescraper/plugins/inband/dkms/dkms_plugin.py +43 -0
- nodescraper/plugins/inband/dkms/dkmsdata.py +33 -0
- nodescraper/plugins/inband/dmesg/__init__.py +28 -0
- nodescraper/plugins/inband/dmesg/analyzer_args.py +33 -0
- nodescraper/plugins/inband/dmesg/collector_args.py +39 -0
- nodescraper/plugins/inband/dmesg/dmesg_analyzer.py +503 -0
- nodescraper/plugins/inband/dmesg/dmesg_collector.py +164 -0
- nodescraper/plugins/inband/dmesg/dmesg_plugin.py +44 -0
- nodescraper/plugins/inband/dmesg/dmesgdata.py +116 -0
- nodescraper/plugins/inband/fabrics/__init__.py +28 -0
- nodescraper/plugins/inband/fabrics/fabrics_collector.py +726 -0
- nodescraper/plugins/inband/fabrics/fabrics_plugin.py +37 -0
- nodescraper/plugins/inband/fabrics/fabricsdata.py +140 -0
- nodescraper/plugins/inband/journal/__init__.py +28 -0
- nodescraper/plugins/inband/journal/collector_args.py +33 -0
- nodescraper/plugins/inband/journal/journal_collector.py +107 -0
- nodescraper/plugins/inband/journal/journal_plugin.py +40 -0
- nodescraper/plugins/inband/journal/journaldata.py +44 -0
- nodescraper/plugins/inband/kernel/__init__.py +25 -0
- nodescraper/plugins/inband/kernel/analyzer_args.py +64 -0
- nodescraper/plugins/inband/kernel/kernel_analyzer.py +91 -0
- nodescraper/plugins/inband/kernel/kernel_collector.py +129 -0
- nodescraper/plugins/inband/kernel/kernel_plugin.py +43 -0
- nodescraper/plugins/inband/kernel/kerneldata.py +32 -0
- nodescraper/plugins/inband/kernel_module/__init__.py +25 -0
- nodescraper/plugins/inband/kernel_module/analyzer_args.py +59 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_analyzer.py +211 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_collector.py +264 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_data.py +60 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_plugin.py +43 -0
- nodescraper/plugins/inband/memory/__init__.py +25 -0
- nodescraper/plugins/inband/memory/analyzer_args.py +45 -0
- nodescraper/plugins/inband/memory/memory_analyzer.py +98 -0
- nodescraper/plugins/inband/memory/memory_collector.py +330 -0
- nodescraper/plugins/inband/memory/memory_plugin.py +43 -0
- nodescraper/plugins/inband/memory/memorydata.py +90 -0
- nodescraper/plugins/inband/network/__init__.py +28 -0
- nodescraper/plugins/inband/network/network_collector.py +1828 -0
- nodescraper/plugins/inband/network/network_plugin.py +37 -0
- nodescraper/plugins/inband/network/networkdata.py +319 -0
- nodescraper/plugins/inband/nvme/__init__.py +28 -0
- nodescraper/plugins/inband/nvme/nvme_collector.py +167 -0
- nodescraper/plugins/inband/nvme/nvme_plugin.py +37 -0
- nodescraper/plugins/inband/nvme/nvmedata.py +45 -0
- nodescraper/plugins/inband/os/__init__.py +25 -0
- nodescraper/plugins/inband/os/analyzer_args.py +64 -0
- nodescraper/plugins/inband/os/os_analyzer.py +73 -0
- nodescraper/plugins/inband/os/os_collector.py +131 -0
- nodescraper/plugins/inband/os/os_plugin.py +43 -0
- nodescraper/plugins/inband/os/osdata.py +31 -0
- nodescraper/plugins/inband/package/__init__.py +25 -0
- nodescraper/plugins/inband/package/analyzer_args.py +48 -0
- nodescraper/plugins/inband/package/package_analyzer.py +253 -0
- nodescraper/plugins/inband/package/package_collector.py +273 -0
- nodescraper/plugins/inband/package/package_plugin.py +43 -0
- nodescraper/plugins/inband/package/packagedata.py +41 -0
- nodescraper/plugins/inband/pcie/__init__.py +29 -0
- nodescraper/plugins/inband/pcie/analyzer_args.py +63 -0
- nodescraper/plugins/inband/pcie/pcie_analyzer.py +1081 -0
- nodescraper/plugins/inband/pcie/pcie_collector.py +690 -0
- nodescraper/plugins/inband/pcie/pcie_data.py +2017 -0
- nodescraper/plugins/inband/pcie/pcie_plugin.py +43 -0
- nodescraper/plugins/inband/process/__init__.py +25 -0
- nodescraper/plugins/inband/process/analyzer_args.py +45 -0
- nodescraper/plugins/inband/process/collector_args.py +31 -0
- nodescraper/plugins/inband/process/process_analyzer.py +91 -0
- nodescraper/plugins/inband/process/process_collector.py +115 -0
- nodescraper/plugins/inband/process/process_plugin.py +46 -0
- nodescraper/plugins/inband/process/processdata.py +34 -0
- nodescraper/plugins/inband/rocm/__init__.py +25 -0
- nodescraper/plugins/inband/rocm/analyzer_args.py +66 -0
- nodescraper/plugins/inband/rocm/rocm_analyzer.py +100 -0
- nodescraper/plugins/inband/rocm/rocm_collector.py +205 -0
- nodescraper/plugins/inband/rocm/rocm_plugin.py +43 -0
- nodescraper/plugins/inband/rocm/rocmdata.py +62 -0
- nodescraper/plugins/inband/storage/__init__.py +25 -0
- nodescraper/plugins/inband/storage/analyzer_args.py +38 -0
- nodescraper/plugins/inband/storage/collector_args.py +31 -0
- nodescraper/plugins/inband/storage/storage_analyzer.py +152 -0
- nodescraper/plugins/inband/storage/storage_collector.py +110 -0
- nodescraper/plugins/inband/storage/storage_plugin.py +44 -0
- nodescraper/plugins/inband/storage/storagedata.py +70 -0
- nodescraper/plugins/inband/sysctl/__init__.py +29 -0
- nodescraper/plugins/inband/sysctl/analyzer_args.py +67 -0
- nodescraper/plugins/inband/sysctl/sysctl_analyzer.py +81 -0
- nodescraper/plugins/inband/sysctl/sysctl_collector.py +101 -0
- nodescraper/plugins/inband/sysctl/sysctl_plugin.py +43 -0
- nodescraper/plugins/inband/sysctl/sysctldata.py +42 -0
- nodescraper/plugins/inband/syslog/__init__.py +28 -0
- nodescraper/plugins/inband/syslog/syslog_collector.py +121 -0
- nodescraper/plugins/inband/syslog/syslog_plugin.py +37 -0
- nodescraper/plugins/inband/syslog/syslogdata.py +46 -0
- nodescraper/plugins/inband/uptime/__init__.py +25 -0
- nodescraper/plugins/inband/uptime/uptime_collector.py +88 -0
- nodescraper/plugins/inband/uptime/uptime_plugin.py +37 -0
- nodescraper/plugins/inband/uptime/uptimedata.py +31 -0
- nodescraper/resultcollators/__init__.py +25 -0
- nodescraper/resultcollators/tablesummary.py +159 -0
- nodescraper/taskresulthooks/__init__.py +28 -0
- nodescraper/taskresulthooks/filesystemloghook.py +88 -0
- nodescraper/typeutils.py +171 -0
- 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)
|