diff --git a/src/components/create-vm-dialog/createVmDialog.jsx b/src/components/create-vm-dialog/createVmDialog.jsx
index d1e9dfed1..ca493a4a1 100644
--- a/src/components/create-vm-dialog/createVmDialog.jsx
+++ b/src/components/create-vm-dialog/createVmDialog.jsx
@@ -24,6 +24,7 @@ import { Divider } from "@patternfly/react-core/dist/esm/components/Divider";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid";
import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup";
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
import { Select as PFSelect, SelectGroup, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select";
@@ -33,7 +34,7 @@ import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";
import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea";
import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner";
-import { ExternalLinkAltIcon } from '@patternfly/react-icons';
+import { ExternalLinkAltIcon, TrashIcon } from '@patternfly/react-icons';
import { DialogsContext } from 'dialogs.jsx';
import cockpit from 'cockpit';
@@ -84,6 +85,7 @@ import { domainCreate } from '../../libvirtApi/domain.js';
import { storagePoolRefresh } from '../../libvirtApi/storagePool.js';
import { getAccessToken } from '../../libvirtApi/rhel-images.js';
import { PasswordFormFields, password_quality } from 'cockpit-components-password.jsx';
+import { DynamicListForm } from 'DynamicListForm.jsx';
import './createVmDialog.scss';
@@ -248,6 +250,8 @@ function validateParams(vmParams) {
}
if (vmParams.userPassword && !vmParams.userLogin) {
validationFailed.userLogin = _("User login must not be empty when user password is set");
+ } else if (vmParams.sshKeys.length > 0 && !vmParams.userLogin) {
+ validationFailed.userLogin = _("User login must not be empty when SSH keys are set");
}
return validationFailed;
@@ -748,6 +752,65 @@ const UsersConfigurationRow = ({
);
};
+// This method needs to be outside of component as re-render would create a new instance of debounce
+const validateKey = debounce(500, (key, setKeyObject) => {
+ if (isEmpty(key))
+ return
+
+ return cockpit.script(`echo ${key} | ssh-keygen -l -f /dev/stdin`, {})
+ .then(() => {
+ const parts = key.split(" ");
+ if (parts.length > 2) {
+ setKeyObject({
+ type: parts[0],
+ data: parts[1],
+ comment: parts[2], // comment is optional in SSH-format
+ });
+ }
+ })
+ .catch(() => setKeyObject({ warning: "Could not validate a public key" }));
+});
+
+const SshKeysRow = ({
+ id, item, onChange, idx, removeitem,
+}) => {
+ const [keyObject, setKeyObject] = useState();
+
+ return (
+
+
+
+ {keyObject?.data
+ ?
+ {keyObject.comment}
+ {keyObject.comment ? " - " + keyObject.type : keyObject.type}
+ {keyObject.data}
+
+ :
+
+
+ }
+ onClick={() => removeitem(idx)} />
+
+
+ );
+};
+
const CloudInitOptionsRow = ({
onValueChanged,
rootPassword,
@@ -755,13 +818,21 @@ const CloudInitOptionsRow = ({
validationFailed,
}) => {
return (
-
+ <>
+
+ onValueChanged('sshKeys', value)}
+ itemcomponent={ } />
+ >
);
};
@@ -979,6 +1050,7 @@ class CreateVmModal extends React.Component {
userLogin: '',
accessToken: '',
offlineToken: '',
+ sshKeys: [],
};
this.onCreateClicked = this.onCreateClicked.bind(this);
this.onValueChanged = this.onValueChanged.bind(this);
@@ -1172,6 +1244,7 @@ class CreateVmModal extends React.Component {
userPassword: this.state.userPassword,
rootPassword: this.state.rootPassword,
userLogin: this.state.userLogin,
+ sshKeys: this.state.sshKeys.map(key => key.value),
startVm,
accessToken: this.state.accessToken,
loggedUser
diff --git a/src/libvirtApi/domain.js b/src/libvirtApi/domain.js
index bbc605b45..edc4cd3c5 100644
--- a/src/libvirtApi/domain.js
+++ b/src/libvirtApi/domain.js
@@ -267,7 +267,8 @@ export function domainCreate({
userPassword,
vmName,
accessToken,
- loggedUser
+ loggedUser,
+ sshKeys,
}) {
// shows dummy vm until we get vm from virsh (cleans up inProgress)
setVmCreateInProgress(vmName, connectionName, { openConsoleTab: startVm });
@@ -297,6 +298,7 @@ export function domainCreate({
userLogin,
userPassword,
vmName,
+ sshKeys,
};
logDebug(`CREATE_VM(${vmName}): install_machine.py '${args}'`);
diff --git a/src/scripts/install_machine.py b/src/scripts/install_machine.py
index f5626cd26..0f95678d1 100755
--- a/src/scripts/install_machine.py
+++ b/src/scripts/install_machine.py
@@ -133,6 +133,10 @@ def prepare_cloud_init(args):
if args['userLogin']:
user_data_file.write("users:\n")
user_data_file.write(f" - name: {args['userLogin']}\n")
+ if 'sshKeys' in args and len(args['sshKeys']) > 0:
+ user_data_file.write(" ssh_authorized_keys:\n")
+ for key in args['sshKeys']:
+ user_data_file.write(f" - {key}\n")
if args['rootPassword'] or args['userPassword']:
# enable SSH password login if any password is set
diff --git a/test/check-machines-create b/test/check-machines-create
index ecdd1a220..635984606 100755
--- a/test/check-machines-create
+++ b/test/check-machines-create
@@ -49,6 +49,18 @@ class TestMachinesCreate(VirtualMachinesCase):
self.login_and_go("/machines")
self.waitPageInit()
+ # user password
+ runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, sourceType='cloud',
+ storage_size=10, storage_size_unit='MiB',
+ location=config.VALID_DISK_IMAGE_PATH,
+ root_password="foobar",
+ user_login="",
+ ssh_keys=[
+ "ssh-ed25519 aaaac3nzac1lzdi1nte5aaaaimtkmgapextexfd9bndtq23udz5dsdexaat9y16t7lgw skobyda@redhat.com"
+ ],
+ create_and_run=True),
+ {"create-vm-dialog-user-login": "User login must not be empty when SSH keys are set"})
+
# test create and import [VM] buttons show appropriate tooltips when hovered over
runner.createAndImportTooltipsTest()
@@ -276,6 +288,37 @@ class TestMachinesCreate(VirtualMachinesCase):
os_short_id=config.FEDORA_28_SHORTID,
create_and_run=True))
+ # Try to create VM with 1 SSH key
+ runner.createCloudBaseImageTest(TestMachinesCreate.VmDialog(self, sourceType='cloud',
+ storage_size=10, storage_size_unit='MiB',
+ location=config.VALID_DISK_IMAGE_PATH,
+ os_name=config.FEDORA_28,
+ os_short_id=config.FEDORA_28_SHORTID,
+ user_password="catsaremybestfr13nds",
+ user_login="foo",
+ root_password="dogsaremybestfr13nds",
+ ssh_keys=[
+ "ssh-ed25519 aaaac3nzac1lzdi1nte5aaaaimtkmgapextexfd9bndtq23udz5dsdexaat9y16t7lgw skobyda@redhat.com"
+ ],
+ create_and_run=True))
+
+ # try to create VM with multiple SSH keys
+ # --cloud-init user-data option exists since virt-install >= 3.0.0
+ runner.createCloudBaseImageTest(TestMachinesCreate.VmDialog(self, sourceType='cloud',
+ storage_size=10, storage_size_unit='MiB',
+ location=config.VALID_DISK_IMAGE_PATH,
+ os_name=config.FEDORA_28,
+ os_short_id=config.FEDORA_28_SHORTID,
+ user_password="catsaremybestfr13nds",
+ user_login="foo",
+ root_password="dogsaremybestfr13nds",
+ ssh_keys=[
+ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMTkmGaPexteXFD9bNdTq23UdZ5dsDexAAT9Y16T7lgW skobyda@redhat.com",
+ "ssh-dss AAAAB3NzaC1kc3MAAACBAKCR4rAzAT5K1RAwFehM+LyXN4/y9XNf+nzafUXVi9UptLf9/RxMAmlNpXERAL3Mx2vHeaN89I5pe/lBnSvywy2nO9Rxv+y2RQOhEV8UBkJ+i/xU1VWCxVTH1xjMIbopFt76Rs9TPsd6ZwjQM4W05BY5+87kFACSlLVqUZcP6VabAAAAFQCinamJ3el0kZvVTUSqb/dyjpFUkwAAAIB11fpN/V5taX83nvwwtEhRw7e1j450/neR7Ybfi2P6OKtBOc/AHO8F3s1rpZDqbtXrVFui3xUTpn2qofkxntpXi2/vSEma8y/fCuLV1IHU5Lc5Hczhjtuh2MoY1afEGT5FkMQuDeiT8rIV5IEgV+mcubS6bHHY6Cb8ZtGJG5okWgAAAIBif7pE29kRKYtoC/6WiwLBTwHM92vJeA5+hl4b3dRVRVYQ8kQzN28cZ0cmbDJsJ00rS4ZnOS7+zrRzx1ERx8q7LxvLQMpZ1dnV4vp+29ZNzxzH6A9c9xwOAg3jHG7cUfevGOu/WU9B0Yy0aOMRAt1nMhsdps5ce6bLxufWNy2Xsg== skobyda@fedora", # noqa: E501
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDDgWb+2py/yxLEj9+wB+q35vQ1wnHtVLKMzuTZP6mMvDwFllFhne3CcDfQwn/mRSbAdeWUmbNQrAXrbzg+IzRk69ma78wZ/8VaA/wD8BKmRL+qjBVVV5RmNG5FyGLejmiTMSsqTN19Vsjk9k02KZ4LDK+Vg8Pb467chNac684G1wtsGhyVXuYHK2xjyK2dkcev7En8SKulE2xKOVqeztQJ8dIdqV7nFmcmr1yYiiPzgVm/29rgan8YBp6YoZ8ux4F1QBx0tPvTNczDJh5AAKxwU23ZgKYsdp4JP2NQ7UbydPmsiSwjHwYCuQ2MHunRrxWlDi0UGrjQWbJad/UCSpbNHmQ5J+SNixOvtb5F4xg3uoXf9NTlK8B+hAPghNnWTsZrCcBdSNg2puqsJV2fIdxkUlS1dJWo7nTGt4WqaN7tAS/3zL6zIxiKjyjf6jiTZMCs56/+iWmiaitW0YLAGG1JXiClbfjbzAPM4FyWF2XOdK0nuazGH1UOKaVyaCcykg8=", # noqa: E501
+ ],
+ create_and_run=True))
+
def testCreateDownloadAnOS(self):
runner = TestMachinesCreate.CreateVmRunner(self)
config = TestMachinesCreate.TestCreateConfig
@@ -802,6 +845,7 @@ vnc_password= "{vnc_passwd}"
root_password=None,
user_password=None,
user_login=None,
+ ssh_keys=None,
storage_pool=NEW_VOLUME_QCOW2, storage_volume='',
create_and_run=False,
delete=True,
@@ -847,6 +891,7 @@ vnc_password= "{vnc_passwd}"
self.root_password = root_password
self.user_password = user_password
self.user_login = user_login
+ self.ssh_keys = ssh_keys
self.create_and_run = create_and_run or is_unattended
self.storage_pool = storage_pool
self.storage_volume = storage_volume
@@ -1072,6 +1117,10 @@ vnc_password= "{vnc_passwd}"
self.assertIn("\nssh_pwauth: true", user_data)
+ if self.ssh_keys is not None:
+ for key in self.ssh_keys:
+ self.assertIn(key, user_data)
+
# --unattended option is conflicting with --cloud-init option, resulting --cloud-init user_data being ignored
# https://bugzilla.redhat.com/show_bug.cgi?id=2096200#c14
self.assertNotIn("--unattended", virt_install_cmd_out)
@@ -1233,6 +1282,12 @@ vnc_password= "{vnc_passwd}"
b.set_input_text("#user-login", self.user_login)
if self.root_password:
b.set_input_text("#create-vm-dialog-root-password-pw1", self.root_password)
+ if self.ssh_keys is not None:
+ for idx, key in enumerate(self.ssh_keys):
+ b.click("button:contains(Add SSH keys)")
+ b.set_input_text(f"#create-vm-dialog-ssh-key-{idx} textarea", key, value_check=False, blur=False)
+ # Check that ssh key was validated
+ # TODO b.wait_visible(f"#create-vm-dialog-ssh-key-{idx} #validated")
if self.is_unattended:
b.wait_visible("#create-and-edit[aria-disabled=true]")
diff --git a/test/reference b/test/reference
index 840b91f14..bf21db262 160000
--- a/test/reference
+++ b/test/reference
@@ -1 +1 @@
-Subproject commit 840b91f1449235ce71461a36355dbda193293d0a
+Subproject commit bf21db262f0332ed31aee6468236380c6041bd86