mirror of https://github.com/rancher/ui.git
HARVESTER: harvester rke1 support control guest agent
This commit is contained in:
parent
371809ca3e
commit
b5e4ce09c4
|
|
@ -5,9 +5,11 @@ import {
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import NodeDriver from 'shared/mixins/node-driver';
|
import NodeDriver from 'shared/mixins/node-driver';
|
||||||
import layout from './template';
|
import layout from './template';
|
||||||
|
import jsyaml from 'js-yaml'
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { throttledObserver } from 'ui/utils/debounce';
|
import { throttledObserver } from 'ui/utils/debounce';
|
||||||
import { hash } from 'rsvp';
|
import { hash } from 'rsvp';
|
||||||
|
import YAML from 'yaml';
|
||||||
|
|
||||||
const DRIVER = 'harvester';
|
const DRIVER = 'harvester';
|
||||||
const CONFIG = 'harvesterConfig';
|
const CONFIG = 'harvesterConfig';
|
||||||
|
|
@ -39,6 +41,22 @@ const PRIORITY = {
|
||||||
|
|
||||||
const STORAGE_NETWORK = 'storage-network.settings.harvesterhci.io'
|
const STORAGE_NETWORK = 'storage-network.settings.harvesterhci.io'
|
||||||
|
|
||||||
|
// init qemu guest agent
|
||||||
|
export const QGA_JSON = {
|
||||||
|
package_update: true,
|
||||||
|
packages: ['qemu-guest-agent'],
|
||||||
|
runcmd: [
|
||||||
|
[
|
||||||
|
'systemctl',
|
||||||
|
'enable',
|
||||||
|
'--now',
|
||||||
|
'qemu-guest-agent.service'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
// Different operating systems may have different guest agents
|
||||||
|
export const QGA_MAP = { default: 'qemu-guest-agent.service' };
|
||||||
|
|
||||||
export default Component.extend(NodeDriver, {
|
export default Component.extend(NodeDriver, {
|
||||||
growl: service(),
|
growl: service(),
|
||||||
settings: service(),
|
settings: service(),
|
||||||
|
|
@ -68,6 +86,8 @@ export default Component.extend(NodeDriver, {
|
||||||
loading: false,
|
loading: false,
|
||||||
disks: [],
|
disks: [],
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
|
installAgent: false,
|
||||||
|
userDataTemplate: '',
|
||||||
|
|
||||||
config: alias(`model.${ CONFIG }`),
|
config: alias(`model.${ CONFIG }`),
|
||||||
|
|
||||||
|
|
@ -83,6 +103,8 @@ export default Component.extend(NodeDriver, {
|
||||||
this.initSchedulings();
|
this.initSchedulings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.initUserData();
|
||||||
|
|
||||||
this.initDisks()
|
this.initDisks()
|
||||||
|
|
||||||
this.initInterfaces()
|
this.initInterfaces()
|
||||||
|
|
@ -170,6 +192,54 @@ export default Component.extend(NodeDriver, {
|
||||||
updatePodScheduling() {
|
updatePodScheduling() {
|
||||||
this.parsePodScheduling();
|
this.parsePodScheduling();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateAgent() {
|
||||||
|
const isInstall = !get(this, 'installAgent');
|
||||||
|
|
||||||
|
set(this, 'installAgent', isInstall)
|
||||||
|
const userData = get(this, 'config.userData')
|
||||||
|
|
||||||
|
const userDataDoc = isInstall ? this.addGuestAgent(userData) : this.deleteGuestAgent(userData);
|
||||||
|
let userDataYaml = userDataDoc.toString();
|
||||||
|
|
||||||
|
if (userDataYaml === '{}\n') {
|
||||||
|
// When the YAML parsed value is '{}\n', it means that the userData is empty.
|
||||||
|
userDataYaml = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCloudComment = this.hasCloudConfigComment(userDataYaml);
|
||||||
|
|
||||||
|
if (!hasCloudComment) {
|
||||||
|
userDataYaml = `#cloud-config\n${ userDataYaml }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(this, 'config.userData', userDataYaml);
|
||||||
|
},
|
||||||
|
|
||||||
|
chooseUserDataTemplate() {
|
||||||
|
const templateValue = get(this, 'userDataTemplate');
|
||||||
|
const isInstallAgent = get(this, 'installAgent');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templateJsonData = this.convertToJson(templateValue);
|
||||||
|
|
||||||
|
let userDataYaml;
|
||||||
|
|
||||||
|
if (isInstallAgent) {
|
||||||
|
const mergedObj = Object.assign(templateJsonData, { ...QGA_JSON });
|
||||||
|
|
||||||
|
userDataYaml = this.addCloudConfigComment(mergedObj);
|
||||||
|
} else {
|
||||||
|
userDataYaml = templateValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(this, 'config.userData', userDataYaml)
|
||||||
|
} catch (e) {
|
||||||
|
const message = this.intl.t('nodeDriver.harvester.templateError')
|
||||||
|
|
||||||
|
get(this, 'growl').fromError(undefined, message);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
clearData: observer('currentCredential.id', function() {
|
clearData: observer('currentCredential.id', function() {
|
||||||
|
|
@ -183,8 +253,8 @@ export default Component.extend(NodeDriver, {
|
||||||
set(this, 'config.diskInfo', '');
|
set(this, 'config.diskInfo', '');
|
||||||
set(this, 'config.networkInfo', '');
|
set(this, 'config.networkInfo', '');
|
||||||
|
|
||||||
|
this.initUserData();
|
||||||
this.initDisks()
|
this.initDisks()
|
||||||
|
|
||||||
this.initInterfaces()
|
this.initInterfaces()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -215,6 +285,14 @@ export default Component.extend(NodeDriver, {
|
||||||
this.parsePodScheduling();
|
this.parsePodScheduling();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
userDataChanged: observer('config.userData', function() {
|
||||||
|
const userData = get(this, 'config.userData');
|
||||||
|
const installAgent = get(this, 'installAgent')
|
||||||
|
const hasInstall = this.hasInstallAgent(userData, installAgent);
|
||||||
|
|
||||||
|
set(this, 'installAgent', hasInstall)
|
||||||
|
}),
|
||||||
|
|
||||||
fetchResource: throttledObserver('currentCredential.id', 'currentCredential.harvestercredentialConfig.clusterId', async function() {
|
fetchResource: throttledObserver('currentCredential.id', 'currentCredential.harvestercredentialConfig.clusterId', async function() {
|
||||||
const clusterId = get(this, 'currentCredential') && get(this, 'currentCredential').harvestercredentialConfig && get(this, 'currentCredential').harvestercredentialConfig.clusterId;
|
const clusterId = get(this, 'currentCredential') && get(this, 'currentCredential').harvestercredentialConfig && get(this, 'currentCredential').harvestercredentialConfig.clusterId;
|
||||||
|
|
||||||
|
|
@ -423,6 +501,124 @@ export default Component.extend(NodeDriver, {
|
||||||
set(this, `model.${ CONFIG }`, config);
|
set(this, `model.${ CONFIG }`, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addGuestAgent(userData) {
|
||||||
|
const userDataDoc = userData ? YAML.parseDocument(userData) : YAML.parseDocument({});
|
||||||
|
const userDataYAML = userDataDoc.toString();
|
||||||
|
const userDataJSON = YAML.parse(userDataYAML);
|
||||||
|
let packages = userDataJSON?.packages || [];
|
||||||
|
let runcmd = userDataJSON?.runcmd || [];
|
||||||
|
|
||||||
|
userDataDoc.setIn(['package_update'], true);
|
||||||
|
if (Array.isArray(packages)) {
|
||||||
|
if (!packages.includes('qemu-guest-agent')) {
|
||||||
|
packages.push('qemu-guest-agent');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
packages = QGA_JSON.packages;
|
||||||
|
}
|
||||||
|
if (Array.isArray(runcmd)) {
|
||||||
|
const hasSameRuncmd = runcmd.find( (S) => Array.isArray(S) && S.join('-') === QGA_JSON.runcmd[0].join('-'));
|
||||||
|
|
||||||
|
if (!hasSameRuncmd) {
|
||||||
|
runcmd.push(QGA_JSON.runcmd[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runcmd = QGA_JSON.runcmd;
|
||||||
|
}
|
||||||
|
if (packages.length > 0) {
|
||||||
|
userDataDoc.setIn(['packages'], packages);
|
||||||
|
} else {
|
||||||
|
userDataDoc.setIn(['packages'], []); // It needs to be set empty first, as it is possible that cloud-init comments are mounted on this node
|
||||||
|
this.deleteYamlDocProp(userDataDoc, ['packages']);
|
||||||
|
this.deleteYamlDocProp(userDataDoc, ['package_update']);
|
||||||
|
}
|
||||||
|
if (runcmd.length > 0) {
|
||||||
|
userDataDoc.setIn(['runcmd'], runcmd);
|
||||||
|
} else {
|
||||||
|
this.deleteYamlDocProp(userDataDoc, ['runcmd']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDataDoc;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteGuestAgent(userData) {
|
||||||
|
const userDataDoc = userData ? YAML.parseDocument(userData) : YAML.parseDocument({});
|
||||||
|
const userDataYAML = userDataDoc.toString();
|
||||||
|
const userDataJSON = YAML.parse(userDataYAML);
|
||||||
|
const packages = userDataJSON?.packages || [];
|
||||||
|
const runcmd = userDataJSON?.runcmd || [];
|
||||||
|
|
||||||
|
if (Array.isArray(packages)) {
|
||||||
|
for (let i = 0; i < packages.length; i++) {
|
||||||
|
if (packages[i] === 'qemu-guest-agent') {
|
||||||
|
packages.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(runcmd)) {
|
||||||
|
for (let i = 0; i < runcmd.length; i++) {
|
||||||
|
if (Array.isArray(runcmd[i]) && runcmd[i].join('-') === QGA_JSON.runcmd[0].join('-')) {
|
||||||
|
runcmd.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (packages.length > 0) {
|
||||||
|
userDataDoc.setIn(['packages'], packages);
|
||||||
|
} else {
|
||||||
|
userDataDoc.setIn(['packages'], []);
|
||||||
|
this.deleteYamlDocProp(userDataDoc, ['packages']);
|
||||||
|
this.deleteYamlDocProp(userDataDoc, ['package_update']);
|
||||||
|
}
|
||||||
|
if (runcmd.length > 0) {
|
||||||
|
userDataDoc.setIn(['runcmd'], runcmd);
|
||||||
|
} else {
|
||||||
|
this.deleteYamlDocProp(userDataDoc, ['runcmd']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDataDoc;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasCloudConfigComment(userScript) {
|
||||||
|
// Check that userData contains: #cloud-config
|
||||||
|
const userDataDoc = userScript ? YAML.parseDocument(userScript) : YAML.parseDocument({});
|
||||||
|
|
||||||
|
const items = userDataDoc?.contents?.items || [];
|
||||||
|
let exist = false;
|
||||||
|
|
||||||
|
if (userDataDoc?.comment === 'cloud-config' || userDataDoc?.comment?.includes('cloud-config\n')) {
|
||||||
|
exist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userDataDoc?.commentBefore === 'cloud-config' || userDataDoc?.commentBefore?.includes('cloud-config\n')) {
|
||||||
|
exist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.map((item) => {
|
||||||
|
const key = item.key;
|
||||||
|
|
||||||
|
if (key?.commentBefore?.trim() === 'cloud-config' || key?.commentBefore?.includes('cloud-config\n') || /\ncloud-config$/.test(key?.commentBefore)) {
|
||||||
|
exist = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return exist;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteYamlDocProp(doc, paths) {
|
||||||
|
try {
|
||||||
|
const item = doc.getIn([])?.items[0];
|
||||||
|
const key = item?.key;
|
||||||
|
const hasCloudConfigComment = !!key?.commentBefore?.includes('cloud-config');
|
||||||
|
const isMatchProp = key.source === paths[paths.length - 1];
|
||||||
|
|
||||||
|
if (key && hasCloudConfigComment && isMatchProp) {
|
||||||
|
// Comments are mounted on the next node and we should not delete the node containing cloud-config
|
||||||
|
} else {
|
||||||
|
doc.deleteIn(paths);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
this._super();
|
this._super();
|
||||||
let errors = get(this, 'errors') || [];
|
let errors = get(this, 'errors') || [];
|
||||||
|
|
@ -568,6 +764,19 @@ export default Component.extend(NodeDriver, {
|
||||||
set(this, 'podSchedulings', podSchedulings);
|
set(this, 'podSchedulings', podSchedulings);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
initUserData() {
|
||||||
|
if (!get(this, 'config.userData')) {
|
||||||
|
let userData = this.addCloudConfigComment(QGA_JSON);
|
||||||
|
|
||||||
|
set(this, 'config.userData', userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = get(this, 'config.userData')
|
||||||
|
const hasInstall = this.hasInstallAgent(userData, true);
|
||||||
|
|
||||||
|
set(this, 'installAgent', hasInstall)
|
||||||
|
},
|
||||||
|
|
||||||
initDisks() {
|
initDisks() {
|
||||||
let disks = [];
|
let disks = [];
|
||||||
|
|
||||||
|
|
@ -595,6 +804,42 @@ export default Component.extend(NodeDriver, {
|
||||||
set(this, 'disks', disks);
|
set(this, 'disks', disks);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
convertToJson(script = '') {
|
||||||
|
let out = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
out = jsyaml.load(script);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Function(convertToJson) error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasInstallAgent(userScript, installAgent) {
|
||||||
|
let dataFormat = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataFormat = this.convertToJson(userScript);
|
||||||
|
} catch {
|
||||||
|
// When the yaml cannot be converted to json, the previous installAgent value should be returned
|
||||||
|
return installAgent;
|
||||||
|
}
|
||||||
|
const hasInstall = dataFormat?.packages?.includes('qemu-guest-agent') && !!dataFormat?.runcmd?.find( (S) => Array.isArray(S) && S.join('-') === QGA_JSON.runcmd[0].join('-'));
|
||||||
|
|
||||||
|
return !!hasInstall;
|
||||||
|
},
|
||||||
|
|
||||||
|
addCloudConfigComment(value) {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return `#cloud-config\n${ jsyaml.safeDump(value) }`;
|
||||||
|
} else if (typeof value === 'string' && !value.startsWith('#cloud-config')) {
|
||||||
|
return `#cloud-config\n${ value }`;
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
initInterfaces() {
|
initInterfaces() {
|
||||||
let _interfaces = [];
|
let _interfaces = [];
|
||||||
|
|
||||||
|
|
@ -796,5 +1041,5 @@ export default Component.extend(NodeDriver, {
|
||||||
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.network.macFormat') }));
|
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.network.macFormat') }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,8 @@
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
@class="form-control"
|
@class="form-control"
|
||||||
@content={{userDataContent}}
|
@content={{userDataContent}}
|
||||||
@value={{config.userData}}
|
@value={{userDataTemplate}}
|
||||||
|
@change={{action "chooseUserDataTemplate" userDataTemplate}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -348,6 +349,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12 mt-0">
|
||||||
|
<label class="acc-label">
|
||||||
|
{{input
|
||||||
|
type="checkbox"
|
||||||
|
classNames="form-control"
|
||||||
|
checked=installAgent
|
||||||
|
click=(action "updateAgent")
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{t "nodeDriver.harvester.installAgent.label"}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<label class="acc-label">
|
<label class="acc-label">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
"ember-auto-import": "*",
|
"ember-auto-import": "*",
|
||||||
"ember-intl": "*",
|
"ember-intl": "*",
|
||||||
"ember-cli-htmlbars": "*",
|
"ember-cli-htmlbars": "*",
|
||||||
"ember-cli-babel": "*"
|
"ember-cli-babel": "*",
|
||||||
|
"yaml": "*",
|
||||||
|
"js-yaml": "*"
|
||||||
},
|
},
|
||||||
"ember-addon": {
|
"ember-addon": {
|
||||||
"paths": [
|
"paths": [
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@
|
||||||
"webpack": "^4.44.1",
|
"webpack": "^4.44.1",
|
||||||
"xterm": "^4.9.0",
|
"xterm": "^4.9.0",
|
||||||
"xterm-addon-fit": "^0.4.0",
|
"xterm-addon-fit": "^0.4.0",
|
||||||
|
"yaml": "^2.2.1",
|
||||||
"yamljs": "^0.3.0"
|
"yamljs": "^0.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -9891,6 +9891,9 @@ nodeDriver:
|
||||||
label: User Data Template
|
label: User Data Template
|
||||||
nodeTemplate:
|
nodeTemplate:
|
||||||
tooltip: Harvester requires virtual machine under the same namespace
|
tooltip: Harvester requires virtual machine under the same namespace
|
||||||
|
installAgent:
|
||||||
|
label: Install guest agent
|
||||||
|
templateError: Incorrect template format
|
||||||
pnap:
|
pnap:
|
||||||
access:
|
access:
|
||||||
title: Account Access
|
title: Account Access
|
||||||
|
|
|
||||||
|
|
@ -4735,6 +4735,9 @@ nodeDriver:
|
||||||
label: User Data YAML
|
label: User Data YAML
|
||||||
template:
|
template:
|
||||||
label: User Data Template
|
label: User Data Template
|
||||||
|
installAgent:
|
||||||
|
label: Install guest agent
|
||||||
|
templateError: Incorrect template format
|
||||||
packet:
|
packet:
|
||||||
accountSection: Acceso a la Cuenta
|
accountSection: Acceso a la Cuenta
|
||||||
projectId:
|
projectId:
|
||||||
|
|
|
||||||
|
|
@ -9237,6 +9237,9 @@ nodeDriver:
|
||||||
label: 用户数据
|
label: 用户数据
|
||||||
template:
|
template:
|
||||||
label: 用户配置模板
|
label: 用户配置模板
|
||||||
|
installAgent:
|
||||||
|
label: 安装访客代理
|
||||||
|
templateError: 模版格式错误
|
||||||
oci:
|
oci:
|
||||||
placement:
|
placement:
|
||||||
title: Placement
|
title: Placement
|
||||||
|
|
|
||||||
|
|
@ -15184,6 +15184,11 @@ yam@^1.0.0:
|
||||||
fs-extra "^4.0.2"
|
fs-extra "^4.0.2"
|
||||||
lodash.merge "^4.6.0"
|
lodash.merge "^4.6.0"
|
||||||
|
|
||||||
|
yaml@^2.2.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4"
|
||||||
|
integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==
|
||||||
|
|
||||||
yamljs@^0.3.0:
|
yamljs@^0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b"
|
resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue