From f1c81d84b574294d6bce3953248d1309b5872db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 20 Jun 2023 10:24:25 +0100 Subject: [PATCH 1/3] [web] Adapt storage client to manage zFCP devices --- web/src/client/storage.js | 405 +++++++++++++++++++++++++++++++++++++- 1 file changed, 396 insertions(+), 9 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 6db1c1d773..fc5c26ddaf 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -26,20 +26,25 @@ import DBusClient from "./dbus"; import { WithIssues, WithStatus, WithProgress } from "./mixins"; import { hex } from "~/utils"; +const STORAGE_OBJECT = "/org/opensuse/Agama/Storage1"; const STORAGE_IFACE = "org.opensuse.Agama.Storage1"; +const STORAGE_JOBS_NAMESPACE = "/org/opensuse/Agama/Storage1/jobs"; +const STORAGE_JOB_IFACE = "org.opensuse.Agama.Storage1.Job"; +const STORAGE_SYSTEM_NAMESPACE = "/org/opensuse/Agama/Storage1/system"; +const PROPOSAL_IFACE = "org.opensuse.Agama.Storage1.Proposal"; const PROPOSAL_CALCULATOR_IFACE = "org.opensuse.Agama.Storage1.Proposal.Calculator"; -const ISCSI_NODE_IFACE = "org.opensuse.Agama.Storage1.ISCSI.Node"; -const ISCSI_NODES_NAMESPACE = "/org/opensuse/Agama/Storage1/iscsi_nodes"; const ISCSI_INITIATOR_IFACE = "org.opensuse.Agama.Storage1.ISCSI.Initiator"; -const DASD_DEVICE_IFACE = "org.opensuse.Agama.Storage1.DASD.Device"; -const DASD_DEVICES_NAMESPACE = "/org/opensuse/Agama/Storage1/dasds"; +const ISCSI_NODES_NAMESPACE = "/org/opensuse/Agama/Storage1/iscsi_nodes"; +const ISCSI_NODE_IFACE = "org.opensuse.Agama.Storage1.ISCSI.Node"; const DASD_MANAGER_IFACE = "org.opensuse.Agama.Storage1.DASD.Manager"; +const DASD_DEVICES_NAMESPACE = "/org/opensuse/Agama/Storage1/dasds"; +const DASD_DEVICE_IFACE = "org.opensuse.Agama.Storage1.DASD.Device"; const DASD_STATUS_IFACE = "org.opensuse.Agama.Storage1.DASD.Format"; -const PROPOSAL_IFACE = "org.opensuse.Agama.Storage1.Proposal"; -const STORAGE_OBJECT = "/org/opensuse/Agama/Storage1"; -const STORAGE_JOB_IFACE = "org.opensuse.Agama.Storage1.Job"; -const STORAGE_JOBS_NAMESPACE = "/org/opensuse/Agama/Storage1/jobs"; -const STORAGE_SYSTEM_NAMESPACE = "/org/opensuse/Agama/Storage1/system"; +const ZFCP_MANAGER_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Manager"; +const ZFCP_CONTROLLERS_NAMESPACE = "/org/opensuse/Agama/Storage1/zfcp_controllers"; +const ZFCP_CONTROLLER_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Controller"; +const ZFCP_DISKS_NAMESPACE = "/org/opensuse/Agama/Storage1/zfcp_disks"; +const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk"; /** * Removes properties with undefined value @@ -60,6 +65,18 @@ const removeUndefinedCockpitProperties = (cockpitObject) => { return Object.fromEntries(filtered); }; +/** + * Gets the basename of a D-Bus path + * + * @example + * dbusBasename("/org/opensuse/Agama/Storage1/object1"); + * //returns "object1" + * + * @param {string} path + * @returns {string} + */ +const dbusBasename = (path) => path.split("/").slice(-1)[0]; + /** * Class providing an API for managing a devices tree through D-Bus */ @@ -688,6 +705,375 @@ class DASDManager { } } +/** + * Class providing an API for managing zFCP through D-Bus + */ +class ZFCPManager { + /** + * @param {string} service - D-Bus service name + * @param {string} address - D-Bus address + */ + constructor(service, address) { + this.service = service; + this.address = address; + this.proxies = {}; + } + + /** + * @return {DBusClient} client + */ + client() { + if (!this._client) { + this._client = new DBusClient(this.service, this.address); + } + + return this._client; + } + + /** + * Whether zFCP is supported + * + * @todo Use info from ObjectManager instead, see + * https://github.com/openSUSE/Agama/pull/501#discussion_r1147707515 + * + * @returns {Promise} + */ + async isSupported() { + const proxy = await this.managerProxy(); + return proxy !== undefined; + } + + /** + * Whether allow_lun_scan option is active + * + * @returns {Promise} + */ + async getAllowLUNScan() { + const proxy = await this.managerProxy(); + return proxy?.AllowLUNScan; + } + + /** + * Probes the zFCP devices + * + * @returns {Promise} + */ + async probe() { + const proxy = await this.managerProxy(); + return proxy?.Probe(); + } + + /** + * Gets the list of probed zFCP controllers + * + * @returns {Promise} + */ + async getControllers() { + const proxy = await this.controllersProxy(); + return Object.values(proxy).map(this.buildController); + } + + /** + * Gets the list of probed zFCP controllers + * + * @returns {Promise} + */ + async getDisks() { + const proxy = await this.disksProxy(); + return Object.values(proxy).map(this.buildDisk); + } + + /** + * Gets the list of available WWPNs for the given zFCP controller + * + * @param {ZFCPController} controller + * @returns {Promise} e.g., ["0x500507630703d3b3", 0x500507630708d3b3] + */ + async getWWPNs(controller) { + const proxy = await this.controllerProxy(controller); + return proxy?.GetWWPNs(); + } + + /** + * Gets the list of available LUNs for the WWPN of the given zFCP controller + * + * @param {ZFCPController} controller + * @param {string} wwpn + * @returns {Promise} e.g., ["0x0000000000000000", "0x0000000000000001"] + */ + async getLUNs(controller, wwpn) { + const proxy = await this.controllerProxy(controller); + return proxy?.GetLUNs(wwpn); + } + + /** + * Tries to activate the given zFCP controller + * + * @param {ZFCPController} controller + * @returns {Promise} Exit code of chzdev command (0 success) + */ + async activateController(controller) { + const proxy = await this.controllerProxy(controller); + return proxy?.Activate(); + } + + /** + * Tries to activate the given zFCP LUN + * + * @param {ZFCPController} controller + * @param {string} wwpn + * @param {string} lun + * @returns {Promise} Exit code of chzdev command (0 success) + */ + async activateDisk(controller, wwpn, lun) { + const proxy = await this.controllerProxy(controller); + return proxy?.ActivateDisk(wwpn, lun); + } + + /** + * Tries to deactivate the given zFCP LUN + * + * @param {ZFCPController} controller + * @param {string} wwpn + * @param {string} lun + * @returns {Promise} Exit code of chzdev command (0 success) + */ + async deactivateDisk(controller, wwpn, lun) { + const proxy = await this.controllerProxy(controller); + return proxy?.DeactivateDisk(wwpn, lun); + } + + /** + * Subscribes to signal that is emitted when a zFCP controller changes + * + * @param {ZFCPControllerSignalHandler} handler + * @returns {Promise} Unsubscribe function + */ + async onControllerChanged(handler) { + const unsubscribeFn = this.controllerEventListener("changed", handler); + return unsubscribeFn; + } + + /** + * Subscribes to signal that is emitted when a zFCP disk is added + * + * @param {ZFCPDiskSignalHandler} handler + * @returns {Promise} Unsubscribe function + */ + async onDiskAdded(handler) { + const unsubscribeFn = this.diskEventListener("added", handler); + return unsubscribeFn; + } + + /** + * Subscribes to signal that is emitted when a zFCP disk is changed + * + * @param {ZFCPDiskSignalHandler} handler + * @returns {Promise} Unsubscribe function + */ + async onDiskChanged(handler) { + const unsubscribeFn = this.diskEventListener("changed", handler); + return unsubscribeFn; + } + + /** + * Subscribes to signal that is emitted when a zFCP disk is removed + * + * @param {ZFCPDiskSignalHandler} handler + * @returns {Promise} Unsubscribe function + */ + async onDiskRemoved(handler) { + const unsubscribeFn = this.diskEventListener("removed", handler); + return unsubscribeFn; + } + + /** + * @private + * Proxy for org.opensuse.Agama.Storage1.ZFCP.Manager iface + * + * @returns {Promise} + * + * @typedef {object} ZFCPManagerProxy + * @property {boolean} AllowLUNScan + * @property {function} Probe + */ + async managerProxy() { + if (!this.proxies.manager) { + this.proxies.manager = await this.client().proxy(ZFCP_MANAGER_IFACE, STORAGE_OBJECT); + } + + return this.proxies.manager; + } + + /** + * @private + * Proxy for objects implementing org.opensuse.Agama.Storage1.ZFCP.Controller iface + * + * @note The zFCP controllers are dynamically exported. + * + * @returns {Promise} + */ + async controllersProxy() { + if (!this.proxies.controllers) + this.proxies.controllers = await this.client().proxies(ZFCP_CONTROLLER_IFACE, ZFCP_CONTROLLERS_NAMESPACE); + + return this.proxies.controllers; + } + + /** + * @private + * Proxy for objects implementing org.opensuse.Agama.Storage1.ZFCP.Disk iface + * + * @note The zFCP disks are dynamically exported. + * + * @returns {Promise} + */ + async disksProxy() { + if (!this.proxies.disks) + this.proxies.disks = await this.client().proxies(ZFCP_DISK_IFACE, ZFCP_DISKS_NAMESPACE); + + return this.proxies.disks; + } + + /** + * @private + * Proxy for org.opensuse.Agama.Storage1.ZFCP.Controller iface + * + * @param {ZFCPController} controller + * @returns {Promise} + * + * @typedef {object} ZFCPControllerProxy + * @property {string} path + * @property {boolean} Active + * @property {boolean} LUNScan + * @property {string} Channel + * @property {function} GetWWPNs + * @property {function} GetLUNs + * @property {function} Activate + * @property {function} ActivateDisk + * @property {function} DeactivateDisk + */ + async controllerProxy(controller) { + const path = this.controllerPath(controller); + const proxy = await this.client().proxy(ZFCP_CONTROLLER_IFACE, path); + return proxy; + } + + /** + * @private + * Subscribes to a signal from a zFCP controller + * + * @param {string} signal - "added", "changed", "removed" + * @param {ZFCPControllerSignalHandler} handler + * @returns {Promise} Unsubscribe function + * + * @callback ZFCPControllerSignalHandler + * @param {ZFCPController} controller + */ + async controllerEventListener(signal, handler) { + const proxy = await this.controllersProxy(); + const eventHandler = (_, proxy) => handler(this.buildController(proxy)); + const unsubscribeFn = await this.addEventListener(proxy, signal, eventHandler); + return unsubscribeFn; + } + + /** + * @private + * Subscribes to a signal from a zFCP disk + * + * @param {string} signal - "added", "changed", "removed" + * @param {ZFCPDiskSignalHandler} handler + * @returns {Promise} Unsubscribe function + * + * @callback ZFCPDiskSignalHandler + * @param {ZFCPDisk} disk + */ + async diskEventListener(signal, handler) { + const proxy = await this.disksProxy(); + const eventHandler = (_, proxy) => handler(this.buildDisk(proxy)); + const unsubscribeFn = await this.addEventListener(proxy, signal, eventHandler); + return unsubscribeFn; + } + + /** + * @private + * Subscribes to a signal + * + * @param {object} proxy + * @param {string} signal + * @param {function} handler + * @returns {Promise} Unsubscribe function + */ + async addEventListener(proxy, signal, handler) { + proxy.addEventListener(signal, handler); + return () => proxy.removeEventListener(signal, handler); + } + + /** + * @private + * Builds a controller object + * + * @param {ZFCPControllerProxy} proxy + * @returns {ZFCPController} + * + * @typedef {object} ZFCPController + * @property {string} id + * @property {boolean} active + * @property {boolean} lunScan + * @property {string} channel + */ + buildController(proxy) { + return { + id: dbusBasename(proxy.path), + active: proxy.Active, + lunScan: proxy.LUNScan, + channel: proxy.Channel + }; + } + + /** + * @private + * Builds a disk object + * + * @param {ZFCPDiskProxy} proxy + * @returns {ZFCPDisk} + * + * @typedef {object} ZFCPDiskProxy + * @property {string} path + * @property {string} Name + * @property {string} Channel + * @property {string} WWPN + * @property {string} LUN + * + * @typedef {object} ZFCPDisk + * @property {string} id + * @property {string} name + * @property {string} channel + * @property {string} wwpn + * @property {string} lun + */ + buildDisk(proxy) { + return { + id: dbusBasename(proxy.path), + name: proxy.Name, + channel: proxy.Channel, + wwpn: proxy.WWPN, + lun: proxy.LUN + }; + } + + /** + * @private + * Builds the D-Bus path for the given zFCP controller + * + * @param {ZFCPController} controller + * @returns {string} + */ + controllerPath(controller) { + return ZFCP_CONTROLLERS_NAMESPACE + "/" + controller.id; + } +} + /** * Class providing an API for managing iSCSI through D-Bus */ @@ -958,6 +1344,7 @@ class StorageBaseClient { this.proposal = new ProposalManager(this.client, this.system); this.iscsi = new ISCSIManager(StorageBaseClient.SERVICE, address); this.dasd = new DASDManager(StorageBaseClient.SERVICE, address); + this.zfcp = new ZFCPManager(StorageBaseClient.SERVICE, address); this.proxies = { storage: this.client.proxy(STORAGE_IFACE) }; From 8aac31ec12e69651f9831841e8a5ed20ab3ab556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 20 Jun 2023 10:24:52 +0100 Subject: [PATCH 2/3] [web] Unit tests --- web/src/client/storage.test.js | 457 ++++++++++++++++++++++++++++++++- 1 file changed, 456 insertions(+), 1 deletion(-) diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 0962fafc3b..9fbaf79ee2 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -20,7 +20,7 @@ */ // @ts-check -// cspell:ignore ECKD dasda ddgdcbibhd +// cspell:ignore ECKD dasda ddgdcbibhd wwpns import DBusClient from "./dbus"; import { StorageClient } from "./storage"; @@ -260,6 +260,46 @@ const contexts = { } }; }, + withoutZFCPControllers: () => { + cockpitProxies.zfcpControllers = {}; + }, + withZFCPControllers: () => { + cockpitProxies.zfcpControllers = { + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": { + path: "/org/opensuse/Agama/Storage1/zfcp_controllers/1", + Active: false, + LUNScan: false, + Channel: "0.0.fa00" + }, + "/org/opensuse/Agama/Storage1/zfcp_controllers/2": { + path: "/org/opensuse/Agama/Storage1/zfcp_controllers/2", + Active: false, + LUNScan: false, + Channel: "0.0.fc00" + } + }; + }, + withoutZFCPDisks: () => { + cockpitProxies.zfcpDisks = {}; + }, + withZFCPDisks: () => { + cockpitProxies.zfcpDisks = { + "/org/opensuse/Agama/Storage1/zfcp_disks/1": { + path: "/org/opensuse/Agama/Storage1/zfcp_disks/1", + Name: "/dev/sda", + Channel: "0.0.fa00", + WWPN: "0x500507630703d3b3", + LUN: "0x0000000000000000" + }, + "/org/opensuse/Agama/Storage1/zfcp_disks/2": { + path: "/org/opensuse/Agama/Storage1/zfcp_disks/2", + Name: "/dev/sdb", + Channel: "0.0.fa00", + WWPN: "0x500507630703d3b3", + LUN: "0x0000000000000001" + } + }; + }, withSystemDevices: () => { managedObjects["/org/opensuse/Agama/Storage1/system/59"] = { "org.opensuse.Agama.Storage1.Drive": { @@ -398,6 +438,8 @@ const mockProxy = (iface, path) => { case "org.opensuse.Agama.Storage1.ISCSI.Initiator": return cockpitProxies.iscsiInitiator; case "org.opensuse.Agama.Storage1.ISCSI.Node": return cockpitProxies.iscsiNode[path]; case "org.opensuse.Agama.Storage1.DASD.Manager": return cockpitProxies.dasdManager; + case "org.opensuse.Agama.Storage1.ZFCP.Manager": return cockpitProxies.zfcpManager; + case "org.opensuse.Agama.Storage1.ZFCP.Controller": return cockpitProxies.zfcpController[path]; } }; @@ -405,6 +447,8 @@ const mockProxies = (iface) => { switch (iface) { case "org.opensuse.Agama.Storage1.ISCSI.Node": return cockpitProxies.iscsiNodes; case "org.opensuse.Agama.Storage1.DASD.Device": return cockpitProxies.dasdDevices; + case "org.opensuse.Agama.Storage1.ZFCP.Controller": return cockpitProxies.zfcpControllers; + case "org.opensuse.Agama.Storage1.ZFCP.Disk": return cockpitProxies.zfcpDisks; } }; @@ -437,6 +481,10 @@ const reset = () => { cockpitProxies.iscsiNode = {}; cockpitProxies.dasdManager = {}; cockpitProxies.dasdDevices = {}; + cockpitProxies.zfcpManager = {}; + cockpitProxies.zfcpControllers = {}; + cockpitProxies.zfcpDisks = {}; + cockpitProxies.zfcpController = {}; managedObjects = {}; }; @@ -887,6 +935,413 @@ describe("#dasd", () => { }); }); +describe("#zfcp", () => { + const probeFn = jest.fn(); + let controllersCallbacks; + let disksCallbacks; + + const mockEventListener = (proxy, callbacks) => { + proxy.addEventListener = jest.fn().mockImplementation( + (signal, handler) => { + if (!callbacks[signal]) callbacks[signal] = []; + callbacks[signal].push(handler); + } + ); + + proxy.removeEventListener = jest.fn(); + }; + + const emitSignals = (callbacks, signal, proxy) => { + callbacks[signal].forEach(handler => handler(null, proxy)); + }; + + beforeEach(() => { + client = new StorageClient(); + cockpitProxies.zfcpManager = { + Probe: probeFn, + AllowLUNScan: true + }; + + controllersCallbacks = {}; + mockEventListener(cockpitProxies.zfcpControllers, controllersCallbacks); + + disksCallbacks = {}; + mockEventListener(cockpitProxies.zfcpDisks, disksCallbacks); + }); + + describe("#isSupported", () => { + describe("if zFCP manager is available", () => { + it("returns true", async () => { + const result = await client.zfcp.isSupported(); + expect(result).toEqual(true); + }); + }); + + describe("if zFCP manager is not available", () => { + beforeEach(() => { + cockpitProxies.zfcpManager = undefined; + }); + + it("returns false", async () => { + const result = await client.zfcp.isSupported(); + expect(result).toEqual(false); + }); + }); + }); + + describe("#getAllowLUNScan", () => { + it("returns whether allow_lun_scan is active", async () => { + const result = await client.zfcp.getAllowLUNScan(); + expect(result).toEqual(true); + }); + + describe("if zFCP manager is not available", () => { + beforeEach(() => { + cockpitProxies.zfcpManager = undefined; + }); + + it("returns undefined", async () => { + const result = await client.zfcp.getAllowLUNScan(); + expect(result).toBeUndefined(); + }); + }); + }); + + describe("#probe", () => { + it("triggers probing", async () => { + await client.zfcp.probe(); + expect(probeFn).toHaveBeenCalled(); + }); + + describe("if zFCP manager is not available", () => { + beforeEach(() => { + cockpitProxies.zfcpManager = undefined; + }); + + it("returns undefined", async () => { + const result = await client.zfcp.probe(); + expect(result).toBeUndefined(); + }); + }); + }); + + describe("#getControllers", () => { + describe("if there is no exported zFCP controllers yet", () => { + beforeEach(() => { + contexts.withoutZFCPControllers(); + }); + + it("returns an empty list", async () => { + const result = await client.zfcp.getControllers(); + expect(result).toStrictEqual([]); + }); + }); + + describe("if there are exported ZFCP controllers", () => { + beforeEach(() => { + contexts.withZFCPControllers(); + }); + + it("returns a list with the exported ZFCP controllers", async () => { + const result = await client.zfcp.getControllers(); + expect(result.length).toEqual(2); + expect(result).toContainEqual({ + id: "1", + active: false, + lunScan: false, + channel: "0.0.fa00" + }); + expect(result).toContainEqual({ + id: "2", + active: false, + lunScan: false, + channel: "0.0.fc00" + }); + }); + }); + }); + + describe("#getDisks", () => { + describe("if there is no exported zFCP disks yet", () => { + beforeEach(() => { + contexts.withoutZFCPDisks(); + }); + + it("returns an empty list", async () => { + const result = await client.zfcp.getDisks(); + expect(result).toStrictEqual([]); + }); + }); + + describe("if there are exported ZFCP disks", () => { + beforeEach(() => { + contexts.withZFCPDisks(); + }); + + it("returns a list with the exported ZFCP disks", async () => { + const result = await client.zfcp.getDisks(); + expect(result.length).toEqual(2); + expect(result).toContainEqual({ + id: "1", + name: "/dev/sda", + channel: "0.0.fa00", + wwpn: "0x500507630703d3b3", + lun: "0x0000000000000000" + }); + expect(result).toContainEqual({ + id: "2", + name: "/dev/sdb", + channel: "0.0.fa00", + wwpn: "0x500507630703d3b3", + lun: "0x0000000000000001" + }); + }); + }); + }); + + describe("#getWWPNs", () => { + const wwpns = ["0x500507630703d3b3", "0x500507630708d3b3"]; + + const controllerProxy = { + GetWWPNs: jest.fn().mockReturnValue(wwpns) + }; + + beforeEach(() => { + cockpitProxies.zfcpController = { + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + }; + }); + + it("returns a list with the WWPNs of the zFCP controller", async () => { + const result = await client.zfcp.getWWPNs({ id: "1" }); + expect(result).toStrictEqual(wwpns); + }); + + describe("if there is no proxy", () => { + beforeEach(() => { + cockpitProxies.zfcpController = {}; + }); + + it("returns undefined", async () => { + const result = await client.zfcp.getWWPNs({ id: "1" }); + expect(result).toBeUndefined(); + }); + }); + }); + + describe("#getLUNs", () => { + const luns = { + "0x500507630703d3b3": ["0x0000000000000000", "0x0000000000000001", "0x0000000000000002"] + }; + + const controllerProxy = { + GetLUNs: jest.fn().mockImplementation(wwpn => luns[wwpn]) + }; + + beforeEach(() => { + cockpitProxies.zfcpController = { + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + }; + }); + + it("returns a list with the LUNs for a WWPN of the zFCP controller", async () => { + const result = await client.zfcp.getLUNs({ id: "1" }, "0x500507630703d3b3"); + expect(result).toStrictEqual(luns["0x500507630703d3b3"]); + }); + + describe("if there is no proxy", () => { + beforeEach(() => { + cockpitProxies.zfcpController = {}; + }); + + it("returns undefined", async () => { + const result = await client.zfcp.getLUNs({ id: "1" }, "0x500507630703d3b3"); + expect(result).toBeUndefined(); + }); + }); + }); + + describe("#activateController", () => { + const activateFn = jest.fn().mockReturnValue(0); + + const controllerProxy = { + Activate: activateFn + }; + + beforeEach(() => { + cockpitProxies.zfcpController = { + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + }; + }); + + it("tries to activate the given zFCP controller", async () => { + const result = await client.zfcp.activateController({ id: "1" }); + expect(activateFn).toHaveBeenCalled(); + expect(result).toEqual(0); + }); + + describe("if there is no proxy", () => { + beforeEach(() => { + cockpitProxies.zfcpController = {}; + }); + + it("returns undefined", async () => { + const result = await client.zfcp.activateController({ id: "1" }); + expect(result).toBeUndefined(); + }); + }); + }); + + describe("#activateDisk", () => { + const activateDiskFn = jest.fn().mockReturnValue(0); + + const controllerProxy = { + ActivateDisk: activateDiskFn + }; + + beforeEach(() => { + cockpitProxies.zfcpController = { + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + }; + }); + + it("tries to activate the given zFCP disk", async () => { + const result = await client.zfcp.activateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + expect(activateDiskFn).toHaveBeenCalledWith("0x500507630703d3b3", "0x0000000000000000"); + expect(result).toEqual(0); + }); + + describe("if there is no proxy", () => { + beforeEach(() => { + cockpitProxies.zfcpController = {}; + }); + + it("returns undefined", async () => { + const result = await client.zfcp.activateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + expect(result).toBeUndefined(); + }); + }); + }); + + describe("#deactivateDisk", () => { + const deactivateDiskFn = jest.fn().mockReturnValue(0); + + const controllerProxy = { + ActivateDisk: deactivateDiskFn + }; + + beforeEach(() => { + cockpitProxies.zfcpController = { + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + }; + }); + + it("tries to deactivate the given zFCP disk", async () => { + const result = await client.zfcp.activateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + expect(deactivateDiskFn).toHaveBeenCalledWith("0x500507630703d3b3", "0x0000000000000000"); + expect(result).toEqual(0); + }); + + describe("if there is no proxy", () => { + beforeEach(() => { + cockpitProxies.zfcpController = {}; + }); + + it("returns undefined", async () => { + const result = await client.zfcp.deactivateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + expect(result).toBeUndefined(); + }); + }); + }); + + describe("#onControllerChanged", () => { + it("runs the handler when a zFCP controller changes", async () => { + const handler = jest.fn(); + await client.zfcp.onControllerChanged(handler); + + emitSignals(controllersCallbacks, "changed", { + path: "/org/opensuse/Agama/Storage1/zfcp_controllers/1", + Active: true, + LUNScan: true, + Channel: "0.0.fa00" + }); + + expect(handler).toHaveBeenCalledWith({ + id: "1", active: true, lunScan: true, channel: "0.0.fa00" + }); + }); + }); + + describe("#onDiskAdded", () => { + it("runs the handler when a zFCP disk is added", async () => { + const handler = jest.fn(); + await client.zfcp.onDiskAdded(handler); + + emitSignals(disksCallbacks, "added", { + path: "/org/opensuse/Agama/Storage1/zfcp_disks/1", + Name: "/dev/sda", + Channel: "0.0.fa00", + WWPN: "0x500507630703d3b3", + LUN: "0x0000000000000000" + }); + + expect(handler).toHaveBeenCalledWith({ + id: "1", + name: "/dev/sda", + channel: "0.0.fa00", + wwpn: "0x500507630703d3b3", + lun: "0x0000000000000000" + }); + }); + }); + + describe("#onDiskChanged", () => { + it("runs the handler when a zFCP disk changes", async () => { + const handler = jest.fn(); + await client.zfcp.onDiskChanged(handler); + + emitSignals(disksCallbacks, "changed", { + path: "/org/opensuse/Agama/Storage1/zfcp_disks/1", + Name: "/dev/sda", + Channel: "0.0.fa00", + WWPN: "0x500507630703d3b3", + LUN: "0x0000000000000000" + }); + + expect(handler).toHaveBeenCalledWith({ + id: "1", + name: "/dev/sda", + channel: "0.0.fa00", + wwpn: "0x500507630703d3b3", + lun: "0x0000000000000000" + }); + }); + }); + + describe("#onDiskRemoved", () => { + it("runs the handler when a zFCP disk is removed", async () => { + const handler = jest.fn(); + await client.zfcp.onDiskRemoved(handler); + + emitSignals(disksCallbacks, "removed", { + path: "/org/opensuse/Agama/Storage1/zfcp_disks/1", + Name: "/dev/sda", + Channel: "0.0.fa00", + WWPN: "0x500507630703d3b3", + LUN: "0x0000000000000000" + }); + + expect(handler).toHaveBeenCalledWith({ + id: "1", + name: "/dev/sda", + channel: "0.0.fa00", + wwpn: "0x500507630703d3b3", + lun: "0x0000000000000000" + }); + }); + }); +}); + describe("#iscsi", () => { beforeEach(() => { client = new StorageClient(); From 64f75673358dad81e252c01eda5345ce604dd848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 20 Jun 2023 10:25:16 +0100 Subject: [PATCH 3/3] [web] Cspell --- web/cspell.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/cspell.json b/web/cspell.json index 31f9953595..51faca87f9 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -23,6 +23,7 @@ "autoconnect", "btrfs", "ccmp", + "chzdev", "dasd", "dasds", "dbus", @@ -60,7 +61,9 @@ "testsuite", "textinput", "tkip", - "udev" + "udev", + "wwpn", + "zfcp" ] } ],