From 721c79faad67db4bdbd90fffab5bf1e30d91282c Mon Sep 17 00:00:00 2001 From: Katerina Koukiou Date: Thu, 19 Oct 2023 12:52:52 +0200 Subject: [PATCH] webui: Add support using ISCSI disks for the installation --- ui/webui/src/apis/storage.js | 10 +- ui/webui/src/apis/storage_iscsi.js | 103 ++++++ .../src/components/storage/ISCSITarget.jsx | 345 ++++++++++++++++++ .../storage/InstallationDestination.jsx | 56 ++- .../storage/InstallationDestination.scss | 13 +- .../storage/SpecializedDisksSelect.jsx | 52 +++ ui/webui/src/helpers/storage.js | 21 ++ ui/webui/src/helpers/utils.js | 18 + ui/webui/test/check-storage | 1 + ui/webui/test/check-storage-iscsi | 111 ++++++ ui/webui/test/helpers/storage_iscsi.py | 123 +++++++ ui/webui/test/reference | 2 +- 12 files changed, 817 insertions(+), 38 deletions(-) create mode 100644 ui/webui/src/apis/storage_iscsi.js create mode 100644 ui/webui/src/components/storage/ISCSITarget.jsx create mode 100644 ui/webui/src/components/storage/SpecializedDisksSelect.jsx create mode 100755 ui/webui/test/check-storage-iscsi create mode 100644 ui/webui/test/helpers/storage_iscsi.py diff --git a/ui/webui/src/apis/storage.js b/ui/webui/src/apis/storage.js index a3ec1271a3c6..a543d6d4e444 100644 --- a/ui/webui/src/apis/storage.js +++ b/ui/webui/src/apis/storage.js @@ -61,10 +61,11 @@ export class StorageClient { * @param {string} task DBus path to a task * @param {string} onSuccess Callback to run after Succeeded signal is received * @param {string} onFail Callback to run as an error handler + * @param {Boolean} getResult True if the result should be fetched, False otherwise * * @returns {Promise} Resolves a DBus path to a task */ -export const runStorageTask = ({ task, onSuccess, onFail }) => { +export const runStorageTask = ({ task, onSuccess, onFail, getResult = false }) => { // FIXME: This is a workaround for 'Succeeded' signal being emited twice let succeededEmitted = false; const taskProxy = new StorageClient().client.proxy( @@ -78,7 +79,12 @@ export const runStorageTask = ({ task, onSuccess, onFail }) => { return; } succeededEmitted = true; - onSuccess(); + + const promise = getResult ? taskProxy.GetResult() : Promise.resolve(); + + return promise + .then(res => onSuccess(res?.v)) + .catch(onFail); }); }; taskProxy.wait(() => { diff --git a/ui/webui/src/apis/storage_iscsi.js b/ui/webui/src/apis/storage_iscsi.js new file mode 100644 index 000000000000..32ef3f1bf817 --- /dev/null +++ b/ui/webui/src/apis/storage_iscsi.js @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with This program; If not, see . + */ +import cockpit from "cockpit"; +import { StorageClient, runStorageTask } from "./storage.js"; +import { objectToDBus } from "../helpers/utils.js"; +import { _callClient, _getProperty, _setProperty } from "./helpers.js"; + +const INTERFACE_NAME = "org.fedoraproject.Anaconda.Modules.Storage.iSCSI"; +const OBJECT_PATH = "/org/fedoraproject/Anaconda/Modules/Storage/iSCSI"; + +const callClient = (...args) => { + return _callClient(StorageClient, OBJECT_PATH, INTERFACE_NAME, ...args); +}; +const getProperty = (...args) => { + return _getProperty(StorageClient, OBJECT_PATH, INTERFACE_NAME, ...args); +}; +const setProperty = (...args) => { + return _setProperty(StorageClient, OBJECT_PATH, INTERFACE_NAME, ...args); +}; + +/** + * @returns {Promise} Module supported + */ +export const getIsSupported = () => { + return callClient("IsSupported", []); +}; + +/** + * @returns {Promise} Can set initiator + */ +export const getCanSetInitiator = () => { + return callClient("CanSetInitiator", []); +}; + +/** + * @returns {Promise} iSCSI initiator name + */ +export const getInitiator = () => { + return getProperty("Initiator"); +}; + +/** + * @param {string} initiator iSCSI initiator name + */ +export const setInitiator = ({ initiator }) => { + return setProperty("Initiator", cockpit.variant("s", initiator)); +}; + +/** + * @param {object} portal The portal information + * @param {object} credentials The iSCSI credentials + * @param {object} interfacesMode + */ +export const runDiscover = async ({ portal, credentials, interfacesMode = "default", onSuccess, onFail }) => { + const args = [ + { ...objectToDBus(portal) }, + { ...objectToDBus(credentials) }, + interfacesMode, + ]; + try { + const discoverWithTask = () => callClient("DiscoverWithTask", args); + const task = await discoverWithTask(); + + return runStorageTask({ task, onFail, onSuccess, getResult: true }); + } catch (error) { + onFail(error); + } +}; + +/** + * @param {object} portal The portal information + * @param {object} credentials The iSCSI credentials + * @param {object} node The iSCSI node + */ +export const runLogin = async ({ portal, credentials, node, onSuccess, onFail }) => { + const args = [ + { ...objectToDBus(portal) }, + { ...objectToDBus(credentials) }, + { ...objectToDBus(node) }, + ]; + try { + const loginWithTask = () => callClient("LoginWithTask", args); + const task = await loginWithTask(); + + return runStorageTask({ task, onFail, onSuccess }); + } catch (error) { + onFail(error); + } +}; diff --git a/ui/webui/src/components/storage/ISCSITarget.jsx b/ui/webui/src/components/storage/ISCSITarget.jsx new file mode 100644 index 000000000000..9af4b82adf77 --- /dev/null +++ b/ui/webui/src/components/storage/ISCSITarget.jsx @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with This program; If not, see . + */ +import cockpit from "cockpit"; +import React, { useEffect, useState } from "react"; + +import { useDialogs } from "dialogs.jsx"; +import { ModalError } from "cockpit-components-inline-notification.jsx"; + +import { + Button, + DropdownItem, + Flex, + FlexItem, + Form, + FormFieldGroup, + FormFieldGroupHeader, + FormGroup, + Label, + List, + ListItem, + Modal, + TextInput, +} from "@patternfly/react-core"; + +import { + getCanSetInitiator, + getInitiator, + getIsSupported, + runDiscover, + runLogin, + setInitiator +} from "../../apis/storage_iscsi.js"; +import { + objectFromDBus +} from "../../helpers/utils.js"; +import { + rescanDevices +} from "../../helpers/storage.js"; + +const _ = cockpit.gettext; +const idPrefix = "add-iscsi-target-dialog"; + +export const AddISCSITarget = ({ devices, dispatch }) => { + const [isSupported, setIsSupported] = useState(false); + const Dialogs = useDialogs(); + + useEffect(() => { + const updateFields = async () => { + const _isSupported = await getIsSupported(); + setIsSupported(_isSupported); + }; + updateFields(); + }, []); + + const open = () => Dialogs.show(); + + return ( + + {_("Add iSCSI target")} + + ); +}; + +const DiscoverISCSITargetModal = ({ devices, dispatch }) => { + const [canSetInitiator, setCanSetInitiator] = useState(false); + const [discoveryUsername, setDiscoveryUsername] = useState(""); + const [discoveryPassword, setDiscoveryPassword] = useState(""); + const [discoveredTargets, setDiscoveredTargets] = useState(); + const [isInProgress, setIsInProgress] = useState(false); + const [error, setError] = useState(null); + const [initiatorName, setInitiatorName] = useState(""); + const [targetIPAddress, setTargetIPAddress] = useState(""); + const portal = { "ip-address": targetIPAddress }; + + const Dialogs = useDialogs(); + + useEffect(() => { + const updateFields = async () => { + try { + const _initiatorName = await getInitiator(); + setInitiatorName(_initiatorName); + + const _canSetInitiator = await getCanSetInitiator(); + setCanSetInitiator(_canSetInitiator); + } catch (e) { + setError(e); + } + }; + updateFields(); + }, []); + + const onSubmit = async () => { + setError(null); + setIsInProgress(true); + await setInitiator({ initiator: initiatorName }); + await runDiscover({ + portal, + credentials: { username: discoveryUsername, password: discoveryPassword }, + onSuccess: async res => { + setDiscoveredTargets(res.map(objectFromDBus)); + setIsInProgress(false); + }, + onFail: async (exc) => { + setIsInProgress(false); + setError(exc); + }, + }); + }; + + return ( + Dialogs.close()} + title={_("Discover iSCSI targets")} + footer={ + <> + + + + } + > +
+ {error && } + + setInitiatorName(value)} + /> + + + setTargetIPAddress(value)} + value={targetIPAddress} + /> + + + } + > + + setDiscoveryUsername(value)} + /> + + + setDiscoveryPassword(value)} + /> + + + + } + > + + {Object.keys(devices) + ?.map(target => devices[target]) + .map(target => ( + + + {target.attrs?.v.target} + + + + )) || []} + {discoveredTargets + ?.map(target => ( + + + {target.name} + + + + )) || []} + + + +
+ ); +}; + +const LoginISCSITargetModal = ({ target, portal, dispatch }) => { + const [chapPassword, setChapPassword] = useState(""); + const [chapUsername, setChapUsername] = useState(""); + const [error, setError] = useState(null); + const [loginInProgress, setLoginInProgress] = useState(false); + + const Dialogs = useDialogs(); + + const onSubmit = () => { + setError(null); + setLoginInProgress(true); + + return ( + runLogin({ + portal, + credentials: { username: chapUsername, password: chapPassword }, + node: target, + onSuccess: () => { + setLoginInProgress(true); + return rescanDevices({ + onSuccess: () => Dialogs.close(), + onFail: setError, + dispatch, + }); + }, + onFail: exc => { + setLoginInProgress(false); + setError(exc); + }, + }) + ); + }; + + return ( + Dialogs.close()} + title={cockpit.format(_("Login to iSCSI target $0"), target.name)} + footer={ + <> + + + + } + > +
+ {error && } + + } + > + + setChapUsername(value)} + /> + + + setChapPassword(value)} + /> + + + +
+ ); +}; diff --git a/ui/webui/src/components/storage/InstallationDestination.jsx b/ui/webui/src/components/storage/InstallationDestination.jsx index ca2a556ba5da..d115a4539c85 100644 --- a/ui/webui/src/components/storage/InstallationDestination.jsx +++ b/ui/webui/src/components/storage/InstallationDestination.jsx @@ -40,16 +40,12 @@ import { SyncAltIcon, TimesIcon } from "@patternfly/react-icons"; import { SystemTypeContext } from "../Common.jsx"; import { ModifyStorage } from "./ModifyStorage.jsx"; +import { SpecializedDisksSelect } from "./SpecializedDisksSelect.jsx"; -import { - runStorageTask, - scanDevicesWithTask, -} from "../../apis/storage.js"; -import { resetPartitioning } from "../../apis/storage_partitioning.js"; import { setSelectedDisks } from "../../apis/storage_disks_selection.js"; -import { getDevicesAction, getDiskSelectionAction } from "../../actions/storage-actions.js"; import { debug } from "../../helpers/log.js"; +import { rescanDevices } from "../../helpers/storage.js"; import { checkIfArraysAreEqual } from "../../helpers/utils.js"; import "./InstallationDestination.scss"; @@ -83,7 +79,7 @@ const selectDefaultDisks = ({ ignoredDisks, selectedDisks, usableDisks }) => { } }; -const LocalDisksSelect = ({ deviceData, diskSelection, idPrefix, isDisabled, setSelectedDisks }) => { +const DisksSelect = ({ deviceData, diskSelection, idPrefix, isDisabled, setSelectedDisks }) => { const [isOpen, setIsOpen] = useState(false); const [inputValue, setInputValue] = useState(""); const [focusedItemIndex, setFocusedItemIndex] = useState(null); @@ -284,27 +280,18 @@ const rescanDisks = (setIsRescanningDisks, refUsableDisks, dispatch, errorHandle setIsRescanningDisks(true); setIsFormDisabled(true); refUsableDisks.current = undefined; - scanDevicesWithTask() - .then(task => { - return runStorageTask({ - task, - onSuccess: () => resetPartitioning() - .then(() => Promise.all([ - dispatch(getDevicesAction()), - dispatch(getDiskSelectionAction()) - ])) - .finally(() => { - setIsFormDisabled(false); - setIsRescanningDisks(false); - }) - .catch(errorHandler), - onFail: exc => { - setIsFormDisabled(false); - setIsRescanningDisks(false); - errorHandler(exc); - } - }); - }); + rescanDevices({ + onSuccess: () => { + setIsFormDisabled(false); + setIsRescanningDisks(false); + }, + onFail: exc => { + setIsFormDisabled(false); + setIsRescanningDisks(false); + errorHandler(exc); + }, + dispatch, + }); }; export const InstallationDestination = ({ @@ -383,8 +370,8 @@ export const InstallationDestination = ({ ); - const localDisksSelect = ( - - {_("Destination")} + + + {_("Destination")} + + + {equalDisksNotify && equalDisks && {(diskSelection.usableDisks.length > 1 || (diskSelection.usableDisks.length === 1 && diskSelection.selectedDisks.length === 0)) - ? localDisksSelect + ? disksSelect : ( diskSelection.usableDisks.length === 1 && diskSelection.selectedDisks.length === 1 ? ( diff --git a/ui/webui/src/components/storage/InstallationDestination.scss b/ui/webui/src/components/storage/InstallationDestination.scss index b8dd5dde5879..aa01236e303d 100644 --- a/ui/webui/src/components/storage/InstallationDestination.scss +++ b/ui/webui/src/components/storage/InstallationDestination.scss @@ -8,7 +8,14 @@ } } -.installation-method-target-disk-size { - color: var(--pf-v5-global--Color--200); - font-size: var(--pf-v5-global--FontSize--sm); +// Remove extra margin above form sections +.pf-v5-c-form.installation-method-selector { + .pf-v5-c-form__section { + margin-top: 0; + } + + // Make the expandable form field groups title bold + .pf-v5-c-form__field-group-header-title-text { + font-weight: var(--pf-v5-c-form__section-title--FontWeight); + } } diff --git a/ui/webui/src/components/storage/SpecializedDisksSelect.jsx b/ui/webui/src/components/storage/SpecializedDisksSelect.jsx new file mode 100644 index 000000000000..c0f6f3269ef0 --- /dev/null +++ b/ui/webui/src/components/storage/SpecializedDisksSelect.jsx @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with This program; If not, see . + */ +import cockpit from "cockpit"; +import React, { useState } from "react"; + +import { Dropdown, DropdownList, MenuToggle } from "@patternfly/react-core"; + +import { AddISCSITarget } from "./ISCSITarget.jsx"; + +const _ = cockpit.gettext; + +export const SpecializedDisksSelect = ({ deviceData, dispatch }) => { + const [isOpen, setIsOpen] = useState(false); + const iscsiDevices = Object.keys(deviceData) + .filter((device) => deviceData[device].type.v === "iscsi") + .reduce((acc, device) => { + acc[device] = deviceData[device]; + return acc; + }, {}); + + return ( + setIsOpen(false)} + onOpenChange={setIsOpen} + toggle={(toggleRef) => ( + setIsOpen(_isOpen => setIsOpen(!_isOpen))} isExpanded={isOpen}> + {_("Configure specialized & network disks")} + + )} + shouldFocusToggleOnSelect + > + + + + + ); +}; diff --git a/ui/webui/src/helpers/storage.js b/ui/webui/src/helpers/storage.js index 7ad213f7ca15..dd73aef71dd5 100644 --- a/ui/webui/src/helpers/storage.js +++ b/ui/webui/src/helpers/storage.js @@ -15,6 +15,10 @@ * along with This program; If not, see . */ +import { getDevicesAction, getDiskSelectionAction } from "../actions/storage-actions.js"; +import { scanDevicesWithTask, runStorageTask } from "../apis/storage.js"; +import { resetPartitioning } from "../apis/storage_partitioning.js"; + /* Get the list of names of all the ancestors of the given device * (including the device itself) * @param {Object} deviceData - The device data object @@ -116,3 +120,20 @@ export const hasDuplicateFields = (requests, fieldName) => { export const isDuplicateRequestField = (requests, fieldName, fieldValue) => { return requests.filter((request) => request[fieldName] === fieldValue).length > 1; }; + +export const rescanDevices = ({ onSuccess, onFail, dispatch }) => { + return scanDevicesWithTask() + .then(task => { + return runStorageTask({ + task, + onSuccess: () => resetPartitioning() + .then(() => Promise.all([ + dispatch(getDevicesAction()), + dispatch(getDiskSelectionAction()) + ])) + .finally(onSuccess) + .catch(onFail), + onFail + }); + }); +}; diff --git a/ui/webui/src/helpers/utils.js b/ui/webui/src/helpers/utils.js index 0ff657cc6e65..3132d9c56b12 100644 --- a/ui/webui/src/helpers/utils.js +++ b/ui/webui/src/helpers/utils.js @@ -15,6 +15,8 @@ * along with This program; If not, see . */ +import cockpit from "cockpit"; + /* Find duplicates in an array * @param {Array} array * @returns {Array} The duplicates @@ -38,3 +40,19 @@ export const checkIfArraysAreEqual = (array1, array2) => { array1Sorted.every((value, index) => value === array2Sorted[index]) ); }; + +/* Converts an object with variant values to an object with normal values + * @param {Object} dbusObject + * @returns {Object} The converted object + */ +export const objectFromDBus = (dbusObject) => { + return Object.keys(dbusObject).reduce((acc, k) => { acc[k] = dbusObject[k].v; return acc }, {}); +}; + +/* Converts an object with normal values to an object with variant string values + * @param {Object} object + * @returns {Object} The converted object + */ +export const objectToDBus = (object) => { + return Object.keys(object).reduce((acc, k) => { acc[k] = cockpit.variant("s", object[k]); return acc }, {}); +}; diff --git a/ui/webui/test/check-storage b/ui/webui/test/check-storage index 7f110632b788..abadb9505e05 100755 --- a/ui/webui/test/check-storage +++ b/ui/webui/test/check-storage @@ -19,6 +19,7 @@ import anacondalib from installer import Installer from storage import Storage +from storage_iscsi import StorageISCSIHelpers, StorageISCSILoginDialog, StorageISCSIDiscoverDialog from review import Review from testlib import nondestructive, test_main # pylint: disable=import-error from storagelib import StorageHelpers # pylint: disable=import-error diff --git a/ui/webui/test/check-storage-iscsi b/ui/webui/test/check-storage-iscsi new file mode 100755 index 000000000000..6e11c246c0f9 --- /dev/null +++ b/ui/webui/test/check-storage-iscsi @@ -0,0 +1,111 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2022 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; If not, see . + +import anacondalib + +from installer import Installer +from storage import Storage +from storage_iscsi import StorageISCSIHelpers, StorageISCSILoginDialog, StorageISCSIDiscoverDialog +from review import Review +from testlib import test_main # pylint: disable=import-error + +class TestStorageISCSI(anacondalib.VirtInstallMachineCase): + provision = { + "0": {"address": "10.111.113.1/20", "dns": "10.111.112.100"}, + "iscsi-server": { + "address": "10.111.113.2/20", "dns": "10.111.112.100", + "image": "fedora-rawhide", "memory_mb": 512, "inherit_machine_class": False + } + } + + def testBasicCHAP(self): + b = self.browser + m = self.machine + i = Installer(b, m) + s = Storage(b, m) + r = Review(b) + + iscsi_server = self.machines['iscsi-server'] + iscsi = StorageISCSIHelpers(iscsi_server, m) + + orig_iqn = iscsi.get_initiator_iqn() + auth_password = iscsi.auth_password + discovery_password = iscsi.discovery_password + initiator_iqn = iscsi.initiator_iqn + target_iqn = iscsi.target_iqn + user_name = iscsi.user_name + + iscsi.setup_iscsi_server() + + i.open() + i.reach(i.steps.INSTALLATION_METHOD) + + discover_dialog = StorageISCSIDiscoverDialog(b) + discover_dialog.open() + # Verify that the default value for IQN is pre-filled + b.wait_val("#add-iscsi-target-dialog-initiator-name", orig_iqn) + discover_dialog.cancel() + + # Fill incorrect password and verify error + discover_dialog = StorageISCSIDiscoverDialog( + b, initiator_iqn, "10.111.113.2", user_name, "einszweidrei" + ) + discover_dialog.open() + discover_dialog.fill() + discover_dialog.submit(xfail="initiator failed authorization") + discover_dialog.cancel() + + # Fill the dialog and submit + discover_dialog = StorageISCSIDiscoverDialog( + b, initiator_iqn, "10.111.113.2", user_name, discovery_password + ) + discover_dialog.open() + discover_dialog.fill() + b.wait_not_present("#add-iscsi-target-dialog-available-targets li") + discover_dialog.submit() + # Expect the discovered target to be present in the list + discover_dialog.check_available_targets([target_iqn]) + + # Login to the target + discover_dialog.login(target_iqn) + + # Login to the target + # Fill incorrect password and verify error + login_dialog = StorageISCSILoginDialog( + self.browser, + target_iqn, + user_name, + "einszweidrei", + ) + login_dialog.fill() + login_dialog.submit(xfail="Login failed") + + login_dialog = StorageISCSILoginDialog( + self.browser, + target_iqn, + user_name, + auth_password, + ) + login_dialog.fill() + login_dialog.submit() + + # Select the iSCSI device for the installation destination + s.select_disk("vda", False) + s.select_disk("sda", True) + +if __name__ == '__main__': + test_main() diff --git a/ui/webui/test/helpers/storage_iscsi.py b/ui/webui/test/helpers/storage_iscsi.py new file mode 100644 index 000000000000..1b93833492e7 --- /dev/null +++ b/ui/webui/test/helpers/storage_iscsi.py @@ -0,0 +1,123 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2022 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; If not, see . + +import os +import sys + +HELPERS_DIR = os.path.dirname(__file__) +sys.path.append(HELPERS_DIR) + + +class StorageISCSIHelpers(): + def __init__( + self, + server=None, + client=None, + target_iqn="iqn.2015-09.cockpit.lan", + initiator_iqn="iqn.2015-10.cockpit.lan" + ): + self.server = server + self.client = client + self.target_iqn = target_iqn + self.initiator_iqn = initiator_iqn + self.user_name = "admin" + self.discovery_password = "foobar" + self.auth_password = "barfoo" + + def setup_iscsi_server(self): + # Setup a iSCSI target with authentication for discovery + self.server.execute(""" + export TERM=dumb + targetcli /backstores/ramdisk create test 50M + targetcli /iscsi set discovery_auth enable=1 userid=admin password=%(discovery_password)s + targetcli /iscsi create %(tgt)s + targetcli /iscsi/%(tgt)s/tpg1/luns create /backstores/ramdisk/test + targetcli /iscsi/%(tgt)s/tpg1 set attribute authentication=1 + targetcli /iscsi/%(tgt)s/tpg1/acls create %(ini)s + targetcli /iscsi/%(tgt)s/tpg1/acls/%(ini)s set auth userid=admin password=%(auth_password)s + """ % { + "tgt": self.target_iqn, + "ini": self.initiator_iqn, + "discovery_password": self.discovery_password, + "auth_password": self.auth_password + }) + self.server.execute(""" + firewall-cmd --add-port=3260/tcp --permanent + systemctl reload firewalld""") + + def get_initiator_iqn(self): + return self.client.execute("sed