wulf-pulse/scripts/deactivate-cis-for-inactive-companies.ts
lorentz ea3471d38d feat: Veeam RPO analysis, comparison, ticket analysis + company teams table
- Add Veeam RPO analysis page (/veeam-analysis) and comparison page (/veeam-comparison)
- Add API routes: /api/veeam/rpo-analyze, rpo-comparison, rpo-offline-log, ticket-analysis
- Add veeam-rpo-service.ts enhancements (RPO logic, offline detection, comparison)
- Add veeam-analysis-state.ts and rmm-device-resolver.ts services
- Add migrations 065-068: company_teams, veeam_rpo_offline_log, rpo_comparison_tables, veeam_ticket_analysis
- Add backup-status page updates and nav links for new Veeam pages
- Add scripts: deactivate-cis-for-inactive-companies, workstation category updates
- Add docs: mimecast-api-guide, veeam-backup-alerting-recommendation, workstation-backup-overview, ticket-analyzer-prompt
- Minor: webhook-service, entity-sync, entity-mapper, sync-helpers, sync.ts, middleware.ts updates
2026-04-29 09:16:46 -04:00

253 lines
10 KiB
TypeScript

/**
* deactivate-cis-for-inactive-companies.ts
*
* Finds all active Configuration Items whose parent company is inactive in
* Autotask and deactivates them (sets isActive = false).
*
* Usage:
* npx tsx scripts/deactivate-cis-for-inactive-companies.ts [options]
*
* Options:
* --dry-run Preview changes without writing (default)
* --commit Apply changes
* --concurrency <n> Parallel PATCH calls (default: 5)
*/
import { config } from 'dotenv';
import { resolve } from 'path';
config({ path: resolve(__dirname, '../.env') });
// ─── Config ───────────────────────────────────────────────────────────────────
const API_BASE = process.env.AUTOTASK_API_URL!;
const USERNAME = process.env.AUTOTASK_USERNAME!;
const SECRET = process.env.AUTOTASK_SECRET!;
const INT_CODE = process.env.AUTOTASK_API_INTEGRATION_CODE!;
// ─── Helpers ──────────────────────────────────────────────────────────────────
function authHeaders(): Record<string, string> {
return {
'Username': USERNAME,
'Secret': SECRET,
'APIIntegrationcode': INT_CODE,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
/** Paginate through all results of a POST /query endpoint */
async function queryAll<T>(entity: string, filter: object[]): Promise<T[]> {
const all: T[] = [];
let nextUrl: string | null = null;
const requestBody = JSON.stringify({ MaxRecords: 500, filter });
while (true) {
const url = nextUrl ?? `${API_BASE}/${entity}/query`;
const res = await fetch(url, {
method: 'POST',
headers: authHeaders(),
body: requestBody,
});
if (!res.ok) throw new Error(`POST ${entity}/query → ${res.status}: ${await res.text()}`);
const data: any = await res.json();
all.push(...(data.items ?? []));
nextUrl = data.pageDetails?.nextPageUrl ?? null;
if (!nextUrl) break;
}
return all;
}
async function patchCI(id: number, patch: object, retries = 5): Promise<void> {
for (let attempt = 0; attempt <= retries; attempt++) {
const res = await fetch(`${API_BASE}/ConfigurationItems`, {
method: 'PATCH',
headers: authHeaders(),
body: JSON.stringify({ id, ...patch }),
});
if (res.ok) return;
if (res.status === 429 && attempt < retries) {
const delay = 2000 * (attempt + 1); // 2s, 4s, 6s, 8s, 10s
await new Promise(r => setTimeout(r, delay));
continue;
}
throw new Error(`PATCH ConfigurationItems/${id}${res.status}: ${await res.text()}`);
}
}
/** Run tasks with a concurrency cap, returns results in original order */
async function pLimit<T>(
tasks: (() => Promise<T>)[],
concurrency: number
): Promise<{ result?: T; error?: Error }[]> {
const results: { result?: T; error?: Error }[] = new Array(tasks.length);
let next = 0;
async function worker() {
while (next < tasks.length) {
const i = next++;
try { results[i] = { result: await tasks[i]() }; }
catch (e) { results[i] = { error: e instanceof Error ? e : new Error(String(e)) }; }
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, worker));
return results;
}
// ─── Arg parsing ──────────────────────────────────────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
let dryRun = true;
let concurrency = 3; // Autotask hard limit is 3 concurrent API threads
for (let i = 0; i < args.length; i++) {
if (args[i] === '--commit') dryRun = false;
if (args[i] === '--dry-run') dryRun = true;
if (args[i] === '--concurrency' && args[i+1]) concurrency = parseInt(args[++i], 10);
}
return { dryRun, concurrency };
}
// ─── Types ────────────────────────────────────────────────────────────────────
interface Company {
id: number;
companyName: string;
isActive: boolean;
}
interface CIItem {
id: number;
companyID: number;
referenceTitle: string;
configurationItemType: number;
configurationItemCategoryID: number | null;
isActive: boolean;
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
const { dryRun, concurrency } = parseArgs();
console.log('');
console.log('╔══════════════════════════════════════════════════════════════╗');
console.log('║ Deactivate CIs for Inactive Companies ║');
console.log('╚══════════════════════════════════════════════════════════════╝');
console.log(` Mode : ${dryRun ? '🔍 DRY RUN (no changes written)' : '✏️ COMMIT (changes will be applied)'}`);
console.log('');
// ── 1. Fetch all inactive companies ─────────────────────────────────────────
process.stdout.write('Fetching inactive companies … ');
const inactiveCompanies = await queryAll<Company>('Companies', [
{ field: 'isActive', op: 'eq', value: false },
]);
console.log(`found ${inactiveCompanies.length}.`);
if (inactiveCompanies.length === 0) {
console.log('No inactive companies found.');
return;
}
const companyMap = new Map<number, string>(
inactiveCompanies.map(c => [c.id, c.companyName])
);
// ── 2. Fetch active CIs for each inactive company ────────────────────────────
console.log('Querying active CIs for each inactive company …');
const cisByCompany = new Map<number, CIItem[]>();
let totalFound = 0;
for (const company of inactiveCompanies) {
const cis = await queryAll<CIItem>('ConfigurationItems', [
{ field: 'companyID', op: 'eq', value: company.id },
{ field: 'isActive', op: 'eq', value: true },
]);
if (cis.length > 0) {
cisByCompany.set(company.id, cis);
totalFound += cis.length;
process.stdout.write(` [${company.id}] ${company.companyName}: ${cis.length} active CI(s)\n`);
}
}
// ── 3. Report ────────────────────────────────────────────────────────────────
console.log('');
console.log('─── Summary ──────────────────────────────────────────────────');
console.log(` Inactive companies checked : ${inactiveCompanies.length}`);
console.log(` Companies with active CIs : ${cisByCompany.size}`);
console.log(` Total active CIs to deactivate : ${totalFound}`);
console.log('');
if (totalFound === 0) {
console.log('✓ Nothing to do — no active CIs found for inactive companies.');
return;
}
console.log('─── Breakdown ────────────────────────────────────────────────');
const sorted = [...cisByCompany.entries()].sort((a, b) =>
(companyMap.get(a[0]) ?? '').localeCompare(companyMap.get(b[0]) ?? '')
);
for (const [companyId, cis] of sorted) {
const name = companyMap.get(companyId) ?? `Company ${companyId}`;
console.log(`\n ${name} (${cis.length} CI${cis.length !== 1 ? 's' : ''})`);
for (const ci of cis) {
console.log(` [${ci.id}] ${(ci.referenceTitle ?? '(no title)').substring(0, 60)}`);
}
}
console.log('');
if (dryRun) {
console.log('─── Dry run complete ─────────────────────────────────────────');
console.log(` ${totalFound} CI(s) across ${cisByCompany.size} company/companies would be deactivated.`);
console.log(' Run with --commit to apply.');
return;
}
// ── 4. Deactivate ────────────────────────────────────────────────────────────
const allCIs = [...cisByCompany.values()].flat();
console.log(`─── Deactivating ${allCIs.length} CIs (concurrency=${concurrency}) ──────`);
const tasks = allCIs.map(ci => () => patchCI(ci.id, { isActive: false }));
const results = await pLimit(tasks, concurrency);
let succeeded = 0;
let failed = 0;
const errors: { ci: CIItem; error: string }[] = [];
for (let i = 0; i < results.length; i++) {
if (results[i].error) {
failed++;
errors.push({ ci: allCIs[i], error: results[i].error!.message });
process.stdout.write('✗');
} else {
succeeded++;
process.stdout.write('.');
}
if ((i + 1) % 80 === 0) process.stdout.write('\n');
}
console.log('\n');
// ── 5. Final report ───────────────────────────────────────────────────────────
console.log('─── Results ──────────────────────────────────────────────────');
console.log(` ✓ Deactivated : ${succeeded}`);
console.log(` ✗ Failed : ${failed}`);
if (errors.length > 0) {
console.log('\n Failures:');
for (const { ci, error } of errors) {
const name = companyMap.get(ci.companyID) ?? `Company ${ci.companyID}`;
console.log(` [${ci.id}] ${ci.referenceTitle ?? '(no title)'} (${name})`);
console.log(` ${error}`);
}
process.exit(1);
}
console.log('\n✓ All done.');
}
main().catch(err => {
console.error('\nFatal error:', err.message);
process.exit(1);
});