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 @@ @@ -348,6 +349,21 @@ +
+
+ +
+
+