Skip to content

Commit

Permalink
feat: display system metadata (#7)
Browse files Browse the repository at this point in the history
Co-authored-by: Carson Moore <carson.moore@ni.com>
Co-authored-by: Cameron Waterman <cameron.waterman@ni.com>
  • Loading branch information
3 people authored Aug 1, 2023
1 parent 9ba4835 commit fa5c1f5
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 7 deletions.
40 changes: 38 additions & 2 deletions src/datasources/system/SystemDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {
MutableDataFrame,
FieldType,
CoreApp,
toDataFrame,
} from '@grafana/data';

import { TestingStatus, getBackendSrv } from '@grafana/runtime';

import { QueryType, SystemQuery, SystemSummary } from './types';
import { QueryType, SystemMetadata, SystemQuery, SystemSummary } from './types';
import { defaultProjection } from './constants';
import { NetworkUtils } from './network-utils';

export class SystemDataSource extends DataSourceApi<SystemQuery> {
baseUrl: string;
Expand All @@ -19,6 +22,24 @@ export class SystemDataSource extends DataSourceApi<SystemQuery> {
this.baseUrl = this.instanceSettings.url + '/nisysmgmt/v1';
}

transformProjection(projections: string[]): string {
let result = "new(";

projections.forEach(function (field) {
if (field === "workspace") {
result = result.concat(field, ")");
} else {
result = result.concat(field, ", ");
}
});

return result;
}

private getIpAddress(ip4Interface: Record<string, string[]>, ip6Interface: Record<string, string[]>): string | null {
return NetworkUtils.getIpAddressFromInterfaces(ip4Interface) || NetworkUtils.getIpAddressFromInterfaces(ip6Interface);
}

async query(options: DataQueryRequest<SystemQuery>): Promise<DataQueryResponse> {
// Return a constant for each query.
const data = await Promise.all(options.targets.map(async (target) => {
Expand All @@ -32,7 +53,22 @@ export class SystemDataSource extends DataSourceApi<SystemQuery> {
],
});
} else {
throw Error("Not implemented");
let metadataResponse = await getBackendSrv().post<{ data: SystemMetadata[] }>(this.baseUrl + '/query-systems', { projection: this.transformProjection(defaultProjection) });

return toDataFrame({
fields: [
{ name: 'id', values: metadataResponse.data.map(m => m.id)},
{ name: 'alias', values: metadataResponse.data.map(m => m.alias)},
{ name: 'connection status', values: metadataResponse.data.map(m => m.state)},
{ name: 'locked status', values: metadataResponse.data.map(m => m.locked)},
{ name: 'system start time', values: metadataResponse.data.map(m => m.systemStartTime)},
{ name: 'model', values: metadataResponse.data.map(m => m.model)},
{ name: 'vendor', values: metadataResponse.data.map(m => m.vendor)},
{ name: 'operating system', values: metadataResponse.data.map(m => m.osFullName)},
{ name: 'ip address', values: metadataResponse.data.map(m => this.getIpAddress(m.ip4Interfaces, m.ip6Interfaces))},
{ name: 'workspace', values: metadataResponse.data.map(m => m.workspace)},
]
});
}
}));

Expand Down
5 changes: 0 additions & 5 deletions src/datasources/system/components/SystemQueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import { enumToOptions } from 'core/utils';

type Props = QueryEditorProps<SystemDataSource, SystemQuery>;

// const QUERY_TYPES = [
// {label: "Metadata", value: QueryType.Metadata},
// {label: "Summary", value: QueryType.Summary}
// ]

export function SystemQueryEditor({ query, onChange, onRunQuery }: Props) {
const onQueryTypeChange = (value: QueryType) => {
onChange({ ...query, queryKind: value })
Expand Down
14 changes: 14 additions & 0 deletions src/datasources/system/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// constants.ts
export const defaultProjection = [
'id',
'alias',
'connected.data.state',
'grains.data.minion_blackout as locked',
'grains.data.boottime as systemStartTime',
'grains.data.productname as model',
'grains.data.manufacturer as vendor',
'grains.data.osfullname as osFullName',
'grains.data.ip4_interfaces as ip4Interfaces',
'grains.data.ip6_interfaces as ip6Interfaces',
'workspace'
]
67 changes: 67 additions & 0 deletions src/datasources/system/network-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Utility functions for parsing IP, MAC addresses, identify IP and hostname
*/
export class NetworkUtils {
static getIpAddressFromInterfaces(ipInterfaces: { [key: string]: string[] }): string | null {
if (ipInterfaces) {
const firstConnectedInterface = NetworkUtils.getFirstConnectedInterface(ipInterfaces);
if (firstConnectedInterface) {
return firstConnectedInterface.address;
}
}

return null;
}

static getFirstConnectedInterface(ipInterfaces: { [key: string]: string[] }): { name: string, address: string } | null {
for (const ipInterfaceName in ipInterfaces) {
if (!ipInterfaceName) {
continue;
}
const ipInterfaceAddresses = ipInterfaces[ipInterfaceName];
if (ipInterfaceName !== 'lo' && this.isInterfaceConnected(ipInterfaceAddresses)) {
return {
name: ipInterfaceName,
address: ipInterfaceAddresses[0]
};
}
}

return null;
}

static getMacAddressFromInterfaceName(ipInterfaceName: string, hwaddrInterfaces: { [key: string]: string }): string | null {
if (!hwaddrInterfaces) {
return null;
}
return hwaddrInterfaces[ipInterfaceName] || null;
}

static isValidIp(hostnameOrIpAddress: string): boolean {
const validIpAddressRegex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
return validIpAddressRegex.test(hostnameOrIpAddress);
}

static isValidHostname(hostnameOrIpAddress: string): boolean {
const validHostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
return validHostnameRegex.test(hostnameOrIpAddress);
}

private static isInterfaceConnected(ipInterfaceAddresses: string[]): boolean {
if (!ipInterfaceAddresses || ipInterfaceAddresses.length === 0) {
return false;
}
if (ipInterfaceAddresses.length > 1) {
return true;
}

const ipInterfaceAddress = ipInterfaceAddresses[0];
if (ipInterfaceAddress === '0.0.0.0' || ipInterfaceAddress === '::') {
// https://tools.ietf.org/html/rfc3513#section-2.5.2
// These addresses are used as non-routable meta-addresses
// to designate an invalid, unknown, or non-applicable address.
return false;
}
return true;
}
}
14 changes: 14 additions & 0 deletions src/datasources/system/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ export interface SystemSummary {
connectedCount: number,
disconnectedCount: number
}

export interface SystemMetadata {
id: string,
alias: string,
state: string,
locked: string,
systemStartTime: string,
model: string,
vendor: string,
osFullName: string,
ip4Interfaces: Record<string, string[]>,
ip6Interfaces: Record<string, string[]>,
workspace: string
}

0 comments on commit fa5c1f5

Please sign in to comment.