From 6d086a883fd64bef0a9c7aa817385e15b5d92364 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 23 Feb 2026 09:08:41 +0100 Subject: [PATCH] Add Cove API test script and update documentation with N-able support findings - Add standalone cove_api_test.py to verify new D9Fxx/D10Fxx/D11Fxx column codes - D02/D03 confirmed as legacy by N-able support; D9/D10/D11 should work - Document session status codes (F00) and timestamp fields (F09/F15/F18) - Update TODO and knowledge docs with breakthrough status Co-Authored-By: Claude Sonnet 4.6 --- TODO-cove-data-protection.md | 17 +- cove_api_test.py | 309 ++++++++++++++++++ docs/changelog-claude.md | 14 + ...ve_data_protection_api_calls_known_info.md | 42 ++- 4 files changed, 371 insertions(+), 11 deletions(-) create mode 100644 cove_api_test.py diff --git a/TODO-cove-data-protection.md b/TODO-cove-data-protection.md index fd74295..df0aa9d 100644 --- a/TODO-cove-data-protection.md +++ b/TODO-cove-data-protection.md @@ -712,16 +712,23 @@ Based on POC results, decide architecture approach and start implementation - Document edge cases and limitations - Consider security implications (API key storage, rate limits, etc.) -### Current Status (2026-02-10) +### Current Status (2026-02-23) πŸŽ‰ BREAKTHROUGH - βœ… **Confirmed:** Cove Data Protection HAS API access (mentioned in documentation) - βœ… **Found:** API user creation location in Cove portal - βœ… **Created:** API user with SuperUser role and token - βœ… **Found:** Complete JSON API documentation (N-able docs site) - βœ… **Tested:** API authentication and multiple methods (with ChatGPT assistance) -- ⚠️ **CRITICAL LIMITATION DISCOVERED:** API heavily restricted by column allow-list -- ⚠️ **BLOCKER:** No reliable backup status (success/failed/warning) available via API -- ⚠️ **BLOCKER:** No error messages, timestamps, or detailed run information accessible -- 🎯 **Next decision:** Determine if metrics-only integration is valuable OR contact N-able for expanded access +- βœ… **BLOCKER RESOLVED:** N-able support (Andrew Robinson) confirmed D02/D03 are legacy! + - Use D10/D11 instead of D02/D03 + - No MSP-level restrictions – all users have same access + - New docs: https://developer.n-able.com/n-able-cove/docs/ +- βœ… **New column codes identified:** + - D9F00 = Last Session Status (2=Failed, 5=Completed, 8=CompletedWithErrors) + - D9F09 = Last Successful Session Timestamp + - D9F15 = Last Session Timestamp + - D9F06 = Error Count +- πŸ”„ **Next step:** Run `cove_api_test.py` to verify new column codes work +- πŸ“‹ **After test:** Implement full integration in Backupchecks website ### Test Results Summary (see docs/cove_data_protection_api_calls_known_info.md) - **Endpoint:** https://api.backup.management/jsonapi (JSON-RPC 2.0) diff --git a/cove_api_test.py b/cove_api_test.py new file mode 100644 index 0000000..83b9e3a --- /dev/null +++ b/cove_api_test.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +""" +Cove Data Protection API – Test Script +======================================= +Tests the new column codes (D9Fxx, D10Fxx, D11Fxx) after feedback from N-able support. +Verifies which columns are available for backup status monitoring. + +Usage: + python3 cove_api_test.py \ + --partner "YourPartnerName" \ + --username "api-user" \ + --password "secret" \ + [--url https://api.backup.management/jsonapi] \ + [--partner-id 12345] + +Or via environment variables: + COVE_PARTNER=... COVE_USERNAME=... COVE_PASSWORD=... python3 cove_api_test.py +""" + +import argparse +import json +import os +import sys +from datetime import datetime, timezone + +import requests + +API_URL = "https://api.backup.management/jsonapi" + +# Status codes for D9F00 / F17 / F16 +SESSION_STATUS = { + 1: "In process", + 2: "Failed", + 3: "Aborted", + 5: "Completed", + 6: "Interrupted", + 7: "NotStarted", + 8: "CompletedWithErrors", + 9: "InProgressWithFaults", + 10: "OverQuota", + 11: "NoSelection", + 12: "Restarted", +} + +# Column sets to test +COLUMN_SETS = { + "safe_legacy": { + "description": "Previously confirmed working (from original testing)", + "columns": ["I1", "I14", "I18", "D01F00", "D01F01", "D09F00"], + }, + "status_and_timestamps": { + "description": "Core backup status and timestamps (D9Fxx – new format)", + "columns": [ + "I1", "I14", "I18", + "D9F00", # Last Session Status + "D9F06", # Last Session Errors Count + "D9F09", # Last Successful Session Timestamp + "D9F12", # Session Duration + "D9F15", # Last Session Timestamp + "D9F17", # Last Completed Session Status + "D9F18", # Last Completed Session Timestamp + ], + }, + "d10_vss_mssql": { + "description": "VssMsSql data source (replaces legacy D02/D03)", + "columns": [ + "I1", "I18", + "D10F00", # Last Session Status + "D10F09", # Last Successful Session Timestamp + "D10F15", # Last Session Timestamp + ], + }, + "d11_vss_sharepoint": { + "description": "VssSharePoint data source (replaces legacy D02/D03)", + "columns": [ + "I1", "I18", + "D11F00", # Last Session Status + "D11F09", # Last Successful Session Timestamp + "D11F15", # Last Session Timestamp + ], + }, + "d1_files_folders": { + "description": "Files and Folders data source", + "columns": [ + "I1", "I18", + "D1F00", # Last Session Status + "D1F09", # Last Successful Session Timestamp + "D1F15", # Last Session Timestamp + ], + }, + "full_set": { + "description": "Full set for Backupchecks integration (if all above work)", + "columns": [ + "I1", "I14", "I18", + "D9F00", "D9F01", "D9F02", "D9F03", "D9F04", "D9F05", + "D9F06", "D9F07", "D9F08", "D9F09", "D9F10", "D9F11", + "D9F12", "D9F13", "D9F15", "D9F16", "D9F17", "D9F18", + ], + }, +} + + +def _post(url: str, payload: dict, timeout: int = 30) -> dict: + headers = {"Content-Type": "application/json"} + resp = requests.post(url, json=payload, headers=headers, timeout=timeout) + resp.raise_for_status() + return resp.json() + + +def login(url: str, partner: str, username: str, password: str) -> tuple[str, int]: + """Authenticate and return (visa, partner_id).""" + payload = { + "jsonrpc": "2.0", + "method": "Login", + "params": { + "partner": partner, + "username": username, + "password": password, + }, + "id": "1", + } + data = _post(url, payload) + + if "error" in data: + raise RuntimeError(f"Login failed: {data['error']}") + + visa = data.get("visa") + if not visa: + raise RuntimeError(f"No visa token in response: {data}") + + partner_id = data["result"]["result"]["PartnerId"] + print(f" Login OK – PartnerId={partner_id}") + return visa, partner_id + + +def enumerate_statistics(url: str, visa: str, partner_id: int, columns: list[str]) -> dict: + """Call EnumerateAccountStatistics with specified columns.""" + payload = { + "jsonrpc": "2.0", + "method": "EnumerateAccountStatistics", + "visa": visa, + "params": { + "query": { + "PartnerId": partner_id, + "SelectionMode": "Merged", + "StartRecordNumber": 0, + "RecordsCount": 20, + "Columns": columns, + } + }, + "id": "2", + } + return _post(url, payload) + + +def format_timestamp(value) -> str: + """Convert Unix timestamp to readable datetime.""" + if value is None or value == 0: + return "(empty)" + try: + ts = int(value) + if ts == 0: + return "(empty)" + dt = datetime.fromtimestamp(ts, tz=timezone.utc) + return dt.strftime("%Y-%m-%d %H:%M:%S UTC") + except (ValueError, TypeError, OSError): + return str(value) + + +def format_status(value) -> str: + """Convert numeric session status to description.""" + if value is None: + return "(empty)" + try: + code = int(value) + label = SESSION_STATUS.get(code, f"Unknown({code})") + return f"{code} – {label}" + except (ValueError, TypeError): + return str(value) + + +def print_section(title: str) -> None: + print() + print("=" * 60) + print(f" {title}") + print("=" * 60) + + +def test_column_set(url: str, visa: str, partner_id: int, name: str, info: dict) -> bool: + """Test a column set. Returns True if successful.""" + print(f"\n[TEST] {name}") + print(f" {info['description']}") + print(f" Columns: {', '.join(info['columns'])}") + + data = enumerate_statistics(url, visa, partner_id, info["columns"]) + + if "error" in data: + err = data["error"] + code = err.get("code", "?") + msg = err.get("message", str(err)) + print(f" ❌ FAILED – error {code}: {msg}") + return False + + result = data.get("result") + if result is None: + print(" ⚠️ result is null (empty? no accounts?)") + return True # Not a security error, just empty + + accounts = result if isinstance(result, list) else result.get("Accounts", []) + if not isinstance(accounts, list): + # Might be a dict with stats + print(f" βœ… SUCCESS – response type: {type(result).__name__}") + print(f" Raw (truncated): {json.dumps(result)[:300]}") + return True + + print(f" βœ… SUCCESS – {len(accounts)} account(s) returned") + + # Show first 3 accounts with parsed values + for i, account in enumerate(accounts[:3]): + print(f"\n Account {i + 1}:") + for col in info["columns"]: + val = account.get(col) + if col.startswith("D9F") and col[3:5] in ("09", "15", "18"): + # Timestamp field + display = format_timestamp(val) + elif col.startswith("D9F") and col[3:5] in ("00", "17", "16"): + # Status field + display = format_status(val) + elif col == "I14" and val is not None: + # Storage bytes + try: + gb = int(val) / (1024 ** 3) + display = f"{val} bytes ({gb:.2f} GB)" + except (ValueError, TypeError): + display = str(val) + else: + display = str(val) if val is not None else "(empty)" + print(f" {col:12s} = {display}") + + if len(accounts) > 3: + print(f" ... and {len(accounts) - 3} more") + + return True + + +def run_tests(url: str, partner: str, username: str, password: str, partner_id_override: int | None) -> None: + print_section("Cove Data Protection API – Column Code Test") + print(f" URL: {url}") + print(f" Partner: {partner}") + print(f" User: {username}") + + # Step 1: Login + print_section("Step 1: Authentication") + try: + visa, partner_id = login(url, partner, username, password) + except Exception as exc: + print(f" ❌ Login failed: {exc}") + sys.exit(1) + + if partner_id_override: + partner_id = partner_id_override + print(f" Using partner_id override: {partner_id}") + + # Step 2: Test column sets + print_section("Step 2: Column Code Tests") + + results = {} + for name, info in COLUMN_SETS.items(): + try: + results[name] = test_column_set(url, visa, partner_id, name, info) + except Exception as exc: + print(f" ❌ Exception: {exc}") + results[name] = False + + # Step 3: Summary + print_section("Step 3: Summary") + for name, ok in results.items(): + icon = "βœ…" if ok else "❌" + print(f" {icon} {name}") + + all_ok = all(results.values()) + print() + if all_ok: + print(" All column sets work! Ready for full integration.") + else: + failed = [n for n, ok in results.items() if not ok] + print(f" Some column sets failed: {', '.join(failed)}") + print(" Check error details above.") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Test Cove Data Protection API column codes") + parser.add_argument("--url", default=os.environ.get("COVE_URL", API_URL)) + parser.add_argument("--partner", default=os.environ.get("COVE_PARTNER", "")) + parser.add_argument("--username", default=os.environ.get("COVE_USERNAME", "")) + parser.add_argument("--password", default=os.environ.get("COVE_PASSWORD", "")) + parser.add_argument("--partner-id", type=int, default=None, help="Override partner ID") + args = parser.parse_args() + + if not args.partner or not args.username or not args.password: + print("Error: --partner, --username and --password are required.") + print("Or set COVE_PARTNER, COVE_USERNAME, COVE_PASSWORD environment variables.") + sys.exit(1) + + run_tests(args.url, args.partner, args.username, args.password, args.partner_id) + + +if __name__ == "__main__": + main() diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 646e133..9ca468e 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -2,6 +2,20 @@ This file documents all changes made to this project via Claude Code. +## [2026-02-23] + +### Added +- `cove_api_test.py` – standalone Python test script to verify Cove Data Protection API column codes + - Tests D9Fxx (Total), D10Fxx (VssMsSql), D11Fxx (VssSharePoint), and D1Fxx (Files&Folders) + - Displays backup status (F00), timestamps (F09/F15/F18), error counts (F06) per account + - Accepts credentials via CLI args or environment variables + - Summary output showing which column sets work +- Updated `docs/cove_data_protection_api_calls_known_info.md` with N-able support feedback: + - D02/D03 are legacy – use D10/D11 or D9 (Total) instead + - All users have the same API access (no MSP-level restriction) + - Session status codes documented (D9F00: 2=Failed, 5=Completed, 8=CompletedWithErrors, etc.) +- Updated `TODO-cove-data-protection.md` with breakthrough status and next steps + ## [2026-02-19] ### Added diff --git a/docs/cove_data_protection_api_calls_known_info.md b/docs/cove_data_protection_api_calls_known_info.md index 1a34329..0baad6f 100644 --- a/docs/cove_data_protection_api_calls_known_info.md +++ b/docs/cove_data_protection_api_calls_known_info.md @@ -1,18 +1,48 @@ # Cove Data Protection (N-able Backup) – Known Information on API Calls -Date: 2026-02-10 -Status: Research phase (validated with live testing) +Date: 2026-02-10 (updated 2026-02-23) +Status: Pending re-test with corrected column codes -## Summary of current findings +## ⚠️ Important Update (2026-02-23) -API access to Cove Data Protection via JSON-RPC **works**, but is **heavily restricted per tenant and per API user scope**. The API is usable for monitoring, but only with a **very limited, allow‑listed set of column codes**. Any request that includes a restricted column immediately fails with: +**N-able support (Andrew Robinson, Applications Engineer) confirmed:** +1. **D02 and D03 are legacy column codes** – use **D10 and D11** instead. +2. **There is no MSP-level restriction** – all API users have the same access level. +3. New documentation: https://developer.n-able.com/n-able-cove/docs/getting-started +4. Column code reference: https://developer.n-able.com/n-able-cove/docs/column-codes + +**Impact:** The security error 13501 was caused by using legacy D02Fxx/D03Fxx codes. +Using D9Fxx (Total aggregate), D10Fxx (VssMsSql), D11Fxx (VssSharePoint) should work. + +**Key newly available columns (pending re-test):** +- `D9F00` = Last Session Status (2=Failed, 5=Completed, 8=CompletedWithErrors, etc.) +- `D9F06` = Last Session Errors Count +- `D9F09` = Last Successful Session Timestamp (Unix) +- `D9F12` = Session Duration +- `D9F15` = Last Session Timestamp (Unix) +- `D9F17` = Last Completed Session Status +- `D9F18` = Last Completed Session Timestamp (Unix) + +**Session status codes (F00):** +1=In process, 2=Failed, 3=Aborted, 5=Completed, 6=Interrupted, +7=NotStarted, 8=CompletedWithErrors, 9=InProgressWithFaults, +10=OverQuota, 11=NoSelection, 12=Restarted + +**Test script:** `cove_api_test.py` in project root – run this to verify new column codes. + +--- + +## Summary of original findings (2026-02-10) + +API access to Cove Data Protection via JSON-RPC **works**, but was **heavily restricted** +because legacy column codes (D02Fxx, D03Fxx) were being used. Now resolved. + +Previous error: ``` Operation failed because of security reasons (error 13501) ``` -This behavior is consistent even when the API user has **SuperUser** and **SecurityOfficer** roles. - --- ## Authentication model (confirmed)