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 org.apache.cloudstack.network.topology;
  
  import java.util.ArrayList;
  import java.util.List;
  
  import javax.inject.Inject;
  
  import org.apache.cloudstack.network.BgpPeer;
  import org.apache.logging.log4j.Logger;
  import org.apache.logging.log4j.LogManager;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.beans.factory.annotation.Qualifier;
  import org.springframework.stereotype.Component;
  
  import com.cloud.dc.DataCenter;
  import com.cloud.dc.DataCenter.NetworkType;
  import com.cloud.dc.Pod;
  import com.cloud.dc.dao.DataCenterDao;
  import com.cloud.deploy.DeployDestination;
  import com.cloud.exception.AgentUnavailableException;
  import com.cloud.exception.ConcurrentOperationException;
  import com.cloud.exception.ResourceUnavailableException;
  import com.cloud.host.Status;
  import com.cloud.host.dao.HostDao;
  import com.cloud.network.Network;
  import com.cloud.network.Networks.TrafficType;
  import com.cloud.network.PublicIpAddress;
  import com.cloud.network.RemoteAccessVpn;
  import com.cloud.network.VpnUser;
  import com.cloud.network.lb.LoadBalancingRule;
  import com.cloud.network.router.NetworkHelper;
  import com.cloud.network.router.VirtualRouter;
  import com.cloud.network.rules.BasicVpnRules;
  import com.cloud.network.rules.DhcpEntryRules;
  import com.cloud.network.rules.DhcpSubNetRules;
  import com.cloud.network.rules.FirewallRule;
  import com.cloud.network.rules.FirewallRules;
  import com.cloud.network.rules.IpAssociationRules;
  import com.cloud.network.rules.LoadBalancingRules;
  import com.cloud.network.rules.PasswordToRouterRules;
  import com.cloud.network.rules.RuleApplier;
  import com.cloud.network.rules.RuleApplierWrapper;
  import com.cloud.network.rules.SshKeyToRouterRules;
  import com.cloud.network.rules.StaticNat;
  import com.cloud.network.rules.StaticNatRules;
  import com.cloud.network.rules.UserdataPwdRules;
  import com.cloud.network.rules.UserdataToRouterRules;
  import com.cloud.network.vpc.NetworkACLItem;
  import com.cloud.network.vpc.PrivateGateway;
  import com.cloud.network.vpc.StaticRouteProfile;
  import com.cloud.utils.exception.CloudRuntimeException;
  import com.cloud.vm.DomainRouterVO;
  import com.cloud.vm.NicProfile;
  import com.cloud.vm.VirtualMachine;
  import com.cloud.vm.VirtualMachine.State;
  import com.cloud.vm.VirtualMachineProfile;
  
  @Component
  public class BasicNetworkTopology implements NetworkTopology {
  
      protected Logger logger = LogManager.getLogger(BasicNetworkTopology.class);
  
      @Autowired
      @Qualifier("basicNetworkVisitor")
      protected BasicNetworkVisitor _basicVisitor;
  
      @Inject
      protected DataCenterDao _dcDao;
  
      @Inject
      protected HostDao _hostDao;
  
      @Autowired
      @Qualifier("networkHelper")
      protected NetworkHelper _networkHelper;
  
      @Override
      public NetworkTopologyVisitor getVisitor() {
          return _basicVisitor;
      }
  
      @Override
      public boolean setupPrivateGateway(final PrivateGateway gateway, final VirtualRouter router) throws ConcurrentOperationException, ResourceUnavailableException {
          throw new CloudRuntimeException("setupPrivateGateway not implemented in Basic Network Topology.");
      }
  
      @Override
      public String[] applyVpnUsers(final RemoteAccessVpn vpn, final List<? extends VpnUser> users, final VirtualRouter router) throws ResourceUnavailableException {
          throw new CloudRuntimeException("applyVpnUsers not implemented in Basic Network Topology.");
      }
  
      @Override
      public boolean applyStaticRoutes(final List<StaticRouteProfile> staticRoutes, final List<DomainRouterVO> routers) throws ResourceUnavailableException {
          throw new CloudRuntimeException("applyStaticRoutes not implemented in Basic Network Topology.");
      }
  
      @Override
      public boolean applyNetworkACLs(final Network network, final List<? extends NetworkACLItem> rules, final VirtualRouter router, final boolean isPrivateGateway)
              throws ResourceUnavailableException {
          throw new CloudRuntimeException("applyNetworkACLs not implemented in Basic Network Topology.");
      }
  
      @Override
      public boolean setupDhcpForPvlan(final boolean add, final DomainRouterVO router, final Long hostId, final NicProfile nic) throws ResourceUnavailableException {
          throw new CloudRuntimeException("setupDhcpForPvlan not implemented in Basic Network Topology.");
      }
  
      @Override
      public boolean configDhcpForSubnet(final Network network, final NicProfile nic, final VirtualMachineProfile profile, final DeployDestination dest,
              final List<DomainRouterVO> routers) throws ResourceUnavailableException {
  
          logger.debug("CONFIG DHCP FOR SUBNETS RULES");
  
          // Assuming we have only one router per network For Now.
          final DomainRouterVO router = routers.get(0);
          if (router.getState() != State.Running) {
              logger.warn("Failed to configure dhcp: router not in running state");
              throw new ResourceUnavailableException("Unable to assign ip addresses, domR is not in right state " + router.getState(), DataCenter.class, network.getDataCenterId());
          }
  
          final DhcpSubNetRules subNetRules = new DhcpSubNetRules(network, nic, profile);
  
          return subNetRules.accept(_basicVisitor, router);
      }
  
      @Override
      public boolean applyDhcpEntry(final Network network, final NicProfile nic, final VirtualMachineProfile profile, final DeployDestination dest,
              final DomainRouterVO router) throws ResourceUnavailableException {
  
          logger.debug("APPLYING DHCP ENTRY RULES");
  
          final String typeString = "dhcp entry";
          final Long podId = dest.getPod().getId();
          boolean isPodLevelException = false;
  
          // for user vm in Basic zone we should try to re-deploy vm in a diff pod
          // if it fails to deploy in original pod; so throwing exception with Pod
          // scope
          if (podId != null && profile.getVirtualMachine().getType() == VirtualMachine.Type.User && network.getTrafficType() == TrafficType.Guest
                  && network.getGuestType() == Network.GuestType.Shared) {
              isPodLevelException = true;
          }
  
          final boolean failWhenDisconnect = false;
  
          final DhcpEntryRules dhcpRules = new DhcpEntryRules(network, nic, profile, dest);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(dhcpRules));
      }
  
      @Override
      public boolean applyUserData(final Network network, final NicProfile nic, final VirtualMachineProfile profile, final DeployDestination dest, final DomainRouterVO router)
              throws ResourceUnavailableException {
  
          logger.debug("APPLYING USERDATA RULES");
  
          final String typeString = "userdata and password entry";
          final Long podId = dest.getPod().getId();
          boolean isPodLevelException = false;
  
          if (podId != null && profile.getVirtualMachine().getType() == VirtualMachine.Type.User && network.getTrafficType() == TrafficType.Guest
                  && network.getGuestType() == Network.GuestType.Shared) {
              isPodLevelException = true;
          }
  
          final boolean failWhenDisconnect = false;
  
          final UserdataPwdRules pwdRules = new UserdataPwdRules(network, nic, profile, dest);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(pwdRules));
      }
  
      @Override
      public boolean applyLoadBalancingRules(final Network network, final List<LoadBalancingRule> rules, final VirtualRouter router)
              throws ResourceUnavailableException {
  
          if (rules == null || rules.isEmpty()) {
              logger.debug("No lb rules to be applied for network {}", network);
              return true;
          }
  
          logger.debug("APPLYING LOAD BALANCING RULES");
  
          final String typeString = "loadbalancing rules";
          final boolean isPodLevelException = false;
          final boolean failWhenDisconnect = false;
          final Long podId = null;
  
          final LoadBalancingRules loadBalancingRules = new LoadBalancingRules(network, rules);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(loadBalancingRules));
      }
  
      @Override
      public boolean applyFirewallRules(final Network network, final List<? extends FirewallRule> rules, final VirtualRouter router)
              throws ResourceUnavailableException {
          if (rules == null || rules.isEmpty()) {
              logger.debug("No firewall rules to be applied for network {}", network);
              return true;
          }
  
          logger.debug("APPLYING FIREWALL RULES");
  
          final String typeString = "firewall rules";
          final boolean isPodLevelException = false;
          final boolean failWhenDisconnect = false;
          final Long podId = null;
  
          final FirewallRules firewallRules = new FirewallRules(network, rules);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(firewallRules));
      }
  
      @Override
      public boolean applyStaticNats(final Network network, final List<? extends StaticNat> rules, final VirtualRouter router) throws ResourceUnavailableException {
          if (rules == null || rules.isEmpty()) {
              logger.debug("No static nat rules to be applied for network {}", network);
              return true;
          }
  
          logger.debug("APPLYING STATIC NAT RULES");
  
          final String typeString = "static nat rules";
          final boolean isPodLevelException = false;
          final boolean failWhenDisconnect = false;
          final Long podId = null;
  
          final StaticNatRules natRules = new StaticNatRules(network, rules);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(natRules));
      }
  
      @Override
      public boolean associatePublicIP(final Network network, final List<? extends PublicIpAddress> ipAddress, final VirtualRouter router)
              throws ResourceUnavailableException {
          if (ipAddress == null || ipAddress.isEmpty()) {
              logger.debug("No ip association rules to be applied for network {}", network);
              return true;
          }
  
          logger.debug("APPLYING IP RULES");
  
          final String typeString = "ip association";
          final boolean isPodLevelException = false;
          final boolean failWhenDisconnect = false;
          final Long podId = null;
  
          final IpAssociationRules ipAddresses = new IpAssociationRules(network, ipAddress);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(ipAddresses));
      }
  
      @Override
      public String[] applyVpnUsers(final Network network, final List<? extends VpnUser> users, final List<DomainRouterVO> routers) throws ResourceUnavailableException {
          if (routers == null || routers.isEmpty()) {
              logger.warn("Failed to add/remove VPN users: no router found for account and zone");
              throw new ResourceUnavailableException(String.format("Unable to assign ip addresses, domR doesn't exist for network %s", network), DataCenter.class, network.getDataCenterId());
          }
  
          logger.debug("APPLYING BASIC VPN RULES");
  
          final BasicVpnRules vpnRules = new BasicVpnRules(network, users);
          boolean agentResults = true;
  
          for (final DomainRouterVO router : routers) {
              if(router.getState() == State.Stopped || router.getState() == State.Stopping){
                  logger.info("The router {} is in the {} state. So not applying the VPN rules. Will be applied once the router gets restarted.", router, router.getState());
                  continue;
              }
              else if (router.getState() != State.Running) {
                  logger.warn("Failed to add/remove VPN users: router not in running state");
                  throw new ResourceUnavailableException("Unable to assign ip addresses, domR is not in right state " + router.getState(), DataCenter.class,
                          network.getDataCenterId());
              }
  
              // Currently we receive just one answer from the agent. In the
              // future we have to parse individual answers and set
              // results accordingly
              final boolean agentResult = vpnRules.accept(_basicVisitor, router);
              agentResults = agentResults && agentResult;
          }
  
          final String[] result = new String[users.size()];
          for (int i = 0; i < result.length; i++) {
              if (agentResults) {
                  result[i] = null;
              } else {
                  result[i] = String.valueOf(agentResults);
              }
          }
  
          return result;
      }
  
      @Override
      public boolean savePasswordToRouter(final Network network, final NicProfile nic, final VirtualMachineProfile profile, final VirtualRouter router)
              throws ResourceUnavailableException {
  
          logger.debug("SAVE PASSWORD TO ROUTE RULES");
  
          final String typeString = "save password entry";
          final boolean isPodLevelException = false;
          final boolean failWhenDisconnect = false;
          final Long podId = null;
  
          final PasswordToRouterRules routerRules = new PasswordToRouterRules(network, nic, profile);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(routerRules));
      }
  
      @Override
      public boolean saveSSHPublicKeyToRouter(final Network network, final NicProfile nic, final VirtualMachineProfile profile, final VirtualRouter router,
              final String sshPublicKey) throws ResourceUnavailableException {
          logger.debug("SAVE SSH PUB KEY TO ROUTE RULES");
  
          final String typeString = "save SSHkey entry";
          final boolean isPodLevelException = false;
          final boolean failWhenDisconnect = false;
          final Long podId = null;
  
          final SshKeyToRouterRules keyToRouterRules = new SshKeyToRouterRules(network, nic, profile, sshPublicKey);
  
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(keyToRouterRules));
      }
  
      @Override
      public boolean saveUserDataToRouter(final Network network, final NicProfile nic, final VirtualMachineProfile profile, final VirtualRouter router)
              throws ResourceUnavailableException {
          logger.debug("SAVE USERDATA TO ROUTE RULES");
  
          final String typeString = "save userdata entry";
          final boolean isPodLevelException = false;
          final boolean failWhenDisconnect = false;
          final Long podId = null;
  
          final UserdataToRouterRules userdataToRouterRules = new UserdataToRouterRules(network, nic, profile);
          return applyRules(network, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(userdataToRouterRules));
      }
  
      @Override
      public boolean applyRules(final Network network, final VirtualRouter router, final String typeString, final boolean isPodLevelException, final Long podId,
              final boolean failWhenDisconnect, final RuleApplierWrapper<RuleApplier> ruleApplierWrapper) throws ResourceUnavailableException {
  
          if (router == null) {
              logger.warn("Unable to apply {}, virtual router doesn't exist in the network {}", typeString, network);
              throw new ResourceUnavailableException("Unable to apply " + typeString, DataCenter.class, network.getDataCenterId());
          }
  
          final RuleApplier ruleApplier = ruleApplierWrapper.getRuleType();
  
          final DataCenter dc = _dcDao.findById(network.getDataCenterId());
          final boolean isZoneBasic = dc.getNetworkType() == NetworkType.Basic;
  
          // isPodLevelException and podId is only used for basic zone
          assert !(!isZoneBasic && isPodLevelException || isZoneBasic && isPodLevelException && podId == null);
  
          final List<VirtualRouter> connectedRouters = new ArrayList<VirtualRouter>();
          final List<VirtualRouter> disconnectedRouters = new ArrayList<VirtualRouter>();
          boolean result = true;
          final String msg = "Unable to apply " + typeString + " on disconnected router ";
          if (router.getState() == State.Running) {
              logger.debug("Applying " + typeString + " in network " + network);
  
              if (router.isStopPending()) {
                  if (_hostDao.findById(router.getHostId()).getState() == Status.Up) {
                      throw new ResourceUnavailableException("Unable to process due to the stop pending router " + router.getInstanceName()
                              + " haven't been stopped after it's host coming back!", DataCenter.class, router.getDataCenterId());
                  }
                  logger.debug("Router {} is stop pending, so not sending apply {} commands to the backend", router, typeString);
                  return false;
              }
  
              try {
                  result = ruleApplier.accept(getVisitor(), router);
                  connectedRouters.add(router);
              } catch (final AgentUnavailableException e) {
                  logger.warn("{}{}", msg, router, e);
                  disconnectedRouters.add(router);
              }
  
              // If rules fail to apply on one domR and not due to
              // disconnection, no need to proceed with the rest
              if (!result) {
                  if (isZoneBasic && isPodLevelException) {
                      throw new ResourceUnavailableException("Unable to apply " + typeString + " on router ", Pod.class, podId);
                  }
                  throw new ResourceUnavailableException("Unable to apply " + typeString + " on router ", DataCenter.class, router.getDataCenterId());
              }
  
          } else if (router.getState() == State.Stopped || router.getState() == State.Stopping) {
              logger.debug("Router {} is in {}, so not sending apply {} commands to the backend", router, router.getState(), typeString);
          } else {
              logger.warn("Unable to apply " + typeString + ", virtual router is not in the right state " + router.getState());
              if (isZoneBasic && isPodLevelException) {
                  throw new ResourceUnavailableException("Unable to apply " + typeString + ", virtual router is not in the right state", Pod.class, podId);
              }
              throw new ResourceUnavailableException("Unable to apply " + typeString + ", virtual router is not in the right state", DataCenter.class, router.getDataCenterId());
          }
  
          if (!connectedRouters.isEmpty()) {
              // Shouldn't we include this check inside the method?
              if (!isZoneBasic && !disconnectedRouters.isEmpty()) {
                  // These disconnected redundant virtual routers are out of sync
                  // now, stop them for synchronization
                  for (final VirtualRouter virtualRouter : disconnectedRouters) {
                      // If we have at least 1 disconnected redundant router, callhandleSingleWorkingRedundantRouter().
                      if (virtualRouter.getIsRedundantRouter()) {
                          _networkHelper.handleSingleWorkingRedundantRouter(connectedRouters, disconnectedRouters, msg);
                          break;
                      }
                  }
              }
          } else if (!disconnectedRouters.isEmpty()) {
              if (logger.isDebugEnabled()) {
                  logger.debug("{}{}", msg, router);
              }
              if (isZoneBasic && isPodLevelException) {
                  throw new ResourceUnavailableException(msg, Pod.class, podId);
              }
              throw new ResourceUnavailableException(msg, DataCenter.class, disconnectedRouters.get(0).getDataCenterId());
          }
  
          result = true;
          if (failWhenDisconnect) {
              result = !connectedRouters.isEmpty();
          }
          return result;
      }
  
      @Override
      public boolean removeDhcpEntry(Network network, NicProfile nic, VirtualMachineProfile profile, VirtualRouter virtualRouter) throws ResourceUnavailableException {
          logger.debug("REMOVING DHCP ENTRY RULE");
  
          final String typeString = "dhcp entry";
          final Long podId = profile.getVirtualMachine().getPodIdToDeployIn();
          boolean isPodLevelException = false;
  
          if (podId != null && profile.getVirtualMachine().getType() == VirtualMachine.Type.User && network.getTrafficType() == TrafficType.Guest
                  && network.getGuestType() == Network.GuestType.Shared) {
              isPodLevelException = true;
          }
  
          final boolean failWhenDisconnect = false;
  
          final DhcpEntryRules dhcpRules = new DhcpEntryRules(network, nic, profile, null);
          dhcpRules.setRemove(true);
  
          return applyRules(network, virtualRouter, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper<RuleApplier>(dhcpRules));
      }
  
      @Override
      public boolean applyBgpPeers(Network network, List<? extends BgpPeer> bpgPeers, VirtualRouter virtualRouter) throws ResourceUnavailableException {
          throw new CloudRuntimeException("applyBgpPeers not implemented in Basic Network Topology.");
      }
  }
  