From 80e1d1275da7e1bd30608d5cdc0d4fee83aa275e Mon Sep 17 00:00:00 2001 From: zhangjingwei Date: Fri, 22 Mar 2024 16:32:20 +0800 Subject: [PATCH] feat: support non-root users to log in Support vm password login for non-root users when creating vm/ironic Change-Id: Iaf692e333686d2563013c0ea41777da8c772ce35 --- ...pport-non-Root-Users-0792a1ba891b28eb.yaml | 6 ++ .../actions/CreateIronic/SystemStep/index.jsx | 56 ++++++++++++++--- .../Instance/actions/CreateIronic/index.jsx | 5 +- .../actions/StepCreate/SystemStep/index.jsx | 60 ++++++++++++------- .../Instance/actions/StepCreate/index.jsx | 5 +- src/resources/nova/instance.jsx | 12 ++-- .../pages/compute/instance.spec.js | 1 + .../integration/pages/compute/ironic.spec.js | 1 + .../pages/compute/server-group.spec.js | 1 + test/e2e/support/resource-commands.js | 2 + 10 files changed, 113 insertions(+), 36 deletions(-) create mode 100644 releasenotes/notes/Support-non-Root-Users-0792a1ba891b28eb.yaml diff --git a/releasenotes/notes/Support-non-Root-Users-0792a1ba891b28eb.yaml b/releasenotes/notes/Support-non-Root-Users-0792a1ba891b28eb.yaml new file mode 100644 index 00000000..c723c980 --- /dev/null +++ b/releasenotes/notes/Support-non-Root-Users-0792a1ba891b28eb.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Support non-root users can log in to VM: + + * When creating a vm/ironic, log in with a password. The username is required. The username comes from the image configuration or user input. Non-root is supported. diff --git a/src/pages/compute/containers/Instance/actions/CreateIronic/SystemStep/index.jsx b/src/pages/compute/containers/Instance/actions/CreateIronic/SystemStep/index.jsx index b937c0c1..4d0af49d 100644 --- a/src/pages/compute/containers/Instance/actions/CreateIronic/SystemStep/index.jsx +++ b/src/pages/compute/containers/Instance/actions/CreateIronic/SystemStep/index.jsx @@ -39,11 +39,28 @@ export class SystemStep extends Base { })); } + get imageInfo() { + const { context = {} } = this.props; + const { image = {} } = context || {}; + const { selectedRows = [] } = image; + return selectedRows.length && selectedRows[0]; + } + + get loginUserName() { + return this.imageInfo?.os_admin_user; + } + + get loginUserNameInContext() { + const { username = '' } = this.props.context || {}; + return username || ''; + } + get defaultValue() { const { context = {} } = this.props; const data = { loginType: context.loginType || this.loginTypes[0], more: false, + username: this.loginUserName || this.loginUserNameInContext, }; return data; } @@ -71,10 +88,32 @@ export class SystemStep extends Base { return ['loginType', 'password', 'confirmPassword']; } - get formItems() { + get isPassword() { const { loginType } = this.state; - const isPassword = loginType === this.loginTypes[1].value; + return loginType === this.loginTypes[1].value; + } + get usernameFormItem() { + const item = { + name: 'username', + label: t('Login Name'), + type: 'input', + extra: this.loginUserName + ? '' + : t( + "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown." + ), + tip: t( + 'Whether the Login Name can be used is up to the feasible configuration of cloud-init or cloudbase-init service in the image.' + ), + required: this.isPassword, + hidden: !this.isPassword, + }; + item.disabled = !!this.loginUserName; + return item; + } + + get formItems() { return [ { name: 'name', @@ -91,6 +130,7 @@ export class SystemStep extends Base { options: this.loginTypes, isWrappedValue: true, }, + this.usernameFormItem, { name: 'keypair', label: t('Keypair'), @@ -98,8 +138,8 @@ export class SystemStep extends Base { data: this.keypairs, isLoading: this.keyPairStore.list.isLoading, isMulti: false, - required: !isPassword, - hidden: isPassword, + required: !this.isPassword, + hidden: this.isPassword, tip: t( 'The SSH key is a way to remotely log in to the instance. The cloud platform only helps to keep the public key. Please keep your private key properly.' ), @@ -125,16 +165,16 @@ export class SystemStep extends Base { name: 'password', label: t('Password'), type: 'input-password', - required: isPassword, - hidden: !isPassword, + required: this.isPassword, + hidden: !this.isPassword, otherRule: getPasswordOtherRule('password', 'instance'), }, { name: 'confirmPassword', label: t('Confirm Password'), type: 'input-password', - required: isPassword, - hidden: !isPassword, + required: this.isPassword, + hidden: !this.isPassword, otherRule: getPasswordOtherRule('confirmPassword', 'instance'), }, ]; diff --git a/src/pages/compute/containers/Instance/actions/CreateIronic/index.jsx b/src/pages/compute/containers/Instance/actions/CreateIronic/index.jsx index 54013efb..2db72c6b 100644 --- a/src/pages/compute/containers/Instance/actions/CreateIronic/index.jsx +++ b/src/pages/compute/containers/Instance/actions/CreateIronic/index.jsx @@ -327,7 +327,10 @@ export class CreateIronic extends StepAction { server.return_reservation_id = true; } if (server.adminPass || userData) { - server.user_data = btoa(getUserData(server.adminPass, userData)); + const { username } = values; + server.user_data = btoa( + getUserData(server.adminPass, userData, username || 'root') + ); } return { server, diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx index d5c55d33..6907dba4 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx @@ -147,6 +147,7 @@ export class SystemStep extends Base { more: false, physicalNodeType: physicalNodeTypes[0], userData: '', + username: this.loginUserName || this.loginUserNameInContext, }; if (servergroup) { data.serverGroup = { @@ -210,6 +211,11 @@ export class SystemStep extends Base { return this.sourceInfo && this.sourceInfo.os_admin_user; } + get loginUserNameInContext() { + const { username = '' } = this.props.context || {}; + return username || ''; + } + onValuesChange = (changedFields) => { if (has(changedFields, 'serverGroup')) { this.onServerGroupChange(changedFields.serverGroup); @@ -223,9 +229,33 @@ export class SystemStep extends Base { }); }; + get isPassword() { + const { loginType } = this.state; + return loginType === this.loginTypes[1].value; + } + + get usernameFormItem() { + const item = { + name: 'username', + label: t('Login Name'), + type: 'input', + extra: this.loginUserName + ? '' + : t( + "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown." + ), + tip: t( + 'Whether the Login Name can be used is up to the feasible configuration of cloud-init or cloudbase-init service in the image.' + ), + required: this.isPassword, + hidden: !this.isPassword, + }; + item.disabled = !!this.loginUserName; + return item; + } + get formItems() { - const { loginType, more = false, physicalNodeType } = this.state; - const isPassword = loginType === this.loginTypes[1].value; + const { more = false, physicalNodeType } = this.state; const isManually = physicalNodeType === physicalNodeTypes[1].value; const { initKeyPair } = this.state; @@ -245,27 +275,15 @@ export class SystemStep extends Base { options: this.loginTypes, isWrappedValue: true, }, - { - name: 'username', - label: t('Login Name'), - content: this.loginUserName || '-', - extra: this.loginUserName - ? '' - : t( - "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown." - ), - tip: t( - 'Whether the Login Name can be used is up to the feasible configuration of cloud-init or cloudbase-init service in the image.' - ), - }, + this.usernameFormItem, { name: 'keypair', label: t('Keypair'), type: 'select-table', data: this.keypairs, isLoading: this.keyPairStore.list.isLoading, - required: !isPassword, - hidden: isPassword, + required: !this.isPassword, + hidden: this.isPassword, header: getKeyPairHeader(this), initValue: initKeyPair, tip: t( @@ -293,16 +311,16 @@ export class SystemStep extends Base { name: 'password', label: t('Login Password'), type: 'input-password', - required: isPassword, - hidden: !isPassword, + required: this.isPassword, + hidden: !this.isPassword, otherRule: getPasswordOtherRule('password', 'instance'), }, { name: 'confirmPassword', label: t('Confirm Password'), type: 'input-password', - required: isPassword, - hidden: !isPassword, + required: this.isPassword, + hidden: !this.isPassword, otherRule: getPasswordOtherRule('confirmPassword', 'instance'), }, { diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx index df1abb46..64356126 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx @@ -743,7 +743,10 @@ export class StepCreate extends StepAction { physicalNode.selectedRows[0].hypervisor_hostname; } if (server.adminPass || userData) { - server.user_data = btoa(getUserData(server.adminPass, userData)); + const { username } = values; + server.user_data = btoa( + getUserData(server.adminPass, userData, username || 'root') + ); } const body = { server, diff --git a/src/resources/nova/instance.jsx b/src/resources/nova/instance.jsx index 471b5c22..03ec5dd7 100644 --- a/src/resources/nova/instance.jsx +++ b/src/resources/nova/instance.jsx @@ -218,7 +218,7 @@ const passwordAndUserData = 'Content-Disposition: attachment; filename="passwd-script.txt" \n' + '\n' + '#!/bin/sh\n' + - "echo 'root:USER_PASSWORD' | chpasswd\n" + + "echo 'USER_NAME:USER_PASSWORD' | chpasswd\n" + '\n' + '--===============2309984059743762475==\n' + 'Content-Type: text/x-shellscript; charset="us-ascii" \n' + @@ -252,7 +252,7 @@ const onlyPassword = 'Content-Disposition: attachment; filename="passwd-script.txt" \n' + '\n' + '#!/bin/sh\n' + - "echo 'root:USER_PASSWORD' | chpasswd\n" + + "echo 'USER_NAME:USER_PASSWORD' | chpasswd\n" + '\n' + '--===============2309984059743762475==--'; @@ -270,13 +270,15 @@ const onlyUserData = '\n' + '--===============2309984059743762475==--'; -export const getUserData = (password, userData) => { +export const getUserData = (password, userData, username = 'root') => { if (password && userData) { - const str = passwordAndUserData.replace(/USER_PASSWORD/g, password); + let str = passwordAndUserData.replace(/USER_PASSWORD/g, password); + str = str.replace(/USER_NAME/g, username); return str.replace(/USER_DATA/g, userData); } if (password) { - return onlyPassword.replace(/USER_PASSWORD/g, password); + const str = onlyPassword.replace(/USER_PASSWORD/g, password); + return str.replace(/USER_NAME/g, username); } return onlyUserData.replace(/USER_DATA/g, userData); }; diff --git a/test/e2e/integration/pages/compute/instance.spec.js b/test/e2e/integration/pages/compute/instance.spec.js index 669068a1..5e67eb1d 100644 --- a/test/e2e/integration/pages/compute/instance.spec.js +++ b/test/e2e/integration/pages/compute/instance.spec.js @@ -58,6 +58,7 @@ describe('The Instance Page', () => { .clickStepActionNextButton() .formInput('name', name) .formRadioChoose('loginType', 1) + .formInput('username', 'root') .formInput('password', password) .formInput('confirmPassword', password) .wait(2000) diff --git a/test/e2e/integration/pages/compute/ironic.spec.js b/test/e2e/integration/pages/compute/ironic.spec.js index fcdb63d8..a4f09b5b 100644 --- a/test/e2e/integration/pages/compute/ironic.spec.js +++ b/test/e2e/integration/pages/compute/ironic.spec.js @@ -66,6 +66,7 @@ onlyOn(ironicServiceEnabled, () => { .clickStepActionNextButton() .formInput('name', name) .formRadioChoose('loginType', 1) + .formInput('username', 'root') .formInput('password', password) .formInput('confirmPassword', password) .wait(2000) diff --git a/test/e2e/integration/pages/compute/server-group.spec.js b/test/e2e/integration/pages/compute/server-group.spec.js index 045bfb4f..ec08ff69 100644 --- a/test/e2e/integration/pages/compute/server-group.spec.js +++ b/test/e2e/integration/pages/compute/server-group.spec.js @@ -62,6 +62,7 @@ describe('The Server Group Page', () => { .clickStepActionNextButton() .formInput('name', instanceName) .formRadioChoose('loginType', 1) + .formInput('username', 'root') .formInput('password', password) .formInput('confirmPassword', password) .clickStepActionNextButton() diff --git a/test/e2e/support/resource-commands.js b/test/e2e/support/resource-commands.js index ef7fbb0d..42f4b303 100644 --- a/test/e2e/support/resource-commands.js +++ b/test/e2e/support/resource-commands.js @@ -48,6 +48,7 @@ Cypress.Commands.add('createInstance', ({ name, networkName }) => { .clickStepActionNextButton() .formInput('name', name) .formRadioChoose('loginType', 1) + .formInput('username', 'root') .formInput('password', password) .formInput('confirmPassword', password) .wait(2000) @@ -159,6 +160,7 @@ Cypress.Commands.add( .clickStepActionNextButton() .formInput('name', name) .formRadioChoose('loginType', 1) + .formInput('username', 'root') .formInput('password', password) .formInput('confirmPassword', password) .clickStepActionNextButton()