From b5e4ce09c44ff0415c76670c5d2dd77e3e02f787 Mon Sep 17 00:00:00 2001
From: wujun <897415845@qq.com>
Date: Wed, 8 Mar 2023 22:31:28 +0800
Subject: [PATCH] HARVESTER: harvester rke1 support control guest agent
---
.../components/driver-harvester/component.js | 249 +++++++++++++++++-
.../components/driver-harvester/template.hbs | 18 +-
lib/nodes/package.json | 4 +-
package.json | 1 +
translations/en-us.yaml | 3 +
translations/es-es.yaml | 3 +
translations/zh-hans.yaml | 3 +
yarn.lock | 5 +
8 files changed, 282 insertions(+), 4 deletions(-)
diff --git a/lib/nodes/addon/components/driver-harvester/component.js b/lib/nodes/addon/components/driver-harvester/component.js
index dc576c879..325892565 100644
--- a/lib/nodes/addon/components/driver-harvester/component.js
+++ b/lib/nodes/addon/components/driver-harvester/component.js
@@ -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') }));
}
});
- }
+ },
});
diff --git a/lib/nodes/addon/components/driver-harvester/template.hbs b/lib/nodes/addon/components/driver-harvester/template.hbs
index 3d2692078..e9c38f64b 100644
--- a/lib/nodes/addon/components/driver-harvester/template.hbs
+++ b/lib/nodes/addon/components/driver-harvester/template.hbs
@@ -326,7 +326,8 @@