incus-contrib/incusos/helpers/aether-browser

307 lines
9.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
// aether-browser: Playwright helper for Aether web UI automation
// Usage: aether-browser <action> [args...]
//
// Actions:
// login - Login and save session cookies
// screenshot <path> - Take screenshot of current page
// haproxy-status - Check HAProxy infrastructure status
// haproxy-images - List HAProxy images
// navigate <url> - Navigate to URL and return page content
// eval <js> - Evaluate JavaScript on current page
//
// Environment:
// AETHER_URL - Aether base URL (default: https://192.168.102.160:8443)
// AETHER_ADMIN_PASSWORD - Admin password (required)
// AETHER_COOKIE_FILE - Cookie persistence file (default: /tmp/aether-cookies.json)
// HEADLESS - Set to "false" for visible browser (default: true)
const { chromium } = require('playwright-core');
const fs = require('fs');
const path = require('path');
const AETHER_URL = process.env.AETHER_URL || 'https://192.168.102.160:8443';
const PASSWORD = process.env.AETHER_ADMIN_PASSWORD;
const COOKIE_FILE = process.env.AETHER_COOKIE_FILE || '/tmp/aether-cookies.json';
const HEADLESS = process.env.HEADLESS !== 'false';
const SCREENSHOT_DIR = '/tmp';
if (!PASSWORD) {
console.error('ERROR: AETHER_ADMIN_PASSWORD not set. Source the env file first.');
process.exit(1);
}
async function getBrowser() {
// Use Playwright's bundled Chromium
const browserPath = require('playwright-core').chromium.executablePath();
return await chromium.launch({
headless: HEADLESS,
executablePath: browserPath,
args: ['--ignore-certificate-errors', '--no-sandbox'],
});
}
async function getContextWithCookies(browser) {
const context = await browser.newContext({
ignoreHTTPSErrors: true,
viewport: { width: 1280, height: 800 },
});
// Restore cookies if available
if (fs.existsSync(COOKIE_FILE)) {
try {
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE, 'utf8'));
await context.addCookies(cookies);
} catch (e) {
// Stale cookies, will re-login
}
}
return context;
}
async function saveCookies(context) {
const cookies = await context.cookies();
fs.writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
}
async function login(page) {
console.log(`Navigating to ${AETHER_URL}/...`);
await page.goto(`${AETHER_URL}/`, { waitUntil: 'networkidle' });
// Check if already logged in
const url = page.url();
if (!url.includes('login') && !await page.locator('input[name="csrf_token"]').count()) {
console.log('Already logged in (session cookies valid).');
return true;
}
console.log('Login page detected. Filling credentials...');
// Fill the login form
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', PASSWORD);
// Submit
await page.click('button[type="submit"], input[type="submit"]');
// Wait for navigation after login
await page.waitForLoadState('networkidle');
// Check success
const afterUrl = page.url();
if (afterUrl.includes('login')) {
console.error('ERROR: Login failed - still on login page.');
await page.screenshot({ path: `${SCREENSHOT_DIR}/aether-login-fail.png` });
console.error(`Screenshot saved to ${SCREENSHOT_DIR}/aether-login-fail.png`);
return false;
}
console.log(`Login successful. Redirected to: ${afterUrl}`);
return true;
}
async function doAction(action, args) {
const browser = await getBrowser();
const context = await getContextWithCookies(browser);
const page = await context.newPage();
try {
switch (action) {
case 'login': {
const success = await login(page);
if (success) {
await saveCookies(context);
console.log(`Session cookies saved to ${COOKIE_FILE}`);
}
break;
}
case 'screenshot': {
const loginOk = await login(page);
if (!loginOk) break;
await saveCookies(context);
const targetUrl = args[0] || '/';
if (targetUrl.startsWith('/')) {
await page.goto(`${AETHER_URL}${targetUrl}`, { waitUntil: 'networkidle' });
}
const outPath = args[1] || `${SCREENSHOT_DIR}/aether-screenshot.png`;
await page.screenshot({ path: outPath, fullPage: true });
console.log(`Screenshot saved to ${outPath}`);
break;
}
case 'haproxy-status': {
const loginOk = await login(page);
if (!loginOk) break;
await saveCookies(context);
// Navigate to HAProxy infrastructure page
console.log('Navigating to HAProxy management...');
await page.goto(`${AETHER_URL}/haproxy`, { waitUntil: 'networkidle' });
await page.screenshot({ path: `${SCREENSHOT_DIR}/aether-haproxy.png`, fullPage: true });
console.log(`Screenshot: ${SCREENSHOT_DIR}/aether-haproxy.png`);
// Try to extract status info from the page
const content = await page.textContent('body');
// Print relevant sections
const lines = content.split('\n').filter(l => l.trim());
console.log('\n--- Page content (filtered) ---');
for (const line of lines.slice(0, 50)) {
console.log(line.trim());
}
break;
}
case 'haproxy-images': {
const loginOk = await login(page);
if (!loginOk) break;
await saveCookies(context);
console.log('Fetching HAProxy images...');
// Use the session to make an API call
const response = await page.evaluate(async () => {
const resp = await fetch('/haproxy/images', {
credentials: 'same-origin',
});
return { status: resp.status, body: await resp.text() };
});
console.log(`Status: ${response.status}`);
console.log(response.body);
break;
}
case 'navigate': {
const loginOk = await login(page);
if (!loginOk) break;
await saveCookies(context);
const navUrl = args[0] || '/';
const fullUrl = navUrl.startsWith('http') ? navUrl : `${AETHER_URL}${navUrl}`;
console.log(`Navigating to ${fullUrl}...`);
await page.goto(fullUrl, { waitUntil: 'networkidle' });
const outPath = `${SCREENSHOT_DIR}/aether-navigate.png`;
await page.screenshot({ path: outPath, fullPage: true });
console.log(`Screenshot: ${outPath}`);
const title = await page.title();
console.log(`Title: ${title}`);
console.log(`URL: ${page.url()}`);
break;
}
case 'api-get': {
const loginOk = await login(page);
if (!loginOk) break;
await saveCookies(context);
const endpoint = args[0];
if (!endpoint) {
console.error('Usage: aether-browser api-get <endpoint>');
break;
}
const response = await page.evaluate(async (url) => {
const resp = await fetch(url, { credentials: 'same-origin' });
const text = await resp.text();
return { status: resp.status, contentType: resp.headers.get('content-type'), body: text };
}, endpoint);
console.log(`GET ${endpoint} -> ${response.status}`);
if (response.contentType && response.contentType.includes('json')) {
try {
console.log(JSON.stringify(JSON.parse(response.body), null, 2));
} catch {
console.log(response.body);
}
} else {
console.log(response.body);
}
break;
}
case 'api-post': {
const loginOk = await login(page);
if (!loginOk) break;
await saveCookies(context);
const endpoint = args[0];
const bodyJson = args[1];
if (!endpoint || !bodyJson) {
console.error('Usage: aether-browser api-post <endpoint> <json-body>');
break;
}
// Get CSRF token from meta tag or cookie
const csrfToken = await page.evaluate(() => {
const meta = document.querySelector('meta[name="csrf-token"]');
if (meta) return meta.getAttribute('content');
// Try from cookie
const match = document.cookie.match(/csrf_token=([^;]+)/);
return match ? match[1] : '';
});
const response = await page.evaluate(async ({ url, body, csrf }) => {
const headers = {
'Content-Type': 'application/json',
};
if (csrf) {
headers['X-CSRFToken'] = csrf;
headers['X-CSRF-Token'] = csrf;
}
const resp = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers,
body,
});
const text = await resp.text();
return { status: resp.status, body: text };
}, { url: endpoint, body: bodyJson, csrf: csrfToken });
console.log(`POST ${endpoint} -> ${response.status}`);
try {
console.log(JSON.stringify(JSON.parse(response.body), null, 2));
} catch {
console.log(response.body);
}
break;
}
case 'eval': {
const loginOk = await login(page);
if (!loginOk) break;
await saveCookies(context);
const js = args.join(' ');
const result = await page.evaluate(js);
console.log(JSON.stringify(result, null, 2));
break;
}
default:
console.error(`Unknown action: ${action}`);
console.error('Actions: login, screenshot, haproxy-status, haproxy-images, navigate, api-get, api-post, eval');
process.exit(1);
}
} finally {
await browser.close();
}
}
// Main
const [action, ...args] = process.argv.slice(2);
if (!action) {
console.error('Usage: aether-browser <action> [args...]');
console.error('Actions: login, screenshot, haproxy-status, haproxy-images, navigate, api-get, api-post, eval');
process.exit(1);
}
doAction(action, args).catch(err => {
console.error('Fatal error:', err.message);
process.exit(1);
});