HARVESTER: harvester rke1 support control guest agent

This commit is contained in:
wujun 2023-03-08 22:31:28 +08:00
parent 371809ca3e
commit b5e4ce09c4
8 changed files with 282 additions and 4 deletions

View File

@ -5,9 +5,11 @@ import {
import Component from '@ember/component';
import NodeDriver from 'shared/mixins/node-driver';
import layout from './template';
import jsyaml from 'js-yaml'
import { inject as service } from '@ember/service';
import { throttledObserver } from 'ui/utils/debounce';
import { hash } from 'rsvp';
import YAML from 'yaml';
const DRIVER = 'harvester';
const CONFIG = 'harvesterConfig';
@ -39,6 +41,22 @@ const PRIORITY = {
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, {
growl: service(),
settings: service(),
@ -68,6 +86,8 @@ export default Component.extend(NodeDriver, {
loading: false,
disks: [],
interfaces: [],
installAgent: false,
userDataTemplate: '',
config: alias(`model.${ CONFIG }`),
@ -83,6 +103,8 @@ export default Component.extend(NodeDriver, {
this.initSchedulings();
}
this.initUserData();
this.initDisks()
this.initInterfaces()
@ -170,6 +192,54 @@ export default Component.extend(NodeDriver, {
updatePodScheduling() {
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() {
@ -183,8 +253,8 @@ export default Component.extend(NodeDriver, {
set(this, 'config.diskInfo', '');
set(this, 'config.networkInfo', '');
this.initUserData();
this.initDisks()
this.initInterfaces()
}),
@ -215,6 +285,14 @@ export default Component.extend(NodeDriver, {
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() {
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);
},
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() {
this._super();
let errors = get(this, 'errors') || [];
@ -568,6 +764,19 @@ export default Component.extend(NodeDriver, {
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() {
let disks = [];
@ -595,6 +804,42 @@ export default Component.extend(NodeDriver, {
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() {
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') }));
}
});
}
},
});

View File

@ -326,7 +326,8 @@
<SearchableSelect
@class="form-control"
@content={{userDataContent}}
@value={{config.userData}}
@value={{userDataTemplate}}
@change={{action "chooseUserDataTemplate" userDataTemplate}}
/>
</div>
</div>
@ -348,6 +349,21 @@
</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="col span-6">
<label class="acc-label">

View File

@ -8,7 +8,9 @@
"ember-auto-import": "*",
"ember-intl": "*",
"ember-cli-htmlbars": "*",
"ember-cli-babel": "*"
"ember-cli-babel": "*",
"yaml": "*",
"js-yaml": "*"
},
"ember-addon": {
"paths": [

View File

@ -112,6 +112,7 @@
"webpack": "^4.44.1",
"xterm": "^4.9.0",
"xterm-addon-fit": "^0.4.0",
"yaml": "^2.2.1",
"yamljs": "^0.3.0"
},
"dependencies": {

View File

@ -9891,6 +9891,9 @@ nodeDriver:
label: User Data Template
nodeTemplate:
tooltip: Harvester requires virtual machine under the same namespace
installAgent:
label: Install guest agent
templateError: Incorrect template format
pnap:
access:
title: Account Access

View File

@ -4735,6 +4735,9 @@ nodeDriver:
label: User Data YAML
template:
label: User Data Template
installAgent:
label: Install guest agent
templateError: Incorrect template format
packet:
accountSection: Acceso a la Cuenta
projectId:

View File

@ -9237,6 +9237,9 @@ nodeDriver:
label: 用户数据
template:
label: 用户配置模板
installAgent:
label: 安装访客代理
templateError: 模版格式错误
oci:
placement:
title: Placement

View File

@ -15184,6 +15184,11 @@ yam@^1.0.0:
fs-extra "^4.0.2"
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:
version "0.3.0"
resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b"