Skip to content
11 changes: 11 additions & 0 deletions nodescraper/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ def _add_cli_root_globals(
help="Skip plugins that require sudo permissions",
)

parser.add_argument(
"--html-artifact",
dest="html_artifact",
action="store_true",
help="Generate browsable HTML artifacts (command_artifacts.html) from "
"command_artifacts.json files in the run directory. Disabled by default.",
)


def build_global_argument_parser(*, add_help: bool = True) -> argparse.ArgumentParser:
"""Globals only (no subcommands), for host CLIs."""
Expand Down Expand Up @@ -633,6 +641,9 @@ def main(
"skip_sudo"
] = True

if getattr(parsed_args, "html_artifact", False) and plugin_config_inst_list:
plugin_config_inst_list[-1].result_collators.setdefault("CommandArtifactHtml", {})

except Exception as e:
parser.error(str(e))

Expand Down
3 changes: 3 additions & 0 deletions nodescraper/enums/eventcategory.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class EventCategory(AutoNameStrEnum):
Network, IT issues, Downtime
- NETWORK
Network configuration, interfaces, routing, neighbors, ethtool data
- SWITCH
Switch configuration, switch OS, command issues
- TELEMETRY
Telemetry / monitored data checks (e.g. Redfish endpoint constraint violations)
- RUNTIME
Expand All @@ -87,6 +89,7 @@ class EventCategory(AutoNameStrEnum):
BIOS = auto()
INFRASTRUCTURE = auto()
NETWORK = auto()
SWITCH = auto()
TELEMETRY = auto()
RUNTIME = auto()
UNKNOWN = auto()
6 changes: 5 additions & 1 deletion nodescraper/interfaces/dataplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,11 @@ def collect(
):
self.connection_manager.connect()

if self.connection_manager.result.status != ExecutionStatus.OK:
# Proceed as long as a connection was established.
if (
self.connection_manager.connection is None
or self.connection_manager.result.status >= ExecutionStatus.ERROR
):
self.collection_result = TaskResult(
task=primary_collector.__name__,
parent=self.__class__.__name__,
Expand Down
29 changes: 29 additions & 0 deletions nodescraper/plugins/inband/switch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
###############################################################################
#
# MIT License
#
# Copyright (c) 2026 Advanced Micro Devices, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
###############################################################################
from .scale_out_arista import ScaleOutAristaPlugin
from .scale_out_dell import ScaleOutDellPlugin

__all__ = ["ScaleOutAristaPlugin", "ScaleOutDellPlugin"]
28 changes: 28 additions & 0 deletions nodescraper/plugins/inband/switch/scale_out_arista/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
###############################################################################
#
# MIT License
#
# Copyright (c) 2026 Advanced Micro Devices, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
###############################################################################
from .scale_out_arista_plugin import ScaleOutAristaPlugin

__all__ = ["ScaleOutAristaPlugin"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
###############################################################################
#
# MIT License
#
# Copyright (c) 2026 Advanced Micro Devices, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
###############################################################################
from typing import List, Optional

from pydantic import Field

from nodescraper.models import AnalyzerArgs


class ScaleOutAristaAnalyzerArgs(AnalyzerArgs):
"""Arguments for the Arista switch analyzer."""

analysis_ports: Optional[List[str]] = Field(
default=None,
description=(
"Restrict per-port analysis to the given ports. Ports are "
"S/P/[SP] where subport is optional (e.g. ['1/1', '1/31', '1/1/1']) "
"When omitted, every port present in the data is analyzed."
"Independent of any collection-time filter."
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
###############################################################################
#
# MIT License
#
# Copyright (c) 2026 Advanced Micro Devices, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
###############################################################################
from pydantic import Field

from nodescraper.models import CollectorArgs


class ScaleOutAristaCollectorArgs(CollectorArgs):
"""Arguments for the Arista switch collector."""

html_view: bool = Field(
default=False,
description=(
"Set true to run the '| json' commands again without the json tag, "
"producing human-readable (non-JSON) command output for HTML artifact."
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
###############################################################################
#
# MIT License
#
# Copyright (c) 2026 Advanced Micro Devices, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
###############################################################################

import re
from typing import Any, ClassVar

from nodescraper.interfaces import DataAnalyzer

from ..switch_analyzer_base import SwitchAnalyzerBase
from .analyzer_args import ScaleOutAristaAnalyzerArgs
from .scaleoutaristadata import ScaleOutAristaDataModel


class ScaleOutAristaAnalyzer(
SwitchAnalyzerBase[ScaleOutAristaDataModel],
DataAnalyzer[ScaleOutAristaDataModel, ScaleOutAristaAnalyzerArgs],
):
"""Check Arista switch data for errors and warnings.

Walks every model in the collected :class:`ScaleOutAristaDataModel` and checks
each ``error_fields`` / ``warning_fields`` ClassVar against an optional
``ports`` filter.
"""

VENDOR_NAME: ClassVar[str] = "Arista"
DATA_MODEL = ScaleOutAristaDataModel

PORT_NAME_RE: ClassVar[re.Pattern] = re.compile(r"^(?:Ethernet)?(\d+(?:/\d+)*)$", re.IGNORECASE)
PORT_FORMAT_HINT: ClassVar[str] = "expected slash-separated decimals (e.g. 'M/S', 'A/B/C')"

def _walk_system(self, switch_data: ScaleOutAristaDataModel) -> list[dict[str, Any]]:
findings: list[dict[str, Any]] = []

if switch_data.system_env is None:
return findings

findings.extend(
self._check_model(
switch_data.system_env,
context={"section": "system_env"},
)
)

for idx, psu in enumerate(switch_data.system_env.power_supply_slots or []):
findings.extend(
self._check_model(
psu,
context={
"section": "power_supply_slots",
"index": idx,
"label": psu.label,
},
)
)

for idx, fan in enumerate(switch_data.system_env.fan_tray_slots or []):
findings.extend(
self._check_model(
fan,
context={
"section": "fan_tray_slots",
"index": idx,
"label": fan.label,
},
)
)

return findings
Loading
Loading