From b77944f693740746904df29bc4319c23f146d63f Mon Sep 17 00:00:00 2001 From: Kushagra Bhargava Date: Mon, 27 Apr 2026 21:42:26 +0000 Subject: [PATCH] ND-17670: Add NDP plugin build system Add non-interactive mode for pre-upgrade validation. Signed-off-by: Kushagra Bhargava --- plugin/.gitignore | 7 + plugin/Makefile | 178 +++++++++++++++ plugin/README.md | 241 +++++++++++++++++++- plugin/pre-upgrade-validation/manifest.yaml | 5 + plugin/pre-upgrade-validation/run.sh | 13 ++ script/ND-Preupgrade-Validation.py | 224 +++++++++++------- script/README.md | 65 +++++- 7 files changed, 643 insertions(+), 90 deletions(-) create mode 100644 plugin/.gitignore create mode 100644 plugin/Makefile create mode 100644 plugin/pre-upgrade-validation/manifest.yaml create mode 100755 plugin/pre-upgrade-validation/run.sh diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 0000000..bb24ed0 --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1,7 @@ +dist/ +.staging/ +.build-env/ +.swims-staging/ +keys/*.pem +*.log +*.der diff --git a/plugin/Makefile b/plugin/Makefile new file mode 100644 index 0000000..35c7e38 --- /dev/null +++ b/plugin/Makefile @@ -0,0 +1,178 @@ +# NDP Plugin Build System +# +# Targets: +# make build - Package the plugin (unsigned) +# make dev - Build + SWIMS dev sign +# make rel - Build + SWIMS release sign +# make sign-local - Build + sign with a local PEM key (offline testing) +# make clean - Remove build artifacts +# make setup - Clone build repo (one-time) +# +# SWIMS signing prerequisites: +# 1. Run 'make setup' once to clone the build repo +# 2. Place SWIMS tokens in the SWIMS_TICKET_DIR: +# .sec_id_new Vault role/secret IDs +# ACI_NEW_SWIMS_DEV.token SWIMS build auth token (dev) +# Copy from a recent build sandbox (e.g. *-build/tools/code_sign/) +# 3. Run: make dev CODE_SIGN_CREDENTIALS=user:token +# +# Uses PROJECT=aci (ACI SWIMS key identity) so that the resulting +# signature verifies against the AbraxasACIDev.pem public key that +# ships on every ND box at /certs/signing/dev.pem. +# +# Signing produces a NAP-style JSON signature file (manifest.yaml.signature) +# verified by imagesign.VerifyAppSignature / verifyPKI (SHA-256, PKCS#1 v1.5). +# SWIMS targets use tsign (not bsign); local target produces the same JSON. +# +# For local testing without SWIMS: +# make sign-local LOCAL_KEY=keys/dev.pem + +PLUGIN_DIR ?= pre-upgrade-validation +OUT_DIR ?= dist +STAGING := .staging + +# Build environment — cloned from nd-org-git/build, same as release repo +BUILD_ENV := $(CURDIR)/.build-env + +# SWIMS ticket directory — where .sec_id_new and ACI_NEW_SWIMS_*.token live +SWIMS_TICKET_DIR ?= $(BUILD_ENV)/swims +SWIMS_STAGING := $(CURDIR)/.swims-staging + +# Artifactory credentials for code_sign.sh (user:token), split into _usr/_psw +CODE_SIGN_CREDENTIALS ?= + +# Local signing key (for make sign-local) +LOCAL_KEY ?= keys/dev.pem + +MANIFEST := $(PLUGIN_DIR)/manifest.yaml +NAME := $(shell grep '^name:' $(MANIFEST) | awk '{print $$2}') +VERSION := $(shell grep '^version:' $(MANIFEST) | sed 's/version: *//;s/"//g') +NDP_FILE := $(OUT_DIR)/$(NAME)-$(VERSION).ndp + +CODE_SIGN_SH := $(BUILD_ENV)/tools/code_sign.sh + +.PHONY: build dev rel sign-local setup clean distclean help + +help: + @echo "NDP Plugin Build System" + @echo "" + @echo " make setup Clone build repo (one-time)" + @echo " make dev CODE_SIGN_CREDENTIALS=user:token" + @echo " Build + SWIMS dev sign" + @echo " make rel CODE_SIGN_CREDENTIALS=user:token" + @echo " Build + SWIMS release sign" + @echo " make build Package only (no signature)" + @echo " make sign-local Sign with local PEM key (offline testing)" + @echo " make clean Remove build artifacts" + @echo " make distclean Remove build artifacts + build env" + @echo "" + @echo " PLUGIN_DIR=$(PLUGIN_DIR) OUT_DIR=$(OUT_DIR)" + +# ---- Bootstrap (one-time setup) ---- + +setup: + @if [ ! -d "$(BUILD_ENV)/tools" ]; then \ + echo "Cloning build repo into $(BUILD_ENV)..."; \ + git clone --quiet --depth=1 -b main git@github.com:nd-org-git/build.git $(BUILD_ENV); \ + else \ + echo "Build env already present at $(BUILD_ENV)"; \ + fi + @mkdir -p $(SWIMS_TICKET_DIR) + @echo "" + @echo "Setup complete. Place SWIMS tokens in $(SWIMS_TICKET_DIR)/:" + @echo " .sec_id_new Vault role/secret IDs" + @echo " ACI_NEW_SWIMS_DEV.token SWIMS build auth token (dev)" + @echo "" + @echo "Copy from a recent build sandbox (e.g. *-build/tools/code_sign/)." + +# ---- Main targets ---- + +build: _stage _copy_sources _package + @echo "Built (unsigned): $(NDP_FILE)" + +dev: _check_swims _stage _copy_sources _sign_swims_dev _package + @echo "Built (SWIMS dev-signed): $(NDP_FILE)" + +rel: _check_swims _stage _copy_sources _sign_swims_rel _package + @echo "Built (SWIMS rel-signed): $(NDP_FILE)" + +sign-local: _stage _copy_sources _sign_local _package + @echo "Built (locally signed): $(NDP_FILE)" + +# ---- SWIMS signing (NAP-style JSON signature) ---- +# Uses PROJECT=aci so the signature matches the ACI Abraxas public key +# baked into every ND image (/certs/signing/dev.pem == AbraxasACIDev.pem). +# +# Uses tsign (not bsign) to produce a JSON signature file compatible with +# imagesign.VerifyAppSignature / verifyPKI (SHA-256, PKCS#1 v1.5, base64). +# This is the same format used by NAP app signatures (index.json.signature). + +_check_swims: + @test -f $(CODE_SIGN_SH) || { echo "ERROR: Build env not found. Run 'make setup' first."; exit 1; } + @test -n "$(CODE_SIGN_CREDENTIALS)" || { echo "ERROR: Set CODE_SIGN_CREDENTIALS=user:token"; exit 1; } + @test -f $(SWIMS_TICKET_DIR)/.sec_id_new || { echo "ERROR: SWIMS tokens not found in $(SWIMS_TICKET_DIR)/"; echo "Copy .sec_id_new and ACI_NEW_SWIMS_*.token from a recent build sandbox."; exit 1; } + +_sign_swims_dev: + @echo "Signing manifest with SWIMS (dev, NAP-style tsign)..." + @rm -rf $(SWIMS_STAGING) + export PROJECT=aci && \ + export code_sign_artifactory_credentials_usr=$$(echo '$(CODE_SIGN_CREDENTIALS)' | cut -d: -f1) && \ + export code_sign_artifactory_credentials_psw=$$(echo '$(CODE_SIGN_CREDENTIALS)' | cut -d: -f2-) && \ + $(CODE_SIGN_SH) "$(SWIMS_STAGING)/code_sign/code_sign.x86_64" $(SWIMS_TICKET_DIR) $(SWIMS_STAGING) false main init + export PROJECT=aci && \ + $(CODE_SIGN_SH) "$(SWIMS_STAGING)/code_sign/code_sign.x86_64" $(SWIMS_TICKET_DIR) $(SWIMS_STAGING) false main \ + tsign $(CURDIR)/$(STAGING)/manifest.yaml $(CURDIR)/$(STAGING)/manifest.yaml.signature + @export PROJECT=aci && \ + $(CODE_SIGN_SH) "$(SWIMS_STAGING)/code_sign/code_sign.x86_64" $(SWIMS_TICKET_DIR) $(SWIMS_STAGING) false main destroy || true + @test -f $(STAGING)/manifest.yaml.signature || { echo "ERROR: SWIMS signing failed — no signature produced"; exit 1; } + @echo "Signed manifest with SWIMS dev key (NAP-style JSON)" + +_sign_swims_rel: + @echo "Signing manifest with SWIMS (release, NAP-style tsign)..." + @rm -rf $(SWIMS_STAGING) + export PROJECT=aci && \ + export code_sign_artifactory_credentials_usr=$$(echo '$(CODE_SIGN_CREDENTIALS)' | cut -d: -f1) && \ + export code_sign_artifactory_credentials_psw=$$(echo '$(CODE_SIGN_CREDENTIALS)' | cut -d: -f2-) && \ + $(CODE_SIGN_SH) "$(SWIMS_STAGING)/code_sign/code_sign.x86_64" $(SWIMS_TICKET_DIR) $(SWIMS_STAGING) true main init + export PROJECT=aci && \ + $(CODE_SIGN_SH) "$(SWIMS_STAGING)/code_sign/code_sign.x86_64" $(SWIMS_TICKET_DIR) $(SWIMS_STAGING) true main \ + tsign $(CURDIR)/$(STAGING)/manifest.yaml $(CURDIR)/$(STAGING)/manifest.yaml.signature + @export PROJECT=aci && \ + $(CODE_SIGN_SH) "$(SWIMS_STAGING)/code_sign/code_sign.x86_64" $(SWIMS_TICKET_DIR) $(SWIMS_STAGING) true main destroy || true + @test -f $(STAGING)/manifest.yaml.signature || { echo "ERROR: SWIMS signing failed — no signature produced"; exit 1; } + @echo "Signed manifest with SWIMS release key (NAP-style JSON)" + +# ---- Local signing (for offline testing without SWIMS) ---- +# Produces the same JSON format as tsign so verifyPKI can parse it. +# To use: generate a keypair, replace /certs/signing/dev.pem on the box +# with the public key, then sign with the private key here. + +_sign_local: + @test -f $(LOCAL_KEY) || { echo "ERROR: Local key not found at $(LOCAL_KEY)"; echo "Generate one: openssl genpkey -algorithm RSA -out $(LOCAL_KEY) -pkeyopt rsa_keygen_bits:2048"; exit 1; } + @RAW_SIG=$$(openssl dgst -sha256 -sign $(LOCAL_KEY) $(STAGING)/manifest.yaml | base64 -w0) && \ + printf '{\n "signatures": {\n "dev": {\n "created": "%s",\n "signature": "%s"\n }\n }\n}\n' \ + "$$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$$RAW_SIG" > $(STAGING)/manifest.yaml.signature + @echo "Signed manifest with local key: $(LOCAL_KEY) (NAP-style JSON)" + +# ---- Internal targets ---- + +_stage: + @rm -rf $(STAGING) + @mkdir -p $(STAGING) $(OUT_DIR) + +_copy_sources: + @cp -a $(PLUGIN_DIR)/* $(STAGING)/ + @cp ../script/ND-Preupgrade-Validation.py $(STAGING)/ + @cp ../script/worker_functions.py $(STAGING)/ + +_package: + @test -f $(STAGING)/manifest.yaml.signature || echo "WARNING: No signature — .ndp will fail verification on ND" + cd $(STAGING) && tar czf ../$(NDP_FILE) . + @rm -rf $(STAGING) + @echo "Output: $(NDP_FILE) ($$(du -h $(NDP_FILE) | cut -f1))" + +clean: + rm -rf $(STAGING) $(OUT_DIR) $(SWIMS_STAGING) + +distclean: clean + rm -rf $(BUILD_ENV) diff --git a/plugin/README.md b/plugin/README.md index 8b1ff0f..79de0b7 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,2 +1,241 @@ -# Validation Plugin +# NDP Plugin Development +Build and package Nexus Dashboard Plugin (`.ndp`) archives for use with `acs plugin` commands. + +## Overview + +An `.ndp` file is a gzip-compressed tar archive containing: + +| File | Purpose | +|------|---------| +| `manifest.yaml` | Plugin metadata (name, version, entry point, etc.) | +| `manifest.yaml.signature` | Abraxas/SWIMS cryptographic signature of the manifest | +| `run.sh` | Entry point script executed by `acs plugin run` | +| `ND-Preupgrade-Validation.py` | Main validation script | +| `worker_functions.py` | Worker script deployed to each node | + +The ND node verifies the signature against Abraxas public keys at +`/certs/signing/{dev,rel}.pem` before allowing installation. These keys +originate from the **ACI** SWIMS key identity (same keys used for SMU/ISO +signing), so the plugin Makefile uses `PROJECT=aci` to match. + +## Directory Structure + +``` +plugin/ +├── Makefile # Build system +├── README.md # This file +├── .gitignore +├── keys/ # Local test keys (not committed) +├── pre-upgrade-validation/ # Plugin source +│ ├── manifest.yaml # Plugin metadata +│ └── run.sh # Entry point +├── .build-env/ # Cloned build repo (not committed) +│ ├── tools/code_sign.sh +│ └── swims/ # SWIMS tokens +└── dist/ # Built .ndp files (not committed) +``` + +## Manifest Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Plugin identifier (e.g. `nd-preupgrade-validation`) | +| `version` | Yes | Version string (e.g. `"1.0.0"`) | +| `description` | Yes | Human-readable description | +| `min_nd_version` | Yes | Minimum ND version required (e.g. `"4.3"`) | +| `entry_point` | Yes | Relative path to the executable entry point (e.g. `run.sh`) | + +## Building + +### Prerequisites + +- GNU Make +- `openssl` CLI (for local signing) +- `git` (for `make setup` to clone the build repo) +- Artifactory credentials for SWIMS signing (see below) + +### Make Targets + +| Target | Description | +|--------|-------------| +| `make build` | Package the plugin (unsigned — will fail verification on ND) | +| `make sign-local` | Build + sign with a local PEM key (offline dev testing) | +| `make dev` | Build + SWIMS dev sign | +| `make rel` | Build + SWIMS release sign | +| `make setup` | Clone build repo into `.build-env/` (one-time) | +| `make clean` | Remove build artifacts | +| `make distclean` | Remove build artifacts + `.build-env/` | + +### Option 1: Local Signing (No SWIMS, for Development) + +Use this when you just need to test the plugin lifecycle without real Abraxas keys. + +```bash +# Generate a test keypair (one-time) +mkdir -p keys +openssl genpkey -algorithm RSA -out keys/dev.pem -pkeyopt rsa_keygen_bits:4096 +openssl rsa -in keys/dev.pem -pubout -out keys/dev.pub.pem + +# Build with local signing +make sign-local +``` + +To test on an ND node, replace the Abraxas public key on the box: + +```bash +# On the ND node (as root): +cp /certs/signing/dev.pem /root/dev.pem.orig # backup original +# Copy keys/dev.pub.pem to /certs/signing/dev.pem # replace with your key + +# When done testing: +cp /root/dev.pem.orig /certs/signing/dev.pem # restore original +``` + +### Option 2: SWIMS Signing (Production) + +Uses the same `code_sign.sh` + `code_sign.x86_64` infrastructure as ISO/OVA builds. +The Makefile clones the `build` repo and downloads tools from Artifactory automatically. + +#### Step 1: One-time Setup + +```bash +make setup +``` + +This clones the `build` repo into `.build-env/` and creates `.build-env/swims/` for tokens. + +#### Step 2: SWIMS Tokens + +SWIMS tokens are ephemeral. The easiest way to get them is to let `code_sign.sh init` +download them from Artifactory (they're bundled inside `code_sign.tar.gz`). After a +successful `make dev`, copy the fresh tokens from the extracted tarball: + +```bash +cp .swims-staging/code_sign/.sec_id_new .build-env/swims/ +cp .swims-staging/code_sign/ACI_NEW_SWIMS_DEV.token .build-env/swims/ +``` + +Alternatively, copy from a recent Jenkins build sandbox. + +#### Step 3: Build + +```bash +# Dev signing +make dev CODE_SIGN_CREDENTIALS=user:artifactory-token + +# Release signing +make rel CODE_SIGN_CREDENTIALS=user:artifactory-token +``` + +`CODE_SIGN_CREDENTIALS` is your `username:identity-token` for +`artifactory.devhub-cloud.cisco.com`. The signing flow is: + +1. `code_sign.sh init` — downloads `code_sign.x86_64` from Artifactory, establishes SWIMS session via Vault +2. `code_sign.sh bsign` — signs `manifest.yaml`, producing `manifest.yaml.signature` +3. `code_sign.sh destroy` — revokes the SWIMS session +4. Everything is packaged into `dist/-.ndp` + +#### Matching Public Keys + +The ND image ships with Abraxas public keys at `/certs/signing/{dev,rel}.pem`. +These are the **ACI** Abraxas keys (`AbraxasACIDev.pem` / `AbraxasACI.pem`), +checked into `bootmgr/shim/certs/` and `spm/cmd/firmwared/certs/`. They are +copied into the initrd at build time and then into `/certs/signing/` at boot +by `bootmgr`. + +Because these are **committed, static** keys (not downloaded from SWIMS at +build time), they do not rotate with SWIMS key rotation. The plugin signing +uses `PROJECT=aci` so the SWIMS signing private key matches the committed +ACI public key on every ND box. + +If you ever hit a signature mismatch (unlikely with committed keys), you can +extract and compare: + +```bash +# Extract public key from the SWIMS ACI dev certificate +openssl x509 -in .build-env/swims/ACI_CODE_SIGN_RSA_DEV.PEM -pubkey -noout > /tmp/swims-dev.pub + +# Compare with the key on the ND box: +diff /tmp/swims-dev.pub <(ssh root@ cat /certs/signing/dev.pem) +``` + +## Testing on an ND Node + +### Installing the Plugin + +Host the `.ndp` file on a web server accessible from the ND node: + +```bash +# From the ND node (as root or rescue-user): +acs plugin download http:///-.ndp +``` + +Supported URL schemes: `http://`, `https://`, `scp://:/`, `file:///path` + +### Plugin Lifecycle Commands + +```bash +acs plugin show # Display installed plugin metadata +acs plugin run # Execute the plugin +acs plugin remove # Uninstall the plugin +``` + +### Design Principle + +NDP plugins should be self-contained — scripts or binaries inside the `.ndp` +archive should run without external dependencies on the ND node. ND boxes are +stripped-down Linux environments with no package manager. + +### `sshpass` Note (pre-upgrade-validation only) + +The `ND-Preupgrade-Validation.py` script specifically requires `sshpass` for +password-based SSH to cluster nodes. This is a dependency of the validation +script itself, not of the NDP framework. If you are packaging this script as +an `.ndp` to test the plugin system, `sshpass` must be present on the ND node: + +```bash +# One-time transfer from a dev machine (repeat for each node in the cluster) +base64 /usr/bin/sshpass | ssh root@ \ + "base64 -d > /usr/local/bin/sshpass && chmod +x /usr/local/bin/sshpass" +``` + +If `sshpass` is missing, `acs plugin run` will exit with a dependency error +for this particular plugin. + +### What Happens on `acs plugin run` + +The entry point `run.sh` is executed with `cwd` set to `/config/plugin/`. It: + +1. Auto-detects the node's management IP via `hostname -I` +2. Reads the rescue-user password from `RESCUE_USER_PASS` environment variable +3. Runs `ND-Preupgrade-Validation.py` in non-interactive mode + +Results are written to `/config/plugin/final-results/` and bundled into a `.tgz`. + +### Environment Variables + +`run.sh` respects these environment variables for override: + +| Variable | Required | Description | +|----------|----------|-------------| +| `RESCUE_USER_PASS` | Yes | rescue-user password (no default; must be set) | +| `ND_IP` | No | ND management IP to validate against (auto-detected if omitted) | + +## Running the Script Standalone (Without NDP) + +The validation script can also be run directly from any Linux host with SSH access to +the ND cluster — no plugin packaging required: + +```bash +cd script/ + +# Non-interactive (default) — fully automated +python3 ND-Preupgrade-Validation.py --ndip 10.1.1.1 -p + +# Interactive — prompted for all decisions +python3 ND-Preupgrade-Validation.py --ndip 10.1.1.1 -p -i +``` + +See [`script/README.md`](../script/README.md) for full CLI options, checks performed, +and example output. diff --git a/plugin/pre-upgrade-validation/manifest.yaml b/plugin/pre-upgrade-validation/manifest.yaml new file mode 100644 index 0000000..7d54fa1 --- /dev/null +++ b/plugin/pre-upgrade-validation/manifest.yaml @@ -0,0 +1,5 @@ +name: nd-preupgrade-validation +version: "1.0.0" +description: "Nexus Dashboard Pre-upgrade Validation - runs health checks on the local ND cluster before upgrade" +min_nd_version: "4.3" +entry_point: run.sh diff --git a/plugin/pre-upgrade-validation/run.sh b/plugin/pre-upgrade-validation/run.sh new file mode 100755 index 0000000..8597212 --- /dev/null +++ b/plugin/pre-upgrade-validation/run.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# NDP entry point — runs the pre-upgrade validation script locally on the ND node. +# The script is invoked without --ndip so it runs against the local cluster. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +if [ -z "${RESCUE_USER_PASS}" ]; then + echo "ERROR: RESCUE_USER_PASS environment variable is required" >&2 + exit 1 +fi +ND_IP="${ND_IP:-$(hostname -I | awk '{print $1}')}" + +exec python3 "${SCRIPT_DIR}/ND-Preupgrade-Validation.py" --ndip "${ND_IP}" -p "${RESCUE_USER_PASS}" "$@" diff --git a/script/ND-Preupgrade-Validation.py b/script/ND-Preupgrade-Validation.py index b4a5f29..11bf603 100755 --- a/script/ND-Preupgrade-Validation.py +++ b/script/ND-Preupgrade-Validation.py @@ -33,6 +33,8 @@ from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed +NON_INTERACTIVE = True + # Set up logging def setup_logging(): """Set up logging configuration""" @@ -1439,6 +1441,10 @@ def select_techsupport(self, node): print(f" Size: {file_info['size']}, Created: {file_info['date']}") # Ask user to select a file + if NON_INTERACTIVE: + selected_file = display_info[0]["path"] + print(f"[non-interactive] Auto-selected: {display_info[0]['name']}") + return selected_file while True: try: range_str = str(len(display_info)) if len(display_info) == 1 else f"1-{len(display_info)}" @@ -1458,6 +1464,9 @@ def select_techsupport(self, node): print("Please enter a valid number") else: print(f"\nNo tech support files found on {node['name']}.") + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-generating tech support for {node['name']}...") + return self.generate_techsupport(node) print(f"Would you like to generate a new tech support? (y/n)") choice = input("> ") @@ -1471,6 +1480,9 @@ def select_techsupport(self, node): except Exception as e: logger.exception(f"Error selecting tech support for node {node['name']}: {str(e)}") print(f"Error listing tech support files: {str(e)}") + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-generating tech support for {node['name']}...") + return self.generate_techsupport(node) print(f"Would you like to generate a new tech support? (y/n)") choice = input("> ") @@ -3952,6 +3964,10 @@ def process_all_nodes_local(node_manager, tech_choice, version=None, password=No print(f"{idx}. {file_info['name']}") print(f" Size: {file_info['size']}, Created: {file_info['date']}") print(" ") + if NON_INTERACTIVE: + tech_support_paths[node_name] = tech_files[0]["path"] + print(f"[non-interactive] Auto-selected: {tech_files[0]['name']}") + continue while True: try: range_str = str(len(tech_files)) if len(tech_files) == 1 else f"1-{len(tech_files)}" @@ -4020,21 +4036,23 @@ def process_all_nodes_local(node_manager, tech_choice, version=None, password=No return {} print(f"\nThe following tech support(s) can be processed: {', '.join(feasible_node_names)}") - while True: - choice = input(f"\nWould you like to proceed with {', '.join(feasible_node_names)} only? (y/n): ").lower().strip() - if choice in ('y', 'yes'): - print("Proceeding with feasible nodes...") - # Drop infeasible nodes from the working set - for n in infeasible_nodes: - tech_support_paths.pop(n, None) - ts_sizes_bytes.pop(n, None) - nodes = [nd for nd in nodes if nd["name"] in tech_support_paths] - break - elif choice in ('n', 'no'): - print("Validation cancelled by user.") - return {} - else: - print("Please enter 'y' or 'n'.") + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-proceeding with feasible nodes...") + else: + while True: + choice = input(f"\nWould you like to proceed with {', '.join(feasible_node_names)} only? (y/n): ").lower().strip() + if choice in ('y', 'yes'): + break + elif choice in ('n', 'no'): + print("Validation cancelled by user.") + return {} + else: + print("Please enter 'y' or 'n'.") + print("Proceeding with feasible nodes...") + for n in infeasible_nodes: + tech_support_paths.pop(n, None) + ts_sizes_bytes.pop(n, None) + nodes = [nd for nd in nodes if nd["name"] in tech_support_paths] # Determine parallelism if space["can_do_parallel"]: @@ -4449,29 +4467,37 @@ def process_all_nodes(node_manager, tech_choice, version=None, password=None, de print(" ") # Ask user to select a file - while True: - try: - range_str = str(len(tech_files)) if len(tech_files) == 1 else f"1-{len(tech_files)}" - choice = input(f"\nSelect tech support file for {node_name} ({range_str}, or 0 to skip): ") - if choice == '0': - print(f"Skipping node {node_name}.") - break - - idx = int(choice) - 1 - if 0 <= idx < len(tech_files): - selected_file = tech_files[idx]["path"] - print(f"Selected: {tech_files[idx]['name']}") - tech_support_paths[node_name] = selected_file - break - else: - print(f"Please enter a number between 0 and {len(tech_files)}") - except ValueError: - print("Please enter a valid number") + if NON_INTERACTIVE: + tech_support_paths[node_name] = tech_files[0]["path"] + print(f"[non-interactive] Auto-selected: {tech_files[0]['name']}") + else: + while True: + try: + range_str = str(len(tech_files)) if len(tech_files) == 1 else f"1-{len(tech_files)}" + choice = input(f"\nSelect tech support file for {node_name} ({range_str}, or 0 to skip): ") + if choice == '0': + print(f"Skipping node {node_name}.") + break + + idx = int(choice) - 1 + if 0 <= idx < len(tech_files): + selected_file = tech_files[idx]["path"] + print(f"Selected: {tech_files[idx]['name']}") + tech_support_paths[node_name] = selected_file + break + else: + print(f"Please enter a number between 0 and {len(tech_files)}") + except ValueError: + print("Please enter a valid number") if node_name not in tech_support_paths and not tech_files: print(f"\nNo tech support files found on {node_name}.") - print(f"Would you like to generate a new tech support? (y/n)") - choice = input("> ") + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-generating tech support for {node_name}...") + choice = 'y' + else: + print(f"Would you like to generate a new tech support? (y/n)") + choice = input("> ") if choice.lower() == 'y': print(f"Generating tech support on {node_name}...") @@ -6286,9 +6312,14 @@ def signal_handler(sig, frame): parser.add_argument("--api-password", default=None, dest="api_password", help="Password for ND API authentication. Defaults to the rescue-user password if not specified.") parser.add_argument("-b", "--bash", action="store_true", help="Run in GitBash/Windows mode (no sshpass)") parser.add_argument("--diagnose", action="store_true", help="Run diagnostic tests only (check Python interpreters and worker script execution)") + parser.add_argument("-i", "--interactive", action="store_true", help="Run with interactive prompts (default is non-interactive)") args = parser.parse_args() + global NON_INTERACTIVE + if args.interactive: + NON_INTERACTIVE = False + print("Nexus Dashboard Pre-upgrade Validation Script") print(f"Running validation checks on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") @@ -6517,22 +6548,25 @@ def signal_handler(sig, frame): print("-" * 40) print("="*80 + "\n") - while True: - choice = ( - input( - f"\nDo you want to continue with validation on {len(nodes)} node(s)? (y/n): " + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-continuing with validation on {len(nodes)} node(s)...") + else: + while True: + choice = ( + input( + f"\nDo you want to continue with validation on {len(nodes)} node(s)? (y/n): " + ) + .lower() + .strip() ) - .lower() - .strip() - ) - if choice in ["y", "yes"]: - print("Proceeding with validation...") - break - elif choice in ["n", "no"]: - print("Validation cancelled by user.") - return 0 - else: - print("Please enter 'y' for yes or 'n' for no.") + if choice in ["y", "yes"]: + print("Proceeding with validation...") + break + elif choice in ["n", "no"]: + print("Validation cancelled by user.") + return 0 + else: + print("Please enter 'y' for yes or 'n' for no.") # For versions below 4.1.1, check for large eventmonitoring log files nodes_with_large_logs = [] @@ -6597,18 +6631,21 @@ def signal_handler(sig, frame): print(f" - {node['name']} ({node['ip']})") # Ask user for confirmation - while True: - choice = input(f"\nDo you want to continue with validation on {len(nodes)} node(s)? (y/n): ").lower().strip() - if choice in ['y', 'yes']: - print("Proceeding with nodes that passed eventmonitoring check...") - # Update node_manager to use only healthy nodes - node_manager.nodes = nodes - break - elif choice in ['n', 'no']: - print("Validation cancelled by user.") - return 0 - else: - print("Please enter 'y' for yes or 'n' for no.") + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-continuing with {len(nodes)} node(s) that passed eventmonitoring check...") + node_manager.nodes = nodes + else: + while True: + choice = input(f"\nDo you want to continue with validation on {len(nodes)} node(s)? (y/n): ").lower().strip() + if choice in ['y', 'yes']: + print("Proceeding with nodes that passed eventmonitoring check...") + node_manager.nodes = nodes + break + elif choice in ['n', 'no']: + print("Validation cancelled by user.") + return 0 + else: + print("Please enter 'y' for yes or 'n' for no.") print() # If any nodes have space issues, report them and exclude from validation @@ -6646,18 +6683,21 @@ def signal_handler(sig, frame): print(f" - {node['name']} ({node['ip']})") # Ask user for confirmation - while True: - choice = input(f"\nDo you want to continue with validation on {len(nodes)} node(s)? (y/n): ").lower().strip() - if choice in ['y', 'yes']: - print("Proceeding with nodes that passed disk space check...") - # Update node_manager to use only healthy nodes - node_manager.nodes = nodes - break - elif choice in ['n', 'no']: - print("Validation cancelled by user.") - return 0 - else: - print("Please enter 'y' for yes or 'n' for no.") + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-continuing with {len(nodes)} node(s) that passed disk space check...") + node_manager.nodes = nodes + else: + while True: + choice = input(f"\nDo you want to continue with validation on {len(nodes)} node(s)? (y/n): ").lower().strip() + if choice in ['y', 'yes']: + print("Proceeding with nodes that passed disk space check...") + node_manager.nodes = nodes + break + elif choice in ['n', 'no']: + print("Validation cancelled by user.") + return 0 + else: + print("Please enter 'y' for yes or 'n' for no.") print() # Check for inactive nodes and get user confirmation @@ -6683,19 +6723,23 @@ def signal_handler(sig, frame): print(f"\n{WARNING} Inactive nodes will be skipped during validation operations.") print("Tech support operations and validations will only run on active nodes.") - while True: - choice = input(f"\nDo you want to continue with validation on {len(active_nodes)} active node(s)? (y/n): ").lower().strip() - if choice in ['y', 'yes']: - print("Proceeding with active nodes only...") - # Update the node_manager to use only active nodes - node_manager.nodes = active_nodes - nodes = active_nodes - break - elif choice in ['n', 'no']: - print("Validation cancelled by user.") - return 0 - else: - print("Please enter 'y' for yes or 'n' for no.") + if NON_INTERACTIVE: + print(f"[non-interactive] Auto-continuing with {len(active_nodes)} active node(s)...") + node_manager.nodes = active_nodes + nodes = active_nodes + else: + while True: + choice = input(f"\nDo you want to continue with validation on {len(active_nodes)} active node(s)? (y/n): ").lower().strip() + if choice in ['y', 'yes']: + print("Proceeding with active nodes only...") + node_manager.nodes = active_nodes + nodes = active_nodes + break + elif choice in ['n', 'no']: + print("Validation cancelled by user.") + return 0 + else: + print("Please enter 'y' for yes or 'n' for no.") else: print(f"\n{PASS} All {len(nodes)} nodes are active and have healthy disk space (<80% usage).") print(f"{PASS} All nodes are ready for validation.") @@ -6728,7 +6772,11 @@ def signal_handler(sig, frame): print("1. Generate new tech supports for analysis") print("2. Use existing tech supports on all nodes") - tech_choice = input("\nEnter your choice (1/2): ") + if NON_INTERACTIVE: + tech_choice = "1" + print(f"\n[non-interactive] Auto-selecting: Generate new tech supports") + else: + tech_choice = input("\nEnter your choice (1/2): ") # Store skipped nodes information for final report skipped_nodes_info = { diff --git a/script/README.md b/script/README.md index 6aca0c7..5d9fbb8 100644 --- a/script/README.md +++ b/script/README.md @@ -94,9 +94,72 @@ Most dependencies are part of Python's standard library. For the few external de - **sshpass**: External system tool for password-based SSH authentication - All other dependencies are part of Python's standard library +### Running on an ND Node (via NDP Plugin) + +This script can be packaged as an NDP plugin and run via `acs plugin run`. +Since the script requires `sshpass` and ND boxes do not ship with it (no +package manager available), the binary must be transferred manually before +the plugin will work (one-time per node): + +```bash +base64 /usr/bin/sshpass | ssh root@ \ + "base64 -d > /usr/local/bin/sshpass && chmod +x /usr/local/bin/sshpass" +``` + +This is a dependency of the validation script, not of the NDP framework +itself. See [`plugin/README.md`](../plugin/README.md) for NDP packaging +details. + ## Usage -Place the main script `ND-Preupgrade-Validation.py` and the worker script `worker_functions.py` in the same directory and execute the main script with the command `python ND-Preupgrade-Validation.py` or `python3 ND-Preupgrade-Validation.py` +Place the main script `ND-Preupgrade-Validation.py` and the worker script `worker_functions.py` in the same directory and execute the main script. + +### Command-line Options + +``` +python3 ND-Preupgrade-Validation.py [OPTIONS] +``` + +| Option | Description | +|--------|-------------| +| `--ndip HOST` | IP address or hostname of the Nexus Dashboard (prompted if omitted in interactive mode) | +| `-p`, `--password PASS` | Password for rescue-user (prompted if omitted in interactive mode) | +| `-i`, `--interactive` | Enable interactive mode with prompts for all decisions (tech support selection, continue/abort on warnings, etc.) | +| `--debug` | Enable debug logging to console and preserve temp files | +| `-b`, `--bash` | Run in GitBash/Windows mode (no sshpass) | +| `--diagnose` | Run diagnostic tests only | + +### Non-interactive Mode (Default) + +By default, the script runs non-interactively: +- Auto-generates new tech supports on all nodes +- Auto-continues when warnings are encountered (disk space, inactive nodes, etc.) +- Auto-selects the most recent tech support file when using existing ones +- Requires `--ndip` and `-p` to be specified on the command line + +```bash +python3 ND-Preupgrade-Validation.py --ndip 10.1.1.1 -p +``` + +### Interactive Mode + +Use `-i` or `--interactive` to get prompted for each decision (original behavior): + +```bash +python3 ND-Preupgrade-Validation.py --ndip 10.1.1.1 -p -i +``` + +In interactive mode: +- You choose between generating new tech supports or using existing ones +- You select which tech support file to use per node +- You confirm whether to continue when warnings are detected +- If `--ndip` or `-p` are omitted, you will be prompted + +### NDP Plugin Mode + +When packaged as a `.ndp` plugin and run via `acs plugin run` on the ND node, the entry +point (`run.sh`) automatically supplies `--ndip`, `-p`, and runs non-interactively. No +user input is required. See [`plugin/README.md`](../plugin/README.md) for packaging details. ### Compatibility Notes - The main script is currently only compatible with Python 3.7+ but will be modified in the future.