feat: support non-root users to log in

Support vm password login for non-root users when creating vm/ironic

Change-Id: Iaf692e333686d2563013c0ea41777da8c772ce35
This commit is contained in:
zhangjingwei 2024-03-22 16:32:20 +08:00
parent d02497a15d
commit 80e1d1275d
10 changed files with 113 additions and 36 deletions

View File

@ -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.

View File

@ -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'),
},
];

View File

@ -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,

View File

@ -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'),
},
{

View File

@ -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,

View File

@ -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);
};

View File

@ -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)

View File

@ -66,6 +66,7 @@ onlyOn(ironicServiceEnabled, () => {
.clickStepActionNextButton()
.formInput('name', name)
.formRadioChoose('loginType', 1)
.formInput('username', 'root')
.formInput('password', password)
.formInput('confirmPassword', password)
.wait(2000)

View File

@ -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()

View File

@ -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()