From 6c40bb133be57366f51c3826c57b7c36604c36c6 Mon Sep 17 00:00:00 2001 From: kithwa Date: Fri, 31 Oct 2025 17:07:24 +0800 Subject: [PATCH 01/15] feat: mps API for AMT linkPreference --- src/amt/DeviceAction.ts | 20 ++++++++ src/routes/amt/index.ts | 6 +++ src/routes/amt/linkPreference.ts | 44 ++++++++++++++++ src/routes/amt/linkPreferenceValidator.ts | 11 ++++ swagger.yaml | 62 +++++++++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 src/routes/amt/linkPreference.ts create mode 100644 src/routes/amt/linkPreferenceValidator.ts diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index bbfcff5ae..61c33a104 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -660,4 +660,24 @@ export class DeviceAction { logger.silly(`putKVMRedirectionSettingData ${messages.COMPLETE}`) return result.Envelope.Body } + + async setEthernetLinkPreference( + linkPreference: AMT.Types.EthernetPortSettings.LinkPreference, + timeoutSeconds: number + ): Promise> { + logger.silly(`setEthernetLinkPreference ${messages.REQUEST}`) + const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds) + const result = await this.ciraHandler.Get(this.ciraSocket, xmlRequestBody) + logger.silly(`setEthernetLinkPreference ${messages.COMPLETE}`) + return result.Envelope + } + + async setLinkPreferenceME(timeoutSeconds: number): Promise> { + return await this.setEthernetLinkPreference(1, timeoutSeconds) + } + + async cancelLinkPreference(): Promise> { + // Set preference back to HOST; timeout 0 implies immediate reversion semantics + return await this.setEthernetLinkPreference(2, 0) + } } diff --git a/src/routes/amt/index.ts b/src/routes/amt/index.ts index 3fb6e78bf..99eff0a1b 100644 --- a/src/routes/amt/index.ts +++ b/src/routes/amt/index.ts @@ -38,6 +38,8 @@ import { validator } from './kvm/validator.js' import { get } from 'http' import { getScreenSettingData } from './kvm/get.js' import { setKVMRedirectionSettingData } from './kvm/set.js' +import { setLinkPreference, cancelLinkPreference } from './linkPreference.js' +import { linkPreferenceValidator } from './linkPreferenceValidator.js' const amtRouter: Router = Router() @@ -68,4 +70,8 @@ amtRouter.post('/certificates/:guid', certValidator(), validateMiddleware, ciraM amtRouter.get('/kvm/displays/:guid', ciraMiddleware, getScreenSettingData) amtRouter.put('/kvm/displays/:guid', validator(), ciraMiddleware, setKVMRedirectionSettingData) +// Link Preference (ME/HOST) +amtRouter.post('/network/linkPreference/:guid', linkPreferenceValidator(), validateMiddleware, ciraMiddleware, setLinkPreference) +amtRouter.get('/network/linkPreference/cancel/:guid', ciraMiddleware, cancelLinkPreference) + export default amtRouter diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts new file mode 100644 index 000000000..052fa038d --- /dev/null +++ b/src/routes/amt/linkPreference.ts @@ -0,0 +1,44 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { type Response, type Request } from 'express' +import { logger, messages } from '../../logging/index.js' +import { ErrorResponse } from '../../utils/amtHelper.js' +import { MqttProvider } from '../../utils/MqttProvider.js' +import { type DeviceAction } from '../../amt/DeviceAction.js' + +export async function setLinkPreference(req: Request, res: Response): Promise { + try { + const guid: string = req.params.guid + const timeout: number = Number(req.body.timeout) + const deviceAction: DeviceAction = req.deviceAction as DeviceAction + + logger.debug(`Set Link Preference to ME for ${guid} with timeout ${timeout}s`) + await deviceAction.setLinkPreferenceME(timeout) + MqttProvider.publishEvent('success', ['AMT_LinkPreference'], 'Link Preference set to ME') + res.status(200).json({ status: 'Link Preference set to ME', timeout }).end() + } catch (error) { + logger.error(`Exception during Set Link Preference: ${error}`) + MqttProvider.publishEvent('fail', ['AMT_LinkPreference'], messages.INTERNAL_SERVICE_ERROR) + res.status(500).json(ErrorResponse(500, 'Exception during Set Link Preference')).end() + } +} + +export async function cancelLinkPreference(req: Request, res: Response): Promise { + try { + const guid: string = req.params.guid + const deviceAction: DeviceAction = req.deviceAction as DeviceAction + + logger.debug(`Cancel Link Preference; revert to HOST for ${guid}`) + await deviceAction.cancelLinkPreference() + MqttProvider.publishEvent('success', ['AMT_LinkPreference'], 'Link Preference reverted to HOST') + res.status(200).json({ status: 'Link Preference reverted to HOST' }).end() + } catch (error) { + logger.error(`Exception during Cancel Link Preference: ${error}`) + MqttProvider.publishEvent('fail', ['AMT_LinkPreference'], messages.INTERNAL_SERVICE_ERROR) + res.status(500).json(ErrorResponse(500, 'Exception during Cancel Link Preference')).end() + } +} + diff --git a/src/routes/amt/linkPreferenceValidator.ts b/src/routes/amt/linkPreferenceValidator.ts new file mode 100644 index 000000000..9cf4ab635 --- /dev/null +++ b/src/routes/amt/linkPreferenceValidator.ts @@ -0,0 +1,11 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { check } from 'express-validator' + +export const linkPreferenceValidator = (): any => [ + check('timeout').isInt({ min: 0 }) +] + diff --git a/swagger.yaml b/swagger.yaml index 788de350b..95a829b0a 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -739,6 +739,68 @@ paths: $ref: '#/components/schemas/DisconnectErrorResponse' 500: description: 'Internal server error' + /api/v1/amt/network/linkPreference/{guid}: + post: + summary: Set link preference to ME with timeout + description: Set Ethernet Link Preference to ME for a duration. + tags: + - AMT + parameters: + - name: guid + in: path + description: GUID of device + example: 123e4567-e89b-12d3-a456-426614174000 + required: true + schema: + type: string + requestBody: + description: Timeout setting in seconds + required: true + content: + application/json: + schema: + type: object + properties: + timeout: + type: integer + description: Timeout in seconds + example: 300 + responses: + 200: + description: 'Link preference set' + 404: + description: 'Device not found' + content: + application/json: + schema: + $ref: '#/components/schemas/DisconnectErrorResponse' + 500: + description: 'Internal server error' + /api/v1/amt/network/linkPreference/cancel/{guid}: + get: + summary: Cancel link preference and revert to HOST + description: Cancels temporary link preference and sets preference back to HOST. + tags: + - AMT + parameters: + - name: guid + in: path + description: GUID of device + example: 123e4567-e89b-12d3-a456-426614174000 + required: true + schema: + type: string + responses: + 200: + description: 'Link preference reverted' + 404: + description: 'Device not found' + content: + application/json: + schema: + $ref: '#/components/schemas/DisconnectErrorResponse' + 500: + description: 'Internal server error' put: summary: Put the changed settings for KVM in AMT description: Modify screen settings for KVM in AMT device From c111180a47307e1ac20b04ace3bf5fdc566417f5 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Thu, 13 Nov 2025 21:52:22 +0800 Subject: [PATCH 02/15] fix: add instanceID as API input param --- src/amt/DeviceAction.ts | 13 +++++++------ src/routes/amt/linkPreference.ts | 14 ++++++++------ src/routes/amt/linkPreferenceValidator.ts | 3 ++- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index 61c33a104..b89b30b80 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -663,21 +663,22 @@ export class DeviceAction { async setEthernetLinkPreference( linkPreference: AMT.Types.EthernetPortSettings.LinkPreference, - timeoutSeconds: number + timeoutSeconds: number, + instanceID: string = 'Intel(r) AMT Ethernet Port Settings 0' ): Promise> { logger.silly(`setEthernetLinkPreference ${messages.REQUEST}`) - const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds) + const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds, instanceID) const result = await this.ciraHandler.Get(this.ciraSocket, xmlRequestBody) logger.silly(`setEthernetLinkPreference ${messages.COMPLETE}`) return result.Envelope } - async setLinkPreferenceME(timeoutSeconds: number): Promise> { - return await this.setEthernetLinkPreference(1, timeoutSeconds) + async setLinkPreferenceME(timeoutSeconds: number, instanceID?: string): Promise> { + return await this.setEthernetLinkPreference(1, timeoutSeconds, instanceID) } - async cancelLinkPreference(): Promise> { + async cancelLinkPreference(instanceID?: string): Promise> { // Set preference back to HOST; timeout 0 implies immediate reversion semantics - return await this.setEthernetLinkPreference(2, 0) + return await this.setEthernetLinkPreference(2, 0, instanceID) } } diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts index 052fa038d..f5fb55970 100644 --- a/src/routes/amt/linkPreference.ts +++ b/src/routes/amt/linkPreference.ts @@ -13,12 +13,13 @@ export async function setLinkPreference(req: Request, res: Response): Promise { try { const guid: string = req.params.guid + const instanceID: string | undefined = req.query.instanceID as string const deviceAction: DeviceAction = req.deviceAction as DeviceAction - logger.debug(`Cancel Link Preference; revert to HOST for ${guid}`) - await deviceAction.cancelLinkPreference() + logger.debug(`Cancel Link Preference; revert to HOST for ${guid}, instanceID: ${instanceID ?? 'default'}`) + await deviceAction.cancelLinkPreference(instanceID) MqttProvider.publishEvent('success', ['AMT_LinkPreference'], 'Link Preference reverted to HOST') - res.status(200).json({ status: 'Link Preference reverted to HOST' }).end() + res.status(200).json({ status: 'Link Preference reverted to HOST', instanceID: instanceID ?? 'Intel(r) AMT Ethernet Port Settings 0' }).end() } catch (error) { logger.error(`Exception during Cancel Link Preference: ${error}`) MqttProvider.publishEvent('fail', ['AMT_LinkPreference'], messages.INTERNAL_SERVICE_ERROR) diff --git a/src/routes/amt/linkPreferenceValidator.ts b/src/routes/amt/linkPreferenceValidator.ts index 9cf4ab635..2c81b7d3b 100644 --- a/src/routes/amt/linkPreferenceValidator.ts +++ b/src/routes/amt/linkPreferenceValidator.ts @@ -6,6 +6,7 @@ import { check } from 'express-validator' export const linkPreferenceValidator = (): any => [ - check('timeout').isInt({ min: 0 }) + check('timeout').isInt({ min: 0 }), + check('instanceID').optional().isString() ] From a94ec962fdaded88829cb4f590cf4e0e92cc4ed8 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 08:59:17 +0800 Subject: [PATCH 03/15] feat: add Get() MPS API for EthernetPortSettings --- src/amt/DeviceAction.ts | 11 ++++ src/routes/amt/getEthernetPortSettings.ts | 27 ++++++++++ src/routes/amt/index.ts | 4 ++ swagger.yaml | 66 +++++++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 src/routes/amt/getEthernetPortSettings.ts diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index b89b30b80..b888d15fb 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -661,6 +661,17 @@ export class DeviceAction { return result.Envelope.Body } + async getEthernetPortSettings( + instanceID: string = 'Intel(r) AMT Ethernet Port Settings 0' + ): Promise { + logger.silly(`getEthernetPortSettings ${messages.REQUEST}`) + const selector = { name: 'InstanceID', value: instanceID } + const xmlRequestBody = this.amt.EthernetPortSettings.Get(selector) + const result = await this.ciraHandler.Get(this.ciraSocket, xmlRequestBody) + logger.silly(`getEthernetPortSettings ${messages.COMPLETE}`) + return result.Envelope.Body + } + async setEthernetLinkPreference( linkPreference: AMT.Types.EthernetPortSettings.LinkPreference, timeoutSeconds: number, diff --git a/src/routes/amt/getEthernetPortSettings.ts b/src/routes/amt/getEthernetPortSettings.ts new file mode 100644 index 000000000..25c2fa8ac --- /dev/null +++ b/src/routes/amt/getEthernetPortSettings.ts @@ -0,0 +1,27 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { type Response, type Request } from 'express' +import { logger, messages } from '../../logging/index.js' +import { ErrorResponse } from '../../utils/amtHelper.js' +import { MqttProvider } from '../../utils/MqttProvider.js' +import { type DeviceAction } from '../../amt/DeviceAction.js' + +export async function getEthernetPortSettings(req: Request, res: Response): Promise { + try { + const guid: string = req.params.guid + const instanceID: string | undefined = req.query.instanceID as string + const deviceAction: DeviceAction = req.deviceAction as DeviceAction + + logger.debug(`Get Ethernet Port Settings for ${guid}, instanceID: ${instanceID ?? 'default'}`) + const result = await deviceAction.getEthernetPortSettings(instanceID) + MqttProvider.publishEvent('success', ['AMT_EthernetPortSettings'], 'Ethernet Port Settings retrieved') + res.status(200).json(result).end() + } catch (error) { + logger.error(`Exception during Get Ethernet Port Settings: ${error}`) + MqttProvider.publishEvent('fail', ['AMT_EthernetPortSettings'], messages.INTERNAL_SERVICE_ERROR) + res.status(500).json(ErrorResponse(500, 'Exception during Get Ethernet Port Settings')).end() + } +} diff --git a/src/routes/amt/index.ts b/src/routes/amt/index.ts index 99eff0a1b..d76988142 100644 --- a/src/routes/amt/index.ts +++ b/src/routes/amt/index.ts @@ -40,6 +40,7 @@ import { getScreenSettingData } from './kvm/get.js' import { setKVMRedirectionSettingData } from './kvm/set.js' import { setLinkPreference, cancelLinkPreference } from './linkPreference.js' import { linkPreferenceValidator } from './linkPreferenceValidator.js' +import { getEthernetPortSettings } from './getEthernetPortSettings.js' const amtRouter: Router = Router() @@ -70,6 +71,9 @@ amtRouter.post('/certificates/:guid', certValidator(), validateMiddleware, ciraM amtRouter.get('/kvm/displays/:guid', ciraMiddleware, getScreenSettingData) amtRouter.put('/kvm/displays/:guid', validator(), ciraMiddleware, setKVMRedirectionSettingData) +// Ethernet Port Settings +amtRouter.get('/network/ethernetPortSettings/:guid', ciraMiddleware, getEthernetPortSettings) + // Link Preference (ME/HOST) amtRouter.post('/network/linkPreference/:guid', linkPreferenceValidator(), validateMiddleware, ciraMiddleware, setLinkPreference) amtRouter.get('/network/linkPreference/cancel/:guid', ciraMiddleware, cancelLinkPreference) diff --git a/swagger.yaml b/swagger.yaml index 95a829b0a..657741d5a 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -739,6 +739,72 @@ paths: $ref: '#/components/schemas/DisconnectErrorResponse' 500: description: 'Internal server error' + /api/v1/amt/network/ethernetPortSettings/{guid}: + get: + summary: Get Ethernet Port Settings + description: Retrieve Ethernet Port Settings including link preference, link policy, and other network configurations. + tags: + - AMT + parameters: + - name: guid + in: path + description: GUID of device + example: 123e4567-e89b-12d3-a456-426614174000 + required: true + schema: + type: string + - name: instanceID + in: query + description: InstanceID of the Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 0") + required: false + schema: + type: string + default: Intel(r) AMT Ethernet Port Settings 0 + responses: + 200: + description: 'Ethernet Port Settings retrieved successfully' + content: + application/json: + schema: + type: object + properties: + InstanceID: + type: string + example: Intel(r) AMT Ethernet Port Settings 0 + ElementName: + type: string + example: Intel(r) AMT Ethernet Port Settings + LinkPreference: + type: integer + description: 1 = ME, 2 = HOST + example: 2 + LinkControl: + type: integer + description: 1 = ME, 2 = HOST + example: 2 + LinkPolicy: + type: array + items: + type: integer + description: Array of link policy values + example: [1, 14, 16] + LinkProtection: + type: integer + example: 1 + SharedMAC: + type: boolean + example: true + MACAddress: + type: string + example: "00:11:22:33:44:55" + 404: + description: 'Device not found' + content: + application/json: + schema: + $ref: '#/components/schemas/DisconnectErrorResponse' + 500: + description: 'Internal server error' /api/v1/amt/network/linkPreference/{guid}: post: summary: Set link preference to ME with timeout From 5b994205d17e3ef09aa5ae5b88d33946655ef658 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 09:12:04 +0800 Subject: [PATCH 04/15] fix: set instanceID as REST Query param instead of in the body --- src/routes/amt/linkPreference.ts | 2 +- src/routes/amt/linkPreferenceValidator.ts | 4 ++-- swagger.yaml | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts index f5fb55970..2c391b986 100644 --- a/src/routes/amt/linkPreference.ts +++ b/src/routes/amt/linkPreference.ts @@ -13,7 +13,7 @@ export async function setLinkPreference(req: Request, res: Response): Promise [ check('timeout').isInt({ min: 0 }), - check('instanceID').optional().isString() + query('instanceID').optional().isString() ] diff --git a/swagger.yaml b/swagger.yaml index 657741d5a..67ec2069b 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -819,6 +819,13 @@ paths: required: true schema: type: string + - name: instanceID + in: query + description: InstanceID of the Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 0") + required: false + schema: + type: string + default: Intel(r) AMT Ethernet Port Settings 0 requestBody: description: Timeout setting in seconds required: true @@ -831,6 +838,8 @@ paths: type: integer description: Timeout in seconds example: 300 + required: + - timeout responses: 200: description: 'Link preference set' @@ -856,6 +865,13 @@ paths: required: true schema: type: string + - name: instanceID + in: query + description: InstanceID of the Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 0") + required: false + schema: + type: string + default: Intel(r) AMT Ethernet Port Settings 0 responses: 200: description: 'Link preference reverted' From 8421b1727081a04b827007669c4fb3023138144c Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 10:06:45 +0800 Subject: [PATCH 05/15] feat: add Enumerate API for EthernetPortSettings --- src/amt/DeviceAction.ts | 16 +++++ .../amt/enumerateEthernetPortSettings.ts | 26 +++++++++ src/routes/amt/index.ts | 2 + swagger.yaml | 58 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 src/routes/amt/enumerateEthernetPortSettings.ts diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index b888d15fb..b90867625 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -672,6 +672,22 @@ export class DeviceAction { return result.Envelope.Body } + async enumerateEthernetPortSettings(): Promise< + Common.Models.Envelope> + > { + logger.silly(`enumerateEthernetPortSettings ${messages.REQUEST}`) + let xmlRequestBody = this.amt.EthernetPortSettings.Enumerate() + const enumResponse = await this.ciraHandler.Enumerate(this.ciraSocket, xmlRequestBody) + if (enumResponse == null) { + logger.error(`enumerateEthernetPortSettings failed. Reason: ${messages.ENUMERATION_RESPONSE_NULL}`) + return null + } + xmlRequestBody = this.amt.EthernetPortSettings.Pull(enumResponse.Envelope.Body.EnumerateResponse.EnumerationContext) + const pullResponse = await this.ciraHandler.Pull(this.ciraSocket, xmlRequestBody) + logger.silly(`enumerateEthernetPortSettings ${messages.COMPLETE}`) + return pullResponse.Envelope + } + async setEthernetLinkPreference( linkPreference: AMT.Types.EthernetPortSettings.LinkPreference, timeoutSeconds: number, diff --git a/src/routes/amt/enumerateEthernetPortSettings.ts b/src/routes/amt/enumerateEthernetPortSettings.ts new file mode 100644 index 000000000..bf957df24 --- /dev/null +++ b/src/routes/amt/enumerateEthernetPortSettings.ts @@ -0,0 +1,26 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { type Response, type Request } from 'express' +import { logger, messages } from '../../logging/index.js' +import { ErrorResponse } from '../../utils/amtHelper.js' +import { MqttProvider } from '../../utils/MqttProvider.js' +import { type DeviceAction } from '../../amt/DeviceAction.js' + +export async function enumerateEthernetPortSettings(req: Request, res: Response): Promise { + try { + const guid: string = req.params.guid + const deviceAction: DeviceAction = req.deviceAction as DeviceAction + + logger.debug(`Enumerate Ethernet Port Settings for ${guid}`) + const result = await deviceAction.enumerateEthernetPortSettings() + MqttProvider.publishEvent('success', ['AMT_EthernetPortSettings'], 'Ethernet Port Settings enumerated') + res.status(200).json(result).end() + } catch (error) { + logger.error(`Exception during Enumerate Ethernet Port Settings: ${error}`) + MqttProvider.publishEvent('fail', ['AMT_EthernetPortSettings'], messages.INTERNAL_SERVICE_ERROR) + res.status(500).json(ErrorResponse(500, 'Exception during Enumerate Ethernet Port Settings')).end() + } +} diff --git a/src/routes/amt/index.ts b/src/routes/amt/index.ts index d76988142..dc446596c 100644 --- a/src/routes/amt/index.ts +++ b/src/routes/amt/index.ts @@ -41,6 +41,7 @@ import { setKVMRedirectionSettingData } from './kvm/set.js' import { setLinkPreference, cancelLinkPreference } from './linkPreference.js' import { linkPreferenceValidator } from './linkPreferenceValidator.js' import { getEthernetPortSettings } from './getEthernetPortSettings.js' +import { enumerateEthernetPortSettings } from './enumerateEthernetPortSettings.js' const amtRouter: Router = Router() @@ -73,6 +74,7 @@ amtRouter.put('/kvm/displays/:guid', validator(), ciraMiddleware, setKVMRedirect // Ethernet Port Settings amtRouter.get('/network/ethernetPortSettings/:guid', ciraMiddleware, getEthernetPortSettings) +amtRouter.get('/network/ethernetPortSettings/enumerate/:guid', ciraMiddleware, enumerateEthernetPortSettings) // Link Preference (ME/HOST) amtRouter.post('/network/linkPreference/:guid', linkPreferenceValidator(), validateMiddleware, ciraMiddleware, setLinkPreference) diff --git a/swagger.yaml b/swagger.yaml index 67ec2069b..da58b0d63 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -805,6 +805,64 @@ paths: $ref: '#/components/schemas/DisconnectErrorResponse' 500: description: 'Internal server error' + /api/v1/amt/network/ethernetPortSettings/enumerate/{guid}: + get: + summary: Enumerate all Ethernet Port Settings + description: Retrieve all Ethernet Port Settings instances (all network ports). + tags: + - AMT + parameters: + - name: guid + in: path + description: GUID of device + example: 123e4567-e89b-12d3-a456-426614174000 + required: true + schema: + type: string + responses: + 200: + description: 'All Ethernet Port Settings retrieved successfully' + content: + application/json: + schema: + type: object + properties: + Body: + type: object + properties: + PullResponse: + type: object + properties: + Items: + type: array + items: + type: object + properties: + InstanceID: + type: string + example: Intel(r) AMT Ethernet Port Settings 0 + ElementName: + type: string + example: Intel(r) AMT Ethernet Port Settings + LinkPreference: + type: integer + description: 1 = ME, 2 = HOST + example: 2 + LinkControl: + type: integer + description: 1 = ME, 2 = HOST + example: 2 + MACAddress: + type: string + example: "00:11:22:33:44:55" + 404: + description: 'Device not found' + content: + application/json: + schema: + $ref: '#/components/schemas/DisconnectErrorResponse' + 500: + description: 'Internal server error' /api/v1/amt/network/linkPreference/{guid}: post: summary: Set link preference to ME with timeout From b8c8e44c2ca995f18f9dbf177b928097d78aced7 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 10:38:33 +0800 Subject: [PATCH 06/15] fix: remove cancelLinkPreferenc API in MPS --- src/routes/amt/index.ts | 3 +- src/routes/amt/linkPreference.ts | 33 ++++-------- src/routes/amt/linkPreferenceValidator.ts | 1 + swagger.yaml | 62 ++++++++++------------- 4 files changed, 41 insertions(+), 58 deletions(-) diff --git a/src/routes/amt/index.ts b/src/routes/amt/index.ts index dc446596c..f80cc46c7 100644 --- a/src/routes/amt/index.ts +++ b/src/routes/amt/index.ts @@ -38,7 +38,7 @@ import { validator } from './kvm/validator.js' import { get } from 'http' import { getScreenSettingData } from './kvm/get.js' import { setKVMRedirectionSettingData } from './kvm/set.js' -import { setLinkPreference, cancelLinkPreference } from './linkPreference.js' +import { setLinkPreference } from './linkPreference.js' import { linkPreferenceValidator } from './linkPreferenceValidator.js' import { getEthernetPortSettings } from './getEthernetPortSettings.js' import { enumerateEthernetPortSettings } from './enumerateEthernetPortSettings.js' @@ -78,6 +78,5 @@ amtRouter.get('/network/ethernetPortSettings/enumerate/:guid', ciraMiddleware, e // Link Preference (ME/HOST) amtRouter.post('/network/linkPreference/:guid', linkPreferenceValidator(), validateMiddleware, ciraMiddleware, setLinkPreference) -amtRouter.get('/network/linkPreference/cancel/:guid', ciraMiddleware, cancelLinkPreference) export default amtRouter diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts index 2c391b986..06bf5df6d 100644 --- a/src/routes/amt/linkPreference.ts +++ b/src/routes/amt/linkPreference.ts @@ -12,35 +12,24 @@ import { type DeviceAction } from '../../amt/DeviceAction.js' export async function setLinkPreference(req: Request, res: Response): Promise { try { const guid: string = req.params.guid + const linkPreference: number = Number(req.body.linkPreference) const timeout: number = Number(req.body.timeout) const instanceID: string | undefined = req.query.instanceID as string const deviceAction: DeviceAction = req.deviceAction as DeviceAction - logger.debug(`Set Link Preference to ME for ${guid} with timeout ${timeout}s, instanceID: ${instanceID ?? 'default'}`) - await deviceAction.setLinkPreferenceME(timeout, instanceID) - MqttProvider.publishEvent('success', ['AMT_LinkPreference'], 'Link Preference set to ME') - res.status(200).json({ status: 'Link Preference set to ME', timeout, instanceID: instanceID ?? 'Intel(r) AMT Ethernet Port Settings 0' }).end() + const linkPrefName = linkPreference === 1 ? 'ME' : 'HOST' + logger.debug(`Set Link Preference to ${linkPrefName} for ${guid} with timeout ${timeout}s, instanceID: ${instanceID ?? 'default'}`) + await deviceAction.setEthernetLinkPreference(linkPreference as 1 | 2, timeout, instanceID) + MqttProvider.publishEvent('success', ['AMT_LinkPreference'], `Link Preference set to ${linkPrefName}`) + res.status(200).json({ + status: `Link Preference set to ${linkPrefName}`, + linkPreference, + timeout, + instanceID: instanceID ?? 'Intel(r) AMT Ethernet Port Settings 0' + }).end() } catch (error) { logger.error(`Exception during Set Link Preference: ${error}`) MqttProvider.publishEvent('fail', ['AMT_LinkPreference'], messages.INTERNAL_SERVICE_ERROR) res.status(500).json(ErrorResponse(500, 'Exception during Set Link Preference')).end() } } - -export async function cancelLinkPreference(req: Request, res: Response): Promise { - try { - const guid: string = req.params.guid - const instanceID: string | undefined = req.query.instanceID as string - const deviceAction: DeviceAction = req.deviceAction as DeviceAction - - logger.debug(`Cancel Link Preference; revert to HOST for ${guid}, instanceID: ${instanceID ?? 'default'}`) - await deviceAction.cancelLinkPreference(instanceID) - MqttProvider.publishEvent('success', ['AMT_LinkPreference'], 'Link Preference reverted to HOST') - res.status(200).json({ status: 'Link Preference reverted to HOST', instanceID: instanceID ?? 'Intel(r) AMT Ethernet Port Settings 0' }).end() - } catch (error) { - logger.error(`Exception during Cancel Link Preference: ${error}`) - MqttProvider.publishEvent('fail', ['AMT_LinkPreference'], messages.INTERNAL_SERVICE_ERROR) - res.status(500).json(ErrorResponse(500, 'Exception during Cancel Link Preference')).end() - } -} - diff --git a/src/routes/amt/linkPreferenceValidator.ts b/src/routes/amt/linkPreferenceValidator.ts index 0390a84d5..83361a41c 100644 --- a/src/routes/amt/linkPreferenceValidator.ts +++ b/src/routes/amt/linkPreferenceValidator.ts @@ -6,6 +6,7 @@ import { check, query } from 'express-validator' export const linkPreferenceValidator = (): any => [ + check('linkPreference').isInt({ min: 1, max: 2 }).withMessage('linkPreference must be 1 (ME) or 2 (HOST)'), check('timeout').isInt({ min: 0 }), query('instanceID').optional().isString() ] diff --git a/swagger.yaml b/swagger.yaml index da58b0d63..38db809e3 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -865,8 +865,8 @@ paths: description: 'Internal server error' /api/v1/amt/network/linkPreference/{guid}: post: - summary: Set link preference to ME with timeout - description: Set Ethernet Link Preference to ME for a duration. + summary: Set link preference with timeout + description: Set Ethernet Link Preference to ME (1) or HOST (2) for a specified duration. tags: - AMT parameters: @@ -885,54 +885,47 @@ paths: type: string default: Intel(r) AMT Ethernet Port Settings 0 requestBody: - description: Timeout setting in seconds + description: Link preference and timeout settings required: true content: application/json: schema: type: object properties: + linkPreference: + type: integer + description: Link preference - 1 = ME, 2 = HOST + enum: [1, 2] + example: 1 timeout: type: integer - description: Timeout in seconds + description: Timeout in seconds (0 for immediate change without timeout) example: 300 required: + - linkPreference - timeout responses: 200: - description: 'Link preference set' - 404: - description: 'Device not found' + description: 'Link preference set successfully' content: application/json: schema: - $ref: '#/components/schemas/DisconnectErrorResponse' - 500: - description: 'Internal server error' - /api/v1/amt/network/linkPreference/cancel/{guid}: - get: - summary: Cancel link preference and revert to HOST - description: Cancels temporary link preference and sets preference back to HOST. - tags: - - AMT - parameters: - - name: guid - in: path - description: GUID of device - example: 123e4567-e89b-12d3-a456-426614174000 - required: true - schema: - type: string - - name: instanceID - in: query - description: InstanceID of the Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 0") - required: false - schema: - type: string - default: Intel(r) AMT Ethernet Port Settings 0 - responses: - 200: - description: 'Link preference reverted' + type: object + properties: + status: + type: string + example: Link Preference set to ME + linkPreference: + type: integer + example: 1 + timeout: + type: integer + example: 300 + instanceID: + type: string + example: Intel(r) AMT Ethernet Port Settings 0 + 400: + description: 'Invalid parameters' 404: description: 'Device not found' content: @@ -941,6 +934,7 @@ paths: $ref: '#/components/schemas/DisconnectErrorResponse' 500: description: 'Internal server error' + /api/v1/amt/screen/{guid}: put: summary: Put the changed settings for KVM in AMT description: Modify screen settings for KVM in AMT device From 08f467f94919c7b24f2139600c37a2e133863ced Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 11:10:35 +0800 Subject: [PATCH 07/15] fix: timeout by default 0 if LinkPreference Host --- src/amt/DeviceAction.ts | 4 +++- src/routes/amt/linkPreference.ts | 2 ++ src/routes/amt/linkPreferenceValidator.ts | 4 +++- swagger.yaml | 8 ++++++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index b90867625..f62c4f6f8 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -694,6 +694,8 @@ export class DeviceAction { instanceID: string = 'Intel(r) AMT Ethernet Port Settings 0' ): Promise> { logger.silly(`setEthernetLinkPreference ${messages.REQUEST}`) + // Note: timeout is only applicable when linkPreference is ME (1) + // When linkPreference is HOST (2), timeout is automatically set to 0 in wsman-messages const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds, instanceID) const result = await this.ciraHandler.Get(this.ciraSocket, xmlRequestBody) logger.silly(`setEthernetLinkPreference ${messages.COMPLETE}`) @@ -705,7 +707,7 @@ export class DeviceAction { } async cancelLinkPreference(instanceID?: string): Promise> { - // Set preference back to HOST; timeout 0 implies immediate reversion semantics + // Set preference back to HOST (timeout is ignored for HOST preference) return await this.setEthernetLinkPreference(2, 0, instanceID) } } diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts index 06bf5df6d..a7459afda 100644 --- a/src/routes/amt/linkPreference.ts +++ b/src/routes/amt/linkPreference.ts @@ -18,6 +18,8 @@ export async function setLinkPreference(req: Request, res: Response): Promise [ check('linkPreference').isInt({ min: 1, max: 2 }).withMessage('linkPreference must be 1 (ME) or 2 (HOST)'), - check('timeout').isInt({ min: 0 }), + check('timeout').isInt({ min: 0 }).withMessage('timeout must be a non-negative integer'), query('instanceID').optional().isString() ] diff --git a/swagger.yaml b/swagger.yaml index 38db809e3..b781180d4 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -866,7 +866,11 @@ paths: /api/v1/amt/network/linkPreference/{guid}: post: summary: Set link preference with timeout - description: Set Ethernet Link Preference to ME (1) or HOST (2) for a specified duration. + description: | + Set Ethernet Link Preference to ME (1) or HOST (2) for a specified duration. + + **Important**: The timeout parameter is only applicable when linkPreference is set to ME (1). + When linkPreference is set to HOST (2), the timeout value is automatically ignored and set to 0 internally. tags: - AMT parameters: @@ -899,7 +903,7 @@ paths: example: 1 timeout: type: integer - description: Timeout in seconds (0 for immediate change without timeout) + description: Timeout in seconds. Only applicable when linkPreference=1 (ME). Automatically ignored when linkPreference=2 (HOST). example: 300 required: - linkPreference From 4dd4c97c4f899b03a11ab5a2cb424b2879bc7ba4 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 12:49:06 +0800 Subject: [PATCH 08/15] fix: revert auto-configure timeout to 0 --- src/amt/DeviceAction.ts | 4 +--- src/routes/amt/linkPreference.ts | 2 -- src/routes/amt/linkPreferenceValidator.ts | 2 -- swagger.yaml | 8 ++------ 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index f62c4f6f8..5d7237d9d 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -694,8 +694,6 @@ export class DeviceAction { instanceID: string = 'Intel(r) AMT Ethernet Port Settings 0' ): Promise> { logger.silly(`setEthernetLinkPreference ${messages.REQUEST}`) - // Note: timeout is only applicable when linkPreference is ME (1) - // When linkPreference is HOST (2), timeout is automatically set to 0 in wsman-messages const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds, instanceID) const result = await this.ciraHandler.Get(this.ciraSocket, xmlRequestBody) logger.silly(`setEthernetLinkPreference ${messages.COMPLETE}`) @@ -707,7 +705,7 @@ export class DeviceAction { } async cancelLinkPreference(instanceID?: string): Promise> { - // Set preference back to HOST (timeout is ignored for HOST preference) + // Set preference back to HOST with timeout 0 return await this.setEthernetLinkPreference(2, 0, instanceID) } } diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts index a7459afda..06bf5df6d 100644 --- a/src/routes/amt/linkPreference.ts +++ b/src/routes/amt/linkPreference.ts @@ -18,8 +18,6 @@ export async function setLinkPreference(req: Request, res: Response): Promise [ check('linkPreference').isInt({ min: 1, max: 2 }).withMessage('linkPreference must be 1 (ME) or 2 (HOST)'), check('timeout').isInt({ min: 0 }).withMessage('timeout must be a non-negative integer'), diff --git a/swagger.yaml b/swagger.yaml index b781180d4..25c65326a 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -866,11 +866,7 @@ paths: /api/v1/amt/network/linkPreference/{guid}: post: summary: Set link preference with timeout - description: | - Set Ethernet Link Preference to ME (1) or HOST (2) for a specified duration. - - **Important**: The timeout parameter is only applicable when linkPreference is set to ME (1). - When linkPreference is set to HOST (2), the timeout value is automatically ignored and set to 0 internally. + description: Set Ethernet Link Preference to ME (1) for a specified duration, or to HOST (2). tags: - AMT parameters: @@ -903,7 +899,7 @@ paths: example: 1 timeout: type: integer - description: Timeout in seconds. Only applicable when linkPreference=1 (ME). Automatically ignored when linkPreference=2 (HOST). + description: Timeout in seconds example: 300 required: - linkPreference From d220293aa9a4f1b36df7698e91ae28288bf8bc03 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 13:15:41 +0800 Subject: [PATCH 09/15] test: add Unit Tests for new network APIs --- .../amt/enumerateEthernetPortSettings.test.ts | 118 +++++++++++++++ .../amt/getEthernetPortSettings.test.ts | 96 ++++++++++++ src/routes/amt/linkPreference.test.ts | 140 ++++++++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 src/routes/amt/enumerateEthernetPortSettings.test.ts create mode 100644 src/routes/amt/getEthernetPortSettings.test.ts create mode 100644 src/routes/amt/linkPreference.test.ts diff --git a/src/routes/amt/enumerateEthernetPortSettings.test.ts b/src/routes/amt/enumerateEthernetPortSettings.test.ts new file mode 100644 index 000000000..fc2d909f6 --- /dev/null +++ b/src/routes/amt/enumerateEthernetPortSettings.test.ts @@ -0,0 +1,118 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { type SpyInstance, spyOn } from 'jest-mock' +import { CIRAHandler } from '../../amt/CIRAHandler.js' +import { DeviceAction } from '../../amt/DeviceAction.js' +import { HttpHandler } from '../../amt/HttpHandler.js' +import { messages } from '../../logging/index.js' +import { createSpyObj } from '../../test/helper/jest.js' +import { ErrorResponse } from '../../utils/amtHelper.js' +import { MqttProvider } from '../../utils/MqttProvider.js' +import { enumerateEthernetPortSettings } from './enumerateEthernetPortSettings.js' + +describe('Enumerate Ethernet Port Settings', () => { + let req: Express.Request + let resSpy + let mqttSpy: SpyInstance + let enumerateEthernetPortSettingsSpy: SpyInstance + let device: DeviceAction + let mockEnumerateResponse + + beforeEach(() => { + const handler = new CIRAHandler(new HttpHandler(), 'admin', 'P@ssw0rd') + device = new DeviceAction(handler, null) + + mockEnumerateResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: [ + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + ElementName: 'Intel(r) AMT Ethernet Port Settings', + MACAddress: 'a4-ae-11-1c-02-4d', + LinkIsUp: true, + LinkPolicy: [1, 14, 16], + LinkPreference: 2, + LinkControl: 2 + }, + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 1', + ElementName: 'Intel(r) AMT Ethernet Port Settings', + MACAddress: 'a4-ae-11-1c-02-4e', + LinkIsUp: false, + LinkPolicy: [1, 14], + LinkPreference: 1, + LinkControl: 1 + } + ] + } + } + } + } + + req = { + params: { + guid: '123456' + }, + deviceAction: device + } + resSpy = createSpyObj('Response', [ + 'status', + 'json', + 'end', + 'send' + ]) + resSpy.status.mockReturnThis() + resSpy.json.mockReturnThis() + resSpy.send.mockReturnThis() + mqttSpy = spyOn(MqttProvider, 'publishEvent') + enumerateEthernetPortSettingsSpy = spyOn(device, 'enumerateEthernetPortSettings').mockResolvedValue(mockEnumerateResponse as any) + }) + + it('should enumerate all ethernet port settings', async () => { + await enumerateEthernetPortSettings(req as any, resSpy) + + expect(enumerateEthernetPortSettingsSpy).toHaveBeenCalled() + expect(mqttSpy).toHaveBeenCalledWith('success', ['AMT_EthernetPortSettings'], 'Ethernet Port Settings enumerated') + expect(resSpy.status).toHaveBeenCalledWith(200) + expect(resSpy.json).toHaveBeenCalledWith(mockEnumerateResponse.Body.PullResponse.Items.AMT_EthernetPortSettings) + }) + + it('should handle single port in enumerate response', async () => { + const singlePortResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + ElementName: 'Intel(r) AMT Ethernet Port Settings', + MACAddress: 'a4-ae-11-1c-02-4d' + } + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(singlePortResponse as any) + + await enumerateEthernetPortSettings(req as any, resSpy) + + expect(resSpy.status).toHaveBeenCalledWith(200) + // Should wrap single object in array + expect(resSpy.json).toHaveBeenCalled() + }) + + it('should handle errors gracefully', async () => { + const errorMessage = 'Failed to enumerate ethernet port settings' + enumerateEthernetPortSettingsSpy.mockRejectedValue(new Error(errorMessage)) + + await enumerateEthernetPortSettings(req as any, resSpy) + + expect(mqttSpy).toHaveBeenCalledWith('fail', ['AMT_EthernetPortSettings'], messages.INTERNAL_SERVICE_ERROR) + expect(resSpy.status).toHaveBeenCalledWith(500) + expect(resSpy.json).toHaveBeenCalledWith(ErrorResponse(500, 'Exception during Enumerate Ethernet Port Settings')) + }) +}) diff --git a/src/routes/amt/getEthernetPortSettings.test.ts b/src/routes/amt/getEthernetPortSettings.test.ts new file mode 100644 index 000000000..dc3f8f436 --- /dev/null +++ b/src/routes/amt/getEthernetPortSettings.test.ts @@ -0,0 +1,96 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { type SpyInstance, spyOn } from 'jest-mock' +import { CIRAHandler } from '../../amt/CIRAHandler.js' +import { DeviceAction } from '../../amt/DeviceAction.js' +import { HttpHandler } from '../../amt/HttpHandler.js' +import { messages } from '../../logging/index.js' +import { createSpyObj } from '../../test/helper/jest.js' +import { ErrorResponse } from '../../utils/amtHelper.js' +import { MqttProvider } from '../../utils/MqttProvider.js' +import { getEthernetPortSettings } from './getEthernetPortSettings.js' + +describe('Get Ethernet Port Settings', () => { + let req: Express.Request + let resSpy + let mqttSpy: SpyInstance + let getEthernetPortSettingsSpy: SpyInstance + let device: DeviceAction + let mockEthernetPortSettings + + beforeEach(() => { + const handler = new CIRAHandler(new HttpHandler(), 'admin', 'P@ssw0rd') + device = new DeviceAction(handler, null) + + mockEthernetPortSettings = { + Body: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + ElementName: 'Intel(r) AMT Ethernet Port Settings', + MACAddress: 'a4-ae-11-1c-02-4d', + LinkIsUp: true, + LinkPolicy: [1, 14, 16], + LinkPreference: 2, + LinkControl: 2, + SharedMAC: true, + SharedStaticIp: false, + SharedDynamicIP: true, + IpSyncEnabled: true, + DHCPEnabled: true, + PhysicalConnectionType: 0 + } + } + } + + req = { + params: { + guid: '123456' + }, + query: {}, + deviceAction: device + } + resSpy = createSpyObj('Response', [ + 'status', + 'json', + 'end', + 'send' + ]) + resSpy.status.mockReturnThis() + resSpy.json.mockReturnThis() + resSpy.send.mockReturnThis() + mqttSpy = spyOn(MqttProvider, 'publishEvent') + getEthernetPortSettingsSpy = spyOn(device, 'getEthernetPortSettings').mockResolvedValue(mockEthernetPortSettings as any) + }) + + it('should get ethernet port settings with default instanceID', async () => { + await getEthernetPortSettings(req as any, resSpy) + + expect(getEthernetPortSettingsSpy).toHaveBeenCalledWith(undefined) + expect(mqttSpy).toHaveBeenCalledWith('success', ['AMT_EthernetPortSettings'], 'Ethernet Port Settings retrieved') + expect(resSpy.status).toHaveBeenCalledWith(200) + expect(resSpy.json).toHaveBeenCalledWith(mockEthernetPortSettings.Body.AMT_EthernetPortSettings) + }) + + it('should get ethernet port settings with custom instanceID', async () => { + req.query.instanceID = 'Intel(r) AMT Ethernet Port Settings 1' + + await getEthernetPortSettings(req as any, resSpy) + + expect(getEthernetPortSettingsSpy).toHaveBeenCalledWith('Intel(r) AMT Ethernet Port Settings 1') + expect(resSpy.status).toHaveBeenCalledWith(200) + }) + + it('should handle errors gracefully', async () => { + const errorMessage = 'Failed to get ethernet port settings' + getEthernetPortSettingsSpy.mockRejectedValue(new Error(errorMessage)) + + await getEthernetPortSettings(req as any, resSpy) + + expect(mqttSpy).toHaveBeenCalledWith('fail', ['AMT_EthernetPortSettings'], messages.INTERNAL_SERVICE_ERROR) + expect(resSpy.status).toHaveBeenCalledWith(500) + expect(resSpy.json).toHaveBeenCalledWith(ErrorResponse(500, 'Exception during Get Ethernet Port Settings')) + }) +}) diff --git a/src/routes/amt/linkPreference.test.ts b/src/routes/amt/linkPreference.test.ts new file mode 100644 index 000000000..289a69c3f --- /dev/null +++ b/src/routes/amt/linkPreference.test.ts @@ -0,0 +1,140 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { type SpyInstance, spyOn } from 'jest-mock' +import { CIRAHandler } from '../../amt/CIRAHandler.js' +import { DeviceAction } from '../../amt/DeviceAction.js' +import { HttpHandler } from '../../amt/HttpHandler.js' +import { messages } from '../../logging/index.js' +import { createSpyObj } from '../../test/helper/jest.js' +import { ErrorResponse } from '../../utils/amtHelper.js' +import { MqttProvider } from '../../utils/MqttProvider.js' +import { setLinkPreference } from './linkPreference.js' + +describe('Link Preference', () => { + let req: Express.Request + let resSpy + let mqttSpy: SpyInstance + let setEthernetLinkPreferenceSpy: SpyInstance + let device: DeviceAction + + beforeEach(() => { + const handler = new CIRAHandler(new HttpHandler(), 'admin', 'P@ssw0rd') + device = new DeviceAction(handler, null) + req = { + params: { + guid: '123456' + }, + body: { + linkPreference: 1, + timeout: 300 + }, + query: {}, + deviceAction: device + } + resSpy = createSpyObj('Response', [ + 'status', + 'json', + 'end', + 'send' + ]) + resSpy.status.mockReturnThis() + resSpy.json.mockReturnThis() + resSpy.send.mockReturnThis() + mqttSpy = spyOn(MqttProvider, 'publishEvent') + setEthernetLinkPreferenceSpy = spyOn(device, 'setEthernetLinkPreference').mockResolvedValue({} as any) + }) + + it('should set link preference to ME (1) with timeout', async () => { + req.body.linkPreference = 1 + req.body.timeout = 300 + + await setLinkPreference(req as any, resSpy) + + expect(setEthernetLinkPreferenceSpy).toHaveBeenCalledWith(1, 300, undefined) + expect(mqttSpy).toHaveBeenCalledWith('success', ['AMT_LinkPreference'], 'Link Preference set to ME') + expect(resSpy.status).toHaveBeenCalledWith(200) + expect(resSpy.json).toHaveBeenCalledWith({ + status: 'Link Preference set to ME', + linkPreference: 1, + timeout: 300, + instanceID: 'Intel(r) AMT Ethernet Port Settings 0' + }) + }) + + it('should set link preference to HOST (2) with timeout', async () => { + req.body.linkPreference = 2 + req.body.timeout = 600 + + await setLinkPreference(req as any, resSpy) + + expect(setEthernetLinkPreferenceSpy).toHaveBeenCalledWith(2, 600, undefined) + expect(mqttSpy).toHaveBeenCalledWith('success', ['AMT_LinkPreference'], 'Link Preference set to HOST') + expect(resSpy.status).toHaveBeenCalledWith(200) + expect(resSpy.json).toHaveBeenCalledWith({ + status: 'Link Preference set to HOST', + linkPreference: 2, + timeout: 600, + instanceID: 'Intel(r) AMT Ethernet Port Settings 0' + }) + }) + + it('should set link preference with custom instanceID', async () => { + req.body.linkPreference = 1 + req.body.timeout = 120 + req.query.instanceID = 'Intel(r) AMT Ethernet Port Settings 1' + + await setLinkPreference(req as any, resSpy) + + expect(setEthernetLinkPreferenceSpy).toHaveBeenCalledWith(1, 120, 'Intel(r) AMT Ethernet Port Settings 1') + expect(resSpy.json).toHaveBeenCalledWith({ + status: 'Link Preference set to ME', + linkPreference: 1, + timeout: 120, + instanceID: 'Intel(r) AMT Ethernet Port Settings 1' + }) + }) + + it('should set link preference to ME with timeout 0', async () => { + req.body.linkPreference = 1 + req.body.timeout = 0 + + await setLinkPreference(req as any, resSpy) + + expect(setEthernetLinkPreferenceSpy).toHaveBeenCalledWith(1, 0, undefined) + expect(resSpy.json).toHaveBeenCalledWith({ + status: 'Link Preference set to ME', + linkPreference: 1, + timeout: 0, + instanceID: 'Intel(r) AMT Ethernet Port Settings 0' + }) + }) + + it('should set link preference to HOST with timeout 0', async () => { + req.body.linkPreference = 2 + req.body.timeout = 0 + + await setLinkPreference(req as any, resSpy) + + expect(setEthernetLinkPreferenceSpy).toHaveBeenCalledWith(2, 0, undefined) + expect(resSpy.json).toHaveBeenCalledWith({ + status: 'Link Preference set to HOST', + linkPreference: 2, + timeout: 0, + instanceID: 'Intel(r) AMT Ethernet Port Settings 0' + }) + }) + + it('should handle errors gracefully', async () => { + const errorMessage = 'Failed to set link preference' + setEthernetLinkPreferenceSpy.mockRejectedValue(new Error(errorMessage)) + + await setLinkPreference(req as any, resSpy) + + expect(mqttSpy).toHaveBeenCalledWith('fail', ['AMT_LinkPreference'], messages.INTERNAL_SERVICE_ERROR) + expect(resSpy.status).toHaveBeenCalledWith(500) + expect(resSpy.json).toHaveBeenCalledWith(ErrorResponse(500, 'Exception during Set Link Preference')) + }) +}) From e43f48680b6a7e782af7b1fc0e23a9fdacb38ecb Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Fri, 14 Nov 2025 14:32:24 +0800 Subject: [PATCH 10/15] fix: fix unit test issues --- .../amt/enumerateEthernetPortSettings.ts | 8 ++++- .../amt/getEthernetPortSettings.test.ts | 34 +++++++++---------- src/routes/amt/linkPreference.test.ts | 2 +- src/utils/tlsConfiguration.test.ts | 4 +++ 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/routes/amt/enumerateEthernetPortSettings.ts b/src/routes/amt/enumerateEthernetPortSettings.ts index bf957df24..06a00f4b2 100644 --- a/src/routes/amt/enumerateEthernetPortSettings.ts +++ b/src/routes/amt/enumerateEthernetPortSettings.ts @@ -16,8 +16,14 @@ export async function enumerateEthernetPortSettings(req: Request, res: Response) logger.debug(`Enumerate Ethernet Port Settings for ${guid}`) const result = await deviceAction.enumerateEthernetPortSettings() + + // Extract the AMT_EthernetPortSettings from the response + const settings = (result?.Body?.PullResponse?.Items as any)?.AMT_EthernetPortSettings + // Ensure settings is always an array + const settingsArray = Array.isArray(settings) ? settings : (settings ? [settings] : []) + MqttProvider.publishEvent('success', ['AMT_EthernetPortSettings'], 'Ethernet Port Settings enumerated') - res.status(200).json(result).end() + res.status(200).json(settingsArray).end() } catch (error) { logger.error(`Exception during Enumerate Ethernet Port Settings: ${error}`) MqttProvider.publishEvent('fail', ['AMT_EthernetPortSettings'], messages.INTERNAL_SERVICE_ERROR) diff --git a/src/routes/amt/getEthernetPortSettings.test.ts b/src/routes/amt/getEthernetPortSettings.test.ts index dc3f8f436..5742a1a4b 100644 --- a/src/routes/amt/getEthernetPortSettings.test.ts +++ b/src/routes/amt/getEthernetPortSettings.test.ts @@ -14,7 +14,7 @@ import { MqttProvider } from '../../utils/MqttProvider.js' import { getEthernetPortSettings } from './getEthernetPortSettings.js' describe('Get Ethernet Port Settings', () => { - let req: Express.Request + let req let resSpy let mqttSpy: SpyInstance let getEthernetPortSettingsSpy: SpyInstance @@ -26,22 +26,20 @@ describe('Get Ethernet Port Settings', () => { device = new DeviceAction(handler, null) mockEthernetPortSettings = { - Body: { - AMT_EthernetPortSettings: { - InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', - ElementName: 'Intel(r) AMT Ethernet Port Settings', - MACAddress: 'a4-ae-11-1c-02-4d', - LinkIsUp: true, - LinkPolicy: [1, 14, 16], - LinkPreference: 2, - LinkControl: 2, - SharedMAC: true, - SharedStaticIp: false, - SharedDynamicIP: true, - IpSyncEnabled: true, - DHCPEnabled: true, - PhysicalConnectionType: 0 - } + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + ElementName: 'Intel(r) AMT Ethernet Port Settings', + MACAddress: 'a4-ae-11-1c-02-4d', + LinkIsUp: true, + LinkPolicy: [1, 14, 16], + LinkPreference: 2, + LinkControl: 2, + SharedMAC: true, + SharedStaticIp: false, + SharedDynamicIP: true, + IpSyncEnabled: true, + DHCPEnabled: true, + PhysicalConnectionType: 0 } } @@ -71,7 +69,7 @@ describe('Get Ethernet Port Settings', () => { expect(getEthernetPortSettingsSpy).toHaveBeenCalledWith(undefined) expect(mqttSpy).toHaveBeenCalledWith('success', ['AMT_EthernetPortSettings'], 'Ethernet Port Settings retrieved') expect(resSpy.status).toHaveBeenCalledWith(200) - expect(resSpy.json).toHaveBeenCalledWith(mockEthernetPortSettings.Body.AMT_EthernetPortSettings) + expect(resSpy.json).toHaveBeenCalledWith(mockEthernetPortSettings) }) it('should get ethernet port settings with custom instanceID', async () => { diff --git a/src/routes/amt/linkPreference.test.ts b/src/routes/amt/linkPreference.test.ts index 289a69c3f..1901fc0ab 100644 --- a/src/routes/amt/linkPreference.test.ts +++ b/src/routes/amt/linkPreference.test.ts @@ -14,7 +14,7 @@ import { MqttProvider } from '../../utils/MqttProvider.js' import { setLinkPreference } from './linkPreference.js' describe('Link Preference', () => { - let req: Express.Request + let req let resSpy let mqttSpy: SpyInstance let setEthernetLinkPreferenceSpy: SpyInstance diff --git a/src/utils/tlsConfiguration.test.ts b/src/utils/tlsConfiguration.test.ts index f7e4f5e2c..62bec9d46 100644 --- a/src/utils/tlsConfiguration.test.ts +++ b/src/utils/tlsConfiguration.test.ts @@ -5,6 +5,7 @@ import { web, mps } from './tlsConfiguration.js' import path from 'node:path' +import { fileURLToPath } from 'node:url' import fs from 'node:fs' import { logger } from '../logging/index.js' import { type mpsConfigType, type webConfigType } from '../models/Config.js' @@ -12,6 +13,9 @@ import { constants } from 'node:crypto' import { jest } from '@jest/globals' import { type SpyInstance, spyOn } from 'jest-mock' +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + let existsSyncSpy: SpyInstance let readFileSyncSpy: SpyInstance let jsonParseSpy: SpyInstance From 455ce9ddce67fa7475b7a7750ec8ddb754d7d67b Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Tue, 18 Nov 2025 17:23:35 +0800 Subject: [PATCH 11/15] fix: validate instanceID as WiFi port before proceeding Signed-off-by: Cheah, Kit Hwa --- src/amt/DeviceAction.ts | 65 +++++++++++ src/amt/deviceAction.test.ts | 149 ++++++++++++++++++++++++++ src/routes/amt/linkPreference.test.ts | 31 ++++++ src/routes/amt/linkPreference.ts | 21 +++- 4 files changed, 265 insertions(+), 1 deletion(-) diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index 5d7237d9d..a8f134752 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -688,12 +688,77 @@ export class DeviceAction { return pullResponse.Envelope } + /** + * Validates if a given instance is a WiFi port by checking PhysicalConnectionType + * @returns Object with isWiFi boolean and connectionType number, or null if not found + */ + async validateWiFiPort(instanceID: string): Promise<{ isWiFi: boolean; connectionType: number; instanceID: string } | null> { + logger.silly(`validateWiFiPort for ${instanceID}`) + try { + // Enumerate all ethernet port settings + const enumResult = await this.enumerateEthernetPortSettings() + if (enumResult?.Body?.PullResponse?.Items == null) { + logger.error('validateWiFiPort: No ethernet port settings found') + return null + } + + // Extract AMT_EthernetPortSettings from Items + const settings: any = (enumResult.Body.PullResponse.Items as any).AMT_EthernetPortSettings + if (settings == null) { + logger.error('validateWiFiPort: AMT_EthernetPortSettings not found in response') + return null + } + + // Ensure ports is always an array + const ports = Array.isArray(settings) ? settings : [settings] + + // Find the port matching the instanceID + const targetPort = ports.find((port: any) => port.InstanceID === instanceID) + if (targetPort == null) { + logger.error(`validateWiFiPort: InstanceID ${instanceID} not found`) + return null + } + + const connectionType = parseInt(targetPort.PhysicalConnectionType, 10) + // PhysicalConnectionType: 0=Integrated LAN, 1=Discrete LAN, 2=Thunderbolt, 3=Wireless LAN + const isWiFi = connectionType === 2 || connectionType === 3 + logger.silly(`validateWiFiPort: ${instanceID} connectionType=${connectionType} isWiFi=${isWiFi}`) + return { isWiFi, connectionType, instanceID } + } catch (err) { + logger.error(`validateWiFiPort error: ${(err as Error).message}`) + return null + } + } + async setEthernetLinkPreference( linkPreference: AMT.Types.EthernetPortSettings.LinkPreference, timeoutSeconds: number, instanceID: string = 'Intel(r) AMT Ethernet Port Settings 0' ): Promise> { logger.silly(`setEthernetLinkPreference ${messages.REQUEST}`) + + // Validate that the target instance is a WiFi port + const validation = await this.validateWiFiPort(instanceID) + if (validation == null) { + logger.error('setEthernetLinkPreference: Failed to validate port') + return null + } + + if (!validation.isWiFi) { + const errorMsg = `SetLinkPreference is only applicable for WiFi ports. InstanceID "${instanceID}" has PhysicalConnectionType=${validation.connectionType} (0=Integrated LAN, 1=Discrete LAN). WiFi ports have type 2 (Thunderbolt) or 3 (Wireless LAN).` + logger.error(`setEthernetLinkPreference: ${errorMsg}`) + // Return an error envelope structure that handlers can recognize + return { + Header: {}, + Body: { + Fault: { + Code: { Value: 'ValidationError' }, + Reason: { Text: errorMsg } + } + } + } as any + } + const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds, instanceID) const result = await this.ciraHandler.Get(this.ciraSocket, xmlRequestBody) logger.silly(`setEthernetLinkPreference ${messages.COMPLETE}`) diff --git a/src/amt/deviceAction.test.ts b/src/amt/deviceAction.test.ts index 78ae1b5be..4210bc0c1 100644 --- a/src/amt/deviceAction.test.ts +++ b/src/amt/deviceAction.test.ts @@ -627,4 +627,153 @@ describe('Device Action Tests', () => { expect(result).toEqual(putKVMRedirectionSettingDataResponse.Envelope.Body) }) }) + + describe('WiFi port validation and link preference', () => { + let enumerateEthernetPortSettingsSpy: SpyInstance + + beforeEach(() => { + enumerateEthernetPortSettingsSpy = spyOn(device, 'enumerateEthernetPortSettings') + }) + + it('should validate WiFi port (PhysicalConnectionType=3)', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: [ + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0', // Integrated LAN + ElementName: 'LAN Port' + }, + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 1', + PhysicalConnectionType: '3', // Wireless LAN + ElementName: 'WiFi Port' + } + ] + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.validateWiFiPort('Intel(r) AMT Ethernet Port Settings 1') + + expect(result).not.toBeNull() + expect(result?.isWiFi).toBe(true) + expect(result?.connectionType).toBe(3) + expect(result?.instanceID).toBe('Intel(r) AMT Ethernet Port Settings 1') + }) + + it('should identify non-WiFi port (PhysicalConnectionType=0)', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0', // Integrated LAN + ElementName: 'LAN Port' + } + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.validateWiFiPort('Intel(r) AMT Ethernet Port Settings 0') + + expect(result).not.toBeNull() + expect(result?.isWiFi).toBe(false) + expect(result?.connectionType).toBe(0) + }) + + it('should identify Thunderbolt as WiFi (PhysicalConnectionType=2)', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 2', + PhysicalConnectionType: '2', // Thunderbolt + ElementName: 'Thunderbolt' + } + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.validateWiFiPort('Intel(r) AMT Ethernet Port Settings 2') + + expect(result?.isWiFi).toBe(true) + expect(result?.connectionType).toBe(2) + }) + + it('should return null when instanceID not found', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0' + } + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.validateWiFiPort('NonExistent Instance') + + expect(result).toBeNull() + }) + + it('should reject setLinkPreference on non-WiFi port', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0' // Integrated LAN + } + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.setEthernetLinkPreference(1, 300, 'Intel(r) AMT Ethernet Port Settings 0') + + expect(result?.Body?.Fault).toBeDefined() + expect(result?.Body?.Fault?.Code?.Value).toBe('ValidationError') + expect(result?.Body?.Fault?.Reason?.Text).toContain('only applicable for WiFi ports') + expect(result?.Body?.Fault?.Reason?.Text).toContain('PhysicalConnectionType=0') + }) + + it('should allow setLinkPreference on WiFi port', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 1', + PhysicalConnectionType: '3' // Wireless LAN + } + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + getSpy.mockResolvedValue({ Envelope: { Body: { SetLinkPreference_OUTPUT: { ReturnValue: '0' } } } }) + + const result = await device.setEthernetLinkPreference(1, 300, 'Intel(r) AMT Ethernet Port Settings 1') + + expect(result?.Body?.Fault).toBeUndefined() + expect(getSpy).toHaveBeenCalled() + }) + }) }) diff --git a/src/routes/amt/linkPreference.test.ts b/src/routes/amt/linkPreference.test.ts index 1901fc0ab..328c5d97a 100644 --- a/src/routes/amt/linkPreference.test.ts +++ b/src/routes/amt/linkPreference.test.ts @@ -137,4 +137,35 @@ describe('Link Preference', () => { expect(resSpy.status).toHaveBeenCalledWith(500) expect(resSpy.json).toHaveBeenCalledWith(ErrorResponse(500, 'Exception during Set Link Preference')) }) + + it('should return 400 when attempting to set link preference on non-WiFi port', async () => { + // Mock a validation failure response - the exact message depends on actual port type + const mockFaultResponse = { + Header: {}, + Body: { + Fault: { + Code: { Value: 'ValidationError' }, + Reason: { Text: 'SetLinkPreference is only applicable for WiFi ports' } + } + } + } + setEthernetLinkPreferenceSpy.mockResolvedValue(mockFaultResponse) + + await setLinkPreference(req as any, resSpy) + + // Verify 400 response with the fault message + expect(resSpy.status).toHaveBeenCalledWith(400) + expect(resSpy.json).toHaveBeenCalledWith({ error: mockFaultResponse.Body.Fault.Reason.Text }) + expect(mqttSpy).toHaveBeenCalledWith('fail', ['AMT_LinkPreference'], mockFaultResponse.Body.Fault.Reason.Text) + }) + + it('should return 500 when port validation returns null', async () => { + setEthernetLinkPreferenceSpy.mockResolvedValue(null) + + await setLinkPreference(req as any, resSpy) + + expect(mqttSpy).toHaveBeenCalledWith('fail', ['AMT_LinkPreference'], 'Port validation failed') + expect(resSpy.status).toHaveBeenCalledWith(500) + expect(resSpy.json).toHaveBeenCalledWith(ErrorResponse(500, 'Failed to validate ethernet port')) + }) }) diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts index 06bf5df6d..f4398480d 100644 --- a/src/routes/amt/linkPreference.ts +++ b/src/routes/amt/linkPreference.ts @@ -19,7 +19,26 @@ export async function setLinkPreference(req: Request, res: Response): Promise Date: Tue, 18 Nov 2025 17:38:37 +0800 Subject: [PATCH 12/15] fix: Wireless LAN is PhysicalConnectionType 3 Signed-off-by: Cheah, Kit Hwa --- src/amt/DeviceAction.ts | 5 +++-- src/amt/deviceAction.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index a8f134752..4d24d12cc 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -721,7 +721,8 @@ export class DeviceAction { const connectionType = parseInt(targetPort.PhysicalConnectionType, 10) // PhysicalConnectionType: 0=Integrated LAN, 1=Discrete LAN, 2=Thunderbolt, 3=Wireless LAN - const isWiFi = connectionType === 2 || connectionType === 3 + // PhysicalConnectionType 3 (Wireless LAN) is WiFi + const isWiFi = connectionType === 3 logger.silly(`validateWiFiPort: ${instanceID} connectionType=${connectionType} isWiFi=${isWiFi}`) return { isWiFi, connectionType, instanceID } } catch (err) { @@ -745,7 +746,7 @@ export class DeviceAction { } if (!validation.isWiFi) { - const errorMsg = `SetLinkPreference is only applicable for WiFi ports. InstanceID "${instanceID}" has PhysicalConnectionType=${validation.connectionType} (0=Integrated LAN, 1=Discrete LAN). WiFi ports have type 2 (Thunderbolt) or 3 (Wireless LAN).` + const errorMsg = `SetLinkPreference is only applicable for WiFi ports. InstanceID "${instanceID}" has PhysicalConnectionType=${validation.connectionType} (0=Integrated LAN, 1=Discrete LAN, 2=Thunderbolt). WiFi ports have type 3 (Wireless LAN).` logger.error(`setEthernetLinkPreference: ${errorMsg}`) // Return an error envelope structure that handlers can recognize return { diff --git a/src/amt/deviceAction.test.ts b/src/amt/deviceAction.test.ts index 4210bc0c1..c2ac89098 100644 --- a/src/amt/deviceAction.test.ts +++ b/src/amt/deviceAction.test.ts @@ -689,7 +689,7 @@ describe('Device Action Tests', () => { expect(result?.connectionType).toBe(0) }) - it('should identify Thunderbolt as WiFi (PhysicalConnectionType=2)', async () => { + it('should identify Thunderbolt as non-WiFi (PhysicalConnectionType=2)', async () => { const mockEnumResponse = { Body: { PullResponse: { @@ -707,7 +707,7 @@ describe('Device Action Tests', () => { const result = await device.validateWiFiPort('Intel(r) AMT Ethernet Port Settings 2') - expect(result?.isWiFi).toBe(true) + expect(result?.isWiFi).toBe(false) expect(result?.connectionType).toBe(2) }) From 73af1da209c8c4bdaf0c3f23f4edc19e27b4c578 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Wed, 19 Nov 2025 09:46:01 +0800 Subject: [PATCH 13/15] feat: auto retrieve instanceID for WiFi port Signed-off-by: Cheah, Kit Hwa --- src/amt/DeviceAction.ts | 73 +++++++++++++++++- src/amt/deviceAction.test.ts | 105 ++++++++++++++++++++++++++ src/routes/amt/linkPreference.test.ts | 40 ++++++++++ src/routes/amt/linkPreference.ts | 12 ++- 4 files changed, 222 insertions(+), 8 deletions(-) diff --git a/src/amt/DeviceAction.ts b/src/amt/DeviceAction.ts index 4d24d12cc..43dc1f4fa 100644 --- a/src/amt/DeviceAction.ts +++ b/src/amt/DeviceAction.ts @@ -688,6 +688,46 @@ export class DeviceAction { return pullResponse.Envelope } + /** + * Finds the first WiFi port by checking PhysicalConnectionType + * @returns InstanceID of the first WiFi port found, or null if none found + */ + async findWiFiPort(): Promise { + logger.silly('findWiFiPort: searching for WiFi port') + try { + const enumResult = await this.enumerateEthernetPortSettings() + if (enumResult?.Body?.PullResponse?.Items == null) { + logger.error('findWiFiPort: No ethernet port settings found') + return null + } + + const settings: any = (enumResult.Body.PullResponse.Items as any).AMT_EthernetPortSettings + if (settings == null) { + logger.error('findWiFiPort: AMT_EthernetPortSettings not found in response') + return null + } + + const ports = Array.isArray(settings) ? settings : [settings] + + // Find the first port with PhysicalConnectionType = 3 (Wireless LAN) + const wifiPort = ports.find((port: any) => { + const connectionType = parseInt(port.PhysicalConnectionType, 10) + return connectionType === 3 + }) + + if (wifiPort == null) { + logger.error('findWiFiPort: No WiFi port found') + return null + } + + logger.silly(`findWiFiPort: Found WiFi port ${wifiPort.InstanceID}`) + return wifiPort.InstanceID + } catch (err) { + logger.error(`findWiFiPort error: ${(err as Error).message}`) + return null + } + } + /** * Validates if a given instance is a WiFi port by checking PhysicalConnectionType * @returns Object with isWiFi boolean and connectionType number, or null if not found @@ -734,19 +774,40 @@ export class DeviceAction { async setEthernetLinkPreference( linkPreference: AMT.Types.EthernetPortSettings.LinkPreference, timeoutSeconds: number, - instanceID: string = 'Intel(r) AMT Ethernet Port Settings 0' + instanceID?: string ): Promise> { logger.silly(`setEthernetLinkPreference ${messages.REQUEST}`) + // If no instanceID provided, auto-detect the WiFi port + let targetInstanceID = instanceID + if (targetInstanceID == null || targetInstanceID.trim() === '') { + logger.silly('setEthernetLinkPreference: No instanceID provided, auto-detecting WiFi port') + targetInstanceID = await this.findWiFiPort() + if (targetInstanceID == null) { + const errorMsg = 'No WiFi port found on this device. SetLinkPreference requires a WiFi interface (PhysicalConnectionType=3).' + logger.error(`setEthernetLinkPreference: ${errorMsg}`) + return { + Header: {}, + Body: { + Fault: { + Code: { Value: 'NoWiFiPort' }, + Reason: { Text: errorMsg } + } + } + } as any + } + logger.info(`setEthernetLinkPreference: Auto-detected WiFi port: ${targetInstanceID}`) + } + // Validate that the target instance is a WiFi port - const validation = await this.validateWiFiPort(instanceID) + const validation = await this.validateWiFiPort(targetInstanceID) if (validation == null) { logger.error('setEthernetLinkPreference: Failed to validate port') return null } if (!validation.isWiFi) { - const errorMsg = `SetLinkPreference is only applicable for WiFi ports. InstanceID "${instanceID}" has PhysicalConnectionType=${validation.connectionType} (0=Integrated LAN, 1=Discrete LAN, 2=Thunderbolt). WiFi ports have type 3 (Wireless LAN).` + const errorMsg = `SetLinkPreference is only applicable for WiFi ports. InstanceID "${targetInstanceID}" has PhysicalConnectionType=${validation.connectionType} (0=Integrated LAN, 1=Discrete LAN, 2=Thunderbolt). WiFi ports have type 3 (Wireless LAN).` logger.error(`setEthernetLinkPreference: ${errorMsg}`) // Return an error envelope structure that handlers can recognize return { @@ -760,9 +821,13 @@ export class DeviceAction { } as any } - const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds, instanceID) + const xmlRequestBody = this.amt.EthernetPortSettings.SetLinkPreference(linkPreference, timeoutSeconds, targetInstanceID) const result = await this.ciraHandler.Get(this.ciraSocket, xmlRequestBody) logger.silly(`setEthernetLinkPreference ${messages.COMPLETE}`) + // Add the detected instanceID to the result for the handler to use + if (result?.Envelope != null) { + (result.Envelope as any)._detectedInstanceID = targetInstanceID + } return result.Envelope } diff --git a/src/amt/deviceAction.test.ts b/src/amt/deviceAction.test.ts index c2ac89098..61246a2f3 100644 --- a/src/amt/deviceAction.test.ts +++ b/src/amt/deviceAction.test.ts @@ -635,6 +635,111 @@ describe('Device Action Tests', () => { enumerateEthernetPortSettingsSpy = spyOn(device, 'enumerateEthernetPortSettings') }) + it('should find WiFi port automatically', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: [ + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0', // Integrated LAN + ElementName: 'LAN Port' + }, + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 1', + PhysicalConnectionType: '3', // Wireless LAN + ElementName: 'WiFi Port' + } + ] + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.findWiFiPort() + + expect(result).toBe('Intel(r) AMT Ethernet Port Settings 1') + }) + + it('should return null when no WiFi port exists', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: [ + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0' // Only LAN + }, + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 1', + PhysicalConnectionType: '1' // Only LAN + } + ] + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.findWiFiPort() + + expect(result).toBeNull() + }) + + it('should auto-detect WiFi port when instanceID not provided', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: [ + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0' + }, + { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 1', + PhysicalConnectionType: '3' // WiFi + } + ] + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + getSpy.mockResolvedValue({ Envelope: { Body: { SetLinkPreference_OUTPUT: { ReturnValue: '0' } } } }) + + const result = await device.setEthernetLinkPreference(1, 300) + + expect(result?.Body?.Fault).toBeUndefined() + expect(getSpy).toHaveBeenCalled() + expect((result as any)._detectedInstanceID).toBe('Intel(r) AMT Ethernet Port Settings 1') + }) + + it('should return error when no WiFi port found and instanceID not provided', async () => { + const mockEnumResponse = { + Body: { + PullResponse: { + Items: { + AMT_EthernetPortSettings: { + InstanceID: 'Intel(r) AMT Ethernet Port Settings 0', + PhysicalConnectionType: '0' // Only LAN + } + } + } + } + } + enumerateEthernetPortSettingsSpy.mockResolvedValue(mockEnumResponse as any) + + const result = await device.setEthernetLinkPreference(1, 300) + + expect(result?.Body?.Fault).toBeDefined() + expect(result?.Body?.Fault?.Code?.Value).toBe('NoWiFiPort') + expect(result?.Body?.Fault?.Reason?.Text).toContain('No WiFi port found') + }) + it('should validate WiFi port (PhysicalConnectionType=3)', async () => { const mockEnumResponse = { Body: { diff --git a/src/routes/amt/linkPreference.test.ts b/src/routes/amt/linkPreference.test.ts index 328c5d97a..8bce49758 100644 --- a/src/routes/amt/linkPreference.test.ts +++ b/src/routes/amt/linkPreference.test.ts @@ -168,4 +168,44 @@ describe('Link Preference', () => { expect(resSpy.status).toHaveBeenCalledWith(500) expect(resSpy.json).toHaveBeenCalledWith(ErrorResponse(500, 'Failed to validate ethernet port')) }) + + it('should auto-detect WiFi port when instanceID not provided', async () => { + req.query.instanceID = undefined + const mockResponse = { + Body: { SetLinkPreference_OUTPUT: { ReturnValue: '0' } }, + _detectedInstanceID: 'Intel(r) AMT Ethernet Port Settings 1' + } + setEthernetLinkPreferenceSpy.mockResolvedValue(mockResponse) + + await setLinkPreference(req as any, resSpy) + + expect(setEthernetLinkPreferenceSpy).toHaveBeenCalledWith(1, 300, undefined) + expect(resSpy.status).toHaveBeenCalledWith(200) + expect(resSpy.json).toHaveBeenCalledWith({ + status: 'Link Preference set to ME', + linkPreference: 1, + timeout: 300, + instanceID: 'Intel(r) AMT Ethernet Port Settings 1' + }) + }) + + it('should return 400 when no WiFi port found during auto-detection', async () => { + req.query.instanceID = undefined + const mockFaultResponse = { + Body: { + Fault: { + Code: { Value: 'NoWiFiPort' }, + Reason: { Text: 'No WiFi port found on this device. SetLinkPreference requires a WiFi interface (PhysicalConnectionType=3).' } + } + } + } + setEthernetLinkPreferenceSpy.mockResolvedValue(mockFaultResponse) + + await setLinkPreference(req as any, resSpy) + + expect(resSpy.status).toHaveBeenCalledWith(400) + expect(resSpy.json).toHaveBeenCalledWith({ + error: 'No WiFi port found on this device. SetLinkPreference requires a WiFi interface (PhysicalConnectionType=3).' + }) + }) }) diff --git a/src/routes/amt/linkPreference.ts b/src/routes/amt/linkPreference.ts index f4398480d..7245d65d0 100644 --- a/src/routes/amt/linkPreference.ts +++ b/src/routes/amt/linkPreference.ts @@ -18,11 +18,12 @@ export async function setLinkPreference(req: Request, res: Response): Promise Date: Wed, 19 Nov 2025 10:35:04 +0800 Subject: [PATCH 14/15] fix: unit tests updates Signed-off-by: Cheah, Kit Hwa --- src/routes/amt/linkPreference.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/routes/amt/linkPreference.test.ts b/src/routes/amt/linkPreference.test.ts index 8bce49758..c3d653295 100644 --- a/src/routes/amt/linkPreference.test.ts +++ b/src/routes/amt/linkPreference.test.ts @@ -44,7 +44,10 @@ describe('Link Preference', () => { resSpy.json.mockReturnThis() resSpy.send.mockReturnThis() mqttSpy = spyOn(MqttProvider, 'publishEvent') - setEthernetLinkPreferenceSpy = spyOn(device, 'setEthernetLinkPreference').mockResolvedValue({} as any) + // Mock with _detectedInstanceID to simulate auto-detection fallback + setEthernetLinkPreferenceSpy = spyOn(device, 'setEthernetLinkPreference').mockResolvedValue({ + _detectedInstanceID: 'Intel(r) AMT Ethernet Port Settings 0' + } as any) }) it('should set link preference to ME (1) with timeout', async () => { @@ -85,6 +88,11 @@ describe('Link Preference', () => { req.body.linkPreference = 1 req.body.timeout = 120 req.query.instanceID = 'Intel(r) AMT Ethernet Port Settings 1' + + // Mock response with the custom instanceID + setEthernetLinkPreferenceSpy.mockResolvedValue({ + _detectedInstanceID: 'Intel(r) AMT Ethernet Port Settings 1' + } as any) await setLinkPreference(req as any, resSpy) From 02db25fe5fd5e79c332a26e2574e29a7ee152075 Mon Sep 17 00:00:00 2001 From: "Cheah, Kit Hwa" Date: Wed, 19 Nov 2025 10:51:40 +0800 Subject: [PATCH 15/15] docs: update swagger yaml file Signed-off-by: Cheah, Kit Hwa --- swagger.yaml | 69 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/swagger.yaml b/swagger.yaml index 25c65326a..8034d89bc 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -741,8 +741,12 @@ paths: description: 'Internal server error' /api/v1/amt/network/ethernetPortSettings/{guid}: get: - summary: Get Ethernet Port Settings - description: Retrieve Ethernet Port Settings including link preference, link policy, and other network configurations. + summary: Get Specific Ethernet Port Settings + description: | + Retrieve detailed Ethernet Port Settings for a specific port by InstanceID. + + Includes link preference, link policy, MAC address, and other network configurations. + Use `/enumerate/{guid}` first to discover available port InstanceIDs. tags: - AMT parameters: @@ -755,11 +759,11 @@ paths: type: string - name: instanceID in: query - description: InstanceID of the Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 0") - required: false + description: InstanceID of the Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 0" for wired, "Intel(r) AMT Ethernet Port Settings 1" for WiFi) + required: true schema: type: string - default: Intel(r) AMT Ethernet Port Settings 0 + example: Intel(r) AMT Ethernet Port Settings 1 responses: 200: description: 'Ethernet Port Settings retrieved successfully' @@ -807,8 +811,17 @@ paths: description: 'Internal server error' /api/v1/amt/network/ethernetPortSettings/enumerate/{guid}: get: - summary: Enumerate all Ethernet Port Settings - description: Retrieve all Ethernet Port Settings instances (all network ports). + summary: Enumerate All Ethernet Port Settings + description: | + Retrieve all Ethernet Port Settings instances for all network ports (wired and wireless). + + **PhysicalConnectionType values**: + - 0 = Integrated LAN + - 1 = Discrete LAN + - 2 = Thunderbolt + - 3 = Wireless LAN (WiFi) + + Use this endpoint to discover available ports and their types before setting link preferences. tags: - AMT parameters: @@ -855,6 +868,16 @@ paths: MACAddress: type: string example: "00:11:22:33:44:55" + PhysicalConnectionType: + type: integer + description: 0=Integrated LAN, 1=Discrete LAN, 2=Thunderbolt, 3=Wireless LAN + example: 3 + LinkIsUp: + type: boolean + example: true + SharedMAC: + type: boolean + example: true 404: description: 'Device not found' content: @@ -865,8 +888,16 @@ paths: description: 'Internal server error' /api/v1/amt/network/linkPreference/{guid}: post: - summary: Set link preference with timeout - description: Set Ethernet Link Preference to ME (1) for a specified duration, or to HOST (2). + summary: Set WiFi Link Preference + description: | + Set Ethernet Link Preference for WiFi ports only. + - **LinkPreference 1 (ME)**: Routes traffic through Management Engine for the specified timeout duration + - **LinkPreference 2 (HOST)**: Routes traffic through host OS (timeout should be 0) + + **WiFi Port Validation**: This API only works on WiFi ports (PhysicalConnectionType = 3). + It will return a 400 error if attempting to set link preference on wired LAN ports (types 0, 1, or 2). + + **Auto-Detection**: If `instanceID` is not provided, the API will automatically detect and use the first available WiFi port. tags: - AMT parameters: @@ -879,11 +910,14 @@ paths: type: string - name: instanceID in: query - description: InstanceID of the Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 0") + description: | + InstanceID of the WiFi Ethernet port (e.g., "Intel(r) AMT Ethernet Port Settings 1"). + If not provided, the API will automatically detect the WiFi port. + Must be a WiFi port (PhysicalConnectionType = 3). required: false schema: type: string - default: Intel(r) AMT Ethernet Port Settings 0 + example: Intel(r) AMT Ethernet Port Settings 1 requestBody: description: Link preference and timeout settings required: true @@ -923,9 +957,18 @@ paths: example: 300 instanceID: type: string - example: Intel(r) AMT Ethernet Port Settings 0 + description: The WiFi port InstanceID that was used (auto-detected or specified) + example: Intel(r) AMT Ethernet Port Settings 1 400: - description: 'Invalid parameters' + description: 'Validation error - port is not a WiFi port or no WiFi port found' + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "SetLinkPreference is only applicable for WiFi ports. InstanceID \"Intel(r) AMT Ethernet Port Settings 0\" has PhysicalConnectionType=0 (0=Integrated LAN, 1=Discrete LAN, 2=Thunderbolt). WiFi ports have type 3 (Wireless LAN)." 404: description: 'Device not found' content: