Add Listener for VM lifecycle to add dnsrecords for associated dns zone

This commit is contained in:
Manoj Kumar 2026-03-02 17:17:13 +05:30
parent 6ca9d5ace8
commit 1c1eef3cc7
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
7 changed files with 297 additions and 37 deletions

View File

@ -37,9 +37,12 @@ import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse;
import org.apache.cloudstack.api.response.DnsZoneResponse;
import org.apache.cloudstack.api.response.ListResponse;
import com.cloud.network.Network;
import com.cloud.user.Account;
import com.cloud.utils.component.Manager;
import com.cloud.utils.component.PluggableService;
import com.cloud.vm.Nic;
import com.cloud.vm.VirtualMachine;
public interface DnsProviderManager extends Manager, PluggableService {
@ -73,4 +76,6 @@ public interface DnsProviderManager extends Manager, PluggableService {
boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd);
void checkDnsServerPermissions(Account caller, DnsServer server);
boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd);
}

View File

@ -0,0 +1,18 @@
# 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.
name=inmemory
parent=event

View File

@ -0,0 +1,34 @@
<!--
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.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd"
>
<bean id="inMemoryEventBus" class="org.apache.cloudstack.mom.inmemory.InMemoryEventBus">
<property name="name" value="inMemoryEventBus"/>
</bean>
</beans>

View File

@ -62,6 +62,7 @@ import org.springframework.stereotype.Component;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.network.Network;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.projects.Project;
@ -76,8 +77,8 @@ import com.cloud.utils.db.Filter;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.NicVO;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.Nic;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.UserVmDao;
@ -671,50 +672,21 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
}
}
/**
* Helper method to handle both Register and Remove logic for Instance
*/
private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boolean isAdd) {
// 1. Fetch VM and verify access
UserVmVO instance = userVmDao.findById(instanceId);
if (instance == null) {
throw new InvalidParameterValueException("Provided Instance not found.");
}
accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, instance);
// 2. Resolve the NIC and Network
NicVO nic;
if (networkId != null) {
nic = nicDao.findByNtwkIdAndInstanceId(networkId, instance.getId());
} else {
nic = nicDao.findDefaultNicForVM(instance.getId());
networkId = nic != null ? nic.getNetworkId() : null;
}
// networkId may not be of Shared network type
// there might be multiple shared networks
// possible to have dns record for secondary ip
if (nic == null) {
throw new CloudRuntimeException("No valid NIC found for this Instance on the specified Network.");
}
// 3. Find if this network is linked to any DNS Zones
@Override
public boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd) {
long networkId = network.getId();
List<DnsZoneNetworkMapVO> mappings = dnsZoneNetworkMapDao.listByNetworkId(networkId);
if (mappings == null || mappings.isEmpty()) {
throw new CloudRuntimeException("No DNS zones are mapped to this network. Please associate a zone first.");
logger.warn("No DNS zones are mapped to this network. Please associate a zone first.");
return false;
}
boolean atLeastOneSuccess = false;
// 4. Iterate over mapped zones and push the record
for (DnsZoneNetworkMapVO map : mappings) {
DnsZoneVO zone = dnsZoneDao.findById(map.getDnsZoneId());
if (zone == null || zone.getState() != DnsZone.State.Active) {
continue;
}
DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId());
// Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain")
String recordName = String.valueOf(instance.getInstanceName());
if (StringUtils.isNotBlank(map.getSubDomain())) {
@ -753,11 +725,13 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
zone.getName(),
ex
);
return false;
}
}
if (!atLeastOneSuccess) {
throw new CloudRuntimeException("Failed to process DNS records. Ensure the Instance has a valid IP address.");
logger.error("Failed to process DNS records. Ensure the Instance has a valid IP address.");
return false;
}
return true;
}

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.
//
package org.apache.cloudstack.dns;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.framework.events.Event;
import org.apache.cloudstack.framework.events.EventBus;
import org.apache.cloudstack.framework.events.EventBusException;
import org.apache.cloudstack.framework.events.EventSubscriber;
import org.apache.cloudstack.framework.events.EventTopic;
import org.springframework.stereotype.Component;
import com.cloud.event.EventTypes;
import com.cloud.network.Network;
import com.cloud.network.dao.NetworkDao;
import com.cloud.utils.StringUtils;
import com.cloud.utils.component.ManagerBase;
import com.cloud.vm.Nic;
import com.cloud.vm.NicVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class DnsVmLifecycleListener extends ManagerBase implements EventSubscriber {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Inject
private EventBus eventBus = null;
@Inject
VMInstanceDao vmInstanceDao;
@Inject
NetworkDao networkDao;
@Inject
NicDao nicDao;
@Inject
DnsProviderManager providerManager;
@Override
public boolean configure(final String name, final Map<String, Object> params) {
if (eventBus == null) {
logger.info("EventBus is not available; DNS Instance lifecycle listener will not subscribe to events");
return true;
}
try {
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_CREATE, null, null, null), this);
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_STOP, null, null, null), this);
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_DESTROY, null, null, null), this);
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_CREATE, null, null, null), this);
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_DELETE, null, null, null), this);
} catch (EventBusException ex) {
logger.error("Failed to subscribe DnsVmLifecycleListener to EventBus", ex);
}
return true;
}
@Override
public void onEvent(Event event) {
logger.debug("Received EventBus event: {}", event);
JsonNode descJson = parseEventDescription(event);
if (!isEventCompleted(descJson)) {
return;
}
String eventType = event.getEventType();
String resourceUuid = event.getResourceUUID();
logger.debug("Processing Event: {}", event);
try {
switch (eventType) {
case EventTypes.EVENT_VM_CREATE:
case EventTypes.EVENT_VM_START:
handleVmEvent(resourceUuid, true);
break;
case EventTypes.EVENT_VM_STOP:
case EventTypes.EVENT_VM_DESTROY:
handleVmEvent(resourceUuid, false);
break;
case EventTypes.EVENT_NIC_CREATE:
handleNicEvent(descJson, true);
break;
case EventTypes.EVENT_NIC_DELETE:
handleNicEvent(descJson, false);
break;
default:
break;
}
} catch (Exception ex) {
logger.error("Failed to process DNS lifecycle event: type={}, resourceUuid={}",
eventType, event.getResourceUUID(), ex);
}
}
private void handleNicEvent(JsonNode eventDesc, boolean isAddDnsRecord) {
JsonNode nicUuid = eventDesc.get("Nic");
JsonNode vmUuid = eventDesc.get("VirtualMachine");
JsonNode networkUuid = eventDesc.get("Network");
if (nicUuid == null || nicUuid.isNull() || vmUuid == null || vmUuid.isNull() || networkUuid == null || networkUuid.isNull()) {
logger.warn("Event has missing data to work on: {}", eventDesc);
return;
}
VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid.asText());
if (vmInstanceVO == null) {
logger.error("Unable to find Instance with ID: {}", vmUuid);
return;
}
Network network = networkDao.findByUuid(networkUuid.asText());
if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) {
logger.warn("Network is not eligible for DNS record registration");
return;
}
Nic nic = nicDao.findByUuid(nicUuid.asText());
if (nic == null) {
logger.error("NIC is not found for the ID: {}", nicUuid);
}
boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord);
if (!dnsRecordAdded) {
logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}",
isAddDnsRecord ? "adding" : "removing", vmUuid, networkUuid);
}
}
private void handleVmEvent(String vmUuid, boolean isAddDnsRecord) {
VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid);
if (vmInstanceVO == null) {
logger.error("Unable to find Instance with ID: {}", vmUuid);
return;
}
List<NicVO> vmNics = nicDao.listByVmId(vmInstanceVO.getId());
for (NicVO nic : vmNics) {
Network network = networkDao.findById(nic.getNetworkId());
if (Network.GuestType.Shared.equals(network.getGuestType())) {
boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord);
if (!dnsRecordAdded) {
logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}",
isAddDnsRecord ? "adding" : "removing", vmUuid, network.getUuid());
}
}
}
}
private JsonNode parseEventDescription(Event event) {
String rawDescription = event.getDescription();
if (StringUtils.isBlank(rawDescription)) {
return null;
}
try {
return OBJECT_MAPPER.readTree(rawDescription);
} catch (Exception ex) {
logger.warn("parseEventDescription: failed to parse description for event [{}]: {}",
event.getEventType(), ex.getMessage());
return null;
}
}
private boolean isEventCompleted(JsonNode descJson) {
if (descJson == null) {
return false;
}
JsonNode statusNode = descJson.get(ApiConstants.STATUS);
if (statusNode == null || statusNode.isNull()) {
return false;
}
return ApiConstants.COMPLETED.equalsIgnoreCase(statusNode.asText());
}
}

View File

@ -0,0 +1,35 @@
<!--
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.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd"
>
<bean id="eventNotificationBus" class="org.apache.cloudstack.mom.inmemory.InMemoryEventBus">
<property name="name" value="eventNotificationBus"/>
</bean>
</beans>

View File

@ -402,4 +402,5 @@
<bean id="dnsProviderManager" class="org.apache.cloudstack.dns.DnsProviderManagerImpl" >
<property name="dnsProviders" value="#{dnsProvidersRegistry.registered}" />
</bean>
<bean id="dnsVmLifecycleListener" class="org.apache.cloudstack.dns.DnsVmLifecycleListener" />
</beans>