add associate and disassociate dns zone to network ui screens, network response changes

This commit is contained in:
Manoj Kumar 2026-03-19 16:42:17 +05:30
parent a1eca74de1
commit 981bb64b86
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
10 changed files with 293 additions and 14 deletions

View File

@ -1352,6 +1352,8 @@ public class ApiConstants {
public static final String DNS_USER_NAME = "dnsusername";
public static final String CREDENTIALS = "credentials";
public static final String DNS_ZONE_ID = "dnszoneid";
public static final String DNS_ZONE = "dnszone";
public static final String DNS_SUB_DOMAIN = "dnssubdomain";
public static final String DNS_SERVER_ID = "dnsserverid";
public static final String CONTENT = "content";
public static final String CONTENTS = "contents";

View File

@ -331,6 +331,14 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement
@Param(description = "The BGP peers for the network", since = "4.20.0")
private Set<BgpPeerResponse> bgpPeers;
@SerializedName(ApiConstants.DNS_ZONE)
@Param(description = "DNS zone associated to the network", since = "4.23.0")
private String dnsZone;
@SerializedName(ApiConstants.DNS_SUB_DOMAIN)
@Param(description = "DNS subdomain associated to the network", since = "4.23.0")
private String dnsSubdomain;
public NetworkResponse() {}
public Boolean getDisplayNetwork() {
@ -702,4 +710,12 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement
public void setIpv6Dns2(String ipv6Dns2) {
this.ipv6Dns2 = ipv6Dns2;
}
public void setDnsZone(String dnsZone) {
this.dnsZone = dnsZone;
}
public void setDnsSubdomain(String dnsSubdomain) {
this.dnsSubdomain = dnsSubdomain;
}
}

View File

@ -41,7 +41,6 @@ import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupRespon
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.dns.DnsServer;
import org.apache.cloudstack.acl.apikeypair.ApiKeyPair;
import org.apache.cloudstack.acl.ControlledEntity;
@ -511,11 +510,6 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
// TODO Auto-generated method stub
}
@Override
public void checkAccess(Account account, DnsServer dnsServer) throws PermissionDeniedException {
// NOOP
}
@Override
public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd){
return null;

View File

@ -82,6 +82,10 @@ import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.dns.dao.DnsZoneDao;
import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao;
import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO;
import org.apache.cloudstack.dns.vo.DnsZoneVO;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@ -365,6 +369,7 @@ import org.apache.cloudstack.acl.dao.ApiKeyPairDao;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;
public class ApiDBUtils {
private static final Logger log = LogManager.getLogger(ApiDBUtils.class);
@ -506,6 +511,8 @@ public class ApiDBUtils {
static SharedFSJoinDao s_sharedFSJoinDao;
static BucketDao s_bucketDao;
static DnsZoneDao s_dnsZoneDao;
static DnsZoneNetworkMapDao s_dnsZoneNetworkMapDao;
static VirtualMachineManager s_virtualMachineManager;
@Inject
@ -778,6 +785,10 @@ public class ApiDBUtils {
private VirtualMachineManager virtualMachineManager;
@Inject
private SharedFSJoinDao sharedFSJoinDao;
@Inject
private DnsZoneDao dnsZoneDao;
@Inject
private DnsZoneNetworkMapDao dnsZoneNetworkMapDao;
@PostConstruct
void init() {
@ -916,6 +927,8 @@ public class ApiDBUtils {
s_bucketDao = bucketDao;
s_virtualMachineManager = virtualMachineManager;
s_sharedFSJoinDao = sharedFSJoinDao;
s_dnsZoneDao = dnsZoneDao;
s_dnsZoneNetworkMapDao = dnsZoneNetworkMapDao;
}
// ///////////////////////////////////////////////////////////
@ -2294,6 +2307,17 @@ public class ApiDBUtils {
return details.isEmpty() ? null : details;
}
public static Pair<String, String> findDnsZoneByNetworkId(long networkId) {
DnsZoneNetworkMapVO dnsNetworkMapVO = s_dnsZoneNetworkMapDao.findByNetworkId(networkId);
if (dnsNetworkMapVO != null) {
DnsZoneVO dnsZoneVO = s_dnsZoneDao.findById(dnsNetworkMapVO.getDnsZoneId());
if (Strings.isNotBlank(dnsZoneVO.getName())) {
return new Pair<> (dnsZoneVO.getName(), dnsNetworkMapVO.getSubDomain());
}
}
return new Pair<>(null, null);
}
public static boolean isAdmin(Account account) {
return s_accountService.isAdmin(account.getId());
}

View File

@ -543,6 +543,7 @@ public class ApiResponseHelper implements ResponseGenerator {
@Inject
ResourceIconManager resourceIconManager;
public static String getPrettyDomainPath(String path) {
if (path == null) {
return null;
@ -2635,6 +2636,14 @@ public class ApiResponseHelper implements ResponseGenerator {
response.setDetails(details);
}
Pair<String, String> dnsZoneAndSubDomain = ApiDBUtils.findDnsZoneByNetworkId(network.getId());
if (StringUtils.isNotBlank(dnsZoneAndSubDomain.first())) {
response.setDnsZone(dnsZoneAndSubDomain.first());
}
if (StringUtils.isNotBlank(dnsZoneAndSubDomain.second())) {
response.setDnsSubdomain(dnsZoneAndSubDomain.second());
}
DataCenter zone = ApiDBUtils.findZoneById(network.getDataCenterId());
if (zone != null) {
response.setZoneId(zone.getUuid());

View File

@ -661,10 +661,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
return null;
}
DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
// Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain")
// Construct FQDN Prefix (e.g., "instance-id.dnsZoneName" or "instance-id.subdomain.dnsZoneName")
String recordName = String.valueOf(instance.getInstanceName());
if (StringUtils.isNotBlank(dnsZoneNetworkMap.getSubDomain())) {
recordName = recordName + "." + dnsZoneNetworkMap.getSubDomain();
recordName = String.join(".", recordName, dnsZoneNetworkMap.getSubDomain(), dnsZone.getName());
}
try {

View File

@ -967,12 +967,15 @@
"label.dns.server": "DNS Server",
"label.dnsserverid": "DNS Server ID",
"label.dnsservername": "DNS Server name",
"label.dnssubdomain": "DNS Subdomain",
"label.dns.servers": "DNS Servers",
"label.dns.zone": "DNS Zone",
"label.dns.zones": "DNS Zones",
"label.dns.update.server": "Update DNS Server",
"label.dns.update.zone": "Update DNS Zone",
"label.dns.zone.select": "Select a DNS zone",
"label.dnsrecords": "DNS Records",
"label.dnszone": "DNS Zone",
"label.domain": "Domain",
"label.domain.id": "Domain ID",
"label.domain.name": "Domain name",
@ -2498,6 +2501,7 @@
"label.storagetype": "Storage type",
"label.storageip": "Storage IP address",
"label.strict": "Strict",
"label.subdomain": "Subdomain",
"label.subdomainaccess": "Subdomain access",
"label.submit": "Submit",
"label.subnet": "Subnet",
@ -2990,6 +2994,9 @@
"message.confirm.delete.dns.record": "Are you sure you want to delete this DNS record?",
"message.action.delete.dns.server": "Please confirm you want to delete this DNS server.",
"message.action.delete.dns.zone": "Please confirm you want to delete this DNS zone.",
"label.action.associate.dns.zone": "Associate DNS Zone",
"label.action.disassociate.dns.zone": "Disassociate DNS Zone",
"message.action.disassociate.dns.zone": "Please confirm you want to disassociate the DNS zone from this network.",
"message.action.delete.domain": "Please confirm that you want to delete this domain.",
"message.action.delete.extension": "Please confirm that you want to delete the extension",
"message.action.delete.external.firewall": "Please confirm that you would like to remove this external firewall. Warning: If you are planning to add back the same external firewall, you must reset usage data on the device.",
@ -3911,6 +3918,8 @@
"message.success.update.dns.server": "Successfully updated DNS server",
"message.success.update.dns.zone": "Successfully updated DNS zone",
"message.success.delete.dns.record": "Successfully deleted DNS record",
"message.success.associate.dns.zone": "Successfully associated DNS zone with network",
"message.error.fetch.dns.zones": "Could not load DNS zones.",
"message.success.add.guest.network": "Successfully created guest Network",
"message.success.add.gpu.device": "Successfully added GPU device",
"message.success.add.interface.static.route": "Successfully added interface Static Route",

View File

@ -18,7 +18,7 @@
import { shallowRef, defineAsyncComponent } from 'vue'
import store from '@/store'
import tungsten from '@/assets/icons/tungsten.svg?inline'
import { isAdmin } from '@/role'
import { isAdmin, isAdminOrDomainAdmin } from '@/role'
import { isZoneCreated } from '@/utils/zone'
import { vueProps } from '@/vue-app'
@ -49,7 +49,10 @@ export default {
return fields
},
details: () => {
var fields = ['name', 'id', 'description', 'type', 'traffictype', 'vpcid', 'vlan', 'broadcasturi', 'cidr', 'ip6cidr', 'netmask', 'gateway', 'asnumber', 'aclname', 'ispersistent', 'restartrequired', 'reservediprange', 'redundantrouter', 'networkdomain', 'egressdefaultpolicy', 'zonename', 'account', 'domainpath', 'associatednetwork', 'associatednetworkid', 'ip4routing', 'ip6firewall', 'ip6routing', 'ip6routes', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu', 'privatemtu']
var fields = ['name', 'id', 'description', 'type', 'traffictype', 'vpcid', 'vlan', 'broadcasturi', 'cidr', 'ip6cidr', 'netmask', 'gateway', 'asnumber',
'aclname', 'ispersistent', 'restartrequired', 'reservediprange', 'redundantrouter', 'networkdomain', 'egressdefaultpolicy', 'zonename', 'account',
'domainpath', 'associatednetwork', 'associatednetworkid', 'ip4routing', 'ip6firewall', 'ip6routing', 'ip6routes', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2',
'publicmtu', 'privatemtu', 'dnszone', 'dnssubdomain']
if (!isAdmin()) {
fields = fields.filter(function (e) { return e !== 'broadcasturi' })
}
@ -202,6 +205,36 @@ export default {
}
}
},
{
api: 'associateDnsZoneToNetwork',
icon: 'link-outlined',
label: 'label.action.associate.dns.zone',
dataView: true,
show: (record, store) => {
return (record.type === 'Shared' && record.dnszone === undefined &&
(record.account === store.userInfo.account || isAdminOrDomainAdmin(store.userInfo.roletype)))
},
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/AssociateDnsZone.vue')))
},
{
api: 'disassociateDnsZoneFromNetwork',
icon: 'disconnect-outlined',
label: 'label.action.disassociate.dns.zone',
message: 'message.action.disassociate.dns.zone',
dataView: true,
popup: true,
args: ['networkid'],
show: (record, store) => {
return record.dnszone !== undefined && record.type === 'Shared' &&
(record.account === store.userInfo.account || isAdminOrDomainAdmin(store.userInfo.roletype))
},
mapping: {
networkid: {
value: (record) => { return record.id }
}
}
},
{
api: 'deleteNetwork',
icon: 'delete-outlined',

View File

@ -57,11 +57,10 @@
<a-descriptions-item :label="$t('label.isolationuri')" v-if="record.isolationuri">
{{ record.isolationuri }}
</a-descriptions-item>
<a-descriptions-item :label="$t('label.dns.record.url')" v-if="record.dnsrecordurl">
{{ record.dnsrecordurl }}
</a-descriptions-item>
</template>
<a-descriptions-item :label="$t('label.dns.record.url')" v-if="record.dnsrecordurl">
{{ record.dnsrecordurl }}
</a-descriptions-item>
</a-descriptions>
</template>
<template #bodyCell="{ column, text, record }">

View File

@ -0,0 +1,193 @@
// 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>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
<a-form-item name="dnszoneid" ref="dnszoneid">
<template #label>
<tooltip-label
:title="$t('label.dns.zone')"
:tooltip="apiParams.dnszoneid?.description" />
</template>
<a-select
v-model:value="form.dnszoneid"
:placeholder="$t('label.dns.zone.select')"
:loading="fetchingZones"
showSearch
optionFilterProp="label"
v-focus="true">
<a-select-option
v-for="zone in dnsZones"
:key="zone.id"
:value="zone.id"
:label="zone.name">
{{ zone.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="subdomain" ref="subdomain">
<template #label>
<tooltip-label
:title="$t('label.subdomain')"
:tooltip="apiParams.subdomain?.description" />
</template>
<a-input
v-model:value="form.subdomain"
:placeholder="apiParams.subdomain?.description" />
</a-form-item>
<div class="action-button">
<a-button @click="closeAction">
{{ $t('label.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
{{ $t('label.ok') }}
</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
name: 'AssociateDnsZone',
components: {
TooltipLabel
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
loading: false,
apiParams: {},
form: {
dnszoneid: undefined,
subdomain: ''
},
rules: {},
fetchingZones: false,
dnsZones: []
}
},
created () {
this.apiParams = this.$getApiParams('associateDnsZoneToNetwork') || {}
this.rules = {
dnszoneid: [{ required: true, message: this.$t('message.error.required.input') }]
}
this.fetchDnsZones()
},
methods: {
async handleSubmit () {
if (this.loading) return
try {
await this.$refs.formRef.validate()
} catch (error) {
const field = error?.errorFields?.[0]?.name
if (field) {
this.$refs.formRef.scrollToField(field)
}
return
}
this.loading = true
try {
const params = {
dnszoneid: this.form.dnszoneid,
networkid: this.resource.id
}
if (this.form.subdomain && this.form.subdomain.trim()) {
params.subdomain = this.form.subdomain.trim()
}
await postAPI('associateDnsZoneToNetwork', params)
this.$notification.success({
message: this.$t('label.action.associate.dns.zone'),
description: this.$t('message.success.associate.dns.zone')
})
this.$emit('refresh-data')
this.closeAction()
} catch (error) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},
closeAction () {
this.$emit('close-action')
},
async fetchDnsZones () {
this.fetchingZones = true
try {
const response = await getAPI('listDnsZones')
const listResponse = response?.listdnszonesresponse || {}
this.dnsZones = listResponse.dnszone || []
if (this.dnsZones.length > 0) {
this.form.dnszoneid = this.dnsZones[0].id
}
} catch (error) {
console.error('Failed to fetch DNS zones', error)
this.$message.warning(this.$t('message.error.fetch.dns.zones'))
} finally {
this.fetchingZones = false
}
}
}
}
</script>
<style lang="less" scoped>
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 450px;
}
}
.action-button {
text-align: right;
margin-top: 20px;
}
.action-button button {
margin-left: 8px;
}
</style>