diff --git a/src/components/common/DynamicListForm.jsx b/src/components/common/DynamicListForm.jsx
deleted file mode 100644
index 70976f794..000000000
--- a/src/components/common/DynamicListForm.jsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Button } from "@patternfly/react-core/dist/esm/components/Button";
-import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState";
-import { FormFieldGroup, FormFieldGroupHeader } from "@patternfly/react-core/dist/esm/components/Form";
-import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText";
-
-import './DynamicListForm.scss';
-
-export class DynamicListForm extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- list: [],
- };
- this.keyCounter = 0;
- this.removeItem = this.removeItem.bind(this);
- this.addItem = this.addItem.bind(this);
- this.onItemChange = this.onItemChange.bind(this);
- }
-
- removeItem(idx, field, value) {
- this.setState(state => {
- const items = state.list.concat();
- items.splice(idx, 1);
- return { list: items };
- }, () => this.props.onChange(this.state.list.concat()));
- }
-
- addItem() {
- this.setState(state => {
- return { list: [...state.list, Object.assign({ key: this.keyCounter++ }, this.props.default)] };
- }, () => this.props.onChange(this.state.list.concat()));
- }
-
- onItemChange(idx, field, value) {
- this.setState(state => {
- const items = state.list.concat();
- items[idx][field] = value || null;
- return { list: items };
- }, () => this.props.onChange(this.state.list.concat()));
- }
-
- render () {
- const { id, label, actionLabel, formclass, emptyStateString, helperText } = this.props;
- const dialogValues = this.state;
- return (
- {actionLabel}}
- />
- } className={"dynamic-form-group " + formclass}>
- {
- dialogValues.list.length
- ? <>
- {dialogValues.list.map((item, idx) => {
- return React.cloneElement(this.props.itemcomponent, {
- idx,
- item,
- id: id + "-" + idx,
- key: idx,
- onChange: this.onItemChange,
- removeitem: this.removeItem,
- additem: this.addItem,
- options: this.props.options,
- itemCount: Object.keys(dialogValues.list).length,
- });
- })
- }
- {helperText &&
-
- {helperText}
-
- }
- >
- :
-
- {emptyStateString}
-
-
- }
-
- );
- }
-}
-DynamicListForm.propTypes = {
- emptyStateString: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
- id: PropTypes.string.isRequired,
- itemcomponent: PropTypes.object.isRequired,
- formclass: PropTypes.string,
- options: PropTypes.object,
-};
diff --git a/src/components/common/DynamicListForm.scss b/src/components/common/DynamicListForm.scss
deleted file mode 100644
index 1dfe40db8..000000000
--- a/src/components/common/DynamicListForm.scss
+++ /dev/null
@@ -1,39 +0,0 @@
-@import "global-variables";
-
-.dynamic-form-group {
- .pf-v5-c-empty-state {
- padding: 0;
- }
-
- .pf-v5-c-form__label {
- // Don't allow labels to wrap
- white-space: nowrap;
- }
-
- .remove-button-group {
- // Move 'Remove' button the the end of the row
- grid-column: -1;
- // Move 'Remove' button to the bottom of the line so as to align with the other form fields
- display: flex;
- align-items: flex-end;
- }
-
- // Set check to the same height as input widgets and vertically align
- .pf-v5-c-form__group-control > .pf-v5-c-check {
- // Set height to the same as inputs
- // Font height is font size * line height (1rem * 1.5)
- // Widgets have 5px padding, 1px border (top & bottom): (5 + 1) * 2 = 12
- // This all equals to 36px
- height: calc(var(--pf-v5-global--FontSize--md) * var(--pf-v5-global--LineHeight--md) + 12px);
- align-content: center;
- }
-
- // We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header
- // However we want to save space and not add indent to the left so we need to override it
- .pf-v5-c-form__field-group-body {
- // Stretch content fully
- --pf-v5-c-form__field-group-body--GridColumn: 1 / -1;
- // Reduce padding at the top
- --pf-v5-c-form__field-group-body--PaddingTop: var(--pf-v5-global--spacer--xs);
- }
-}
diff --git a/src/components/create-vm-dialog/createVmDialog.jsx b/src/components/create-vm-dialog/createVmDialog.jsx
index d1e9dfed1..509ac9fde 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,44 @@ const UsersConfigurationRow = ({
);
};
+const SshKeysRow = ({
+ id, item, onChange, idx, removeitem,
+}) => {
+ const [finalized, setFinalized] = useState(false);
+
+ const validateKey = (key) => {
+ cockpit.script(`echo ${key} | ssh-keygen -l -f /dev/stdin`, {})
+ .then(() => setFinalized(true))
+ .catch(() => setFinalized(false));
+ };
+
+ return (
+
+
+ {finalized
+ ? {item.value}
+ :
+
+ }
+ onClick={() => removeitem(idx)} />
+
+
+ );
+};
+
const CloudInitOptionsRow = ({
onValueChanged,
rootPassword,
@@ -755,13 +797,21 @@ const CloudInitOptionsRow = ({
validationFailed,
}) => {
return (
-
+ <>
+
+ onValueChanged('sshKeys', value)}
+ itemcomponent={ } />
+ >
);
};
@@ -979,6 +1029,7 @@ class CreateVmModal extends React.Component {
userLogin: '',
accessToken: '',
offlineToken: '',
+ sshKeys: [],
};
this.onCreateClicked = this.onCreateClicked.bind(this);
this.onValueChanged = this.onValueChanged.bind(this);
@@ -1172,6 +1223,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/scripts/install_machine.py b/src/scripts/install_machine.py
index f5626cd26..21cbb6ea6 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 args['sshKeys'] and len(args['sshKeys']) > 9:
+ 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