Skip to content
Open
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
101 changes: 101 additions & 0 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,24 @@ type InfoMemory implements Node {
layout: [MemoryLayout!]!
}

"""IPv4 address assigned to a network interface"""
type InfoNetworkIpv4Address {
"""IPv4 address"""
address: String!

"""IPv4 netmask"""
netmask: String!
}

"""IPv6 address assigned to a network interface"""
type InfoNetworkIpv6Address {
"""IPv6 address"""
address: String!

"""IPv6 prefix length"""
prefixLength: Int
}

type InfoNetworkInterface implements Node {
id: PrefixedID!

Expand All @@ -1853,6 +1871,36 @@ type InfoNetworkInterface implements Node {
"""MAC Address"""
macAddress: String

"""Maximum transmission unit"""
mtu: Int

"""Link speed in Mbps"""
speed: Int

"""Link duplex mode"""
duplex: String

"""Whether this is an internal interface"""
internal: Boolean

"""Whether this is a virtual interface"""
virtual: Boolean

"""Operational state"""
operstate: String

"""Interface type"""
type: String

"""VLAN identifier parsed from the interface name"""
vlanId: Int

"""IPv4 addresses assigned to this interface"""
ipv4Addresses: [InfoNetworkIpv4Address!]!

"""IPv6 addresses assigned to this interface"""
ipv6Addresses: [InfoNetworkIpv6Address!]!

"""Connection status"""
status: String

Expand Down Expand Up @@ -2566,6 +2614,52 @@ type TemperatureMetrics implements Node {
summary: TemperatureSummary!
}

type NetworkMetrics implements Node {
id: PrefixedID!

"""Interface identifier"""
name: String!

"""Operational state"""
operstate: String

"""Total received bytes"""
bytesReceived: BigInt!

"""Total transmitted bytes"""
bytesSent: BigInt!

"""Total received packets"""
packetsReceived: BigInt!

"""Total transmitted packets"""
packetsSent: BigInt!

"""Receive errors"""
receiveErrors: BigInt!

"""Transmit errors"""
transmitErrors: BigInt!

"""Dropped receive packets"""
receiveDropped: BigInt!

"""Dropped transmit packets"""
transmitDropped: BigInt!

"""Receive throughput in bytes per second"""
rxSec: Float!

"""Transmit throughput in bytes per second"""
txSec: Float!

"""Estimated link utilization percentage"""
utilizationPercent: Float

"""Metric collection timestamp"""
lastUpdated: DateTime!
}

"""System metrics including CPU and memory utilization"""
type Metrics implements Node {
id: PrefixedID!
Expand All @@ -2578,6 +2672,9 @@ type Metrics implements Node {

"""Temperature metrics"""
temperature: TemperatureMetrics

"""Current network metrics for all interfaces"""
network: [NetworkMetrics!]!
}

type SensorConfig {
Expand Down Expand Up @@ -3242,6 +3339,9 @@ type Query {

"""Validate an OIDC session token (internal use for CLI validation)"""
validateOidcSession(token: String!): OidcSessionValidation!

"""Network interfaces"""
networkInterfaces: [InfoNetworkInterface!]!
metrics: Metrics!

"""Retrieve current system time configuration"""
Expand Down Expand Up @@ -3606,6 +3706,7 @@ type Subscription {
systemMetricsCpu: CpuUtilization!
systemMetricsCpuTelemetry: CpuPackages!
systemMetricsMemory: MemoryUtilization!
systemMetricsNetwork: [NetworkMetrics!]!
systemMetricsTemperature: TemperatureMetrics
upsUpdates: UPSDevice!
pluginInstallUpdates(operationId: ID!): PluginInstallEvent!
Expand Down
48 changes: 48 additions & 0 deletions api/src/unraid-api/graph/resolvers/info/network/network.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ import { Field, Int, ObjectType } from '@nestjs/graphql';

import { Node } from '@unraid/shared/graphql.model.js';

@ObjectType({ description: 'IPv4 address assigned to a network interface' })
export class InfoNetworkIpv4Address {
@Field({ description: 'IPv4 address' })
address!: string;

@Field({ description: 'IPv4 netmask' })
netmask!: string;
}

@ObjectType({ description: 'IPv6 address assigned to a network interface' })
export class InfoNetworkIpv6Address {
@Field({ description: 'IPv6 address' })
address!: string;

@Field(() => Int, { nullable: true, description: 'IPv6 prefix length' })
prefixLength?: number;
}

@ObjectType({ implements: () => Node })
export class InfoNetworkInterface extends Node {
@Field({ description: 'Interface name (e.g. eth0)' })
Expand All @@ -13,6 +31,36 @@ export class InfoNetworkInterface extends Node {
@Field({ nullable: true, description: 'MAC Address' })
macAddress?: string;

@Field(() => Int, { nullable: true, description: 'Maximum transmission unit' })
mtu?: number;

@Field(() => Int, { nullable: true, description: 'Link speed in Mbps' })
speed?: number;

@Field({ nullable: true, description: 'Link duplex mode' })
duplex?: string;

@Field({ nullable: true, description: 'Whether this is an internal interface' })
internal?: boolean;

@Field({ nullable: true, description: 'Whether this is a virtual interface' })
virtual?: boolean;

@Field({ nullable: true, description: 'Operational state' })
operstate?: string;

@Field({ nullable: true, description: 'Interface type' })
type?: string;

@Field(() => Int, { nullable: true, description: 'VLAN identifier parsed from the interface name' })
vlanId?: number;

@Field(() => [InfoNetworkIpv4Address], { description: 'IPv4 addresses assigned to this interface' })
ipv4Addresses!: InfoNetworkIpv4Address[];

@Field(() => [InfoNetworkIpv6Address], { description: 'IPv6 addresses assigned to this interface' })
ipv6Addresses!: InfoNetworkIpv6Address[];

@Field({ nullable: true, description: 'Connection status' })
status?: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,23 @@ import { NetworkService } from '@app/unraid-api/graph/resolvers/info/network/net
export class InfoNetworkResolver {
constructor(private readonly networkService: NetworkService) {}

@ResolveField(() => [InfoNetworkInterface], { description: 'Network interfaces' })
@Query(() => [InfoNetworkInterface], { description: 'Network interfaces' })
@UsePermissions({
action: AuthAction.READ_ANY,
resource: Resource.INFO,
})
async networkInterfaces(): Promise<InfoNetworkInterface[]> {
return this.networkService.getNetworkInterfaces();
}

@ResolveField(() => [InfoNetworkInterface], {
name: 'networkInterfaces',
description: 'Network interfaces',
})
async infoNetworkInterfaces(): Promise<InfoNetworkInterface[]> {
return this.networkService.getNetworkInterfaces();
}

@ResolveField(() => InfoNetworkInterface, {
nullable: true,
description: 'Primary management interface',
Expand Down
61 changes: 58 additions & 3 deletions api/src/unraid-api/graph/resolvers/info/network/network.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';

import { networkInterfaces } from 'systeminformation';

import { getters } from '@app/store/index.js';
import { InfoNetworkInterface } from '@app/unraid-api/graph/resolvers/info/network/network.model.js';

@Injectable()
Expand All @@ -18,6 +17,30 @@ export class NetworkService {
name: iface.iface,
description: iface.ifaceName, // Label
macAddress: iface.mac,
mtu: iface.mtu ?? undefined,
speed: iface.speed ?? undefined,
duplex: iface.duplex,
internal: iface.internal,
virtual: iface.virtual,
operstate: iface.operstate,
type: iface.type,
vlanId: this.parseVlanId(iface.iface),
ipv4Addresses: iface.ip4
? [
{
address: iface.ip4,
netmask: iface.ip4subnet,
},
]
: [],
ipv6Addresses: iface.ip6
? [
{
address: iface.ip6,
prefixLength: this.parseIpv6PrefixLength(iface.ip6subnet),
},
]
: [],
status: iface.operstate,
protocol: iface.ip4 ? (iface.ip6 ? 'ipv4+ipv6' : 'ipv4') : iface.ip6 ? 'ipv6' : 'none',
ipAddress: iface.ip4,
Expand All @@ -27,7 +50,7 @@ export class NetworkService {
ipv6Address: iface.ip6,
ipv6Netmask: iface.ip6subnet,
useDhcp6: false,
} as InfoNetworkInterface;
} satisfies InfoNetworkInterface;
});
}

Expand Down Expand Up @@ -58,6 +81,38 @@ export class NetworkService {
netmask: primary.ip4subnet,
useDhcp: primary.dhcp,
ipv6Address: primary.ip6,
} as InfoNetworkInterface;
ipv4Addresses: primary.ip4
? [
{
address: primary.ip4,
netmask: primary.ip4subnet,
},
]
: [],
ipv6Addresses: primary.ip6
? [
{
address: primary.ip6,
prefixLength: this.parseIpv6PrefixLength(primary.ip6subnet),
},
]
: [],
} satisfies InfoNetworkInterface;
}

private parseVlanId(iface: string): number | undefined {
const match = iface.match(/\.(\d+)$/);
if (!match) return undefined;

const vlanId = Number(match[1]);
return Number.isInteger(vlanId) ? vlanId : undefined;
}

private parseIpv6PrefixLength(subnet: string): number | undefined {
const trimmed = subnet.trim();
if (!/^\d+$/.test(trimmed)) return undefined;

const parsed = Number(trimmed);
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 128 ? parsed : undefined;
}
}
6 changes: 6 additions & 0 deletions api/src/unraid-api/graph/resolvers/metrics/metrics.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Node } from '@unraid/shared/graphql.model.js';

import { CpuUtilization } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.model.js';
import { MemoryUtilization } from '@app/unraid-api/graph/resolvers/info/memory/memory.model.js';
import { NetworkMetrics } from '@app/unraid-api/graph/resolvers/metrics/network/network.model.js';
import { TemperatureMetrics } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature.model.js';

@ObjectType({
Expand All @@ -25,4 +26,9 @@ export class Metrics extends Node {
description: 'Temperature metrics',
})
temperature?: TemperatureMetrics;

@Field(() => [NetworkMetrics], {
description: 'Current network metrics for all interfaces',
})
network!: NetworkMetrics[];
}
3 changes: 2 additions & 1 deletion api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { Module } from '@nestjs/common';
import { CpuModule } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.module.js';
import { MemoryService } from '@app/unraid-api/graph/resolvers/info/memory/memory.service.js';
import { MetricsResolver } from '@app/unraid-api/graph/resolvers/metrics/metrics.resolver.js';
import { NetworkMetricsService } from '@app/unraid-api/graph/resolvers/metrics/network/network.service.js';
import { TemperatureModule } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature.module.js';
import { ServicesModule } from '@app/unraid-api/graph/services/services.module.js';

@Module({
imports: [ServicesModule, CpuModule, TemperatureModule],
providers: [MetricsResolver, MemoryService],
providers: [MetricsResolver, MemoryService, NetworkMetricsService],
exports: [MetricsResolver],
})
export class MetricsModule {}
Loading