diff --git a/ui/webui/src/apis/storage.js b/ui/webui/src/apis/storage.js
index a3ec1271a3c..a543d6d4e44 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 00000000000..32ef3f1bf81
--- /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 00000000000..9af4b82adf7
--- /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={
+ <>
+
+
+ >
+ }
+ >
+
+
+ );
+};
+
+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={
+ <>
+
+
+ >
+ }
+ >
+
+
+ );
+};
diff --git a/ui/webui/src/components/storage/InstallationDestination.jsx b/ui/webui/src/components/storage/InstallationDestination.jsx
index ca2a556ba5d..d115a4539c8 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 b8dd5dde587..aa01236e303 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 00000000000..c0f6f3269ef
--- /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 7ad213f7ca1..dd73aef71dd 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 0ff657cc6e6..3132d9c56b1 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 7f110632b78..abadb9505e0 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 00000000000..6e11c246c0f
--- /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 00000000000..02bfa9c3fde
--- /dev/null
+++ b/ui/webui/test/helpers/storage_iscsi.py
@@ -0,0 +1,127 @@
+#!/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 .
import os
+import random
import socket
import subprocess
import sys
@@ -136,6 +137,9 @@ def start(self):
else:
boot_arg = ""
+ mac = random.randint(0, 255)
+ mac = f'52:54:01:{(mac >> 16) & 0xff:02x}:{(mac >> 8) & 0xff:02x}:{mac & 0xff:02x}'
+
try:
self._execute(
"virt-install "
@@ -149,13 +153,15 @@ def start(self):
"--noautoconsole "
f"--graphics vnc,listen={self.ssh_address} "
"--extra-args "
- f"'inst.sshd inst.webui.remote inst.webui inst.updates=http://10.0.2.2:{self.http_updates_img_port}/updates.img' "
+ f"'inst.sshd inst.nokill inst.webui.remote inst.webui inst.updates=http://10.0.2.2:{self.http_updates_img_port}/updates.img' "
"--network none "
f"--qemu-commandline="
"'-netdev user,id=hostnet0,"
f"hostfwd=tcp:{self.ssh_address}:{self.ssh_port}-:22,"
f"hostfwd=tcp:{self.web_address}:{self.web_port}-:80 "
- "-device virtio-net-pci,netdev=hostnet0,id=net0,addr=0x16' "
+ "-device virtio-net-pci,netdev=hostnet0,id=net0,addr=0x16 "
+ "-netdev socket,mcast=230.0.0.1:5500,id=mcast0,localaddr=127.0.0.1 "
+ f"-device virtio-net-pci,netdev=mcast0,mac={mac},addr=0x0f' "
f"--initrd-inject {self.payload_ks_path} "
f"--extra-args '{extra_args}' "
f"--disk path={disk_image},bus=virtio,cache=unsafe "
@@ -165,7 +171,7 @@ def start(self):
# Live install ISO does not have sshd service enabled by default
# so we can't run any Machine.* methods on it.
if not self.is_live():
- Machine.wait_boot(self)
+ Machine.wait_boot(self, timeout_sec=180)
for _ in range(30):
try:
diff --git a/ui/webui/test/reference b/ui/webui/test/reference
index e05382c968a..f3b0d1079f9 160000
--- a/ui/webui/test/reference
+++ b/ui/webui/test/reference
@@ -1 +1 @@
-Subproject commit e05382c968a80a76c717eca43882e12f982bff22
+Subproject commit f3b0d1079f954b0719fc162884210f54acf93726