Skip to content

Commit 1c1eef3

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

File tree

7 files changed

+297
-37
lines changed

7 files changed

+297
-37
lines changed

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

0 commit comments

Comments
 (0)