Initial commit of AthonetTools

This commit is contained in:
2025-08-21 12:59:43 +00:00
commit cd932b8fcb
2483 changed files with 433999 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
{
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "left", "label": "Cell Site" }, "position": { "x": 100, "y": 250 } },
{ "data": { "id": "core", "label": "Core" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 700, "y": 250 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 400, "y": 50 } }
],
"edges": [
{ "data": { "id": "e1", "source": "left", "target": "core", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e2", "source": "core", "target": "dn", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e3", "source": "mgmt", "target": "core", "sourceLabel": "", "targetLabel": "" } }
]
}
}

View File

@@ -0,0 +1,65 @@
{
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "ran", "label": "RAN" }, "position": { "x": 100, "y": 240 } },
{ "data": { "id": "proxmox", "label": "Proxmox Host" }, "position": { "x": 450, "y": 240 } },
{ "data": { "id": "core", "label": "All-in-One Core", "parent": "proxmox" }, "position": { "x": 450, "y": 240 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 800, "y": 240 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 450, "y": 0 } }
],
"edges": [
{
"data": {
"id": "e_ran_core",
"source": "ran",
"target": "core",
"midLabel": "RAN Netw",
"sourceLabel": "",
"targetLabel": "S1/N2/N3"
}
},
{
"data": {
"id": "e_core_dn",
"source": "core",
"target": "dn",
"midLabel": "DNN Netw",
"sourceLabel": "SGi/N6 IP",
"targetLabel": ""
}
},
{
"data": {
"id": "e_mgmt_core",
"source": "mgmt",
"target": "core",
"midLabel": "Mgmt Netw",
"sourceLabel": "",
"targetLabel": "Mgmt IP"
}
}
]
},
"styles": [
{
"selector": "#proxmox",
"style": {
"width": 340,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#core",
"style": { "width": 140, "height": 70 }
}
]
}

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

View 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
View 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
View 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" }
}

View 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 || '' };
}

View 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}`);
}
});
}

View 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
View 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();
}