diff --git a/api/src/main/java/com/cloud/configuration/ConfigurationService.java b/api/src/main/java/com/cloud/configuration/ConfigurationService.java index 6a51f7e1547..38d9d3243cf 100644 --- a/api/src/main/java/com/cloud/configuration/ConfigurationService.java +++ b/api/src/main/java/com/cloud/configuration/ConfigurationService.java @@ -24,6 +24,7 @@ import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -204,6 +205,14 @@ public interface ConfigurationService { */ void deletePodIpRange(DeleteManagementNetworkIpRangeCmd cmd) throws ResourceUnavailableException, ConcurrentOperationException; + /** + * Updates a mutually exclusive IP range in the pod. + * @param cmd - The command specifying pod ID, current Start IP, current End IP, new Start IP, new End IP. + * @throws com.cloud.exception.ConcurrentOperationException + * @return Success + */ + void updatePodIpRange(UpdatePodManagementNetworkIpRangeCmd cmd) throws ConcurrentOperationException; + /** * Edits a pod in the database. Will not allow you to edit pods that are being used anywhere in the system. * diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 808a8689606..c74a28f628c 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -328,6 +328,7 @@ public class EventTypes { public static final String EVENT_MANAGEMENT_IP_RANGE_CREATE = "MANAGEMENT.IP.RANGE.CREATE"; public static final String EVENT_MANAGEMENT_IP_RANGE_DELETE = "MANAGEMENT.IP.RANGE.DELETE"; + public static final String EVENT_MANAGEMENT_IP_RANGE_UPDATE = "MANAGEMENT.IP.RANGE.UPDATE"; public static final String EVENT_STORAGE_IP_RANGE_CREATE = "STORAGE.IP.RANGE.CREATE"; public static final String EVENT_STORAGE_IP_RANGE_DELETE = "STORAGE.IP.RANGE.DELETE"; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index fd0dc7800d5..0fff4efef4f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -98,6 +98,8 @@ public class ApiConstants { public static final String CUSTOMIZED_IOPS = "customizediops"; public static final String CUSTOM_ID = "customid"; public static final String CUSTOM_JOB_ID = "customjobid"; + public static final String CURRENT_START_IP = "currentstartip"; + public static final String CURRENT_END_IP = "currentendip"; public static final String MIN_IOPS = "miniops"; public static final String MAX_IOPS = "maxiops"; public static final String HYPERVISOR_SNAPSHOT_RESERVE = "hypervisorsnapshotreserve"; @@ -252,6 +254,8 @@ public class ApiConstants { public static final String NIC = "nic"; public static final String NIC_NETWORK_LIST = "nicnetworklist"; public static final String NIC_IP_ADDRESS_LIST = "nicipaddresslist"; + public static final String NEW_START_IP = "newstartip"; + public static final String NEW_END_IP = "newendip"; public static final String NUM_RETRIES = "numretries"; public static final String OFFER_HA = "offerha"; public static final String IS_SYSTEM_OFFERING = "issystem"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePodManagementNetworkIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePodManagementNetworkIpRangeCmd.java new file mode 100644 index 00000000000..ee59631b9eb --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePodManagementNetworkIpRangeCmd.java @@ -0,0 +1,154 @@ +// 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.api.command.admin.network; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiArgValidator; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.PodResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; + +@APICommand(name = UpdatePodManagementNetworkIpRangeCmd.APINAME, + description = "Updates a management network IP range. Only allowed when no IPs are allocated.", + responseObject = SuccessResponse.class, + since = "4.16.0.0", + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin}) +public class UpdatePodManagementNetworkIpRangeCmd extends BaseAsyncCmd { + + public static final Logger s_logger = Logger.getLogger(UpdatePodManagementNetworkIpRangeCmd.class); + + public static final String APINAME = "updatePodManagementNetworkIpRange"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.POD_ID, + type = CommandType.UUID, + entityType = PodResponse.class, + required = true, + description = "UUID of POD, where the IP range belongs to.", + validations = {ApiArgValidator.PositiveNumber}) + private Long podId; + + @Parameter(name = ApiConstants.CURRENT_START_IP, + type = CommandType.STRING, + entityType = PodResponse.class, + required = true, + description = "The current starting IP address.", + validations = {ApiArgValidator.NotNullOrEmpty}) + private String currentStartIp; + + @Parameter(name = ApiConstants.CURRENT_END_IP, + type = CommandType.STRING, + entityType = PodResponse.class, + required = true, + description = "The current ending IP address.", + validations = {ApiArgValidator.NotNullOrEmpty}) + private String currentEndIp; + + @Parameter(name = ApiConstants.NEW_START_IP, + type = CommandType.STRING, + description = "The new starting IP address.", + validations = {ApiArgValidator.NotNullOrEmpty}) + private String newStartIp; + + @Parameter(name = ApiConstants.NEW_END_IP, + type = CommandType.STRING, + description = "The new ending IP address.", + validations = {ApiArgValidator.NotNullOrEmpty}) + private String newEndIp; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getPodId() { + return podId; + } + + public String getCurrentStartIP() { + return currentStartIp; + } + + public String getCurrentEndIP() { + return currentEndIp; + } + + public String getNewStartIP() { + return newStartIp; + } + + public String getNewEndIP() { + return newEndIp; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_MANAGEMENT_IP_RANGE_UPDATE; + } + + @Override + public String getEventDescription() { + return "Updating pod management IP range " + getNewStartIP() + "-" + getNewEndIP() + " of Pod: " + getPodId(); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseAsyncCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + if (getNewStartIP() == null && getNewEndIP() == null) { + throw new InvalidParameterValueException("Either new starting IP address or new ending IP address must be specified"); + } + + try { + _configService.updatePodIpRange(this); + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } catch (ConcurrentOperationException ex) { + s_logger.warn("Exception: ", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } catch (Exception e) { + s_logger.warn("Failed to update pod management IP range " + getNewStartIP() + "-" + getNewEndIP() + " of Pod: " + getPodId(), e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } +} diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDao.java index bb840b5adec..2c65539dd65 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDao.java @@ -41,6 +41,8 @@ public interface DataCenterIpAddressDao extends GenericDao listByPodIdDcId(long podId, long dcId); + List listIpAddressUsage(final long podId, final long dcId, final boolean onlyListAllocated); + int countIPs(long podId, long dcId, boolean onlyCountAllocated); int countIPs(long dcId, boolean onlyCountAllocated); diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java index 8b0a44bbbf0..e58b08da4c1 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java @@ -21,7 +21,6 @@ import java.sql.SQLException; import java.util.Date; import java.util.List; - import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.log4j.Logger; @@ -238,6 +237,17 @@ public class DataCenterIpAddressDaoImpl extends GenericDaoBase listIpAddressUsage(final long podId, final long dcId, final boolean onlyListAllocated) { + SearchCriteria sc = createSearchCriteria(); + if(onlyListAllocated) { + sc.addAnd("takenAt", SearchCriteria.Op.NNULL); + } + sc.addAnd("podId", SearchCriteria.Op.EQ, podId); + sc.addAnd("dataCenterId", SearchCriteria.Op.EQ, dcId); + return listBy(sc); + } + @Override public List listByPodIdDcIdIpAddress(long podId, long dcId, String ipAddress) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 5a0a34baa10..4e9828a9b22 100755 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -57,6 +57,7 @@ import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -1530,6 +1531,178 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati } } + @Override + @DB + public void updatePodIpRange(final UpdatePodManagementNetworkIpRangeCmd cmd) throws ConcurrentOperationException { + final long podId = cmd.getPodId(); + final HostPodVO pod = _podDao.findById(podId); + if (pod == null) { + throw new InvalidParameterValueException("Unable to find pod by id: " + podId); + } + + final String currentStartIP = cmd.getCurrentStartIP(); + final String currentEndIP = cmd.getCurrentEndIP(); + String newStartIP = cmd.getNewStartIP(); + String newEndIP = cmd.getNewEndIP(); + + if (newStartIP == null) { + newStartIP = currentStartIP; + } + + if (newEndIP == null) { + newEndIP = currentEndIP; + } + + if (newStartIP.equals(currentStartIP) && newEndIP.equals(currentEndIP)) { + throw new InvalidParameterValueException("New starting and ending IP address are the same as current starting and ending IP address"); + } + + final String[] existingPodIpRanges = pod.getDescription().split(","); + if (existingPodIpRanges.length == 0) { + throw new InvalidParameterValueException("The IP range cannot be found in the pod: " + podId + " since the existing IP range is empty."); + } + + verifyIpRangeParameters(currentStartIP,currentEndIP); + verifyIpRangeParameters(newStartIP,newEndIP); + checkIpRangeContainsTakenAddresses(pod,currentStartIP,currentEndIP,newStartIP,newEndIP); + + String vlan = verifyPodIpRangeExists(podId,existingPodIpRanges,currentStartIP,currentEndIP,newStartIP,newEndIP); + + List currentIpRange = listAllIPsWithintheRange(currentStartIP,currentEndIP); + List newIpRange = listAllIPsWithintheRange(newStartIP,newEndIP); + + try { + final String finalNewEndIP = newEndIP; + final String finalNewStartIP = newStartIP; + final Integer vlanId = vlan.equals(Vlan.UNTAGGED) ? null : Integer.parseInt(vlan); + + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(final TransactionStatus status) { + final long zoneId = pod.getDataCenterId(); + pod.setDescription(pod.getDescription().replace(currentStartIP + "-", + finalNewStartIP + "-").replace(currentEndIP, finalNewEndIP)); + updatePodIpRangeInDb(zoneId,podId,vlanId,pod,newIpRange,currentIpRange); + } + }); + } catch (final Exception e) { + s_logger.error("Unable to update Pod " + podId + " IP range due to " + e.getMessage(), e); + throw new CloudRuntimeException("Failed to update Pod " + podId + " IP range. Please contact Cloud Support."); + } + } + + private String verifyPodIpRangeExists(long podId,String[] existingPodIpRanges, String currentStartIP, + String currentEndIP, String newStartIP, String newEndIP) { + boolean foundRange = false; + String vlan = null; + + for (String podIpRange: existingPodIpRanges) { + final String[] existingPodIpRange = podIpRange.split("-"); + + if (existingPodIpRange.length > 1) { + if (!NetUtils.isValidIp4(existingPodIpRange[0]) || !NetUtils.isValidIp4(existingPodIpRange[1])) { + continue; + } + if (currentStartIP.equals(existingPodIpRange[0]) && currentEndIP.equals(existingPodIpRange[1])) { + foundRange = true; + vlan = existingPodIpRange[3]; + } + if (!foundRange && NetUtils.ipRangesOverlap(newStartIP, newEndIP, existingPodIpRange[0], existingPodIpRange[1])) { + throw new InvalidParameterValueException("The Start and End IP address range: (" + newStartIP + "-" + newEndIP + ") overlap with the pod IP range: " + podIpRange); + } + } + } + + if (!foundRange) { + throw new InvalidParameterValueException("The input IP range: " + currentStartIP + "-" + currentEndIP + " of pod: " + podId + " is not present. Please input an existing range."); + } + + return vlan; + } + + private void updatePodIpRangeInDb (long zoneId, long podId, Integer vlanId, HostPodVO pod, List newIpRange, List currentIpRange) { + HostPodVO lock = null; + try { + lock = _podDao.acquireInLockTable(podId); + if (lock == null) { + String msg = "Unable to acquire lock on table to update the ip range of POD: " + pod.getName() + ", Update failed."; + s_logger.warn(msg); + throw new CloudRuntimeException(msg); + } + List iPaddressesToAdd = new ArrayList(newIpRange); + iPaddressesToAdd.removeAll(currentIpRange); + if (iPaddressesToAdd.size() > 0) { + for (Long startIP : iPaddressesToAdd) { + _zoneDao.addPrivateIpAddress(zoneId, podId, NetUtils.long2Ip(startIP), NetUtils.long2Ip(startIP), false, vlanId); + } + } else { + currentIpRange.removeAll(newIpRange); + if (currentIpRange.size() > 0) { + for (Long startIP: currentIpRange) { + if (!_privateIpAddressDao.deleteIpAddressByPodDc(NetUtils.long2Ip(startIP),podId,zoneId)) { + throw new CloudRuntimeException("Failed to remove private ip address: " + NetUtils.long2Ip(startIP) + " of Pod: " + podId + " DC: " + pod.getDataCenterId()); + } + } + } + } + _podDao.update(podId, pod); + } catch (final Exception e) { + s_logger.error("Unable to update Pod " + podId + " IP range due to database error " + e.getMessage(), e); + throw new CloudRuntimeException("Failed to update Pod " + podId + " IP range. Please contact Cloud Support."); + } finally { + if (lock != null) { + _podDao.releaseFromLockTable(podId); + } + } + } + + private List listAllIPsWithintheRange(String startIp, String endIP) { + verifyIpRangeParameters(startIp,endIP); + long startIpLong = NetUtils.ip2Long(startIp); + long endIpLong = NetUtils.ip2Long(endIP); + + List listOfIpsinRange = new ArrayList<>(); + while (startIpLong <= endIpLong) { + listOfIpsinRange.add(startIpLong); + startIpLong++; + } + return listOfIpsinRange; + } + + private void verifyIpRangeParameters(String startIP, String endIp) { + + if (!Strings.isNullOrEmpty(startIP) && !NetUtils.isValidIp4(startIP)) { + throw new InvalidParameterValueException("The current start address of the IP range " + startIP + " is not a valid IP address."); + } + + if (!Strings.isNullOrEmpty(endIp) && !NetUtils.isValidIp4(endIp)) { + throw new InvalidParameterValueException("The current end address of the IP range " + endIp + " is not a valid IP address."); + } + + if (NetUtils.ip2Long(startIP) > NetUtils.ip2Long(endIp)) { + throw new InvalidParameterValueException("The start IP address must have a lower value than the end IP address."); + } + } + + private void checkIpRangeContainsTakenAddresses(final HostPodVO pod,final String currentStartIP, + final String currentEndIP,final String newStartIp, final String newEndIp) { + + List newIpRange = listAllIPsWithintheRange(newStartIp,newEndIp); + List currentIpRange = listAllIPsWithintheRange(currentStartIP,currentEndIP); + List takenIpsList = new ArrayList<>(); + final List takenIps = _privateIpAddressDao.listIpAddressUsage(pod.getId(),pod.getDataCenterId(),true); + + for (DataCenterIpAddressVO takenIp : takenIps) { + takenIpsList.add(NetUtils.ip2Long(takenIp.getIpAddress())); + } + + takenIpsList.retainAll(currentIpRange); + if (!newIpRange.containsAll(takenIpsList)) { + throw new InvalidParameterValueException("The IP range does not contain some IP addresses that have " + + "already been taken. Please adjust your IP range to include all IP addresses already taken."); + } + } + @Override public Pod editPod(final UpdatePodCmd cmd) { return editPod(cmd.getId(), cmd.getPodName(), null, null, cmd.getGateway(), cmd.getNetmask(), cmd.getAllocationState()); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 075b338f1bb..e0db746676c 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -148,6 +148,7 @@ import org.apache.cloudstack.api.command.admin.network.UpdateNetworkCmdByAdmin; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkServiceProviderCmd; import org.apache.cloudstack.api.command.admin.network.UpdatePhysicalNetworkCmd; +import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.UpdateStorageNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; @@ -3052,6 +3053,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(UpdateRegionCmd.class); cmdList.add(ListAlertsCmd.class); cmdList.add(ListCapacityCmd.class); + cmdList.add(UpdatePodManagementNetworkIpRangeCmd.class); cmdList.add(UploadCustomCertificateCmd.class); cmdList.add(ConfigureVirtualRouterElementCmd.class); cmdList.add(CreateVirtualRouterElementCmd.class); diff --git a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java index d6dbee844b1..112d20ba847 100644 --- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -204,6 +205,14 @@ public class MockConfigurationManagerImpl extends ManagerBase implements Configu return; } + /* (non-Javadoc) + * @see com.cloud.configuration.ConfigurationService#updatePodIpRange(org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd) + */ + public void updatePodIpRange(final UpdatePodManagementNetworkIpRangeCmd cmd) throws ConcurrentOperationException { + // TODO Auto-generated method stub + return; + } + /* (non-Javadoc) * @see com.cloud.configuration.ConfigurationService#editPod(org.apache.cloudstack.api.commands.UpdatePodCmd) */