Skip to content

Commit 54a6b6a

Browse files
committed
Add Listener for VM lifecycle to add dnsrecords for associated dns zone
1 parent 6ca9d5a commit 54a6b6a

7 files changed

Lines changed: 259 additions & 37 deletions

File tree

api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@
3737
import org.apache.cloudstack.api.response.DnsZoneResponse;
3838
import org.apache.cloudstack.api.response.ListResponse;
3939

40+
import com.cloud.network.Network;
4041
import com.cloud.user.Account;
4142
import com.cloud.utils.component.Manager;
4243
import com.cloud.utils.component.PluggableService;
44+
import com.cloud.vm.Nic;
45+
import com.cloud.vm.VirtualMachine;
4346

4447
public interface DnsProviderManager extends Manager, PluggableService {
4548

@@ -73,4 +76,6 @@ public interface DnsProviderManager extends Manager, PluggableService {
7376
boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd);
7477

7578
void checkDnsServerPermissions(Account caller, DnsServer server);
79+
80+
boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd);
7681
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
name=inmemory
18+
parent=event
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
<beans xmlns="http://www.springframework.org/schema/beans"
20+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21+
xmlns:context="http://www.springframework.org/schema/context"
22+
xmlns:aop="http://www.springframework.org/schema/aop"
23+
xsi:schemaLocation="http://www.springframework.org/schema/beans
24+
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
25+
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
26+
http://www.springframework.org/schema/context
27+
http://www.springframework.org/schema/context/spring-context-3.0.xsd"
28+
>
29+
30+
<bean id="inMemoryEventBus" class="org.apache.cloudstack.mom.inmemory.InMemoryEventBus">
31+
<property name="name" value="inMemoryEventBus"/>
32+
</bean>
33+
34+
</beans>

server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import com.cloud.domain.dao.DomainDao;
6363
import com.cloud.exception.InvalidParameterValueException;
6464
import com.cloud.exception.PermissionDeniedException;
65+
import com.cloud.network.Network;
6566
import com.cloud.network.dao.NetworkDao;
6667
import com.cloud.network.dao.NetworkVO;
6768
import com.cloud.projects.Project;
@@ -76,8 +77,8 @@
7677
import com.cloud.utils.db.SearchBuilder;
7778
import com.cloud.utils.db.SearchCriteria;
7879
import com.cloud.utils.exception.CloudRuntimeException;
79-
import com.cloud.vm.NicVO;
80-
import com.cloud.vm.UserVmVO;
80+
import com.cloud.vm.Nic;
81+
import com.cloud.vm.VirtualMachine;
8182
import com.cloud.vm.dao.NicDao;
8283
import com.cloud.vm.dao.UserVmDao;
8384

@@ -671,50 +672,21 @@ public void checkDnsServerPermissions(Account caller, DnsServer server) {
671672
}
672673
}
673674

674-
/**
675-
* Helper method to handle both Register and Remove logic for Instance
676-
*/
677-
private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boolean isAdd) {
678-
// 1. Fetch VM and verify access
679-
UserVmVO instance = userVmDao.findById(instanceId);
680-
if (instance == null) {
681-
throw new InvalidParameterValueException("Provided Instance not found.");
682-
}
683-
accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, instance);
684-
685-
// 2. Resolve the NIC and Network
686-
NicVO nic;
687-
if (networkId != null) {
688-
nic = nicDao.findByNtwkIdAndInstanceId(networkId, instance.getId());
689-
} else {
690-
nic = nicDao.findDefaultNicForVM(instance.getId());
691-
networkId = nic != null ? nic.getNetworkId() : null;
692-
}
693-
694-
// networkId may not be of Shared network type
695-
// there might be multiple shared networks
696-
// possible to have dns record for secondary ip
697-
698-
if (nic == null) {
699-
throw new CloudRuntimeException("No valid NIC found for this Instance on the specified Network.");
700-
}
701-
702-
// 3. Find if this network is linked to any DNS Zones
675+
@Override
676+
public boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd) {
677+
long networkId = network.getId();
703678
List<DnsZoneNetworkMapVO> mappings = dnsZoneNetworkMapDao.listByNetworkId(networkId);
704679
if (mappings == null || mappings.isEmpty()) {
705-
throw new CloudRuntimeException("No DNS zones are mapped to this network. Please associate a zone first.");
680+
logger.warn("No DNS zones are mapped to this network. Please associate a zone first.");
681+
return false;
706682
}
707-
708683
boolean atLeastOneSuccess = false;
709-
// 4. Iterate over mapped zones and push the record
710684
for (DnsZoneNetworkMapVO map : mappings) {
711685
DnsZoneVO zone = dnsZoneDao.findById(map.getDnsZoneId());
712686
if (zone == null || zone.getState() != DnsZone.State.Active) {
713687
continue;
714688
}
715-
716689
DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId());
717-
718690
// Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain")
719691
String recordName = String.valueOf(instance.getInstanceName());
720692
if (StringUtils.isNotBlank(map.getSubDomain())) {
@@ -753,11 +725,13 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo
753725
zone.getName(),
754726
ex
755727
);
728+
return false;
756729
}
757730
}
758731

759732
if (!atLeastOneSuccess) {
760-
throw new CloudRuntimeException("Failed to process DNS records. Ensure the Instance has a valid IP address.");
733+
logger.error("Failed to process DNS records. Ensure the Instance has a valid IP address.");
734+
return false;
761735
}
762736
return true;
763737
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package org.apache.cloudstack.dns;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
import javax.inject.Inject;
7+
8+
import org.apache.cloudstack.api.ApiConstants;
9+
import org.apache.cloudstack.framework.events.Event;
10+
import org.apache.cloudstack.framework.events.EventBus;
11+
import org.apache.cloudstack.framework.events.EventBusException;
12+
import org.apache.cloudstack.framework.events.EventSubscriber;
13+
import org.apache.cloudstack.framework.events.EventTopic;
14+
import org.springframework.stereotype.Component;
15+
16+
import com.cloud.event.EventTypes;
17+
import com.cloud.network.Network;
18+
import com.cloud.network.dao.NetworkDao;
19+
import com.cloud.utils.StringUtils;
20+
import com.cloud.utils.component.ManagerBase;
21+
import com.cloud.vm.Nic;
22+
import com.cloud.vm.NicVO;
23+
import com.cloud.vm.VMInstanceVO;
24+
import com.cloud.vm.dao.NicDao;
25+
import com.cloud.vm.dao.VMInstanceDao;
26+
import com.fasterxml.jackson.databind.JsonNode;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
29+
@Component
30+
public class DnsVmLifecycleListener extends ManagerBase implements EventSubscriber {
31+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
32+
33+
@Inject
34+
private EventBus eventBus = null;
35+
36+
@Inject
37+
VMInstanceDao vmInstanceDao;
38+
@Inject
39+
NetworkDao networkDao;
40+
@Inject
41+
NicDao nicDao;
42+
@Inject
43+
DnsProviderManager providerManager;
44+
45+
@Override
46+
public boolean configure(final String name, final Map<String, Object> params) {
47+
if (eventBus == null) {
48+
logger.info("EventBus is not available; DNS Instance lifecycle listener will not subscribe to events");
49+
return true;
50+
}
51+
try {
52+
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_CREATE, null, null, null), this);
53+
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_STOP, null, null, null), this);
54+
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_DESTROY, null, null, null), this);
55+
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_CREATE, null, null, null), this);
56+
eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_DELETE, null, null, null), this);
57+
} catch (EventBusException ex) {
58+
logger.error("Failed to subscribe DnsVmLifecycleListener to EventBus", ex);
59+
}
60+
return true;
61+
}
62+
63+
@Override
64+
public void onEvent(Event event) {
65+
logger.debug("Received EventBus event: {}", event);
66+
JsonNode descJson = parseEventDescription(event);
67+
if (!isEventCompleted(descJson)) {
68+
return;
69+
}
70+
71+
String eventType = event.getEventType();
72+
String resourceUuid = event.getResourceUUID();
73+
logger.debug("Processing Event: {}", event);
74+
try {
75+
switch (eventType) {
76+
case EventTypes.EVENT_VM_CREATE:
77+
case EventTypes.EVENT_VM_START:
78+
handleVmEvent(resourceUuid, true);
79+
break;
80+
case EventTypes.EVENT_VM_STOP:
81+
case EventTypes.EVENT_VM_DESTROY:
82+
handleVmEvent(resourceUuid, false);
83+
break;
84+
case EventTypes.EVENT_NIC_CREATE:
85+
handleNicEvent(descJson, true);
86+
break;
87+
case EventTypes.EVENT_NIC_DELETE:
88+
handleNicEvent(descJson, false);
89+
break;
90+
default:
91+
break;
92+
}
93+
} catch (Exception ex) {
94+
logger.error("Failed to process DNS lifecycle event: type={}, resourceUuid={}",
95+
eventType, event.getResourceUUID(), ex);
96+
97+
}
98+
}
99+
100+
private void handleNicEvent(JsonNode eventDesc, boolean isAddDnsRecord) {
101+
JsonNode nicUuid = eventDesc.get("Nic");
102+
JsonNode vmUuid = eventDesc.get("VirtualMachine");
103+
JsonNode networkUuid = eventDesc.get("Network");
104+
if (nicUuid == null || nicUuid.isNull() || vmUuid == null || vmUuid.isNull() || networkUuid == null || networkUuid.isNull()) {
105+
logger.warn("Event has missing data to work on: {}", eventDesc);
106+
return;
107+
}
108+
VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid.asText());
109+
if (vmInstanceVO == null) {
110+
logger.error("Unable to find Instance with ID: {}", vmUuid);
111+
return;
112+
}
113+
114+
Network network = networkDao.findByUuid(networkUuid.asText());
115+
if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) {
116+
logger.warn("Network is not eligible for DNS record registration");
117+
return;
118+
}
119+
Nic nic = nicDao.findByUuid(nicUuid.asText());
120+
if (nic == null) {
121+
logger.error("NIC is not found for the ID: {}", nicUuid);
122+
}
123+
124+
boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord);
125+
if (!dnsRecordAdded) {
126+
logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}",
127+
isAddDnsRecord ? "adding" : "removing", vmUuid, networkUuid);
128+
}
129+
}
130+
131+
private void handleVmEvent(String vmUuid, boolean isAddDnsRecord) {
132+
VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid);
133+
if (vmInstanceVO == null) {
134+
logger.error("Unable to find Instance with ID: {}", vmUuid);
135+
return;
136+
}
137+
List<NicVO> vmNics = nicDao.listByVmId(vmInstanceVO.getId());
138+
for (NicVO nic : vmNics) {
139+
Network network = networkDao.findById(nic.getNetworkId());
140+
if (Network.GuestType.Shared.equals(network.getGuestType())) {
141+
boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord);
142+
if (!dnsRecordAdded) {
143+
logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}",
144+
isAddDnsRecord ? "adding" : "removing", vmUuid, network.getUuid());
145+
}
146+
}
147+
}
148+
}
149+
150+
private JsonNode parseEventDescription(Event event) {
151+
String rawDescription = event.getDescription();
152+
if (StringUtils.isBlank(rawDescription)) {
153+
return null;
154+
}
155+
try {
156+
return OBJECT_MAPPER.readTree(rawDescription);
157+
} catch (Exception ex) {
158+
logger.warn("parseEventDescription: failed to parse description for event [{}]: {}",
159+
event.getEventType(), ex.getMessage());
160+
return null;
161+
}
162+
}
163+
164+
private boolean isEventCompleted(JsonNode descJson) {
165+
if (descJson == null) {
166+
return false;
167+
}
168+
JsonNode statusNode = descJson.get(ApiConstants.STATUS);
169+
if (statusNode == null || statusNode.isNull()) {
170+
return false;
171+
}
172+
return ApiConstants.COMPLETED.equalsIgnoreCase(statusNode.asText());
173+
}
174+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<beans xmlns="http://www.springframework.org/schema/beans"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns:context="http://www.springframework.org/schema/context"
4+
xmlns:aop="http://www.springframework.org/schema/aop"
5+
xsi:schemaLocation="http://www.springframework.org/schema/beans
6+
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
7+
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
8+
http://www.springframework.org/schema/context
9+
http://www.springframework.org/schema/context/spring-context-3.0.xsd"
10+
>
11+
12+
<bean id="eventNotificationBus" class="org.apache.cloudstack.mom.inmemory.InMemoryEventBus">
13+
<property name="name" value="eventNotificationBus"/>
14+
</bean>
15+
16+
</beans>

server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,4 +402,5 @@
402402
<bean id="dnsProviderManager" class="org.apache.cloudstack.dns.DnsProviderManagerImpl" >
403403
<property name="dnsProviders" value="#{dnsProvidersRegistry.registered}" />
404404
</bean>
405+
<bean id="dnsVmLifecycleListener" class="org.apache.cloudstack.dns.DnsVmLifecycleListener" />
405406
</beans>

0 commit comments

Comments
 (0)