diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java index e3db63a3531..f9b4ecfd582 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -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); } diff --git a/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties new file mode 100644 index 00000000000..c33f70e8919 --- /dev/null +++ b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties @@ -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 diff --git a/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml new file mode 100644 index 00000000000..4b021517063 --- /dev/null +++ b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java index 0da957cac76..7d8d8f0b0f8 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -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 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; } diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java new file mode 100644 index 00000000000..5ebf5f928d5 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java @@ -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 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 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()); + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml new file mode 100644 index 00000000000..36c26050aaa --- /dev/null +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 1869a4862af..f4957389259 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -402,4 +402,5 @@ +