content: // 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;
      }
  }
  