This commit is contained in:
Gabriel P. Santos 2026-05-12 19:09:06 -03:00
parent 380f6b2a6a
commit 369984b55c
3 changed files with 200 additions and 130 deletions

View File

@ -83,40 +83,16 @@ import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse;
import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse;
import org.apache.cloudstack.api.response.ConsoleSessionResponse;
import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
import org.apache.cloudstack.api.response.NicSecondaryIpResponse;
import org.apache.cloudstack.api.response.ResourceIconResponse;
import org.apache.cloudstack.api.response.TemplateResponse;
import org.apache.cloudstack.api.response.UnmanagedInstanceResponse;
import org.apache.cloudstack.api.response.UsageRecordResponse;
import org.apache.cloudstack.api.response.TrafficTypeResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.usage.UsageService;
import org.apache.cloudstack.vm.UnmanagedInstanceTO;
import com.cloud.capacity.Capacity;
import com.cloud.configuration.Resource;
import com.cloud.domain.DomainVO;
import com.cloud.host.HostVO;
import com.cloud.network.Networks;
import com.cloud.network.PhysicalNetworkTrafficType;
import com.cloud.network.PublicIpQuarantine;
import com.cloud.network.as.AutoScaleVmGroup;
import com.cloud.network.as.AutoScaleVmGroupVO;
import com.cloud.network.as.AutoScaleVmProfileVO;
import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.dao.PhysicalNetworkTrafficTypeVO;
import com.cloud.resource.icon.ResourceIconVO;
@ -124,19 +100,7 @@ import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceIconManager;
import com.cloud.server.ResourceTag;
import com.cloud.storage.GuestOsCategory;
import com.cloud.storage.VMTemplateVO;
import com.cloud.usage.UsageVO;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.User;
import com.cloud.user.UserData;
import com.cloud.user.UserDataVO;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserDataDao;
import com.cloud.utils.net.Ip;
import com.cloud.vm.ConsoleSessionVO;
import com.cloud.vm.NicSecondaryIp;
import com.cloud.vm.VMInstanceVO;
import static org.junit.Assert.assertEquals;
@ -190,11 +154,12 @@ public class ApiResponseHelperTest {
private HostVO hostVOMock;
@Mock
private VMInstanceVO vmInstanceVOMock;
VpcVO vpcVOMock;
@Mock
NetworkACL networkACLMock;
private VpcVO vpcVOMock;
@Mock
private NetworkACL networkACLMock;
@Spy
@InjectMocks

View File

@ -626,6 +626,7 @@
<router-link v-if="!isStatic && $router.resolve('/diskoffering/' + resource.rootdiskofferingid).matched[0].redirect !== '/exception/404'" :to="{ path: '/diskoffering/' + resource.rootdiskofferingid }">{{ resource.rootdiskofferingdisplaytext }}</router-link>
<span v-else>{{ resource.rootdiskofferingdisplaytext }}</span>
</div>
</div>
<div class="resource-detail-item" v-if="resource.keypairs && resource.keypairs.length > 0">
<div class="resource-detail-item__label">{{ $t('label.keypairs') }}</div>
<div class="resource-detail-item__details">

View File

@ -47,12 +47,12 @@
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option key="all" value="">
{{ $t('label.view.all') }}
</a-select-option>
<a-select-option v-for="network in networksList" :key="network.id" :value="network.id">
<a-select-option v-for="network in networksList" :key="network.id" :value="network.id" :label="network.name">
{{ network.name }}
</a-select-option>
</a-select>
@ -65,35 +65,57 @@
:rowKey="item => item.id"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:pagination="false" >
<template #ipaddress="{ text, record }">
<router-link v-if="record.forvirtualnetwork === true" :to="{ path: '/publicip/' + record.id }" >{{ text }} </router-link>
<div v-else>{{ text }}</div>
<a-tag v-if="record.issourcenat === true">source-nat</a-tag>
</template>
<template #bodyCell="{ column, text, record }">
<template v-if="column.key === 'ipaddress'">
<router-link v-if="record.forvirtualnetwork === true" :to="{ path: '/publicip/' + record.id }" >{{ text }}&nbsp;</router-link>
<div v-else>{{ text }}</div>
<template v-if="record.issourcenat === true">
<a-tag>{{ $t('label.sourcenat') }}</a-tag>
</template>
<template v-else-if="record.isstaticnat === true">
<a-tag>{{ $t('label.staticnat') }}</a-tag>
</template>
<template v-else-if="record.hasrules === false">
<tooltip-button
v-if="record.forvirtualnetwork === true"
:tooltip="$t('label.action.set.as.source.nat.ip')"
type="primary"
:danger="false"
icon="aim-outlined"
:disabled="!('updateNetwork' in $store.getters.apis)"
@onClick="showChangeSourceNat(record)"></tooltip-button>
</template>
<template v-else><!-- -if="record.hasrules === true" -->
<Tooltip placement="topLeft" :title="$t('message.sourcenatip.change.inhibited')" >
<a-tag>{{ $t('label.hasrules') }}</a-tag>
</Tooltip>
</template>
</template>
<template #state="{ record }">
<status :text="record.state" displayText />
</template>
<template v-if="column.key === 'state'">
<status :text="record.state" displayText />
</template>
<template #virtualmachineid="{ record }">
<desktop-outlined v-if="record.virtualmachineid" />
<router-link :to="{ path: '/vm/' + record.virtualmachineid }" > {{ record.virtualmachinename || record.virtualmachineid }} </router-link>
</template>
<template v-if="column.key === 'virtualmachineid'">
<desktop-outlined v-if="record.virtualmachineid" />
<router-link :to="{ path: getVmRouteUsingType(record) + record.virtualmachineid }" > {{ record.virtualmachinename || record.virtualmachineid }} </router-link>
</template>
<template #associatednetworkname="{ record }">
<router-link v-if="record.forvirtualnetwork === true" :to="{ path: '/guestnetwork/' + record.associatednetworkid }" > {{ record.associatednetworkname || record.associatednetworkid }} </router-link>
<div v-else>{{ record.networkname }}</div>
</template>
<template v-if="column.key === 'associatednetworkname'">
<router-link v-if="record.forvirtualnetwork === true" :to="{ path: '/guestnetwork/' + record.associatednetworkid }" > {{ record.associatednetworkname || record.associatednetworkid }} </router-link>
<div v-else>{{ record.networkname }}</div>
</template>
<template #action="{ record }">
<tooltip-button
v-if="record.issourcenat !== true && record.forvirtualnetwork === true"
:tooltip="$t('label.action.release.ip')"
type="primary"
:danger="true"
icon="delete-outlined"
:disabled="!('disassociateIpAddress' in $store.getters.apis)"
@onClick="releaseIpAddress(record)" />
<template v-if="column.key === 'actions'">
<tooltip-button
v-if="record.issourcenat !== true && record.forvirtualnetwork === true"
:tooltip="$t('label.action.release.ip')"
type="primary"
:danger="true"
icon="delete-outlined"
:disabled="!('disassociateIpAddress' in $store.getters.apis)"
@onClick="releaseIpAddress(record)" />
</template>
</template>
</a-table>
<a-divider/>
@ -126,19 +148,17 @@
<a-alert :message="$t('message.action.acquire.ip')" type="warning" />
<a-form layout="vertical" style="margin-top: 10px">
<a-form-item :label="$t('label.ipaddress')">
<a-select
<infinite-scroll-select
v-focus="true"
style="width: 100%;"
v-model:value="acquireIp"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option
v-for="ip in listPublicIpAddress"
:key="ip.ipaddress">{{ ip.ipaddress }} ({{ ip.state }})</a-select-option>
</a-select>
api="listPublicIpAddresses"
:apiParams="listApiParamsForAssociate"
resourceType="publicipaddress"
optionValueKey="ipaddress"
:optionLabelFn="ip => ip.ipaddress + ' (' + ip.state + ')'"
defaultIcon="environment-outlined"
:autoSelectFirstOption="true"
@change-option-value="(ip) => acquireIp = ip" />
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="onCloseModal">{{ $t('label.cancel') }}</a-button>
@ -147,6 +167,24 @@
</a-form>
</a-spin>
</a-modal>
<a-modal
v-if="changeSourceNat"
:title="$t('message.sourcenatip.change.warning')"
:visible="changeSourceNat"
:closable="true"
:footer="null"
@cancel="cancelChangeSourceNat"
centered
:disabled="!('updateNetwork' in $store.getters.apis)"
width="450px">
<template>
<a-alert :message="$t('message.sourcenatip.change.warning')" type="warning" />
</template>
<div :span="24" class="action-button">
<a-button @click="cancelChangeSourceNat">{{ $t('label.cancel') }}</a-button>
<a-button ref="submit" type="primary" @click="setSourceNatIp(record)">{{ $t('label.ok') }}</a-button>
</div>
</a-modal>
<bulk-action-view
v-if="showConfirmationAction || showGroupActionModal"
:showConfirmationAction="showConfirmationAction"
@ -164,19 +202,22 @@
@close-modal="closeModal" />
</div>
</template>
<script>
import { api } from '@/api'
import { getAPI, postAPI } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionView from '@/components/view/BulkActionView'
import eventBus from '@/config/eventBus'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect'
export default {
name: 'IpAddressesTab',
components: {
Status,
TooltipButton,
BulkActionView
BulkActionView,
InfiniteScrollSelect
},
props: {
resource: {
@ -204,7 +245,7 @@ export default {
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['Action'],
filterColumns: ['Actions'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.release.public.ip.address'),
@ -212,37 +253,39 @@ export default {
},
columns: [
{
key: 'ipaddress',
title: this.$t('label.ipaddress'),
dataIndex: 'ipaddress',
slots: { customRender: 'ipaddress' }
dataIndex: 'ipaddress'
},
{
key: 'state',
title: this.$t('label.state'),
dataIndex: 'state',
slots: { customRender: 'state' }
dataIndex: 'state'
},
{
key: 'virtualmachineid',
title: this.$t('label.vm'),
dataIndex: 'virtualmachineid',
slots: { customRender: 'virtualmachineid' }
dataIndex: 'virtualmachineid'
},
{
key: 'associatednetworkname',
title: this.$t('label.network'),
dataIndex: 'associatednetworkname',
slots: { customRender: 'associatednetworkname' }
dataIndex: 'associatednetworkname'
},
{
title: '',
slots: { customRender: 'action' }
key: 'actions',
title: ''
}
],
showAcquireIp: false,
acquireLoading: false,
acquireIp: null,
listPublicIpAddress: []
changeSourceNat: false,
zoneExtNetProvider: ''
}
},
created () {
async created () {
await this.fetchZones()
this.fetchData()
},
watch: {
@ -257,6 +300,26 @@ export default {
}
},
inject: ['parentFetchData'],
computed: {
listApiParams () {
const params = {
zoneid: this.resource.zoneid,
domainid: this.resource.domainid,
account: this.resource.account,
forvirtualnetwork: true,
allocatedonly: false
}
if (['nsx', 'netris'].includes(this.zoneExtNetProvider?.toLowerCase())) {
params.forprovider = true
}
return params
},
listApiParamsForAssociate () {
const params = this.listApiParams
params.state = 'Free,Reserved'
return params
}
},
methods: {
fetchData () {
const params = {
@ -278,24 +341,30 @@ export default {
} else {
params.associatednetworkid = this.resource.id
}
if (['nsx', 'netris'].includes(this.zoneExtNetProvider?.toLowerCase())) {
params.forprovider = true
}
this.fetchLoading = true
api('listPublicIpAddresses', params).then(json => {
getAPI('listPublicIpAddresses', params).then(json => {
this.totalIps = json.listpublicipaddressesresponse.count || 0
this.ips = json.listpublicipaddressesresponse.publicipaddress || []
}).finally(() => {
this.fetchLoading = false
})
},
fetchListPublicIpAddress () {
fetchZones () {
return new Promise((resolve, reject) => {
const params = {
zoneid: this.resource.zoneid,
domainid: this.resource.domainid,
account: this.resource.account,
forvirtualnetwork: true,
allocatedonly: false
}
api('listPublicIpAddresses', params).then(json => {
getAPI('listZones', {
id: this.resource.zoneid
}).then(json => {
this.zoneExtNetProvider = json?.listzonesresponse?.zone?.[0]?.provider || null
resolve(this.zoneExtNetProvider)
}).catch(reject)
})
},
fetchListPublicIpAddress (state) {
return new Promise((resolve, reject) => {
getAPI('listPublicIpAddresses', this.listApiParams).then(json => {
const listPublicIps = json.listpublicipaddressesresponse.publicipaddress || []
resolve(listPublicIps)
}).catch(reject)
@ -312,6 +381,50 @@ export default {
return selection.indexOf(item.id) !== -1
}))
},
setSourceNatIp (ipaddress) {
if (this.settingsourcenat) return
if (this.$route.path.startsWith('/vpc')) {
this.updateVpc(ipaddress)
} else {
this.updateNetwork(ipaddress)
}
},
updateNetwork (ipaddress) {
const params = {}
params.sourcenatipaddress = this.sourceNatIp.ipaddress
params.id = this.resource.id
this.settingsourcenat = true
postAPI('updateNetwork', params).then(response => {
this.fetchData()
}).catch(error => {
this.$notification.error({
message: `${this.$t('label.error')} ${error.response.status}`,
description: error.response.data.updatenetworkresponse.errortext || error.response.data.errorresponse.errortext,
duration: 0
})
}).finally(() => {
this.settingsourcenat = false
this.cancelChangeSourceNat()
})
},
updateVpc (ipaddress) {
const params = {}
params.sourcenatipaddress = this.sourceNatIp.ipaddress
params.id = this.resource.id
this.settingsourcenat = true
postAPI('updateVPC', params).then(response => {
this.fetchData()
}).catch(error => {
this.$notification.error({
message: `${this.$t('label.error')} ${error.response.status}`,
description: error.response.data.updatevpcresponse.errortext || error.response.data.errorresponse.errortext,
duration: 0
})
}).finally(() => {
this.settingsourcenat = false
this.cancelChangeSourceNat()
})
},
resetSelection () {
this.setSelection([])
},
@ -349,7 +462,7 @@ export default {
params.ipaddress = this.acquireIp
this.acquireLoading = true
api('associateIpAddress', params).then(response => {
postAPI('associateIpAddress', params).then(response => {
this.$pollJob({
jobId: response.associateipaddressresponse.jobid,
successMessage: `${this.$t('message.success.acquire.ip')} ${this.$t('label.for')} ${this.resource.name}`,
@ -366,8 +479,8 @@ export default {
this.onCloseModal()
}).catch(error => {
this.$notification.error({
message: `${this.$t('label.error')} ${error.response.status}`,
description: error.response.data.associateipaddressresponse.errortext || error.response.data.errorresponse.errortext,
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message,
duration: 0
})
}).finally(() => {
@ -385,9 +498,9 @@ export default {
releaseIpAddresses (e) {
this.showConfirmationAction = false
this.selectedColumns.splice(0, 0, {
key: 'status',
dataIndex: 'status',
title: this.$t('label.operation.status'),
slots: { customRender: 'status' },
filters: [
{ text: 'In Progress', value: 'InProgress' },
{ text: 'Success', value: 'success' },
@ -403,7 +516,7 @@ export default {
},
releaseIpAddress (ip) {
this.fetchLoading = true
api('disassociateIpAddress', {
postAPI('disassociateIpAddress', {
id: ip.id
}).then(response => {
const jobId = response.disassociateipaddressresponse.jobid
@ -439,38 +552,29 @@ export default {
})
})
},
getVmRouteUsingType (record) {
switch (record.virtualmachinetype) {
case 'DomainRouter' : return '/router/'
case 'ConsoleProxy' :
case 'SecondaryStorageVm': return '/systemvm/'
default: return '/vm/'
}
},
async onShowAcquireIp () {
this.showAcquireIp = true
this.acquireLoading = true
this.listPublicIpAddress = []
try {
const listPublicIpAddress = await this.fetchListPublicIpAddress()
listPublicIpAddress.forEach(item => {
if (item.state === 'Free' || item.state === 'Reserved') {
this.listPublicIpAddress.push({
ipaddress: item.ipaddress,
state: item.state
})
}
})
this.listPublicIpAddress.sort(function (a, b) {
if (a.ipaddress < b.ipaddress) { return -1 }
if (a.ipaddress > b.ipaddress) { return 1 }
return 0
})
this.acquireIp = this.listPublicIpAddress && this.listPublicIpAddress.length > 0 ? this.listPublicIpAddress[0].ipaddress : null
this.acquireLoading = false
} catch (e) {
this.acquireLoading = false
this.$notifyError(e)
}
},
onCloseModal () {
this.showAcquireIp = false
},
closeModal () {
this.showConfirmationAction = false
},
showChangeSourceNat (ipaddress) {
this.changeSourceNat = true
this.sourceNatIp = ipaddress
},
cancelChangeSourceNat () {
this.changeSourceNat = false
}
}
}