Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ which drives the real extension in headless Chromium.
| ![Switch Port Inventory](mist-toolbox/docs/screenshots/09-port-inventory.png)<br>**Switch Port Inventory** | ![Switch Config Export](mist-toolbox/docs/screenshots/05-switch-config-export.png)<br>**Switch Config Export** |
| ![IP Blocks / IRB Report](mist-toolbox/docs/screenshots/08-ip-blocks.png)<br>**IP Blocks / IRB Report** | ![Site Alarms](mist-toolbox/docs/screenshots/06-site-alarms.png)<br>**Site Alarms** |
| ![Wi-Fi Clients Export](mist-toolbox/docs/screenshots/07-wifi-clients.png)<br>**Wi-Fi Clients Export** | ![Disconnect Console](mist-toolbox/docs/screenshots/13-disconnect-console.png)<br>**Disconnect Console** |
| ![Switch PSU Status](mist-toolbox/docs/screenshots/09b-switch-psu-status.png)<br>**Switch PSU Status** | ![Switch Additional CLI](mist-toolbox/docs/screenshots/09c-switch-additional-cli.png)<br>**Switch Additional CLI** |
| ![Client Wi-Fi PHY Inspector](mist-toolbox/docs/screenshots/07b-client-wifi-phy.png)<br>**Client Wi-Fi PHY Inspector** | |
| ![SSR post-check, changes highlighted](mist-toolbox/docs/screenshots/11-ssr-post-highlighted.png)<br>**SSR Pre/Post**: changed fields highlighted | ![SSR pre and post side by side](mist-toolbox/docs/screenshots/12-ssr-side-by-side.png)<br>**SSR Pre/Post**: side by side |

Every screenshot with a caption: [`mist-toolbox/docs/screenshots/`](mist-toolbox/docs/screenshots/README.md).
Expand Down
Binary file modified mist-toolbox/docs/screenshots/00-chrome-extensions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified mist-toolbox/docs/screenshots/01-home-signed-out.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified mist-toolbox/docs/screenshots/02-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified mist-toolbox/docs/screenshots/20-home-added-tool.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions mist-toolbox/docs/screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Generated by `scripts/screenshots/capture.mjs` against synthetic data (`fixtures

![Wi-Fi Clients Export](07-wifi-clients.png)

### Client Wi-Fi PHY Inspector — one client's radio link graded, site found automatically

![Client Wi-Fi PHY Inspector — one client's radio link graded, site found automatically](07b-client-wifi-phy.png)

### IP Blocks / IRB Report — duplicate subnet flagged across sites

![IP Blocks / IRB Report — duplicate subnet flagged across sites](08-ip-blocks.png)
Expand All @@ -42,6 +46,14 @@ Generated by `scripts/screenshots/capture.mjs` against synthetic data (`fixtures

![Switch Port Inventory](09-port-inventory.png)

### Switch PSU Status — a failed supply and a switch without redundancy flagged

![Switch PSU Status — a failed supply and a switch without redundancy flagged](09b-switch-psu-status.png)

### Switch Additional CLI — template, rule, site and device commands side by side

![Switch Additional CLI — template, rule, site and device commands side by side](09c-switch-additional-cli.png)

### SSR Pre/Post Check — pre-check output, laid out like the CLI

![SSR Pre/Post Check — pre-check output, laid out like the CLI](10-ssr-pre-check.png)
Expand Down
11 changes: 10 additions & 1 deletion mist-toolbox/scripts/screenshots/capture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { tmpdir } from "node:os";
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";

import { CONDUCTOR, ORG_ID, conductor, mist } from "./fixtures.mjs";
import { CONDUCTOR, ORG_ID, PHY_MAC, conductor, mist } from "./fixtures.mjs";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const outDir = join(root, "docs", "screenshots");
Expand Down Expand Up @@ -293,12 +293,21 @@ try {
await runTool("wifi-clients");
await shot("07-wifi-clients", "Wi-Fi Clients Export");

await runTool("client-wifi-phy", { params: { mac: PHY_MAC } });
await shot("07b-client-wifi-phy", "Client Wi-Fi PHY Inspector — one client's radio link graded, site found automatically");

await runTool("ip-blocks");
await shot("08-ip-blocks", "IP Blocks / IRB Report — duplicate subnet flagged across sites");

await runTool("port-inventory");
await shot("09-port-inventory", "Switch Port Inventory");

await runTool("switch-psu-status");
await shot("09b-switch-psu-status", "Switch PSU Status — a failed supply and a switch without redundancy flagged");

await runTool("switch-additional-cli");
await shot("09c-switch-additional-cli", "Switch Additional CLI — template, rule, site and device commands side by side");

// SSR Pre/Post: connect, pre-check, change, post-check.
await click("#btnHome");
await click('[data-tool="ssr-pre-post"]');
Expand Down
148 changes: 138 additions & 10 deletions mist-toolbox/scripts/screenshots/fixtures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export const ORG_ID = "demo-org";
export const CONDUCTOR = "https://conductor.example.net";

const SITES = [
{ id: "site-hq", name: "HQ - Toronto", address: "100 Example Ave, Toronto ON", country_code: "CA", timezone: "America/Toronto" },
{ id: "site-ott", name: "Branch - Ottawa", address: "20 Sample St, Ottawa ON", country_code: "CA", timezone: "America/Toronto" },
{ id: "site-mtl", name: "Warehouse - Montreal", address: "5 Demo Rd, Montreal QC", country_code: "CA", timezone: "America/Toronto" },
{ id: "site-hq", name: "HQ - Toronto", networktemplate_id: "swtpl-campus", address: "100 Example Ave, Toronto ON", country_code: "CA", timezone: "America/Toronto" },
{ id: "site-ott", name: "Branch - Ottawa", networktemplate_id: "swtpl-campus", address: "20 Sample St, Ottawa ON", country_code: "CA", timezone: "America/Toronto" },
{ id: "site-mtl", name: "Warehouse - Montreal", networktemplate_id: "swtpl-branch", address: "5 Demo Rd, Montreal QC", country_code: "CA", timezone: "America/Toronto" },
];

const NOW = 1760000000; // fixed, so every regeneration matches
Expand Down Expand Up @@ -57,6 +57,17 @@ function configCmd(sw) {
return lines;
}

// Power supplies per VC member (Switch PSU Status): hq-core's backup member has
// lost a supply, hq-idf-1 has an empty slot, mtl-sw1 is a single-PSU model.
const PSUS = {
"dev-core": [["ok", "ok"], ["ok", "Failed"]],
"dev-idf1": [["ok", "absent"]],
"dev-idf2": [["ok", "ok"]],
"dev-ott": [["ok", "ok"]],
"dev-mtl": [["ok"]],
};
const psus = (id, fpc) => (PSUS[id]?.[fpc] || []).map((status, i) => ({ name: `Power Supply ${i}`, status }));

function switchStats(sw) {
const ports = [];
for (let p = 0; p < 8; p += 1) {
Expand All @@ -75,12 +86,12 @@ function switchStats(sw) {
last_seen: NOW - (sw.status === "connected" ? 30 : 86400),
ports,
};
if (sw.vc) {
base.module_stat = [
{ fpc_idx: 0, vc_role: "master", serial: sw.serial, version: sw.version, mac: sw.mac },
...sw.vc.map((m, i) => ({ fpc_idx: i + 1, vc_role: "backup", serial: m.serial, version: sw.version, mac: m.mac })),
];
}
base.module_stat = sw.vc
? [
{ fpc_idx: 0, vc_role: "master", serial: sw.serial, version: sw.version, mac: sw.mac, psus: psus(sw.id, 0) },
...sw.vc.map((m, i) => ({ fpc_idx: i + 1, vc_role: "backup", serial: m.serial, version: sw.version, mac: m.mac, psus: psus(sw.id, i + 1) })),
]
: [{ fpc_idx: 0, serial: sw.serial, version: sw.version, mac: sw.mac, psus: psus(sw.id, 0) }];
return base;
}

Expand Down Expand Up @@ -113,9 +124,15 @@ const PORT_USAGES = {
uplink: { mode: "trunk", all_networks: true },
};

const DEVICE_CLI = {
"dev-core": ["set protocols mstp bridge-priority 4k", "set chassis aggregated-devices ethernet device-count 8"],
"dev-mtl": ["set poe interface ge-0/0/3 disable"],
};

function siteDevice(sw) {
return {
id: sw.id, mac: sw.mac, name: sw.name, model: sw.model, serial: sw.serial, site_id: sw.site, type: "switch",
...(DEVICE_CLI[sw.id] ? { additional_config_cmds: DEVICE_CLI[sw.id] } : {}),
port_config: { "ge-0/0/0": { usage: "uplink" }, "ge-0/0/1-3": { usage: "ap" }, "ge-0/0/4-7": { usage: "access" } },
port_usages: PORT_USAGES,
};
Expand All @@ -125,6 +142,22 @@ function siteDevice(sw) {

const TEMPLATES = [{ id: "tpl-corp", name: "Corporate WLANs" }];

const SWITCH_TEMPLATES = [
{ id: "swtpl-campus", name: "Campus Switching",
additional_config_cmds: ["set system syslog host 192.0.2.10 any notice", "set system ntp server 192.0.2.123"],
switch_matching: { enable: true, rules: [
{ name: "access-48p", match_model: "EX2300", additional_config_cmds: ["set poe management class", "set protocols lldp-med interface all"] },
] } },
{ id: "swtpl-branch", name: "Small Branch",
additional_config_cmds: ["set system ntp server 192.0.2.123"] },
];
const SITE_SETTING = {
"site-hq": { additional_config_cmds: ["set snmp location \"HQ - Toronto, 3rd floor MDF\""],
switch_matching: { enable: true, rules: [{ name: "core", match_role: "core", additional_config_cmds: ["set protocols ospf area 0.0.0.0 interface irb.10 passive"] }] } },
"site-ott": { additional_config_cmds: ["set snmp location \"Branch - Ottawa\"", "set system ntp server 192.0.2.123"] },
"site-mtl": {},
};

const WLANS = {
"site-hq": [
{ id: "w-corp", ssid: "DemoCorp", enabled: true, auth: { type: "eap", pairwise: ["wpa2-ccmp", "wpa3"] }, vlan_enabled: true, vlan_id: 10, bands: ["5", "6"], interface: "all", template_id: "tpl-corp" },
Expand Down Expand Up @@ -159,6 +192,81 @@ function clients(siteId, count, subnet) {
}
const CLIENTS = { "site-hq": clients("site-hq", 9, 10), "site-ott": clients("site-ott", 5, 20), "site-mtl": clients("site-mtl", 3, 30) };

// One HQ client in detail, for the Client Wi-Fi PHY Inspector. Its readings are
// chosen to show the dashboard's grades: usable signal but poor SNR on a busy,
// noisy, overlapped 80 MHz channel, with some ping-pong roaming. Times are
// relative to the run, not NOW, because the tool charts the past 24 hours.
export const PHY_MAC = mac(0x900a07);
const PHY_SITE = "site-hq";
const liveNow = () => Math.floor(Date.now() / 600000) * 600;

const AP_NAMES = ["hq-ap-01", "hq-ap-02", "hq-ap-03", "hq-ap-04", "hq-ap-05", "hq-ap-06"];
// [5 GHz primary, width, clients, util] per AP; hq-ap-03 serves the client.
const AP_5G = [[36, 80, 9, 31], [149, 80, 12, 28], [52, 80, 22, 62], [56, 20, 7, 40], [52, 40, 11, 47], [100, 80, 6, 18]];
function apStats() {
return AP_NAMES.map((name, i) => {
const [ch, bw, clients, util] = AP_5G[i];
const serving = i === 2;
return {
id: `ap-${i}`, name, mac: mac(0x600 + i), type: "ap", model: "AP45", status: "connected", site_id: PHY_SITE,
radio_stat: {
band_24: { channel: [1, 6, 11][i % 3], bandwidth: 20, num_clients: 3, noise_floor: -91, util_all: 35, power: 8 },
band_5: { mac: mac(0x6a0 + i), channel: ch, bandwidth: bw, num_clients: clients, power: serving ? 14 : 17,
noise_floor: serving ? -88 : -94, util_all: util,
...(serving ? { util_tx: 12, util_rx_in_bss: 30, util_rx_other_bss: 14, util_unknown_wifi: 2, util_non_wifi: 14 } : {}) },
},
};
});
}

function phyClient() {
const base = CLIENTS[PHY_SITE].find((c) => c.mac === PHY_MAC);
return { ...base, ap_mac: mac(0x602), band: "5", channel: 52, channel_width: 80, proto: "ax", num_streams: 2,
rssi: -68, snr: 18, tx_rate: 480, rx_rate: 360, tx_retries: 1400, tx_pkts: 7800, rx_retries: 300, rx_pkts: 9100,
dual_band: true, last_seen: liveNow() - 20 };
}

// A smooth, repeatable wobble for the time series.
const wave = (i, a, b) => Math.sin(i / 9) * a + Math.sin(i / 2.7) * b;
function phySeries(metric) {
const t1 = liveNow();
const rt = Array.from({ length: 144 }, (_, i) => t1 - (143 - i) * 600);
const at = (f) => rt.map((_, i) => Math.round(f(i) * 10) / 10);
const series = {
rssi: at((i) => -63 + wave(i, 4, 2) - (i > 95 && i < 110 ? 9 : 0)),
snr: at((i) => 24 + wave(i, 4, 2) - (i > 95 && i < 110 ? 8 : 0)),
tx_rate: at((i) => Math.max(86, 620 + wave(i, 160, 70))),
rx_rate: at((i) => Math.max(65, 470 + wave(i, 120, 50))),
tx_retries: at((i) => Math.max(0, 140 + wave(i, 80, 40))),
rx_retries: at((i) => Math.max(0, 30 + wave(i, 15, 8))),
}[metric];
return series ? { rt, results: series } : null;
}

function phySessions() {
const t1 = liveNow();
const ap = (i) => mac(0x600 + i);
// [AP index, start hours ago, end hours ago or null]
return [[2, 23, 17.5], [4, 17.5, 17.45], [2, 17.45, 9.2], [4, 9.2, 9.17], [2, 9.17, 9.1], [3, 9.1, 6.0], [2, 6.0, null]]
.map(([i, a, b]) => ({ ap: ap(i), band: "5", ssid: "DemoCorp", connect: t1 - a * 3600, disconnect: b == null ? null : t1 - b * 3600,
duration: Math.round(((b == null ? 0 : -b) + a) * 3600) }));
}

function phyEvents() {
const t1 = liveNow();
const ev = (h, type, text, o = {}) => ({ timestamp: t1 - h * 3600, type, text, ap: mac(0x602), band: "5", channel: 52, ...o });
return [
ev(22.9, "CLIENT_ASSOCIATION", "Associated"),
ev(17.5, "CLIENT_DEAUTHENTICATION", "Deauthenticated", { reason: 4, ap: mac(0x602) }),
ev(17.45, "CLIENT_REASSOCIATION", "Reassociated", { ap: mac(0x604) }),
ev(9.2, "CLIENT_DISASSOCIATION", "Disassociated", { reason: 8, ap: mac(0x602) }),
ev(9.17, "CLIENT_REASSOCIATION", "Reassociated", { ap: mac(0x604) }),
ev(8.3, "CLIENT_DEAUTHENTICATION", "Deauthenticated", { reason: 4, ap: mac(0x602) }),
ev(6.0, "CLIENT_REASSOCIATION", "Reassociated", { ap: mac(0x602) }),
ev(2.1, "CLIENT_DHCP_SUCCESS", "DHCP Success"),
];
}

// ---- Alarms -----------------------------------------------------------------

const ALARM_DEFS = [
Expand Down Expand Up @@ -208,7 +316,27 @@ export function mist(path, query) {
}
if ((r = m(/^\/sites\/([^/]+)\/wlans\/derived$/))) return { body: WLANS[r[1]] || [] };
if ((r = m(/^\/sites\/([^/]+)\/stats\/clients$/))) return { body: CLIENTS[r[1]] || [] };
if ((r = m(/^\/sites\/([^/]+)\/stats\/devices$/))) return { body: SWITCHES.filter((s) => s.site === r[1]).map(switchStats) };
if ((r = m(/^\/sites\/([^/]+)\/stats\/devices$/))) {
if (query.get("type") === "ap") return { body: r[1] === PHY_SITE ? apStats() : [] };
return { body: SWITCHES.filter((s) => s.site === r[1]).map(switchStats) };
}
if (path === `/orgs/${ORG_ID}/networktemplates`) return { body: SWITCH_TEMPLATES };
if ((r = m(/^\/sites\/([^/]+)\/setting$/))) return { body: SITE_SETTING[r[1]] || {} };
if (path === `/orgs/${ORG_ID}/clients/search`) {
const hit = query.get("mac") === PHY_MAC;
return { body: { results: hit ? [{ mac: PHY_MAC, site_id: PHY_SITE, last_seen: liveNow() - 20 }] : [], total: hit ? 1 : 0 } };
}
if (path === `/sites/${PHY_SITE}/stats/clients/${PHY_MAC}`) return { body: phyClient() };
if (path === `/sites/${PHY_SITE}/clients/search`) return { body: { results: [phyClient()], total: 1 } };
if (path === `/sites/${PHY_SITE}/clients/${PHY_MAC}/events`) return { body: { results: phyEvents() } };
if (path === `/sites/${PHY_SITE}/clients/sessions/search`) {
const rows = phySessions();
return { body: { results: rows, total: rows.length } };
}
if ((r = m(new RegExp(`^/sites/${PHY_SITE}/insights/client/${PHY_MAC}/([^/]+)$`)))) {
const body = phySeries(decodeURIComponent(r[1]));
return body ? { body } : null;
}
if ((r = m(/^\/sites\/([^/]+)\/stats\/ports\/search$/))) {
const rows = SWITCHES.filter((s) => s.site === r[1]).flatMap((sw) => switchStats(sw).ports.map((p) => ({ ...p, mac: sw.mac })));
return { body: { results: rows, total: rows.length } };
Expand Down
Loading