diff --git a/api/dev/configs/api.json b/api/dev/configs/api.json index d57ab4fd43..cade101923 100644 --- a/api/dev/configs/api.json +++ b/api/dev/configs/api.json @@ -1,9 +1,9 @@ { - "version": "4.29.2", + "version": "4.35.0", "extraOrigins": [], "sandbox": false, "ssoSubIds": [], "plugins": [ "unraid-api-plugin-connect" ] -} +} \ No newline at end of file diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index db6919a533..c697f98867 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -1210,6 +1210,16 @@ type ArrayMutations { input ArrayStateInput { """Array state""" desiredState: ArrayStateInputState! + + """ + Optional password used to unlock encrypted array disks when starting the array + """ + decryptionPassword: String + + """ + Optional keyfile contents used to unlock encrypted array disks when starting the array. Accepts a data URL or raw base64 payload. + """ + decryptionKeyfile: String } enum ArrayStateInputState { @@ -1232,6 +1242,9 @@ type DockerMutations { """Stop a container""" stop(id: PrefixedID!): DockerContainer! + """Restart a container""" + restart(id: PrefixedID!): DockerContainer! + """Pause (Suspend) a container""" pause(id: PrefixedID!): DockerContainer! @@ -2514,6 +2527,52 @@ type LogFileContent { startLine: Int } +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! +} + type TemperatureReading { """Temperature value""" value: Float! @@ -2614,52 +2673,6 @@ 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! @@ -3315,6 +3328,9 @@ type Query { isFreshInstall: Boolean! publicTheme: Theme! info: Info! + + """Network interfaces""" + networkInterfaces: [InfoNetworkInterface!]! docker: Docker! disks: [Disk!]! assignableDisks: [Disk!]! @@ -3339,9 +3355,6 @@ 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""" diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index 0b0c497690..ff32209237 100644 --- a/api/src/unraid-api/cli/generated/graphql.ts +++ b/api/src/unraid-api/cli/generated/graphql.ts @@ -366,6 +366,10 @@ export enum ArrayState { } export type ArrayStateInput = { + /** Optional keyfile contents used to unlock encrypted array disks when starting the array. Accepts a data URL or raw base64 payload. */ + decryptionKeyfile?: InputMaybe; + /** Optional password used to unlock encrypted array disks when starting the array */ + decryptionPassword?: InputMaybe; /** Array state */ desiredState: ArrayStateInputState; }; @@ -948,6 +952,8 @@ export type DockerMutations = { pause: DockerContainer; /** Remove a container */ removeContainer: Scalars['Boolean']['output']; + /** Restart a container */ + restart: DockerContainer; /** Start a container */ start: DockerContainer; /** Stop a container */ @@ -976,6 +982,11 @@ export type DockerMutationsRemoveContainerArgs = { }; +export type DockerMutationsRestartArgs = { + id: Scalars['PrefixedID']['input']; +}; + + export type DockerMutationsStartArgs = { id: Scalars['PrefixedID']['input']; }; @@ -1311,31 +1322,69 @@ export type InfoNetworkInterface = Node & { __typename?: 'InfoNetworkInterface'; /** Interface description/label */ description?: Maybe; + /** Link duplex mode */ + duplex?: Maybe; /** IPv4 Gateway */ gateway?: Maybe; id: Scalars['PrefixedID']['output']; + /** Whether this is an internal interface */ + internal?: Maybe; /** IPv4 Address */ ipAddress?: Maybe; + /** IPv4 addresses assigned to this interface */ + ipv4Addresses: Array; /** IPv6 Address */ ipv6Address?: Maybe; + /** IPv6 addresses assigned to this interface */ + ipv6Addresses: Array; /** IPv6 Gateway */ ipv6Gateway?: Maybe; /** IPv6 Netmask */ ipv6Netmask?: Maybe; /** MAC Address */ macAddress?: Maybe; + /** Maximum transmission unit */ + mtu?: Maybe; /** Interface name (e.g. eth0) */ name: Scalars['String']['output']; /** IPv4 Netmask */ netmask?: Maybe; + /** Operational state */ + operstate?: Maybe; /** IPv4 Protocol mode */ protocol?: Maybe; + /** Link speed in Mbps */ + speed?: Maybe; /** Connection status */ status?: Maybe; + /** Interface type */ + type?: Maybe; /** Using DHCP for IPv4 */ useDhcp?: Maybe; /** Using DHCP for IPv6 */ useDhcp6?: Maybe; + /** Whether this is a virtual interface */ + virtual?: Maybe; + /** VLAN identifier parsed from the interface name */ + vlanId?: Maybe; +}; + +/** IPv4 address assigned to a network interface */ +export type InfoNetworkIpv4Address = { + __typename?: 'InfoNetworkIpv4Address'; + /** IPv4 address */ + address: Scalars['String']['output']; + /** IPv4 netmask */ + netmask: Scalars['String']['output']; +}; + +/** IPv6 address assigned to a network interface */ +export type InfoNetworkIpv6Address = { + __typename?: 'InfoNetworkIpv6Address'; + /** IPv6 address */ + address: Scalars['String']['output']; + /** IPv6 prefix length */ + prefixLength?: Maybe; }; export type InfoOs = Node & { @@ -1576,6 +1625,8 @@ export type Metrics = Node & { id: Scalars['PrefixedID']['output']; /** Current memory utilization metrics */ memory?: Maybe; + /** Current network metrics for all interfaces */ + network: Array; /** Temperature metrics */ temperature?: Maybe; }; @@ -1827,6 +1878,39 @@ export type Network = Node & { id: Scalars['PrefixedID']['output']; }; +export type NetworkMetrics = Node & { + __typename?: 'NetworkMetrics'; + /** Total received bytes */ + bytesReceived: Scalars['BigInt']['output']; + /** Total transmitted bytes */ + bytesSent: Scalars['BigInt']['output']; + id: Scalars['PrefixedID']['output']; + /** Metric collection timestamp */ + lastUpdated: Scalars['DateTime']['output']; + /** Interface identifier */ + name: Scalars['String']['output']; + /** Operational state */ + operstate?: Maybe; + /** Total received packets */ + packetsReceived: Scalars['BigInt']['output']; + /** Total transmitted packets */ + packetsSent: Scalars['BigInt']['output']; + /** Dropped receive packets */ + receiveDropped: Scalars['BigInt']['output']; + /** Receive errors */ + receiveErrors: Scalars['BigInt']['output']; + /** Receive throughput in bytes per second */ + rxSec: Scalars['Float']['output']; + /** Dropped transmit packets */ + transmitDropped: Scalars['BigInt']['output']; + /** Transmit errors */ + transmitErrors: Scalars['BigInt']['output']; + /** Transmit throughput in bytes per second */ + txSec: Scalars['Float']['output']; + /** Estimated link utilization percentage */ + utilizationPercent?: Maybe; +}; + export type Node = { id: Scalars['PrefixedID']['output']; }; @@ -1985,12 +2069,21 @@ export type OnboardingInternalBootContext = { assignableDisks: Array; bootEligible?: Maybe; bootedFromFlashWithInternalBootSetup: Scalars['Boolean']['output']; + driveWarnings: Array; enableBootTransfer?: Maybe; poolNames: Array; reservedNames: Array; shareNames: Array; }; +/** Warning metadata for an assignable internal boot drive */ +export type OnboardingInternalBootDriveWarning = { + __typename?: 'OnboardingInternalBootDriveWarning'; + device: Scalars['String']['output']; + diskId: Scalars['String']['output']; + warnings: Array; +}; + /** Result of attempting internal boot pool setup */ export type OnboardingInternalBootResult = { __typename?: 'OnboardingInternalBootResult'; @@ -2318,6 +2411,8 @@ export type Query = { me: UserAccount; metrics: Metrics; network: Network; + /** Network interfaces */ + networkInterfaces: Array; /** Get all notifications */ notifications: Notifications; /** Get the full OIDC configuration (admin only) */ @@ -2727,6 +2822,7 @@ export type Subscription = { systemMetricsCpu: CpuUtilization; systemMetricsCpuTelemetry: CpuPackages; systemMetricsMemory: MemoryUtilization; + systemMetricsNetwork: Array; systemMetricsTemperature?: Maybe; upsUpdates: UpsDevice; }; diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts index 6dafe8bb53..fe7ea04ec1 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts @@ -20,6 +20,7 @@ describe('DockerMutationsResolver', () => { useValue: { start: vi.fn(), stop: vi.fn(), + restart: vi.fn(), }, }, ], @@ -54,6 +55,27 @@ describe('DockerMutationsResolver', () => { expect(dockerService.start).toHaveBeenCalledWith('1'); }); + it('should restart', async () => { + const mockContainer: DockerContainer = { + id: '1', + autoStart: false, + command: 'test', + created: 1234567890, + image: 'test-image', + imageId: 'test-image-id', + ports: [], + state: ContainerState.RUNNING, + status: 'Up 2 seconds', + names: ['test-container'], + isOrphaned: false, + }; + vi.mocked(dockerService.restart).mockResolvedValue(mockContainer); + + const result = await resolver.restart('1'); + expect(result).toEqual(mockContainer); + expect(dockerService.restart).toHaveBeenCalledWith('1'); + }); + it('should stop', async () => { const mockContainer: DockerContainer = { id: '1', diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts index d2423b5cc0..fc06f270c6 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts @@ -36,6 +36,15 @@ export class DockerMutationsResolver { public async stop(@Args('id', { type: () => PrefixedID }) id: string) { return this.dockerService.stop(id); } + @ResolveField(() => DockerContainer, { description: 'Restart a container' }) + @UsePermissions({ + action: AuthAction.UPDATE_ANY, + resource: Resource.DOCKER, + }) + public async restart(@Args('id', { type: () => PrefixedID }) id: string) { + return this.dockerService.restart(id); + } + @ResolveField(() => DockerContainer, { description: 'Pause (Suspend) a container' }) @UsePermissions({ action: AuthAction.UPDATE_ANY, diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts index f9ab378a42..09519e4180 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts @@ -38,6 +38,7 @@ const { mockDockerInstance, mockListContainers, mockGetContainer, mockListNetwor stop: vi.fn(), pause: vi.fn(), unpause: vi.fn(), + restart: vi.fn(), inspect: vi.fn(), }; @@ -77,6 +78,10 @@ vi.mock('execa', () => ({ execa: vi.fn(), })); +vi.mock('@app/core/utils/misc/sleep.js', () => ({ + sleep: vi.fn().mockResolvedValue(undefined), +})); + const { mockEmhttpGetter } = vi.hoisted(() => ({ mockEmhttpGetter: vi.fn().mockReturnValue({ networks: [], @@ -168,6 +173,8 @@ describe('DockerService', () => { let service: DockerService; beforeEach(async () => { + (pubsub.publish as ReturnType).mockClear(); + // Reset mocks before each test mockListContainers.mockReset(); mockListNetworks.mockReset(); @@ -175,6 +182,7 @@ describe('DockerService', () => { mockContainer.stop.mockReset(); mockContainer.pause.mockReset(); mockContainer.unpause.mockReset(); + mockContainer.restart.mockReset(); mockContainer.inspect.mockReset(); statMock.mockReset(); @@ -375,4 +383,77 @@ describe('DockerService', () => { ); }); }); + + describe('restart', () => { + const containerInfo = { + Id: 'abc123', + Names: ['/test-container'], + Image: 'test-image', + ImageID: 'sha256:abc', + Command: 'test', + Created: 1700000000, + Status: 'Up', + Ports: [], + Labels: {}, + HostConfig: { NetworkMode: 'bridge' }, + NetworkSettings: { Networks: {} }, + Mounts: [], + }; + + it('returns the container when it reaches RUNNING state', async () => { + mockListContainers.mockResolvedValue([{ ...containerInfo, State: 'running' }]); + mockContainer.restart.mockResolvedValue(undefined); + mockContainer.inspect.mockResolvedValue({ State: { Status: 'running' } }); + + const result = await service.restart('abc123'); + + expect(mockContainer.restart).toHaveBeenCalledWith({ t: 10 }); + expect(mockContainer.inspect).toHaveBeenCalled(); + expect(result).toMatchObject({ id: 'abc123', state: ContainerState.RUNNING }); + expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.INFO, expect.anything()); + }); + + it('stops polling as soon as the target state is observed', async () => { + mockListContainers.mockResolvedValue([{ ...containerInfo, State: 'running' }]); + mockContainer.restart.mockResolvedValue(undefined); + mockContainer.inspect + .mockResolvedValueOnce({ State: { Status: 'restarting' } }) + .mockResolvedValueOnce({ State: { Status: 'running' } }); + + await service.restart('abc123'); + + expect(mockContainer.inspect).toHaveBeenCalledTimes(2); + }); + + it('throws when the container cannot be found after restart', async () => { + mockListContainers.mockResolvedValue([]); + mockContainer.restart.mockResolvedValue(undefined); + mockContainer.inspect.mockResolvedValue({ State: { Status: 'running' } }); + + await expect(service.restart('abc123')).rejects.toThrow(); + }); + + it('warns and returns the container when it does not reach RUNNING state after retries', async () => { + mockListContainers.mockResolvedValue([{ ...containerInfo, State: 'exited' }]); + mockContainer.restart.mockResolvedValue(undefined); + mockContainer.inspect.mockResolvedValue({ State: { Status: 'exited' } }); + + const result = await service.restart('abc123'); + + expect(result).toMatchObject({ id: 'abc123', state: ContainerState.EXITED }); + expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.INFO, expect.anything()); + }); + + it('tolerates inspect failures during polling', async () => { + mockListContainers.mockResolvedValue([{ ...containerInfo, State: 'running' }]); + mockContainer.restart.mockResolvedValue(undefined); + mockContainer.inspect + .mockRejectedValueOnce(new Error('temporary failure')) + .mockResolvedValueOnce({ State: { Status: 'running' } }); + + const result = await service.restart('abc123'); + + expect(result).toMatchObject({ id: 'abc123', state: ContainerState.RUNNING }); + }); + }); }); diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts index 1ea534d9de..d699c2a108 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts @@ -199,14 +199,7 @@ export class DockerService { public async start(id: string): Promise { const container = this.client.getContainer(id); await container.start(); - const containers = await this.getContainers(); - const updatedContainer = containers.find((c) => c.id === id); - if (!updatedContainer) { - throw new Error(`Container ${id} not found after starting`); - } - const appInfo = await this.getAppInfo(); - await pubsub.publish(PUBSUB_CHANNEL.INFO, appInfo); - return updatedContainer; + return this.finalizeMutation(id, 'starting'); } public async removeContainer(id: string, options?: { withImage?: boolean }): Promise { @@ -248,80 +241,95 @@ export class DockerService { public async stop(id: string): Promise { const container = this.client.getContainer(id); await container.stop({ t: 10 }); - - let containers = await this.getContainers(); - let updatedContainer: DockerContainer | undefined; - for (let i = 0; i < 5; i++) { - await sleep(500); - containers = await this.getContainers(); - updatedContainer = containers.find((c) => c.id === id); - this.logger.debug( - `Container ${id} state after stop attempt ${i + 1}: ${updatedContainer?.state}` - ); - if (updatedContainer?.state === ContainerState.EXITED) { - break; - } - } - - if (!updatedContainer) { - throw new Error(`Container ${id} not found after stopping`); - } else if (updatedContainer.state !== ContainerState.EXITED) { + const finalState = await this.waitForContainerState( + container, + id, + ContainerState.EXITED, + 'stop', + 5 + ); + if (finalState && finalState !== ContainerState.EXITED) { this.logger.warn(`Container ${id} did not reach EXITED state after stop command.`); } - const appInfo = await this.getAppInfo(); - await pubsub.publish(PUBSUB_CHANNEL.INFO, appInfo); - return updatedContainer; + return this.finalizeMutation(id, 'stopping'); } public async pause(id: string): Promise { const container = this.client.getContainer(id); await container.pause(); + await this.waitForContainerState(container, id, ContainerState.PAUSED, 'pause', 5); + return this.finalizeMutation(id, 'pausing'); + } - let containers: DockerContainer[]; - let updatedContainer: DockerContainer | undefined; - for (let i = 0; i < 5; i++) { - await sleep(500); - containers = await this.getContainers(); - updatedContainer = containers.find((c) => c.id === id); - this.logger.debug( - `Container ${id} state after pause attempt ${i + 1}: ${updatedContainer?.state}` - ); - if (updatedContainer?.state === ContainerState.PAUSED) { - break; - } - } - - if (!updatedContainer) { - throw new Error(`Container ${id} not found after pausing`); + public async restart(id: string): Promise { + const container = this.client.getContainer(id); + await container.restart({ t: 10 }); + const finalState = await this.waitForContainerState( + container, + id, + ContainerState.RUNNING, + 'restart', + 20 + ); + if (finalState && finalState !== ContainerState.RUNNING) { + this.logger.warn(`Container ${id} did not reach RUNNING state after restart command.`); } - const appInfo = await this.getAppInfo(); - await pubsub.publish(PUBSUB_CHANNEL.INFO, appInfo); - return updatedContainer; + return this.finalizeMutation(id, 'restarting'); } public async unpause(id: string): Promise { const container = this.client.getContainer(id); await container.unpause(); + await this.waitForContainerState(container, id, ContainerState.RUNNING, 'unpause', 5); + return this.finalizeMutation(id, 'unpausing'); + } - let containers: DockerContainer[]; - let updatedContainer: DockerContainer | undefined; - for (let i = 0; i < 5; i++) { + /** Polls a container via `inspect()` until it reaches `targetState` or `attempts` polls have been made */ + private async waitForContainerState( + container: Docker.Container, + id: string, + targetState: ContainerState, + operationName: string, + attempts: number + ): Promise { + let lastState: ContainerState | undefined; + for (let i = 0; i < attempts; i++) { await sleep(500); - containers = await this.getContainers(); - updatedContainer = containers.find((c) => c.id === id); + try { + const info = await container.inspect(); + const statusStr = info.State?.Status ?? ''; + lastState = + ContainerState[statusStr.toUpperCase() as keyof typeof ContainerState] ?? + ContainerState.EXITED; + } catch (error) { + this.logger.debug( + `Inspect failed during ${operationName} attempt ${i + 1} for ${id}: ${this.getDockerErrorMessage(error)}` + ); + } this.logger.debug( - `Container ${id} state after unpause attempt ${i + 1}: ${updatedContainer?.state}` + `Container ${id} state after ${operationName} attempt ${i + 1}: ${lastState}` ); - if (updatedContainer?.state === ContainerState.RUNNING) { - break; + if (lastState === targetState) { + return lastState; } } + return lastState; + } + private async finalizeMutation(id: string, operationName: string): Promise { + const containers = await this.getContainers(); + const updatedContainer = containers.find((c) => c.id === id); if (!updatedContainer) { - throw new Error(`Container ${id} not found after unpausing`); + throw new Error(`Container ${id} not found after ${operationName}`); } - const appInfo = await this.getAppInfo(); - await pubsub.publish(PUBSUB_CHANNEL.INFO, appInfo); + await pubsub.publish(PUBSUB_CHANNEL.INFO, { + info: { + apps: { + installed: containers.length, + running: containers.filter((c) => c.state === ContainerState.RUNNING).length, + }, + }, + }); return updatedContainer; } diff --git a/web/src/composables/gql/graphql.ts b/web/src/composables/gql/graphql.ts index 91e8636a79..96384bef26 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -366,6 +366,10 @@ export enum ArrayState { } export type ArrayStateInput = { + /** Optional keyfile contents used to unlock encrypted array disks when starting the array. Accepts a data URL or raw base64 payload. */ + decryptionKeyfile?: InputMaybe; + /** Optional password used to unlock encrypted array disks when starting the array */ + decryptionPassword?: InputMaybe; /** Array state */ desiredState: ArrayStateInputState; }; @@ -948,6 +952,8 @@ export type DockerMutations = { pause: DockerContainer; /** Remove a container */ removeContainer: Scalars['Boolean']['output']; + /** Restart a container */ + restart: DockerContainer; /** Start a container */ start: DockerContainer; /** Stop a container */ @@ -976,6 +982,11 @@ export type DockerMutationsRemoveContainerArgs = { }; +export type DockerMutationsRestartArgs = { + id: Scalars['PrefixedID']['input']; +}; + + export type DockerMutationsStartArgs = { id: Scalars['PrefixedID']['input']; }; @@ -1311,31 +1322,69 @@ export type InfoNetworkInterface = Node & { __typename?: 'InfoNetworkInterface'; /** Interface description/label */ description?: Maybe; + /** Link duplex mode */ + duplex?: Maybe; /** IPv4 Gateway */ gateway?: Maybe; id: Scalars['PrefixedID']['output']; + /** Whether this is an internal interface */ + internal?: Maybe; /** IPv4 Address */ ipAddress?: Maybe; + /** IPv4 addresses assigned to this interface */ + ipv4Addresses: Array; /** IPv6 Address */ ipv6Address?: Maybe; + /** IPv6 addresses assigned to this interface */ + ipv6Addresses: Array; /** IPv6 Gateway */ ipv6Gateway?: Maybe; /** IPv6 Netmask */ ipv6Netmask?: Maybe; /** MAC Address */ macAddress?: Maybe; + /** Maximum transmission unit */ + mtu?: Maybe; /** Interface name (e.g. eth0) */ name: Scalars['String']['output']; /** IPv4 Netmask */ netmask?: Maybe; + /** Operational state */ + operstate?: Maybe; /** IPv4 Protocol mode */ protocol?: Maybe; + /** Link speed in Mbps */ + speed?: Maybe; /** Connection status */ status?: Maybe; + /** Interface type */ + type?: Maybe; /** Using DHCP for IPv4 */ useDhcp?: Maybe; /** Using DHCP for IPv6 */ useDhcp6?: Maybe; + /** Whether this is a virtual interface */ + virtual?: Maybe; + /** VLAN identifier parsed from the interface name */ + vlanId?: Maybe; +}; + +/** IPv4 address assigned to a network interface */ +export type InfoNetworkIpv4Address = { + __typename?: 'InfoNetworkIpv4Address'; + /** IPv4 address */ + address: Scalars['String']['output']; + /** IPv4 netmask */ + netmask: Scalars['String']['output']; +}; + +/** IPv6 address assigned to a network interface */ +export type InfoNetworkIpv6Address = { + __typename?: 'InfoNetworkIpv6Address'; + /** IPv6 address */ + address: Scalars['String']['output']; + /** IPv6 prefix length */ + prefixLength?: Maybe; }; export type InfoOs = Node & { @@ -1576,6 +1625,8 @@ export type Metrics = Node & { id: Scalars['PrefixedID']['output']; /** Current memory utilization metrics */ memory?: Maybe; + /** Current network metrics for all interfaces */ + network: Array; /** Temperature metrics */ temperature?: Maybe; }; @@ -1827,6 +1878,39 @@ export type Network = Node & { id: Scalars['PrefixedID']['output']; }; +export type NetworkMetrics = Node & { + __typename?: 'NetworkMetrics'; + /** Total received bytes */ + bytesReceived: Scalars['BigInt']['output']; + /** Total transmitted bytes */ + bytesSent: Scalars['BigInt']['output']; + id: Scalars['PrefixedID']['output']; + /** Metric collection timestamp */ + lastUpdated: Scalars['DateTime']['output']; + /** Interface identifier */ + name: Scalars['String']['output']; + /** Operational state */ + operstate?: Maybe; + /** Total received packets */ + packetsReceived: Scalars['BigInt']['output']; + /** Total transmitted packets */ + packetsSent: Scalars['BigInt']['output']; + /** Dropped receive packets */ + receiveDropped: Scalars['BigInt']['output']; + /** Receive errors */ + receiveErrors: Scalars['BigInt']['output']; + /** Receive throughput in bytes per second */ + rxSec: Scalars['Float']['output']; + /** Dropped transmit packets */ + transmitDropped: Scalars['BigInt']['output']; + /** Transmit errors */ + transmitErrors: Scalars['BigInt']['output']; + /** Transmit throughput in bytes per second */ + txSec: Scalars['Float']['output']; + /** Estimated link utilization percentage */ + utilizationPercent?: Maybe; +}; + export type Node = { id: Scalars['PrefixedID']['output']; }; @@ -2327,6 +2411,8 @@ export type Query = { me: UserAccount; metrics: Metrics; network: Network; + /** Network interfaces */ + networkInterfaces: Array; /** Get all notifications */ notifications: Notifications; /** Get the full OIDC configuration (admin only) */ @@ -2736,6 +2822,7 @@ export type Subscription = { systemMetricsCpu: CpuUtilization; systemMetricsCpuTelemetry: CpuPackages; systemMetricsMemory: MemoryUtilization; + systemMetricsNetwork: Array; systemMetricsTemperature?: Maybe; upsUpdates: UpsDevice; };