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 <noreply@anthropic.com>
This commit is contained in:
parent
f35ec25163
commit
6d086a883f
@ -712,16 +712,23 @@ Based on POC results, decide architecture approach and start implementation
|
|||||||
- Document edge cases and limitations
|
- Document edge cases and limitations
|
||||||
- Consider security implications (API key storage, rate limits, etc.)
|
- 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)
|
- ✅ **Confirmed:** Cove Data Protection HAS API access (mentioned in documentation)
|
||||||
- ✅ **Found:** API user creation location in Cove portal
|
- ✅ **Found:** API user creation location in Cove portal
|
||||||
- ✅ **Created:** API user with SuperUser role and token
|
- ✅ **Created:** API user with SuperUser role and token
|
||||||
- ✅ **Found:** Complete JSON API documentation (N-able docs site)
|
- ✅ **Found:** Complete JSON API documentation (N-able docs site)
|
||||||
- ✅ **Tested:** API authentication and multiple methods (with ChatGPT assistance)
|
- ✅ **Tested:** API authentication and multiple methods (with ChatGPT assistance)
|
||||||
- ⚠️ **CRITICAL LIMITATION DISCOVERED:** API heavily restricted by column allow-list
|
- ✅ **BLOCKER RESOLVED:** N-able support (Andrew Robinson) confirmed D02/D03 are legacy!
|
||||||
- ⚠️ **BLOCKER:** No reliable backup status (success/failed/warning) available via API
|
- Use D10/D11 instead of D02/D03
|
||||||
- ⚠️ **BLOCKER:** No error messages, timestamps, or detailed run information accessible
|
- No MSP-level restrictions – all users have same access
|
||||||
- 🎯 **Next decision:** Determine if metrics-only integration is valuable OR contact N-able for expanded 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)
|
### Test Results Summary (see docs/cove_data_protection_api_calls_known_info.md)
|
||||||
- **Endpoint:** https://api.backup.management/jsonapi (JSON-RPC 2.0)
|
- **Endpoint:** https://api.backup.management/jsonapi (JSON-RPC 2.0)
|
||||||
|
|||||||
309
cove_api_test.py
Normal file
309
cove_api_test.py
Normal file
@ -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()
|
||||||
@ -2,6 +2,20 @@
|
|||||||
|
|
||||||
This file documents all changes made to this project via Claude Code.
|
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]
|
## [2026-02-19]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1,18 +1,48 @@
|
|||||||
# Cove Data Protection (N-able Backup) – Known Information on API Calls
|
# Cove Data Protection (N-able Backup) – Known Information on API Calls
|
||||||
|
|
||||||
Date: 2026-02-10
|
Date: 2026-02-10 (updated 2026-02-23)
|
||||||
Status: Research phase (validated with live testing)
|
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)
|
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)
|
## Authentication model (confirmed)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user