| // 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 com.cloud.bgp; | |
| import com.cloud.dc.ASNumberRangeVO; | |
| import com.cloud.dc.ASNumberVO; | |
| import com.cloud.dc.DataCenterVO; | |
| import com.cloud.dc.dao.ASNumberDao; | |
| import com.cloud.dc.dao.ASNumberRangeDao; | |
| import com.cloud.dc.dao.DataCenterDao; | |
| import com.cloud.domain.Domain; | |
| import com.cloud.domain.dao.DomainDao; | |
| import com.cloud.exception.InvalidParameterValueException; | |
| import com.cloud.exception.ResourceUnavailableException; | |
| import com.cloud.network.Network; | |
| import com.cloud.network.NetworkModel; | |
| import com.cloud.network.dao.NetworkDao; | |
| import com.cloud.network.dao.NetworkServiceMapDao; | |
| import com.cloud.network.dao.NetworkVO; | |
| import com.cloud.network.element.BgpServiceProvider; | |
| import com.cloud.network.element.NetworkElement; | |
| import com.cloud.network.vpc.Vpc; | |
| import com.cloud.network.vpc.VpcOfferingVO; | |
| import com.cloud.network.vpc.VpcVO; | |
| import com.cloud.network.vpc.dao.VpcDao; | |
| import com.cloud.network.vpc.dao.VpcOfferingDao; | |
| import com.cloud.network.vpc.dao.VpcServiceMapDao; | |
| import com.cloud.offering.NetworkOffering; | |
| import com.cloud.offerings.NetworkOfferingVO; | |
| import com.cloud.offerings.dao.NetworkOfferingDao; | |
| import com.cloud.event.ActionEvent; | |
| import com.cloud.event.EventTypes; | |
| import com.cloud.user.Account; | |
| import com.cloud.user.dao.AccountDao; | |
| import com.cloud.utils.Pair; | |
| import com.cloud.utils.db.DB; | |
| import com.cloud.utils.db.Transaction; | |
| import com.cloud.utils.db.TransactionCallback; | |
| import com.cloud.utils.db.TransactionCallbackNoReturn; | |
| import com.cloud.utils.db.TransactionStatus; | |
| import com.cloud.utils.exception.CloudRuntimeException; | |
| import org.apache.cloudstack.api.command.user.bgp.ListASNumbersCmd; | |
| import org.apache.cloudstack.context.CallContext; | |
| import org.apache.cloudstack.network.BgpPeer; | |
| import org.apache.cloudstack.network.BgpPeerVO; | |
| import org.apache.cloudstack.network.RoutedIpv4Manager; | |
| import org.apache.cloudstack.network.dao.BgpPeerDao; | |
| import org.apache.commons.collections.CollectionUtils; | |
| import org.apache.commons.lang3.BooleanUtils; | |
| import org.apache.logging.log4j.LogManager; | |
| import org.apache.logging.log4j.Logger; | |
| import javax.inject.Inject; | |
| import java.security.InvalidParameterException; | |
| import java.util.ArrayList; | |
| import java.util.Date; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import java.util.stream.Collectors; | |
| public class BGPServiceImpl implements BGPService { | |
| public static final Logger LOGGER = LogManager.getLogger(BGPServiceImpl.class); | |
| @Inject | |
| private DataCenterDao dataCenterDao; | |
| @Inject | |
| private ASNumberRangeDao asNumberRangeDao; | |
| @Inject | |
| private ASNumberDao asNumberDao; | |
| @Inject | |
| private NetworkDao networkDao; | |
| @Inject | |
| private VpcDao vpcDao; | |
| @Inject | |
| private VpcOfferingDao vpcOfferingDao; | |
| @Inject | |
| private NetworkOfferingDao networkOfferingDao; | |
| @Inject | |
| private AccountDao accountDao; | |
| @Inject | |
| private DomainDao domainDao; | |
| @Inject | |
| NetworkServiceMapDao ntwkSrvcDao; | |
| @Inject | |
| NetworkModel networkModel; | |
| @Inject | |
| BgpPeerDao bgpPeerDao; | |
| @Inject | |
| RoutedIpv4Manager routedIpv4Manager; | |
| @Inject | |
| VpcServiceMapDao vpcServiceMapDao; | |
| public BGPServiceImpl() { | |
| } | |
| @Override | |
| @DB | |
| @ActionEvent(eventType = EventTypes.EVENT_AS_RANGE_CREATE, eventDescription = "AS Range creation") | |
| public ASNumberRange createASNumberRange(long zoneId, long startASNumber, long endASNumber) { | |
| DataCenterVO zone = dataCenterDao.findById(zoneId); | |
| if (zone == null) { | |
| String msg = String.format("Cannot find a zone with ID %s", zoneId); | |
| LOGGER.error(msg); | |
| throw new InvalidParameterException(msg); | |
| } | |
| if (!routedIpv4Manager.isRoutedNetworkVpcEnabled(zoneId)) { | |
| throw new InvalidParameterValueException("Cannot create ASN range as Routed networks and VPCs are not enabled for the zone."); | |
| } | |
| if (startASNumber > endASNumber) { | |
| String msg = "Please specify a valid AS Number range"; | |
| LOGGER.error(msg); | |
| throw new InvalidParameterException(msg); | |
| } | |
| List<ASNumberRangeVO> asNumberRanges = asNumberRangeDao.listByZoneId(zoneId); | |
| for (ASNumberRangeVO asNumberRange : asNumberRanges) { | |
| if (isASNumbersOverlap(asNumberRange.getStartASNumber(), asNumberRange.getEndASNumber(), startASNumber, endASNumber)) { | |
| throw new InvalidParameterException(String.format("New AS number range (%s-%s) has conflict with an existing AS number range (%s-%s)", | |
| startASNumber, endASNumber, asNumberRange.getStartASNumber(), asNumberRange.getEndASNumber())); | |
| } | |
| } | |
| try { | |
| return Transaction.execute((TransactionCallback<ASNumberRange>) status -> { | |
| LOGGER.debug("Persisting AS Number Range {}-{} for the zone {}", startASNumber, endASNumber, zone); | |
| ASNumberRangeVO asNumberRangeVO = new ASNumberRangeVO(zoneId, startASNumber, endASNumber); | |
| asNumberRangeDao.persist(asNumberRangeVO); | |
| for (long asn = startASNumber; asn <= endASNumber; asn++) { | |
| LOGGER.debug("Persisting AS Number {} for zone {}", asn, zone); | |
| ASNumberVO asNumber = new ASNumberVO(asn, asNumberRangeVO.getId(), zoneId); | |
| asNumberDao.persist(asNumber); | |
| } | |
| return asNumberRangeVO; | |
| }); | |
| } catch (Exception e) { | |
| String err = String.format("Error creating AS Number range %s-%s for zone %s: %s", startASNumber, endASNumber, zone, e.getMessage()); | |
| LOGGER.error(err, e); | |
| throw new CloudRuntimeException(err); | |
| } | |
| } | |
| protected boolean isASNumbersOverlap(long startNumber1, long endNumber1, long startNumber2, long endNumber2) { | |
| if (startNumber1 > endNumber2 || startNumber2 > endNumber1) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| @Override | |
| public List<ASNumberRange> listASNumberRanges(Long zoneId) { | |
| List<ASNumberRangeVO> rangeVOList = zoneId != null ? asNumberRangeDao.listByZoneId(zoneId) : asNumberRangeDao.listAll(); | |
| return new ArrayList<>(rangeVOList); | |
| } | |
| @Override | |
| public Pair<List<ASNumber>, Integer> listASNumbers(ListASNumbersCmd cmd) { | |
| Long zoneId = cmd.getZoneId(); | |
| Long asNumberRangeId = cmd.getAsNumberRangeId(); | |
| Integer asNumber = cmd.getAsNumber(); | |
| Boolean allocated = cmd.getAllocated(); | |
| Long networkId = cmd.getNetworkId(); | |
| Long vpcId = cmd.getVpcId(); | |
| String accountName = cmd.getAccount(); | |
| Long domainId = cmd.getDomainId(); | |
| Long startIndex = cmd.getStartIndex(); | |
| Long pageSizeVal = cmd.getPageSizeVal(); | |
| String keyword = cmd.getKeyword(); | |
| Account userAccount = null; | |
| Domain domain = null; | |
| final Account caller = CallContext.current().getCallingAccount(); | |
| if (Objects.nonNull(accountName)) { | |
| if (domainId != null) { | |
| userAccount = accountDao.findActiveAccount(accountName, domainId); | |
| domain = domainDao.findById(domainId); | |
| } else { | |
| userAccount = accountDao.findActiveAccount(accountName, caller.getDomainId()); | |
| domain = domainDao.findById(caller.getDomainId()); | |
| } | |
| } | |
| if (Objects.nonNull(accountName) && Objects.isNull(userAccount)) { | |
| throw new InvalidParameterException(String.format("Failed to find user Account: %s", accountName)); | |
| } | |
| Long networkSearchId = networkId; | |
| Long vpcSerchId = vpcId; | |
| if (networkId != null) { | |
| NetworkVO network = networkDao.findById(networkId); | |
| if (network == null) { | |
| throw new InvalidParameterException(String.format("Failed to find network with ID: %s", networkId)); | |
| } | |
| if (network.getVpcId() != null) { | |
| LOGGER.debug("The network {} is a VPC tier, searching for the AS number on the VPC {}", | |
| network::toString, () -> vpcDao.findById(network.getVpcId())); | |
| networkSearchId = null; | |
| vpcSerchId = network.getVpcId(); | |
| } | |
| } | |
| Pair<List<ASNumberVO>, Integer> pair = asNumberDao.searchAndCountByZoneOrRangeOrAllocated(zoneId, asNumberRangeId, | |
| asNumber, networkSearchId, vpcSerchId, allocated, Objects.nonNull(userAccount) ? userAccount.getId() : null, | |
| Objects.nonNull(domain) ? domain.getId() : null, keyword, caller, startIndex, pageSizeVal); | |
| return new Pair<>(new ArrayList<>(pair.first()), pair.second()); | |
| } | |
| @Override | |
| public boolean allocateASNumber(long zoneId, Long asNumber, Long networkId, Long vpcId) { | |
| ASNumberVO asNumberVO = isOfferingSpecifyAsNumber(networkId, vpcId) ? | |
| asNumberDao.findByAsNumber(asNumber) : | |
| asNumberDao.findOneByAllocationStateAndZone(zoneId, false); | |
| if (asNumberVO == null || asNumberVO.getDataCenterId() != zoneId) { | |
| if (asNumber != null) { | |
| LOGGER.error("Cannot find AS number {} in zone {} with id {}", asNumber, dataCenterDao.findById(zoneId), zoneId); | |
| return false; | |
| } | |
| throw new CloudRuntimeException(String.format("Cannot allocate AS number in zone with ID %s", zoneId)); | |
| } | |
| long accountId, domainId; | |
| String netName; | |
| VpcVO vpc = null; | |
| NetworkVO network = null; | |
| if (Objects.nonNull(vpcId)) { | |
| vpc = vpcDao.findById(vpcId); | |
| if (vpc == null) { | |
| LOGGER.error(String.format("Cannot find VPC with ID %s", vpcId)); | |
| return false; | |
| } | |
| accountId = vpc.getAccountId(); | |
| domainId = vpc.getDomainId(); | |
| netName = vpc.getName(); | |
| } else { | |
| network = networkDao.findById(networkId); | |
| if (network == null) { | |
| LOGGER.error(String.format("Cannot find network with ID %s", networkId)); | |
| return false; | |
| } | |
| accountId = network.getAccountId(); | |
| domainId = network.getDomainId(); | |
| netName = network.getName(); | |
| } | |
| String networkName = Objects.nonNull(vpcId) ? ("VPC " + vpc) : ("network " + network); | |
| LOGGER.debug("Allocating the AS Number {} to {} on zone {}", asNumberVO::toString, | |
| networkName::toString, () -> dataCenterDao.findById(zoneId)); | |
| asNumberVO.setAllocated(true); | |
| asNumberVO.setAllocatedTime(new Date()); | |
| if (Objects.nonNull(vpcId)) { | |
| asNumberVO.setVpcId(vpcId); | |
| } else { | |
| asNumberVO.setNetworkId(networkId); | |
| } | |
| asNumberVO.setAccountId(accountId); | |
| asNumberVO.setDomainId(domainId); | |
| return asNumberDao.update(asNumberVO.getId(), asNumberVO); | |
| } | |
| private boolean isOfferingSpecifyAsNumber(Long networkId, Long vpcId) { | |
| if (Objects.nonNull(vpcId)) { | |
| VpcVO vpc = vpcDao.findById(vpcId); | |
| if (vpc == null) { | |
| throw new CloudRuntimeException(String.format("Cannot find VPC with ID %s", vpcId)); | |
| } | |
| VpcOfferingVO vpcOffering = vpcOfferingDao.findById(vpc.getVpcOfferingId()); | |
| return NetworkOffering.RoutingMode.Dynamic == vpcOffering.getRoutingMode() && BooleanUtils.toBoolean(vpcOffering.isSpecifyAsNumber()); | |
| } else { | |
| NetworkVO network = networkDao.findById(networkId); | |
| NetworkOfferingVO networkOffering = networkOfferingDao.findById(network.getNetworkOfferingId()); | |
| return NetworkOffering.RoutingMode.Dynamic == networkOffering.getRoutingMode() && BooleanUtils.toBoolean(networkOffering.isSpecifyAsNumber()); | |
| } | |
| } | |
| private Pair<Boolean, String> logAndReturnErrorMessage(String msg) { | |
| LOGGER.error(msg); | |
| return new Pair<>(false, msg); | |
| } | |
| @Override | |
| @ActionEvent(eventType = EventTypes.EVENT_AS_NUMBER_RELEASE, eventDescription = "Releasing AS Number") | |
| public Pair<Boolean, String> releaseASNumber(long zoneId, long asNumber, boolean isDestroyNetworkOperation) { | |
| ASNumberVO asNumberVO = asNumberDao.findByAsNumber(asNumber); | |
| DataCenterVO zone = dataCenterDao.findById(zoneId); | |
| if (asNumberVO == null) { | |
| return logAndReturnErrorMessage(String.format("Cannot find AS Number %s on zone %s", asNumber, zone)); | |
| } | |
| if (!asNumberVO.isAllocated()) { | |
| LOGGER.debug("The AS Number {} is not allocated to any network on zone {}, ignoring release", asNumber, zone); | |
| return new Pair<>(true, ""); | |
| } | |
| Long networkId = asNumberVO.getNetworkId(); | |
| Long vpcId = asNumberVO.getVpcId(); | |
| if (!isDestroyNetworkOperation) { | |
| Pair<Boolean, String> checksResult = performReleaseASNumberChecks(networkId, vpcId, asNumber); | |
| if (checksResult != null) { | |
| return checksResult; | |
| } | |
| } | |
| LOGGER.debug("Releasing AS Number {} on zone {} from previous allocation", asNumber, zone); | |
| asNumberVO.setAllocated(false); | |
| asNumberVO.setAllocatedTime(null); | |
| asNumberVO.setDomainId(null); | |
| asNumberVO.setAccountId(null); | |
| if (vpcId != null) { | |
| asNumberVO.setVpcId(null); | |
| } else { | |
| asNumberVO.setNetworkId(null); | |
| } | |
| boolean update = asNumberDao.update(asNumberVO.getId(), asNumberVO); | |
| String msg = update ? "OK" : "Could not update database record for AS number"; | |
| return new Pair<>(update, msg); | |
| } | |
| private Pair<Boolean, String> performReleaseASNumberChecks(Long networkId, Long vpcId, long asNumber) { | |
| if (networkId != null) { | |
| NetworkVO network = networkDao.findById(networkId); | |
| if (network == null) { | |
| return logAndReturnErrorMessage(String.format("Cannot find a network with ID %s which acquired the AS number %s", networkId, asNumber)); | |
| } | |
| NetworkOfferingVO offering = networkOfferingDao.findById(network.getNetworkOfferingId()); | |
| if (offering == null) { | |
| return logAndReturnErrorMessage(String.format("Cannot find a network offering with ID %s", network.getNetworkOfferingId())); | |
| } | |
| if (offering.isSpecifyAsNumber()) { | |
| return logAndReturnErrorMessage(String.format("Cannot release the AS number %s as it is acquired by a network that requires AS number", asNumber)); | |
| } | |
| } else if (vpcId != null) { | |
| VpcVO vpc = vpcDao.findById(vpcId); | |
| if (vpc == null) { | |
| return logAndReturnErrorMessage(String.format("Cannot find a VPC with ID %s which acquired the AS number %s", vpcId, asNumber)); | |
| } | |
| VpcOfferingVO vpcOffering = vpcOfferingDao.findById(vpc.getVpcOfferingId()); | |
| if (vpcOffering == null) { | |
| return logAndReturnErrorMessage(String.format("Cannot find a VPC offering with ID %s", vpc.getVpcOfferingId())); | |
| } | |
| if (vpcOffering.isSpecifyAsNumber()) { | |
| return logAndReturnErrorMessage(String.format("Cannot release the AS number %s as it is acquired by a VPC that requires AS number", asNumber)); | |
| } | |
| } | |
| return null; | |
| } | |
| @Override | |
| @DB | |
| @ActionEvent(eventType = EventTypes.EVENT_AS_RANGE_DELETE, eventDescription = "Deleting AS Range") | |
| public boolean deleteASRange(long id) { | |
| final ASNumberRange asRange = asNumberRangeDao.findById(id); | |
| if (asRange == null) { | |
| throw new CloudRuntimeException(String.format("Could not find a AS range with id: %s", id)); | |
| } | |
| long startASNumber = asRange.getStartASNumber(); | |
| long endASNumber = asRange.getEndASNumber(); | |
| long zoneId = asRange.getDataCenterId(); | |
| DataCenterVO zone = dataCenterDao.findById(zoneId); | |
| List<ASNumberVO> allocatedAsNumbers = asNumberDao.listAllocatedByASRange(asRange.getId()); | |
| if (Objects.nonNull(allocatedAsNumbers) && !allocatedAsNumbers.isEmpty()) { | |
| throw new CloudRuntimeException(String.format("There are %s AS numbers in use from the range %s-%s, cannot remove the range", | |
| allocatedAsNumbers.size(), startASNumber, endASNumber)); | |
| } | |
| try { | |
| Transaction.execute(new TransactionCallbackNoReturn() { | |
| @Override | |
| public void doInTransactionWithoutResult(TransactionStatus status) { | |
| int removedASNumbers = asNumberDao.removeASRangeNumbers(asRange.getId()); | |
| LOGGER.debug(String.format("Removed %s AS numbers from the range %s-%s", removedASNumbers, | |
| startASNumber, endASNumber)); | |
| asNumberRangeDao.remove(id); | |
| LOGGER.debug("Removing the AS Number Range {}-{} for the zone {}", startASNumber, endASNumber, zone); | |
| } | |
| }); | |
| } catch (Exception e) { | |
| String err = String.format("Error removing AS Number range %s-%s for zone %s: %s", | |
| startASNumber, endASNumber, zone, e.getMessage()); | |
| LOGGER.error(err, e); | |
| throw new CloudRuntimeException(err); | |
| } | |
| return true; | |
| } | |
| @Override | |
| public boolean applyBgpPeers(Network network, boolean continueOnError) throws ResourceUnavailableException { | |
| if (!routedIpv4Manager.isDynamicRoutedNetwork(network)) { | |
| return true; | |
| } | |
| final String gatewayProviderStr = ntwkSrvcDao.getProviderForServiceInNetwork(network.getId(), Network.Service.Gateway); | |
| if (gatewayProviderStr != null) { | |
| NetworkElement provider = networkModel.getElementImplementingProvider(gatewayProviderStr); | |
| if (provider != null && provider instanceof BgpServiceProvider) { | |
| List<? extends BgpPeer> bgpPeers = getBgpPeersForNetwork(network); | |
| LOGGER.debug(String.format("Applying BPG Peers for network [%s]: [%s]", network, bgpPeers)); | |
| return ((BgpServiceProvider) provider).applyBgpPeers(null, network, bgpPeers); | |
| } | |
| } | |
| return true; | |
| } | |
| @Override | |
| public boolean applyBgpPeers(Vpc vpc, boolean continueOnError) throws ResourceUnavailableException { | |
| if (!routedIpv4Manager.isDynamicRoutedVpc(vpc)) { | |
| return true; | |
| } | |
| final String gatewayProviderStr = vpcServiceMapDao.getProviderForServiceInVpc(vpc.getId(), Network.Service.Gateway); | |
| if (gatewayProviderStr != null) { | |
| NetworkElement provider = networkModel.getElementImplementingProvider(gatewayProviderStr); | |
| if (provider != null && provider instanceof BgpServiceProvider) { | |
| List<? extends BgpPeer> bgpPeers = getBgpPeersForVpc(vpc); | |
| LOGGER.debug(String.format("Applying BPG Peers for VPC [%s]: [%s]", vpc, bgpPeers)); | |
| return ((BgpServiceProvider) provider).applyBgpPeers(vpc, null, bgpPeers); | |
| } | |
| } | |
| return true; | |
| } | |
| @Override | |
| public List<? extends BgpPeer> getBgpPeersForNetwork(Network network) { | |
| List<BgpPeerVO> bgpPeers; | |
| if (network.getVpcId() != null) { | |
| bgpPeers = bgpPeerDao.listNonRevokeByVpcId(network.getVpcId()); | |
| } else { | |
| bgpPeers = bgpPeerDao.listNonRevokeByNetworkId(network.getId()); | |
| } | |
| if (CollectionUtils.isEmpty(bgpPeers)) { | |
| Account owner = accountDao.findByIdIncludingRemoved(network.getAccountId()); | |
| List<Long> bgpPeerIds = routedIpv4Manager.getBgpPeerIdsForAccount(owner, network.getDataCenterId()); | |
| bgpPeers = bgpPeerIds.stream() | |
| .map(bgpPeerId -> bgpPeerDao.findById(bgpPeerId)) | |
| .collect(Collectors.toList()); | |
| } | |
| return bgpPeers; | |
| } | |
| @Override | |
| public List<? extends BgpPeer> getBgpPeersForVpc(Vpc vpc) { | |
| List<BgpPeerVO> bgpPeers = bgpPeerDao.listNonRevokeByVpcId(vpc.getId()); | |
| if (CollectionUtils.isEmpty(bgpPeers)) { | |
| Account owner = accountDao.findByIdIncludingRemoved(vpc.getAccountId()); | |
| List<Long> bgpPeerIds = routedIpv4Manager.getBgpPeerIdsForAccount(owner, vpc.getZoneId()); | |
| bgpPeers = bgpPeerIds.stream() | |
| .map(bgpPeerId -> bgpPeerDao.findById(bgpPeerId)) | |
| .collect(Collectors.toList()); | |
| } | |
| return bgpPeers; | |
| } | |
| } |