From 6e24adfc36218ac4b69706378401102eb18a4e5c Mon Sep 17 00:00:00 2001 From: JS Choi <77760789+jschoiRR@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:53:22 +0900 Subject: [PATCH 1/2] =?UTF-8?q?*=20L2=20NIC=20IP=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94/=EC=A0=9C=EA=B1=B0=20qemu-agent=20stats=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20L2=20NIC=EC=9D=98?= =?UTF-8?q?=20=EC=8B=A4=EC=A0=9C=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20IPv4?= =?UTF-8?q?=EB=A5=BC=20nics.ip4=5Faddress=EC=97=90=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EA=B2=8C=EC=8A=A4=ED=8A=B8=20=EB=82=B4=EB=B6=80=EC=97=90=20IP?= =?UTF-8?q?=EA=B0=80=20=EC=97=86=EC=9C=BC=EB=A9=B4=20DB=20IP=EB=8F=84=20nu?= =?UTF-8?q?ll=EB=A1=9C=20=EC=A0=95=EB=A6=AC=20L2=20NIC=EB=A5=BC=20DOWN=20?= =?UTF-8?q?=ED=96=88=EC=9D=84=20=EB=95=8C=20DB=EC=9D=98=20NIC=20IP?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cloud/server/StatsCollector.java | 28 +++++++++++++------ .../java/com/cloud/vm/UserVmManagerImpl.java | 4 +++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index a6b2641b26b7..3f1c7491c33d 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -1442,11 +1442,15 @@ protected void runInContext() { logger.debug(String.format("VmStatsCollector is running to process VMs across %d UP hosts", hosts.size())); List listL2netVMs = userVmJoinDao.listGuestTypeVMs(Network.GuestType.L2); - ArrayList listL2NicMacAddr = new ArrayList(); + Map> l2NicMacsByVmId = new HashMap<>(); for (UserVmJoinVO vm : listL2netVMs) { - if(!listL2NicMacAddr.contains(vm.getMacAddress())){ - listL2NicMacAddr.add(vm.getMacAddress()); + if (StringUtils.isBlank(vm.getMacAddress())) { + continue; + } + List l2NicMacs = l2NicMacsByVmId.computeIfAbsent(vm.getId(), k -> new ArrayList<>()); + if (!l2NicMacs.contains(vm.getMacAddress())) { + l2NicMacs.add(vm.getMacAddress()); } } @@ -1472,10 +1476,18 @@ protected void runInContext() { Map agentNicMap = statsForCurrentIteration.getNicAddrMap(); if (agentNicMap != null) { - for (String key : agentNicMap.keySet()) { - NicVO nicVO = _nicDao.findByMacAddress(key); - if (listL2NicMacAddr.contains(key)) { - nicVO.setIPv4Address(agentNicMap.get(key)); + List l2NicMacs = l2NicMacsByVmId.get(vmId); + if (CollectionUtils.isNotEmpty(l2NicMacs)) { + for (String macAddress : l2NicMacs) { + NicVO nicVO = _nicDao.findByMacAddress(macAddress); + if (nicVO == null) { + continue; + } + String guestIpAddress = agentNicMap.get(macAddress); + if (StringUtils.equals(nicVO.getIPv4Address(), guestIpAddress)) { + continue; + } + nicVO.setIPv4Address(guestIpAddress); _nicDao.update(nicVO.getId(), nicVO); } } @@ -2474,4 +2486,4 @@ public ConfigKey[] getConfigKeys() { public double getImageStoreCapacityThreshold() { return CapacityManager.SecondaryStorageCapacityThreshold.value(); } -} \ No newline at end of file +} diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index e0e2bc38865c..acfa0b3141cb 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -9757,6 +9757,10 @@ public UserVm updateVmNicLinkState(UpdateVmNicLinkStateCmd cmd){ throw new CloudRuntimeException(ex.getMessage()); } } + if (!cmd.getLinkState() && network.getGuestType() == Network.GuestType.L2 && nic.getIPv4Address() != null) { + nic.setIPv4Address(null); + _nicDao.update(nicId, nic); + } return _vmDao.findById(vmInstance.getId()); } From 88dcc8af9e76dd88d9bf5cc1c053472fb6d58293 Mon Sep 17 00:00:00 2001 From: JS Choi <77760789+jschoiRR@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:57:45 +0900 Subject: [PATCH 2/2] =?UTF-8?q?*=20=20VM=20=EB=AA=A9=EB=A1=9D=20=E2=80=9C?= =?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EC=9E=90=EC=9B=90=E2=80=9D=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=B6=94=EA=B0=80=20=EA=B0=80=EC=83=81=EB=A8=B8?= =?UTF-8?q?=EC=8B=A0=20=EB=AA=A9=EB=A1=9D=EC=97=90=20=ED=95=A0=EB=8B=B9=20?= =?UTF-8?q?=EC=9E=90=EC=9B=90=20/=20Allocated=20resources=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=B6=94=EA=B0=80=20CPU/Memory=EB=A5=BC=20?= =?UTF-8?q?=ED=95=9C=20=EC=85=80=EC=97=90=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?+=20=ED=85=8D=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Scale VM KVM 오퍼링 처리 Running + KVM VM에서 오퍼링 필터링 로직 복구/보완 현재 오퍼링이 페이징 목록에 없을 때 개별 조회 사용자 지정 오퍼링과 고정 오퍼링 구분 처리 고정 KVM 오퍼링인 경우만 스케일 제한 메시지 표시 * Kubernetes 관리용 방화벽/포트포워딩 룰 삭제 방지 Kubernetes 관리 포트를 네트워크 룰 화면에 전달 현재 페이지에 없는 컴퓨트 오퍼링 선택 상태 유지 비밀번호 지원 템플릿일 때만 VNF credential password 표시 --- ui/public/locales/en.json | 2 + ui/public/locales/ko_KR.json | 2 + ui/src/components/view/ListView.vue | 48 +++++++++ ui/src/config/section/compute.js | 4 +- ui/src/views/AutogenView.vue | 11 +- ui/src/views/compute/DeployVnfAppliance.vue | 11 +- ui/src/views/compute/KubernetesServiceTab.vue | 18 +++- ui/src/views/compute/ScaleVM.vue | 54 ++++++++-- .../wizard/ComputeOfferingSelection.vue | 4 + ui/src/views/network/FirewallRules.vue | 90 ++++++++++++++-- ui/src/views/network/PortForwarding.vue | 100 ++++++++++++++++-- 11 files changed, 310 insertions(+), 34 deletions(-) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index ae03926eb83d..7f647d25723c 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -567,6 +567,7 @@ "label.compute": "Compute", "label.compute.offerings": "Compute offerings", "label.compute.offering.for.sharedfs.instance": "Compute offering for Instance", +"label.compute.resources": "Allocated resources", "label.computeonly.offering": "Compute only disk offering", "label.computeonly.offering.tooltip": "Option to specify root disk related information in the compute offering or to directly link a disk offering to the compute offering", "label.conditions": "Conditions", @@ -3423,6 +3424,7 @@ "message.kubernetes.cluster.start": "Please confirm that you want to start the cluster.", "message.kubernetes.cluster.stop": "Please confirm that you want to stop the cluster.", "message.kubernetes.cluster.upgrade": "Please select new Kubernetes version.", +"message.kubernetes.management.rule.delete.disabled": "Management rules created by Kubernetes cannot be deleted.", "message.kubernetes.version.delete": "Please confirm that you want to delete this Kubernetes version.", "message.l2.network.unsupported.for.nsx": "L2 networks aren't supported for NSX enabled zones", "message.launch.zone": "Zone is ready to launch; please proceed to the next step.", diff --git a/ui/public/locales/ko_KR.json b/ui/public/locales/ko_KR.json index c68316bc6201..1c84282f6e9a 100644 --- a/ui/public/locales/ko_KR.json +++ b/ui/public/locales/ko_KR.json @@ -567,6 +567,7 @@ "label.compute": "\ucef4\ud4e8\ud2b8", "label.compute.offerings": "\ucef4\ud4e8\ud2b8 \uc624\ud37c\ub9c1", "label.compute.offering.for.sharedfs.instance": "\uac00\uc0c1\uba38\uc2e0\uc6a9 \ucef4\ud4e8\ud2b8 \uc624\ud37c\ub9c1", +"label.compute.resources": "\ud560\ub2f9 \uc790\uc6d0", "label.computeonly.offering": "\ucef4\ud4e8\ud305 \uc804\uc6a9 \ub514\uc2a4\ud06c \uc81c\uacf5", "label.computeonly.offering.tooltip": "\ucef4\ud4e8\ud305 \uc624\ud37c\ub9c1\uc5d0\uc11c \ub8e8\ud2b8 \ub514\uc2a4\ud06c \uad00\ub828 \uc815\ubcf4\ub97c \uc9c0\uc815\ud558\uac70\ub098 \ub514\uc2a4\ud06c \uc624\ud37c\ub9c1\uc744 \ucef4\ud4e8\ud305 \uc624\ud37c\ub9c1\uc5d0 \uc9c1\uc811 \uc5f0\uacb0\ud558\ub294 \uc635\uc158", "label.conditions": "\uc870\uac74", @@ -3423,6 +3424,7 @@ "message.kubernetes.cluster.start": "\ud074\ub7ec\uc2a4\ud130\ub97c \uc2dc\uc791\ud560 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", "message.kubernetes.cluster.stop": "\ud074\ub7ec\uc2a4\ud130\ub97c \uc911\uc9c0 \ud560 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", "message.kubernetes.cluster.upgrade": "\uc0c8 \ucfe0\ubc84\ub124\ud2f0\uc2a4 \ubc84\uc804\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", +"message.kubernetes.management.rule.delete.disabled": "Kubernetes\uc5d0\uc11c \uc0dd\uc131\ud55c \uad00\ub9ac \uaddc\uce59\uc740 \uc0ad\uc81c\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "message.kubernetes.version.delete": "\uc774 \ucfe0\ubc84\ub124\ud2f0\uc2a4 \ubc84\uc804\uc744 \uc0ad\uc81c\ud560 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", "message.l2.network.unsupported.for.nsx": "L2 \ub124\ud2b8\uc6cc\ud06c\ub294 NSX \uc9c0\uc6d0 \uc601\uc5ed\uc5d0\uc11c \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "message.launch.zone": "Zone\uc744 \uc2dc\uc791\ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc74c \uc21c\uc11c\uc5d0 \ub530\ub77c \uc9c4\ud589\ud574 \uc8fc\uc2ed\uc2dc\uc624.", diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 0dee8297f97d..bb396184d6aa 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -303,6 +303,24 @@ {{ this.$t('label.state.qemuagentversion.notinstalled') }} {{ text }} + @@ -1324,6 +1342,9 @@ export default { var duration = Date.parse(enddate) - Date.parse(startdate) return (duration > 0 ? duration / 1000.0 : 0) + '' }, + hasValue (value) { + return value !== undefined && value !== null && value !== '' + }, getUsageTypes () { if (this.$route.path.split('/')[1] === 'usage') { api('listUsageTypes').then(json => { @@ -1456,6 +1477,33 @@ export default { padding: 10%; } + .resource-summary { + display: inline-flex; + align-items: center; + gap: 10px; + line-height: 20px; + white-space: nowrap; + color: inherit; + + .resource-item { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .resource-icon { + font-size: 13px; + } + + .resource-item--cpu .resource-icon { + color: #5b6b84; + } + + .resource-item--memory .resource-icon { + color: #68758a; + } + } + .quickview-context-menu { position: fixed; z-index: 2000; diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 5214d42bee2b..26829fb404d9 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -46,7 +46,7 @@ export default { return filters }, columns: () => { - const fields = [{ field: 'displayname', customTitle: 'vm.displayname' }, 'state', 'qemuagentversion', 'ipaddress', 'templatetype'] + const fields = [{ field: 'displayname', customTitle: 'vm.displayname' }, 'state', { field: 'resources', customTitle: 'compute.resources' }, 'qemuagentversion', 'ipaddress', 'templatetype'] const metricsFields = ['cpunumber', 'cputotal', 'cpuused', 'memorytotal', { memoryused: (record) => { @@ -72,7 +72,7 @@ export default { fields.push(...metricsFields) } if (store.getters.userInfo.roletype === 'Admin') { - fields.splice(3, 0, 'instancename') + fields.splice(1, 0, 'instancename') fields.push('hostname') fields.push('account') } else if (store.getters.userInfo.roletype === 'DomainAdmin') { diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index ddad00850499..4899ee1af968 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -1310,11 +1310,20 @@ export default { customRender[key] = columnKey[key] } } + const sorter = key === 'resources' + ? (a, b) => { + const cpuCompare = Number(a.cpunumber || 0) - Number(b.cpunumber || 0) + if (cpuCompare !== 0) { + return cpuCompare + } + return Number(a.memory || 0) - Number(b.memory || 0) + } + : (a, b) => genericCompare(a[key] || '', b[key] || '') this.columns.push({ key: key, title: this.$t('label.' + String(title).toLowerCase()), dataIndex: key, - sorter: (a, b) => genericCompare(a[key] || '', b[key] || '') + sorter: sorter }) this.selectedColumns.push(key) } diff --git a/ui/src/views/compute/DeployVnfAppliance.vue b/ui/src/views/compute/DeployVnfAppliance.vue index 2d4f5fa5c899..f77c44177516 100644 --- a/ui/src/views/compute/DeployVnfAppliance.vue +++ b/ui/src/views/compute/DeployVnfAppliance.vue @@ -2297,13 +2297,14 @@ export default { successMethod: result => { const vm = result.jobresult.virtualmachine const name = vm.displayname || vm.name || vm.id + const passwordEnabled = this.template?.passwordenabled === true const username = vm.vnfdetails?.username || null - const password = vm.vnfdetails?.password || null - const effectivePassword = vm.password || password + const password = passwordEnabled ? vm.vnfdetails?.password || null : null + const effectivePassword = passwordEnabled ? (vm.password || password) : null const sshUsername = vm.vnfdetails?.ssh_user || null - const sshPassword = vm.vnfdetails?.ssh_password || null + const sshPassword = passwordEnabled ? vm.vnfdetails?.ssh_password || null : null const webUsername = vm.vnfdetails?.web_user || null - const webPassword = vm.vnfdetails?.web_password || null + const webPassword = passwordEnabled ? vm.vnfdetails?.web_password || null : null const credentials = [] if (username) { credentials.push(this.$t('label.username') + ' : ' + username) @@ -2325,8 +2326,6 @@ export default { } if (credentials.length > 0) { credentials.push(this.$t('message.vnf.credentials.change')) - } else { - credentials.push(this.$t('message.vnf.no.credentials')) } this.$notification.success({ message: `${this.$t('message.vnf.credentials.default')} ` + name, diff --git a/ui/src/views/compute/KubernetesServiceTab.vue b/ui/src/views/compute/KubernetesServiceTab.vue index 01fae48ca2d9..10d3dbc1ff69 100644 --- a/ui/src/views/compute/KubernetesServiceTab.vue +++ b/ui/src/views/compute/KubernetesServiceTab.vue @@ -146,10 +146,16 @@ - + - + @@ -283,6 +289,14 @@ export default { this.setCurrentTab() } }, + computed: { + kubernetesManagementPorts () { + const sshPorts = this.virtualmachines + .map((vm, index) => Number(vm.port || vm.sshport || vm.sshPort || this.cksSshStartingPort + index)) + .filter(port => Number.isInteger(port)) + return [...new Set([6443, ...sshPorts])] + } + }, mounted () { if (this.$store.getters.apis.scaleKubernetesCluster?.params?.filter(x => x.name === 'nodeids').length > 0 && this.resource.clustertype === 'CloudManaged') { this.vmColumns.push({ diff --git a/ui/src/views/compute/ScaleVM.vue b/ui/src/views/compute/ScaleVM.vue index c776b4d928c0..e5e9f0608042 100644 --- a/ui/src/views/compute/ScaleVM.vue +++ b/ui/src/views/compute/ScaleVM.vue @@ -139,6 +139,7 @@ export default { this.total = 0 this.offerings = [] this.offeringsMap = [] + this.fixedOfferingKvm = false api('listServiceOfferings', { virtualmachineid: this.resource.id, keyword: options.keyword, @@ -149,22 +150,57 @@ export default { response: 'json' }).then(response => { this.total = response.listserviceofferingsresponse.count - if (this.total === 0) { - return - } this.offerings = response.listserviceofferingsresponse.serviceoffering || [] if (this.resource.state === 'Running' && this.resource.hypervisor === 'KVM') { - this.offerings = this.offerings.filter(offering => offering.id === this.resource.serviceofferingid) - this.currentOffer = this.offerings[0] - if (this.currentOffer === undefined) { - this.fixedOfferingKvm = true - } + return this.applyRunningKvmOfferingFilter() } - this.offerings.map(i => { this.offeringsMap[i.id] = i }) + if (this.total === 0) { + return + } + this.setOfferingsMap() }).finally(() => { this.loading = false }) }, + applyRunningKvmOfferingFilter () { + const currentOffer = this.offerings.find(offering => offering.id === this.resource.serviceofferingid) + if (currentOffer) { + this.offerings = [currentOffer] + this.total = this.offerings.length + this.setOfferingsMap() + return Promise.resolve() + } + + return this.fetchCurrentServiceOffering().then(offering => { + if (offering?.iscustomized) { + this.offerings = [offering] + this.total = this.offerings.length + } else { + this.offerings = [] + this.total = 0 + this.fixedOfferingKvm = true + } + this.setOfferingsMap() + }).catch(error => { + this.offerings = [] + this.total = 0 + this.fixedOfferingKvm = true + this.$notifyError(error) + }) + }, + fetchCurrentServiceOffering () { + return api('listServiceOfferings', { + id: this.resource.serviceofferingid, + details: 'min', + response: 'json' + }).then(response => { + return response.listserviceofferingsresponse.serviceoffering?.[0] + }) + }, + setOfferingsMap () { + this.offeringsMap = [] + this.offerings.map(i => { this.offeringsMap[i.id] = i }) + }, getMinCpu () { // We can only scale up while a VM is running if (this.resource.state === 'Running') { diff --git a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue index 3edb9d7e09d2..f7fe4d21caf3 100644 --- a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue +++ b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue @@ -256,6 +256,10 @@ export default { selectInitialRow () { const selectedId = this.value || this.preFillContent?.computeofferingid const selectedRow = this.tableSource.find(row => row.key === selectedId && !row.disabled) + if (selectedId && !selectedRow) { + this.selectedRowKeys = [selectedId] + return + } const fallbackRow = this.tableSource.find(row => !row.disabled) const row = selectedRow || fallbackRow if (!row) { diff --git a/ui/src/views/network/FirewallRules.vue b/ui/src/views/network/FirewallRules.vue index 43ee9536be58..dc5bcdf2cb9f 100644 --- a/ui/src/views/network/FirewallRules.vue +++ b/ui/src/views/network/FirewallRules.vue @@ -99,7 +99,7 @@ :columns="columns" :dataSource="firewallRules" :pagination="false" - :rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}" + :rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange, getCheckboxProps: getCheckboxProps}" :rowKey="record => record.id">