From 2da8b0bc1ec7076fb1412bda12d52ef0100dc918 Mon Sep 17 00:00:00 2001 From: tkishida Date: Tue, 8 Sep 2026 23:26:30 -0700 Subject: [PATCH 1/2] Bump to v4.2.1 --- aci-preupgrade-validation-script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 094f0e7..73b0a98 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -38,7 +38,7 @@ import os import re -SCRIPT_VERSION = "v4.2.0" +SCRIPT_VERSION = "v4.2.1" DEFAULT_TIMEOUT = 600 # sec # result constants DONE = 'DONE' From 06168069047a112ebdce6f730253f252834db26c Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 9 Sep 2026 12:10:42 -0400 Subject: [PATCH 2/2] checks: generate CIMC support from release notes Add a guarded release-time scraper that embeds model-specific CIMC support in the standalone validator and rejects incomplete documentation parses. Cover the 4.1(1g) regression reported in #435 while preserving offline APIC execution.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 20 ++ aci-preupgrade-validation-script.py | 129 +++++-- docs/docs/validations.md | 2 +- .../test_cimc_compatibilty_check.py | 32 +- .../test_update_cimc_release_note_support.py | 82 +++++ tools/__init__.py | 0 tools/cimc_release_note_sources.json | 28 ++ tools/update_cimc_release_note_support.py | 323 ++++++++++++++++++ 8 files changed, 574 insertions(+), 42 deletions(-) create mode 100644 tests/tools/test_update_cimc_release_note_support.py create mode 100644 tools/__init__.py create mode 100644 tools/cimc_release_note_sources.json create mode 100644 tools/update_cimc_release_note_support.py diff --git a/README.md b/README.md index 36d19da..4193bfa 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,23 @@ For every check that has been flagged as `FAIL`, a general recommended action ha [1]: https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/ [2]: https://www.cisco.com/c/en/us/td/docs/switches/datacenter/aci/apic/sw/all/apic-installation-upgrade-downgrade/Cisco-APIC-Installation-Upgrade-Downgrade-Guide.html + +# Maintainer Workflows + +## Refresh CIMC Release-Note Support + +Before preparing a script release, add any new APIC target release, its Cisco +release-note URL, expected APIC models, and per-model minimum version counts to +`tools/cimc_release_note_sources.json`, then run: + +```sh +python tools/update_cimc_release_note_support.py +python tools/update_cimc_release_note_support.py --check +``` + +The updater extracts supported CIMC versions by APIC model, ignores deferred +releases, normalizes Cisco's displayed version format, and rejects incomplete +or unrecognized model mappings. Legacy entries without model applicability must +be explicitly listed as ignored in the source configuration. The updater then +refreshes the generated data embedded in `aci-preupgrade-validation-script.py`. +Review the generated diff and run the test suite before publishing the script. diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 73b0a98..c0f5f41 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -52,39 +52,106 @@ # message constants TVER_MISSING = "Target version not supplied. Skipping." VER_NOT_AFFECTED = "Version not affected." -# APIC 6.1(5) release notes, verified 2026-08-10. -CIMC_RELEASE_NOTE_SUPPORT_615_M5 = ( - "4.3(2.260007)", - "4.3(2.250016)", - "4.3(2.240077)", - "4.3(2.240009)", - "4.3(2.230207)", - "4.2(3e)", - "4.2(3b)", - "4.2(2a)", - "4.1(3m)", - "4.1(3f)", - "4.1(3d)", - "4.1(3c)", -) -CIMC_RELEASE_NOTE_SUPPORT_615_M6 = ( - "6.0(2.260044)", - "6.0(1.250192)", - "6.0(1.250131)", - "4.3(6.250053)", - "4.3(4.252002)", - "4.3(4.241063)", - "4.3(2.240009)", - "4.3(2.230207)", - "4.2(3e)", - "4.2(3b)", -) +# BEGIN GENERATED CIMC RELEASE NOTE SUPPORT +# Generated by tools/update_cimc_release_note_support.py; do not edit manually. CIMC_RELEASE_NOTE_SUPPORT = { - ("6.1(5)", "apicl3"): CIMC_RELEASE_NOTE_SUPPORT_615_M5, - ("6.1(5)", "apicm3"): CIMC_RELEASE_NOTE_SUPPORT_615_M5, - ("6.1(5)", "apicl4"): CIMC_RELEASE_NOTE_SUPPORT_615_M6, - ("6.1(5)", "apicm4"): CIMC_RELEASE_NOTE_SUPPORT_615_M6, + ("6.1(5)", "apicg5"): ( + "6.0(2.260143)", + "6.0(2.260044)", + "6.0(1.250192)", + "6.0(1.250131)", + "4.3(6.250044)", + ), + ("6.1(5)", "apicl2"): ( + "4.1(2m)", + "4.1(2k)", + "4.1(2g)", + "4.1(2b)", + "4.1(1g)", + "4.1(1c)", + "4.0(2g)", + "3.0(4d)", + "3.0(3f)", + ), + ("6.1(5)", "apicl3"): ( + "4.3(2.260020)", + "4.3(2.260007)", + "4.3(2.250016)", + "4.3(2.240077)", + "4.3(2.240009)", + "4.3(2.230207)", + "4.2(3e)", + "4.2(3b)", + "4.2(2a)", + "4.1(3m)", + "4.1(3f)", + "4.1(3d)", + "4.1(3c)", + "4.1(1g)", + "4.1(1d)", + "4.0(4e)", + "4.0(2g)", + "4.0(1a)", + ), + ("6.1(5)", "apicl4"): ( + "6.0(2.260143)", + "6.0(2.260044)", + "6.0(1.250192)", + "6.0(1.250131)", + "4.3(6.250053)", + "4.3(4.252002)", + "4.3(4.241063)", + "4.3(2.240009)", + "4.3(2.230207)", + "4.2(3e)", + "4.2(3b)", + ), + ("6.1(5)", "apicm2"): ( + "4.1(2m)", + "4.1(2k)", + "4.1(2g)", + "4.1(2b)", + "4.1(1g)", + "4.1(1c)", + "4.0(2g)", + "3.0(4d)", + "3.0(3f)", + ), + ("6.1(5)", "apicm3"): ( + "4.3(2.260020)", + "4.3(2.260007)", + "4.3(2.250016)", + "4.3(2.240077)", + "4.3(2.240009)", + "4.3(2.230207)", + "4.2(3e)", + "4.2(3b)", + "4.2(2a)", + "4.1(3m)", + "4.1(3f)", + "4.1(3d)", + "4.1(3c)", + "4.1(1g)", + "4.1(1d)", + "4.0(4e)", + "4.0(2g)", + "4.0(1a)", + ), + ("6.1(5)", "apicm4"): ( + "6.0(2.260143)", + "6.0(2.260044)", + "6.0(1.250192)", + "6.0(1.250131)", + "4.3(6.250053)", + "4.3(4.252002)", + "4.3(4.241063)", + "4.3(2.240009)", + "4.3(2.230207)", + "4.2(3e)", + "4.2(3b)", + ), } +# END GENERATED CIMC RELEASE NOTE SUPPORT # regex constants node_regex = r'topology/pod-(?P\d+)/node-(?P\d+)' port_regex = node_regex + r'/sys/phys-\[(?P.+)\]' diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 717c175..b48605a 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -264,7 +264,7 @@ The script checks the minimum recommended CIMC version for the given APIC model As the `compatRsSuppHw` object recommendation is strictly tied to the target software image, it is possible that the [Release Note Documentation][4] for your model/target version has a different recommendation than what the software recommends. Always check the release note of your Target version and APIC model to ensure you are getting the latest recommendations. -The APIC 6.1(5) release notes explicitly support multiple CIMC releases on UCS C220/C240 M5 (APIC-L3/M3) and UCS C225 M6 (APIC-L4/M4) that may be older than the image catalog recommendation. The check uses an embedded, model-specific list for those release-note-supported combinations before applying the image catalog recommendation to other CIMC releases. +The APIC release notes explicitly support multiple model-specific CIMC releases that may be older than the image catalog recommendation. The check uses release-note support data refreshed from Cisco documentation during script release preparation before applying the image catalog recommendation to other CIMC releases. The released validator remains standalone and does not require internet access. Due to the defect CSCwo74485, APIC-SERVER-M4/L4 systems will fail to boot correctly after upgrading CIMC firmware to version 4.3.5 or later while on Non-fixed APIC releases 5.3.x/6.0.9d/6.1(3g) and below. Upgrade the APIC software first, then proceed with the CIMC upgrade for the releases 6.0.9e/ 6.1.4h and above, will avoid this issue. Follow the software advisory for this defect [CSCwo74485][73]. diff --git a/tests/checks/cimc_compatibilty_check/test_cimc_compatibilty_check.py b/tests/checks/cimc_compatibilty_check/test_cimc_compatibilty_check.py index 1092eca..7f79741 100644 --- a/tests/checks/cimc_compatibilty_check/test_cimc_compatibilty_check.py +++ b/tests/checks/cimc_compatibilty_check/test_cimc_compatibilty_check.py @@ -32,16 +32,12 @@ compatRsSuppHwM4_api: read_data(dir, "compatRsSuppHw_615_M6.json"), } -release_note_model_data = { - "apicl3": ("APIC-SERVER-L3", compatRsSuppHwL3_api, "compatRsSuppHw_615_M5.json"), - "apicm3": ("APIC-SERVER-M3", compatRsSuppHwM3_api, "compatRsSuppHw_615_M5.json"), - "apicl4": ("APIC-SERVER-L4", compatRsSuppHwL4_api, "compatRsSuppHw_615_M6.json"), - "apicm4": ("APIC-SERVER-M4", compatRsSuppHwM4_api, "compatRsSuppHw_615_M6.json"), -} - - def release_note_supported_outputs(model, cimc_version): - apic_model, compat_api, compat_fixture = release_note_model_data[model] + apic_model = "APIC-SERVER-{}".format(model[4:].upper()) + compat_api = ( + "uni/fabric/compcat-default/ctlrfw-apic-6.1(5)" + "/rssuppHw-[uni/fabric/compcat-default/ctlrhw-{}].json".format(model) + ) return { eqptCh_api: [ { @@ -55,7 +51,16 @@ def release_note_supported_outputs(model, cimc_version): } } ], - compat_api: read_data(dir, compat_fixture), + compat_api: [ + { + "compatRsSuppHw": { + "attributes": { + "cimcVersion": "9.9(9z)", + "dn": compat_api[:-5], + } + } + } + ], } @@ -76,6 +81,13 @@ def release_note_supported_outputs(model, cimc_version): "5.2(8g)", script.PASS, ), + # Issue #435: CIMC 4.1(1g) is release-note supported on APIC-L3/M3. + ( + release_note_supported_outputs("apicl3", "4.1(1g)"), + "6.1(5e)", + "5.2(8g)", + script.PASS, + ), # The release-note exception must not bypass the CSCwo74485 upgrade ordering check. ( release_note_supported_615_outputs, diff --git a/tests/tools/test_update_cimc_release_note_support.py b/tests/tools/test_update_cimc_release_note_support.py new file mode 100644 index 0000000..11c1057 --- /dev/null +++ b/tests/tools/test_update_cimc_release_note_support.py @@ -0,0 +1,82 @@ +import pytest + +from tools import update_cimc_release_note_support as updater + + +RELEASE_NOTE_HTML = """ + + + + + + + + +
ProductSupported Release
CIMC HUU ISO +

Note: Install only the CIMC versions mentioned here.

+

6.0.2.260143 (recommended) CIMC HUU ISO for UCS C225 M8 (APIC-G5)

+

4.3.6.250053 CIMC HUU ISO (recommended) for UCS C225 M6 (APIC-L4/M4)

+

4.1(1g) CIMC HUU ISO for UCS C220/C240 M4 (APIC-L2/M2) and M5 (APIC-L3/M3)

+

4.1(1d) CIMC HUU ISO for UCS C220 M5 (APIC-L3/M3)

+

4.0(2g) CIMC HUU ISO for UCS C220/C240 M4 and M5 (APIC-L2/M2 and APIC-L3/M3)

+

2.0(13i) CIMC HUU ISO

+

4.1(2a) CIMC HUU ISO for UCS C220 M4 (APIC-L2/M2) (deferred release)

+
+ + +""" + + +def test_extract_cimc_support_maps_models_and_normalizes_versions(): + support = updater.extract_cimc_support( + RELEASE_NOTE_HTML, ignored_unqualified_versions=["2.0(13i)"] + ) + + assert support["apicg5"] == ["6.0(2.260143)"] + assert support["apicl4"] == ["4.3(6.250053)"] + assert support["apicm4"] == ["4.3(6.250053)"] + assert support["apicl3"] == ["4.1(1g)", "4.1(1d)", "4.0(2g)"] + assert support["apicm3"] == ["4.1(1g)", "4.1(1d)", "4.0(2g)"] + assert support["apicl2"] == ["4.1(1g)", "4.0(2g)"] + assert support["apicm2"] == ["4.1(1g)", "4.0(2g)"] + + +def test_extract_cimc_support_rejects_missing_table(): + with pytest.raises(ValueError): + updater.extract_cimc_support("No compatibility table") + + +def test_extract_cimc_support_rejects_unknown_unqualified_version(): + with pytest.raises(ValueError): + updater.extract_cimc_support(RELEASE_NOTE_HTML) + + +def test_collect_support_rejects_model_specific_version_loss(): + sources = { + "6.1(5)": { + "url": "https://example.invalid/release-notes", + "expected_models": ["apicl3"], + "minimum_versions_per_model": {"apicl3": 4}, + "ignored_unqualified_versions": ["2.0(13i)"], + } + } + + with pytest.raises(ValueError): + updater.collect_support(sources, fetcher=lambda url: RELEASE_NOTE_HTML) + + +def test_replace_generated_block_requires_exact_markers(): + with pytest.raises(ValueError): + updater.replace_generated_block("CIMC_RELEASE_NOTE_SUPPORT = {}", "generated") + + +def test_render_generated_block_is_deterministic(): + support = { + ("6.1(5)", "apicl3"): ("4.1(1g)", "4.1(1d)"), + ("6.1(5)", "apicg5"): ("6.0(2.260143)",), + } + + generated = updater.render_generated_block(support) + + assert generated.index('"apicg5"') < generated.index('"apicl3"') + assert generated.index('"4.1(1g)"') < generated.index('"4.1(1d)"') diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/cimc_release_note_sources.json b/tools/cimc_release_note_sources.json new file mode 100644 index 0000000..b6ab587 --- /dev/null +++ b/tools/cimc_release_note_sources.json @@ -0,0 +1,28 @@ +{ + "6.1(5)": { + "url": "https://www.cisco.com/c/en/us/td/docs/dcn/aci/apic/6x/release-notes/cisco-apic-release-notes-615.html#Miscellaneouscompatibilityinformation", + "expected_models": [ + "apicg5", + "apicl2", + "apicl3", + "apicl4", + "apicm2", + "apicm3", + "apicm4" + ], + "minimum_versions_per_model": { + "apicg5": 5, + "apicl2": 9, + "apicl3": 18, + "apicl4": 11, + "apicm2": 9, + "apicm3": 18, + "apicm4": 11 + }, + "ignored_unqualified_versions": [ + "2.0(13i)", + "2.0(9c)", + "2.0(3i)" + ] + } +} diff --git a/tools/update_cimc_release_note_support.py b/tools/update_cimc_release_note_support.py new file mode 100644 index 0000000..110eda2 --- /dev/null +++ b/tools/update_cimc_release_note_support.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python +from __future__ import print_function + +import argparse +import json +import os +import re +import stat +import sys +import tempfile + +try: + from html.parser import HTMLParser + from urllib.request import Request, urlopen +except ImportError: + from HTMLParser import HTMLParser + from urllib2 import Request, urlopen + + +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DEFAULT_SOURCE_CONFIG = os.path.join( + PROJECT_DIR, "tools", "cimc_release_note_sources.json" +) +DEFAULT_OUTPUT = os.path.join(PROJECT_DIR, "aci-preupgrade-validation-script.py") +GENERATED_BLOCK_START = "# BEGIN GENERATED CIMC RELEASE NOTE SUPPORT" +GENERATED_BLOCK_END = "# END GENERATED CIMC RELEASE NOTE SUPPORT" +USER_AGENT = "ACI-Pre-Upgrade-Validation-Script CIMC support updater" + +CIMC_VERSION_PATTERN = re.compile( + r"(?P\d+\.\d+(?:\(\d+(?:\.\d+)?[a-z]?\)|\.\d+(?:\.\d+)?))" + r"\s*(?:\(recommended\)\s*)?" + r"CIMC HUU ISO", + re.IGNORECASE, +) +APIC_MODEL_PATTERN = re.compile( + r"APIC-(?P[A-Z]\d+)(?:/(?P[A-Z]\d+))?", + re.IGNORECASE, +) + + +class ReleaseNoteTableParser(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self.rows = [] + self._row = None + self._cell = None + + def handle_starttag(self, tag, attrs): + tag = tag.lower() + if tag == "tr": + self._row = [] + elif tag in ("td", "th") and self._row is not None: + self._cell = [] + elif tag == "br" and self._cell is not None: + self._cell.append("\n") + + def handle_startendtag(self, tag, attrs): + self.handle_starttag(tag, attrs) + + def handle_endtag(self, tag): + tag = tag.lower() + if tag in ("p", "li") and self._cell is not None: + self._cell.append("\n") + elif tag in ("td", "th") and self._row is not None and self._cell is not None: + self._row.append("".join(self._cell)) + self._cell = None + elif tag == "tr" and self._row is not None: + self.rows.append(self._row) + self._row = None + + def handle_data(self, data): + if self._cell is not None: + self._cell.append(data) + + def handle_entityref(self, name): + if self._cell is not None: + self._cell.append(" " if name == "nbsp" else "&{};".format(name)) + + def handle_charref(self, name): + if self._cell is not None: + self._cell.append(" ") + + +def _clean_text(value): + return re.sub(r"[ \t\r\f\v]+", " ", value.replace(u"\xa0", " ")).strip() + + +def _normalize_cimc_version(version): + if "(" in version: + return version + + parts = version.split(".") + if len(parts) < 3: + raise ValueError("Unsupported CIMC version format: {}".format(version)) + return "{}.{}({})".format(parts[0], parts[1], ".".join(parts[2:])) + + +def _extract_cimc_cell(html): + parser = ReleaseNoteTableParser() + parser.feed(html) + parser.close() + + for row in parser.rows: + if row and _clean_text(row[0]).lower() == "cimc huu iso": + if len(row) < 2: + raise ValueError("CIMC HUU ISO table row has no supported-release cell") + return "\n".join(row[1:]) + + raise ValueError("Unable to find the CIMC HUU ISO release-note table row") + + +def extract_cimc_support(html, ignored_unqualified_versions=()): + support = {} + ignored_unqualified_versions = set(ignored_unqualified_versions) + found_ignored_versions = set() + cimc_cell = _extract_cimc_cell(html) + + for raw_entry in re.split(u"[\n\u2022\u25cf]+", cimc_cell): + entry = _clean_text(raw_entry) + version_match = CIMC_VERSION_PATTERN.search(entry) + if not version_match or "deferred release" in entry.lower(): + continue + + version = _normalize_cimc_version(version_match.group("version")) + model_matches = list(APIC_MODEL_PATTERN.finditer(entry)) + if not model_matches: + if version in ignored_unqualified_versions: + found_ignored_versions.add(version) + continue + raise ValueError( + "Unable to identify an APIC model in release-note entry: {}".format( + entry + ) + ) + for model_match in model_matches: + models = [ + model + for model in model_match.groups() + if model is not None + ] + for model in models: + model = model.lower() + model_key = "apic{}".format(model) + versions = support.setdefault(model_key, []) + if version not in versions: + versions.append(version) + + missing_ignored_versions = sorted( + ignored_unqualified_versions - found_ignored_versions + ) + if missing_ignored_versions: + raise ValueError( + "Configured unqualified CIMC versions were not found: {}".format( + ", ".join(missing_ignored_versions) + ) + ) + if not support: + raise ValueError("No supported CIMC versions were extracted from the release notes") + return support + + +def fetch_release_note(url, timeout=30): + request = Request(url, headers={"User-Agent": USER_AGENT}) + response = urlopen(request, timeout=timeout) + try: + body = response.read() + finally: + response.close() + return body.decode("utf-8") + + +def load_sources(path): + with open(path, "rb") as source_file: + sources = json.loads(source_file.read().decode("utf-8")) + if not isinstance(sources, dict) or not sources: + raise ValueError("Source configuration must contain target-version definitions") + for target_version, source in sources.items(): + if not isinstance(source, dict) or not source.get("url"): + raise ValueError( + "Source definition for {} must contain a URL".format(target_version) + ) + return sources + + +def collect_support(sources, fetcher=fetch_release_note): + support = {} + for target_version in sorted(sources): + source = sources[target_version] + release_support = extract_cimc_support( + fetcher(source["url"]), + source.get("ignored_unqualified_versions", ()), + ) + missing_models = sorted( + set(source.get("expected_models", ())) - set(release_support) + ) + if missing_models: + raise ValueError( + "{} release notes are missing expected models: {}".format( + target_version, ", ".join(missing_models) + ) + ) + for model, minimum_versions in source.get( + "minimum_versions_per_model", {} + ).items(): + version_count = len(release_support.get(model, ())) + if version_count < minimum_versions: + raise ValueError( + "{} release notes yielded {} versions for {}; expected at least {}".format( + target_version, version_count, model, minimum_versions + ) + ) + for model in sorted(release_support): + support[(target_version, model)] = tuple(release_support[model]) + return support + + +def render_generated_block(support): + lines = [ + GENERATED_BLOCK_START, + "# Generated by tools/update_cimc_release_note_support.py; do not edit manually.", + "CIMC_RELEASE_NOTE_SUPPORT = {", + ] + for target_model in sorted(support): + target_version, model = target_model + lines.append(' ("{}", "{}"): ('.format(target_version, model)) + for version in support[target_model]: + lines.append(' "{}",'.format(version)) + lines.append(" ),") + lines.extend(["}", GENERATED_BLOCK_END]) + return "\n".join(lines) + + +def replace_generated_block(source, generated_block): + if source.count(GENERATED_BLOCK_START) != 1 or source.count(GENERATED_BLOCK_END) != 1: + raise ValueError("Expected exactly one generated CIMC support block") + + start = source.index(GENERATED_BLOCK_START) + end = source.index(GENERATED_BLOCK_END, start) + len(GENERATED_BLOCK_END) + return source[:start] + generated_block + source[end:] + + +def read_text(path): + with open(path, "rb") as input_file: + return input_file.read().decode("utf-8") + + +def write_text_atomic(path, content): + directory = os.path.dirname(path) + file_mode = stat.S_IMODE(os.stat(path).st_mode) + temp_file = tempfile.NamedTemporaryFile( + prefix=".cimc-support-", dir=directory, delete=False + ) + try: + temp_file.write(content.encode("utf-8")) + temp_file.close() + os.chmod(temp_file.name, file_mode) + os.rename(temp_file.name, path) + finally: + if not temp_file.closed: + temp_file.close() + if os.path.exists(temp_file.name): + os.remove(temp_file.name) + + +def update_support(source_config, output, check=False): + sources = load_sources(source_config) + generated_block = render_generated_block(collect_support(sources)) + current_source = read_text(output) + updated_source = replace_generated_block(current_source, generated_block) + + if updated_source == current_source: + print("CIMC release-note support is up to date.") + return 0 + if check: + print( + "CIMC release-note support is stale. Run {}.".format( + os.path.relpath(__file__, PROJECT_DIR) + ), + file=sys.stderr, + ) + return 1 + + write_text_atomic(output, updated_source) + print("Updated CIMC release-note support in {}.".format(output)) + return 0 + + +def parse_args(argv=None): + parser = argparse.ArgumentParser( + description="Refresh embedded CIMC support from Cisco APIC release notes." + ) + parser.add_argument( + "--source-config", + default=DEFAULT_SOURCE_CONFIG, + help="JSON definitions for target APIC release-note sources", + ) + parser.add_argument( + "--output", + default=DEFAULT_OUTPUT, + help="standalone validator source to update", + ) + parser.add_argument( + "--check", + action="store_true", + help="return a nonzero status instead of updating stale generated data", + ) + return parser.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + try: + return update_support(args.source_config, args.output, args.check) + except (IOError, ValueError) as error: + print( + "Unable to update CIMC release-note support: {}".format(error), + file=sys.stderr, + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main())