Initial commit of AthonetTools
This commit is contained in:
18
static/js/graph/initGraph.js
Normal file
18
static/js/graph/initGraph.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { baseStyles } from '/static/js/graph/style.js';
|
||||
|
||||
export function mountCy(container, graphJson) {
|
||||
const styles = Array.isArray(graphJson.styles)
|
||||
? [...baseStyles, ...graphJson.styles]
|
||||
: baseStyles;
|
||||
|
||||
const cy = cytoscape({
|
||||
container,
|
||||
style: styles,
|
||||
elements: graphJson.elements,
|
||||
layout: graphJson.layout || { name: 'preset' }
|
||||
});
|
||||
|
||||
cy.once('render', () => cy.fit(undefined, 40));
|
||||
window.addEventListener('resize', () => cy && cy.fit(undefined, 40));
|
||||
return cy;
|
||||
}
|
||||
68
static/js/graph/style.js
Normal file
68
static/js/graph/style.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// /static/js/graph/style.js
|
||||
export const baseStyles = [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'shape': 'round-rectangle',
|
||||
'background-color': '#e8f0ff',
|
||||
'border-color': '#0ea5e9',
|
||||
'border-width': 2,
|
||||
'label': 'data(label)',
|
||||
'color': '#0f172a',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'font-size': 12,
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': 140,
|
||||
'padding': '8px',
|
||||
'width': 100,
|
||||
'height': 50
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'line-color': '#64748b',
|
||||
'width': 2,
|
||||
'curve-style': 'straight',
|
||||
|
||||
// NEW: static “middle of edge” label
|
||||
'label': 'data(midLabel)',
|
||||
'text-rotation': 'autorotate',
|
||||
'text-margin-y': -8, // nudge mid-label off the line a bit
|
||||
|
||||
// keep your side labels for “where the IP sits”
|
||||
'source-label': 'data(sourceLabel)',
|
||||
'target-label': 'data(targetLabel)',
|
||||
'source-text-offset': 50,
|
||||
'target-text-offset': 50,
|
||||
|
||||
'font-size': 11,
|
||||
'color': '#334155',
|
||||
'text-background-color': '#ffffff',
|
||||
'text-background-opacity': 0.9,
|
||||
'text-background-shape': 'round-rectangle',
|
||||
'text-background-padding': 2,
|
||||
'text-outline-color': '#ffffff',
|
||||
'text-outline-width': 1
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: ':selected',
|
||||
style: { 'border-width': 3, 'border-color': '#22d3ee', 'line-color': '#22d3ee' }
|
||||
},
|
||||
|
||||
// Optional: make compound parents (like Proxmox) look like containers
|
||||
{
|
||||
selector: ':parent',
|
||||
style: {
|
||||
'background-color': '#f8fafc',
|
||||
'border-color': '#94a3b8',
|
||||
'border-width': 2,
|
||||
'padding': '12px',
|
||||
'text-valign': 'top',
|
||||
'text-halign': 'center',
|
||||
'font-weight': '600'
|
||||
}
|
||||
}
|
||||
];
|
||||
39
static/js/wizard/api.js
Normal file
39
static/js/wizard/api.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// static/js/graph/wizard/api.js
|
||||
export async function setTarget(ip) {
|
||||
const r = await fetch('/api/host/target', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ ip })
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || 'Failed to set target IP');
|
||||
return j;
|
||||
}
|
||||
|
||||
export async function getTarget() {
|
||||
const r = await fetch('/api/host/target');
|
||||
// returns { ip: "x.x.x.x" } or { ip: null }
|
||||
return r.ok ? r.json() : { ip: null };
|
||||
}
|
||||
|
||||
export async function bootstrapAccess() {
|
||||
const r = await fetch('/api/host/bootstrap_access', { method: 'POST' });
|
||||
const j = await r.json();
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || 'Failed to bootstrap access');
|
||||
return j;
|
||||
}
|
||||
|
||||
// static/js/wizard/api.js
|
||||
export async function renderAnsible(payload) {
|
||||
const r = await fetch('/api/ansible/render', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const j = await r.json().catch(() => ({}));
|
||||
if (!r.ok || !j.ok) {
|
||||
const msg = j.error || `Render failed (HTTP ${r.status})`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return j; // { ok: true, staging: "ansible_workspace/staging" }
|
||||
}
|
||||
60
static/js/wizard/step0_target.js
Normal file
60
static/js/wizard/step0_target.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// static/js/graph/wizard/step0_target.js
|
||||
import { setTarget, bootstrapAccess, getTarget } from './api.js';
|
||||
|
||||
export function mountTargetControls() {
|
||||
const row = document.querySelector('#target-host-row');
|
||||
if (!row) return;
|
||||
|
||||
// Use explicit IDs so we always hit the right elements
|
||||
const ipInput = document.getElementById('target-ip-input');
|
||||
const btn = document.getElementById('btn-enable-access');
|
||||
const badge = document.getElementById('access-badge');
|
||||
|
||||
if (!ipInput || !btn || !badge) return;
|
||||
|
||||
// Prefill: localStorage first, then try backend GET /api/host/target
|
||||
const saved = localStorage.getItem('targetHostIp');
|
||||
if (saved) ipInput.value = saved;
|
||||
getTarget()
|
||||
.then(({ ip }) => { if (ip && !saved) ipInput.value = ip; })
|
||||
.catch(() => { /* ignore if endpoint missing */ });
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const ip = (ipInput.value || '').trim();
|
||||
if (!ip) {
|
||||
badge.className = 'badge bg-warning';
|
||||
badge.textContent = 'Enter IP first';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
btn.disabled = true;
|
||||
const old = btn.textContent;
|
||||
btn.textContent = 'Enabling…';
|
||||
badge.className = 'badge bg-secondary';
|
||||
badge.textContent = 'Working';
|
||||
|
||||
await setTarget(ip); // POST /api/host/target
|
||||
await bootstrapAccess(); // POST /api/host/bootstrap_access (SSH+webconsole)
|
||||
// Persist for Stage 2 (and page reloads)
|
||||
localStorage.setItem('targetHostIp', ip);
|
||||
|
||||
badge.className = 'badge bg-success';
|
||||
badge.textContent = 'SSH & Webconsole Ready';
|
||||
btn.textContent = old;
|
||||
btn.disabled = false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
badge.className = 'badge bg-danger';
|
||||
badge.textContent = 'Failed';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper for other steps to read the latest target IP
|
||||
export function getTargetHostData() {
|
||||
const inputIp = (document.getElementById('target-ip-input')?.value || '').trim();
|
||||
const stored = localStorage.getItem('targetHostIp') || '';
|
||||
return { ip: inputIp || stored || '' };
|
||||
}
|
||||
77
static/js/wizard/step2_render.js
Normal file
77
static/js/wizard/step2_render.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// static/js/graph/wizard/step2_render.js
|
||||
import { renderAnsible } from './api.js';
|
||||
import { getTargetHostData } from './step0_target.js';
|
||||
|
||||
export function mountStep2Render() {
|
||||
const btn = document.getElementById('btn-create-yaml');
|
||||
const badge = document.getElementById('yaml-badge');
|
||||
if (!btn) return;
|
||||
|
||||
btn.onclick = null; // Remove any previous handler
|
||||
btn.addEventListener('click', async () => {
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Creating…';
|
||||
badge.className = 'badge bg-secondary';
|
||||
badge.textContent = 'Running';
|
||||
|
||||
try {
|
||||
const val = (id) => (document.getElementById(id)?.value || '').trim();
|
||||
// Target IP priority: localStorage → helper → DOM
|
||||
const lsIp = localStorage.getItem('targetHostIp') || '';
|
||||
const target = (typeof getTargetHostData === 'function' ? getTargetHostData() : {}) || {};
|
||||
const domIp = (document.querySelector('#target-host-row input')?.value || '').trim();
|
||||
const ansibleHostIp = (lsIp || target.ip || domIp || '').trim();
|
||||
|
||||
// Get eth0 info from Stage 1 UI fields
|
||||
const eth0Cidr = val('ip-core-mgmt');
|
||||
const eth0Gw = val('ip-core-mgmt-gw');
|
||||
|
||||
const payload = {
|
||||
hostname: val('network-name-input') || 'AIO-1',
|
||||
network_name: val('network-name-input'),
|
||||
plmn: val('plmn-input') || '315-010',
|
||||
dns: (val('dns-input') || '8.8.8.8').split(',').map(s => s.trim()).filter(Boolean),
|
||||
ntp: (val('ntp-input') || '0.pool.ntp.org, 1.pool.ntp.org').split(',').map(s => s.trim()).filter(Boolean),
|
||||
ran: { cidr: val('ip-core-ran'), gw: val('ip-core-ran-gw') },
|
||||
mgmt: { cidr: eth0Cidr, gw: eth0Gw },
|
||||
dn: {
|
||||
cidr: val('ip-core-dn'),
|
||||
gw: val('ip-core-dn-gw'),
|
||||
vlan: val('ip-core-dn-vlan') ? Number(val('ip-core-dn-vlan')) : undefined,
|
||||
ue_pool: val('ip-core-dn-uepool'),
|
||||
dnn: 'internet'
|
||||
},
|
||||
inventory_host: 'GBP08-AIO-1',
|
||||
esxi_host: 'ESXI-1',
|
||||
version: '25.1',
|
||||
ova_file: '/home/mjensen/OVA/HPE_ANW_P5G_Core-1.25.1.1-qemux86-64.ova',
|
||||
ansible_host_ip: ansibleHostIp
|
||||
};
|
||||
|
||||
const res = await renderAnsible(payload);
|
||||
|
||||
badge.className = 'badge bg-success';
|
||||
badge.textContent = 'Created';
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
|
||||
// Show YAML context in the UI for debug
|
||||
let debugDiv = document.getElementById('yaml-debug-info');
|
||||
if (!debugDiv) {
|
||||
debugDiv = document.createElement('div');
|
||||
debugDiv.id = 'yaml-debug-info';
|
||||
debugDiv.className = 'mt-3 alert alert-info';
|
||||
btn.parentNode.appendChild(debugDiv);
|
||||
}
|
||||
debugDiv.innerHTML = `<strong>YAML files created in:</strong> ${res.staging}<br><strong>Payload used:</strong><pre>${JSON.stringify(payload, null, 2)}</pre>`;
|
||||
|
||||
} catch (err) {
|
||||
badge.className = 'badge bg-danger';
|
||||
badge.textContent = 'Failed';
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
alert(`Failed to create YAML files:\n${err.message || err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
24
static/js/wizard/step3_deploy.js
Normal file
24
static/js/wizard/step3_deploy.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// static/js/wizard/step3_deploy.js
|
||||
export function mountStep3Deploy() {
|
||||
const btn = document.getElementById('btn-run-gaf');
|
||||
const outputDiv = document.getElementById('gaf-output');
|
||||
if (!btn || !outputDiv) return;
|
||||
|
||||
btn.onclick = null;
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Running…';
|
||||
outputDiv.textContent = '';
|
||||
try {
|
||||
const res = await fetch('/api/ansible/deploy', { method: 'POST' });
|
||||
const text = await res.text();
|
||||
outputDiv.textContent = text;
|
||||
btn.textContent = 'Run';
|
||||
btn.disabled = false;
|
||||
} catch (err) {
|
||||
outputDiv.textContent = `Error: ${err.message || err}`;
|
||||
btn.textContent = 'Run';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
11
static/js/wizard/steps.js
Normal file
11
static/js/wizard/steps.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// static/js/graph/wizard/steps.js
|
||||
|
||||
import { mountTargetControls } from './step0_target.js';
|
||||
import { mountStep2Render } from './step2_render.js';
|
||||
import { mountStep3Deploy } from './step3_deploy.js';
|
||||
|
||||
export function mountWizard() {
|
||||
mountTargetControls();
|
||||
mountStep2Render();
|
||||
mountStep3Deploy();
|
||||
}
|
||||
Reference in New Issue
Block a user