Compare commits

...

6 Commits

Author SHA1 Message Date
Imvedansh 6af35c4c46
Merge a84c486854 into cd5bb09d0d 2026-01-22 17:07:02 +05:30
Imvedansh a84c486854 Adding Cleanup Flag 2026-01-22 17:06:58 +05:30
Imvedansh 6a9b9f3260 handle deleteDomain as async job and poll job result 2026-01-22 17:06:58 +05:30
Imvedansh fda8f627b5 ui: Add comprehensive domain deletion confirmation dialog
Implements a confirmation modal for domain deletion that shows detailed
impact before proceeding, making it consistent with account deletion
2026-01-22 17:06:58 +05:30
Abhisar Sinha cd5bb09d0d
Fix potential leaks in executePipedCommands (#12478) 2026-01-22 10:59:41 +01:00
Wei Zhou b5e9178078
UI: fix issues when deploy VNF applicance on network with SG (#12436) 2026-01-22 10:56:03 +01:00
7 changed files with 237 additions and 7 deletions

View File

@ -932,6 +932,7 @@
"label.endpoint": "Endpoint",
"label.endport": "End port",
"label.enter.account.name": "Enter the account name",
"label.enter.domain.name": "Enter the domain name",
"label.enter.code": "Enter 2FA code to verify",
"label.enter.static.pin": "Enter static PIN to verify",
"label.enter.token": "Enter token",
@ -2527,7 +2528,7 @@
"label.vnf.app.action.reinstall": "Reinstall VNF Appliance",
"label.vnf.cidr.list": "CIDR from which access to the VNF appliance's Management interface should be allowed from",
"label.vnf.cidr.list.tooltip": "the CIDR list to forward traffic from to the VNF management interface. Multiple entries must be separated by a single comma character (,). The default value is 0.0.0.0/0.",
"label.vnf.configure.management": "Configure Firewall and Port Forwarding rules for VNF's management interfaces",
"label.vnf.configure.management": "Configure network rules for VNF's management interfaces",
"label.vnf.configure.management.tooltip": "True by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. False otherwise. Learn what rules are configured at http://docs.cloudstack.apache.org/en/latest/adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances",
"label.vnf.detail.add": "Add VNF detail",
"label.vnf.detail.remove": "Remove VNF detail",
@ -2970,6 +2971,9 @@
"message.delete.account.processing": "Deleting account",
"message.delete.account.success": "Successfully deleted account",
"message.delete.account.warning": "Deleting this account will delete all of the instances, volumes and snapshots associated with the account.",
"message.delete.domain.confirm": "Please confirm that you want to delete this domain by entering the name of the domain below.",
"message.delete.domain.warning": "This domain may contain accounts, users, or sub-domains. All of these must be removed before the domain can be deleted. This action cannot be undone.",
"message.delete.domain.failed": "Delete domain failed",
"message.delete.acl.processing": "Removing ACL rule...",
"message.delete.acl.rule": "Remove ACL rule",
"message.delete.acl.rule.failed": "Failed to remove ACL rule.",

View File

@ -0,0 +1,155 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<a-modal
:visible="true"
:title="$t('label.action.delete.domain') + ': ' + domain.name"
:okText="$t('label.delete.domain')"
okType="danger"
:confirmLoading="loading"
:ok-button-props="{ disabled: !canDelete }"
@cancel="emitClose"
@ok="emitConfirm">
<a-alert
type="warning"
show-icon
style="margin-bottom: 16px">
<template #message>
<div v-html="$t('message.delete.domain.warning')"></div>
</template>
</a-alert>
<a-spin v-if="loading" />
<a-table
v-else
size="small"
:columns="columns"
:dataSource="accountVmSummary"
:pagination="false"
rowKey="account" />
<div style="margin-top: 16px">
<a-alert style="margin-bottom: 10px">
<template #message>
<div v-html="$t('message.delete.domain.confirm')"></div>
</template>
</a-alert>
<a-input
v-model:value="confirmText"
:placeholder="$t('label.enter.domain.name')" />
</div>
</a-modal>
</template>
<script>
import { api } from '@/api'
export default {
name: 'DomainDeleteConfirm',
props: {
domain: {
type: Object,
required: true
}
},
data () {
return {
loading: false,
confirmText: '',
accountVmSummary: []
}
},
computed: {
canDelete () {
return this.confirmText.trim() === this.domain.name.trim()
},
columns () {
return [
{ title: this.$t('label.account'), dataIndex: 'account' },
{ title: this.$t('label.total') + ' VMs', dataIndex: 'total' },
{ title: this.$t('label.running') + ' VMs', dataIndex: 'running' },
{ title: this.$t('label.stopped') + ' VMs', dataIndex: 'stopped' }
]
}
},
mounted () {
this.fetchDomainImpact()
},
methods: {
emitClose () {
this.$emit('close')
},
emitConfirm () {
if (this.canDelete) {
this.$emit('confirm')
}
},
async fetchDomainImpact () {
this.loading = true
try {
const accResp = await api('listAccounts', {
domainid: this.domain.id,
listall: true
})
const accounts =
accResp.listaccountsresponse &&
accResp.listaccountsresponse.account
? accResp.listaccountsresponse.account
: []
const vmResp = await api('listVirtualMachines', {
domainid: this.domain.id,
listall: true
})
const vms =
vmResp.listvirtualmachinesresponse &&
vmResp.listvirtualmachinesresponse.virtualmachine
? vmResp.listvirtualmachinesresponse.virtualmachine
: []
this.accountVmSummary = accounts.map(account => {
const accountVms = vms.filter(vm => vm.account === account.name)
const running = accountVms.filter(vm => vm.state === 'Running').length
const stopped = accountVms.length - running
return {
account: account.name,
total: accountVms.length,
running,
stopped
}
})
} catch (e) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: e.response?.headers['x-description'] || this.$t('message.request.failed')
})
} finally {
this.loading = false
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -356,7 +356,10 @@ export default {
permission: ['listVnfAppliances'],
resourceType: 'UserVm',
params: () => {
return { details: 'servoff,tmpl,nics', isvnf: true }
return {
details: 'group,nics,secgrp,tmpl,servoff,diskoff,iso,volume,affgrp,backoff,vnfnics',
isvnf: true
}
},
columns: () => {
const fields = ['name', 'state', 'ipaddress']

View File

@ -1305,7 +1305,7 @@ export default {
for (const deviceId of managementDeviceIds) {
if (this.vnfNicNetworks && this.vnfNicNetworks[deviceId] &&
((this.vnfNicNetworks[deviceId].type === 'Isolated' && this.vnfNicNetworks[deviceId].vpcid === undefined) ||
(this.vnfNicNetworks[deviceId].type === 'Shared' && this.zone.securitygroupsenabled))) {
(this.vnfNicNetworks[deviceId].type === 'Shared' && this.vnfNicNetworks[deviceId].service.filter(svc => svc.name === 'SecurityGroupProvider')))) {
return true
}
}

View File

@ -74,6 +74,11 @@
:resource="resource"
:action="action"/>
</div>
<domain-delete-confirm
v-if="showDeleteConfirm"
:domain="deleteDomainResource"
@close="showDeleteConfirm = false"
@confirm="confirmDeleteDomain" />
</div>
</template>
@ -87,6 +92,7 @@ import ActionButton from '@/components/view/ActionButton'
import TreeView from '@/components/view/TreeView'
import DomainActionForm from '@/views/iam/DomainActionForm'
import ResourceView from '@/components/view/ResourceView'
import DomainDeleteConfirm from '@/components/view/DomainDeleteConfirm'
import eventBus from '@/config/eventBus'
export default {
@ -96,7 +102,8 @@ export default {
ActionButton,
TreeView,
DomainActionForm,
ResourceView
ResourceView,
DomainDeleteConfirm
},
mixins: [mixinDevice],
data () {
@ -111,7 +118,9 @@ export default {
action: {},
dataView: false,
domainStore: {},
treeDeletedKey: null
treeDeletedKey: null,
showDeleteConfirm: false,
deleteDomainResource: null
}
},
computed: {
@ -205,7 +214,12 @@ export default {
})
},
execAction (action) {
this.treeDeletedKey = action.api === 'deleteDomain' ? this.resource.key : null
if (action.api === 'deleteDomain') {
this.deleteDomainResource = this.resource
this.showDeleteConfirm = true
return
}
this.treeDeletedKey = null
this.actionData = []
this.action = action
this.action.params = store.getters.apis[this.action.api].params
@ -319,6 +333,38 @@ export default {
closeAction () {
this.showAction = false
},
confirmDeleteDomain () {
const domain = this.deleteDomainResource
const params = { id: domain.id, cleanup: true }
api('deleteDomain', params).then(json => {
const jobId = json.deletedomainresponse.jobid
this.$pollJob({
jobId,
title: this.$t('label.action.delete.domain'),
description: domain.name,
loadingMessage: `${this.$t('label.action.delete.domain')} ${domain.name}`,
successMessage: `${this.$t('label.action.delete.domain')} ${domain.name}`,
catchMessage: this.$t('error.fetching.async.job.result'),
successMethod: () => {
if (this.$route.params.id === domain.id) {
this.$router.push({ path: '/domain' })
}
this.fetchData()
}
})
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: error.response?.headers['x-description'] || this.$t('message.request.failed')
})
}).finally(() => {
this.showDeleteConfirm = false
this.deleteDomainResource = null
this.treeDeletedKey = null
})
},
forceRerender () {
this.treeViewKey += 1
}

View File

@ -120,7 +120,7 @@ export default {
methods: {
fetchData () {
var params = {
details: 'servoff,tmpl,nics',
details: 'group,nics,secgrp,tmpl,servoff,diskoff,iso,volume,affgrp,backoff,vnfnics',
isVnf: true,
listAll: true
}

View File

@ -40,9 +40,11 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -708,13 +710,31 @@ public class Script implements Callable<String> {
return executeCommandForExitValue(0, command);
}
private static void cleanupProcesses(AtomicReference<List<Process>> processesRef) {
List<Process> processes = processesRef.get();
if (CollectionUtils.isNotEmpty(processes)) {
for (Process process : processes) {
if (process == null) {
continue;
}
LOGGER.trace(String.format("Cleaning up process [%s] from piped commands.", process.pid()));
IOUtils.closeQuietly(process.getErrorStream());
IOUtils.closeQuietly(process.getOutputStream());
IOUtils.closeQuietly(process.getInputStream());
process.destroyForcibly();
}
}
}
public static Pair<Integer, String> executePipedCommands(List<String[]> commands, long timeout) {
if (timeout <= 0) {
timeout = DEFAULT_TIMEOUT;
}
final AtomicReference<List<Process>> processesRef = new AtomicReference<>();
Callable<Pair<Integer, String>> commandRunner = () -> {
List<ProcessBuilder> builders = commands.stream().map(ProcessBuilder::new).collect(Collectors.toList());
List<Process> processes = ProcessBuilder.startPipeline(builders);
processesRef.set(processes);
Process last = processes.get(processes.size()-1);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(last.getInputStream()))) {
String line;
@ -741,6 +761,8 @@ public class Script implements Callable<String> {
result.second(ERR_TIMEOUT);
} catch (InterruptedException | ExecutionException e) {
LOGGER.error("Error executing piped commands", e);
} finally {
cleanupProcesses(processesRef);
}
return result;
}