diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 75eef04f..041ac247 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -309,6 +309,25 @@ const getManifestData = async (manifestUuid?: string) => { return getData('manifests'); }; +const getNetworkData = async (hostUuid: string, hostName?: string) => { + let replacementKey = hostName; + + if (!replacementKey) { + ({ + host_uuid: { + [hostUuid]: { short_host_name: replacementKey }, + }, + } = await getHostData()); + } + + await subroutine('load_interfces', { + params: [{ host: replacementKey, host_uuid: hostUuid }], + pre: ['Network'], + }); + + return getData('network'); +}; + const getPeerData: GetPeerDataFunction = async ( target, { password, port } = {}, @@ -358,6 +377,7 @@ export { getLocalHostName, getLocalHostUuid as getLocalHostUUID, getManifestData, + getNetworkData, getPeerData, getUpsSpec, query, diff --git a/striker-ui-api/src/lib/request_handlers/anvil/getAnvilNetwork.ts b/striker-ui-api/src/lib/request_handlers/anvil/getAnvilNetwork.ts new file mode 100644 index 00000000..4a5db698 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/anvil/getAnvilNetwork.ts @@ -0,0 +1,159 @@ +import assert from 'assert'; +import { RequestHandler } from 'express'; + +import { REP_UUID } from '../../consts'; + +import { getAnvilData, getHostData, getNetworkData } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { stderr } from '../../shell'; + +const degrade = (current: string) => + current === 'optimal' ? 'degraded' : current; + +const compare = (a: string, b: string) => (a > b ? 1 : -1); + +const buildSubnodeBonds = ( + ifaces: AnvilDataSubnodeNetwork['interface'], +): AnvilDetailSubnodeBond[] => { + const bondList = Object.entries(ifaces) + .sort(([an, { type: at }], [bn, { type: bt }]) => { + const ab = at === 'bond'; + const bb = bt === 'bond'; + + if (ab && bb) return compare(an, bn); + if (ab) return -1; + if (bb) return 1; + + return compare(an, bn); + }) + .reduce<{ + [bondUuid: string]: AnvilDetailSubnodeBond; + }>((previous, [ifname, ifvalue]) => { + const { type } = ifvalue; + + if (type === 'bond') { + const { active_interface, uuid: bondUuid } = + ifvalue as AnvilDataHostNetworkBond; + + previous[bondUuid] = { + active_interface, + bond_name: ifname, + bond_uuid: bondUuid, + links: [], + }; + } else if (type === 'interface') { + const { + bond_uuid: bondUuid, + operational, + speed, + uuid: linkUuid, + } = ifvalue as AnvilDataHostNetworkLink; + + // Link without bond UUID can be ignored + if (!REP_UUID.test(bondUuid)) return previous; + + const { + [bondUuid]: { active_interface, links }, + } = previous; + + let linkState: string = operational === 'up' ? 'optimal' : 'down'; + + links.forEach((xLink) => { + const { link_speed: xlSpeed, link_state: xlState } = xLink; + + if (xlSpeed < speed) { + // Seen link is slower than current link, mark seen link as 'degraded' + xLink.link_state = degrade(xlState); + } else if (xlSpeed > speed) { + // Current link is slower than seen link, mark current link as 'degraded' + linkState = degrade(linkState); + } + }); + + links.push({ + is_active: ifname === active_interface, + link_name: ifname, + link_speed: speed, + link_state: linkState, + link_uuid: linkUuid, + }); + } + + return previous; + }, {}); + + return Object.values(bondList); +}; + +export const getAnvilNetwork: RequestHandler< + AnvilDetailParamsDictionary +> = async (request, response) => { + const { + params: { anvilUuid: rAnUuid }, + } = request; + + const anUuid = sanitize(rAnUuid, 'string', { modifierType: 'sql' }); + + try { + assert( + REP_UUID.test(anUuid), + `Param UUID must be a valid UUIDv4; got [${anUuid}]`, + ); + } catch (error) { + stderr(`Failed to assert value during get anvil network; CAUSE: ${error}`); + + return response.status(400).send(); + } + + let ans: AnvilDataAnvilListHash; + let hosts: AnvilDataHostListHash; + + try { + ans = await getAnvilData(); + hosts = await getHostData(); + } catch (error) { + stderr(`Failed to get anvil and host data; CAUSE: ${error}`); + + return response.status(500).send(); + } + + const { + anvil_uuid: { + [anUuid]: { + anvil_node1_host_uuid: n1uuid, + anvil_node2_host_uuid: n2uuid, + }, + }, + } = ans; + + const rsbody: AnvilDetailNetworkSummary = { hosts: [] }; + + for (const hostUuid of [n1uuid, n2uuid]) { + try { + const { + host_uuid: { + [hostUuid]: { short_host_name: hostName }, + }, + } = hosts; + + const { [hostName]: subnodeNetwork } = await getNetworkData( + hostUuid, + hostName, + ); + + const { interface: ifaces } = subnodeNetwork as AnvilDataSubnodeNetwork; + + rsbody.hosts.push({ + bonds: buildSubnodeBonds(ifaces), + host_name: hostName, + host_uuid: hostUuid, + }); + } catch (error) { + stderr(`Failed to get host ${hostUuid} network data; CAUSE: ${error}`); + + return response.status(500).send(); + } + } + + return response.json(rsbody); +}; diff --git a/striker-ui-api/src/lib/request_handlers/anvil/index.ts b/striker-ui-api/src/lib/request_handlers/anvil/index.ts index c0750c82..d42bccca 100644 --- a/striker-ui-api/src/lib/request_handlers/anvil/index.ts +++ b/striker-ui-api/src/lib/request_handlers/anvil/index.ts @@ -2,3 +2,4 @@ export * from './getAnvil'; export * from './getAnvilCpu'; export * from './getAnvilDetail'; export * from './getAnvilMemory'; +export * from './getAnvilNetwork'; diff --git a/striker-ui-api/src/routes/anvil.ts b/striker-ui-api/src/routes/anvil.ts index 36ec4d0d..f971e7ab 100644 --- a/striker-ui-api/src/routes/anvil.ts +++ b/striker-ui-api/src/routes/anvil.ts @@ -5,6 +5,7 @@ import { getAnvilCpu, getAnvilDetail, getAnvilMemory, + getAnvilNetwork, } from '../lib/request_handlers/anvil'; const router = express.Router(); @@ -13,6 +14,7 @@ router .get('/', getAnvil) .get('/:anvilUuid/cpu', getAnvilCpu) .get('/:anvilUuid/memory', getAnvilMemory) + .get('/:anvilUuid/network', getAnvilNetwork) .get('/:anvilUuid', getAnvilDetail); export default router; diff --git a/striker-ui-api/src/types/ApiAn.d.ts b/striker-ui-api/src/types/ApiAn.d.ts index 5e437048..2d17e694 100644 --- a/striker-ui-api/src/types/ApiAn.d.ts +++ b/striker-ui-api/src/types/ApiAn.d.ts @@ -36,6 +36,31 @@ type AnvilDetailHostForProvisionServer = { hostMemory: string; }; +type AnvilDetailSubnodeLink = { + is_active: boolean; + link_name: string; + link_speed: number; + link_state: string; + link_uuid: string; +}; + +type AnvilDetailSubnodeBond = { + active_interface: string; + bond_name: string; + bond_uuid: string; + links: AnvilDetailSubnodeLink[]; +}; + +type AnvilDetailSubnodeNetwork = { + bonds: AnvilDetailSubnodeBond[]; + host_name: string; + host_uuid: string; +}; + +type AnvilDetailNetworkSummary = { + hosts: AnvilDetailSubnodeNetwork[]; +}; + type AnvilDetailServerForProvisionServer = { serverUUID: string; serverName: string; diff --git a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts index 8e830b98..7bc13f51 100644 --- a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts +++ b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts @@ -47,6 +47,133 @@ type AnvilDataHostListHash = { }; }; +type AnvilDataHostNetworkBond = { + active_interface: string; + bridge_uuid: string; + down_delay: number; + interfaces: string[]; + mac_address: string; + mii_polling_interval: number; + mode: string; + mtu: number; + operational: 'up' | 'down'; + primary_interface: string; + primary_reselect: string; + type: 'bond'; + up_delay: number; + uuid: string; +}; + +type AnvilDataHostNetworkBridge = { + id: string; + interfaces: string[]; + mac_address: string; + mtu: number; + stp_enabled: string; + type: 'bridge'; + uuid: string; +}; + +type AnvilDataHostNetworkLink = { + bond_name: string; + bond_uuid: string; + bridge_name: string; + bridge_uuid: string; + changed_order: number; + duplex: string; + link_state: string; + mac_address: string; + medium: string; + mtu: number; + operational: 'up' | 'down'; + speed: number; + type: 'interface'; + uuid: string; +}; + +type AnvilDataHostNetworkPrimaryLink = AnvilDataHostNetworkLink & { + default_gateway: NumberBoolean; + dns: string; + gateway: string; + ip: string; + network_interface_uuid: string; + subnet_mask: string; +}; + +type AnvilDataStrikerNetworkPrimaryLink = Omit< + AnvilDataHostNetworkPrimaryLink, + | 'bond_name' + | 'bond_uuid' + | 'bridge_name' + | 'bridge_uuid' + | 'changed_order' + | 'duplex' + | 'link_state' + | 'medium' + | 'mtu' + | 'operational' + | 'speed' + | 'type' + | 'uuid' + | 'network_interface_uuid' +> & { + file: string; + mtu: string; + rx_bytes: string; + status: string; + tx_bytes: string; + variable: { + BOOTPROTO: string; + DEFROUTE: string; + DEVICE: string; + DSN1?: string; + GATEWAY?: string; + HWADDR: string; + IPADDR: string; + IPV6INIT: string; + NAME: string; + NM_CONTROLLED: string; + ONBOOT: string; + PREFIX: string; + TYPE: string; + USERCTL: string; + UUID: string; + ZONE: string; + }; +}; + +type AnvilDataSubnodeNetwork = { + bond_uuid: { + [uuid: string]: { name: string }; + }; + bridge_uuid: { + [uuid: string]: { name: string }; + }; + interface: { + [name: string]: + | AnvilDataHostNetworkBond + | AnvilDataHostNetworkBridge + | AnvilDataHostNetworkLink + | AnvilDataHostNetworkPrimaryLink; + }; +}; + +type AnvilDataStrikerNetwork = { + interface: { + [ifname: string]: + | AnvilDataHostNetworkLink + | AnvilDataHostNetworkPrimaryLink; + }; +}; + +type AnvilDataHostNetworkHash = + | AnvilDataSubnodeNetwork + | AnvilDataStrikerNetwork; + +type AnvilDataNetworkListHash = { + [hostId: string]: AnvilDataHostNetworkHash; +}; + type AnvilDataManifestListHash = { manifest_uuid: { [manifestUUID: string]: {