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.affinity;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import javax.inject.Inject;
  import javax.naming.ConfigurationException;
  
  import org.apache.cloudstack.acl.ControlledEntity;
  import org.apache.cloudstack.acl.ControlledEntity.ACLType;
  import org.apache.cloudstack.acl.SecurityChecker.AccessType;
  import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
  import org.apache.cloudstack.affinity.dao.AffinityGroupDomainMapDao;
  import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
  import org.apache.cloudstack.api.ApiConstants;
  import org.apache.cloudstack.api.command.user.affinitygroup.CreateAffinityGroupCmd;
  import org.apache.cloudstack.context.CallContext;
  import org.apache.cloudstack.framework.messagebus.MessageBus;
  import org.apache.cloudstack.framework.messagebus.PublishScope;
  
  import com.cloud.domain.DomainVO;
  import com.cloud.domain.dao.DomainDao;
  import com.cloud.event.ActionEvent;
  import com.cloud.event.EventTypes;
  import com.cloud.exception.InvalidParameterValueException;
  import com.cloud.exception.PermissionDeniedException;
  import com.cloud.hypervisor.Hypervisor;
  import com.cloud.user.Account;
  import com.cloud.user.AccountManager;
  import com.cloud.user.DomainManager;
  import com.cloud.uservm.UserVm;
  import com.cloud.utils.Pair;
  import com.cloud.utils.component.Manager;
  import com.cloud.utils.component.ManagerBase;
  import com.cloud.utils.db.DB;
  import com.cloud.utils.db.EntityManager;
  import com.cloud.utils.db.SearchBuilder;
  import com.cloud.utils.db.SearchCriteria;
  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.fsm.StateListener;
  import com.cloud.utils.fsm.StateMachine2;
  import com.cloud.vm.UserVmManager;
  import com.cloud.vm.UserVmVO;
  import com.cloud.vm.VirtualMachine;
  import com.cloud.vm.VirtualMachine.Event;
  import com.cloud.vm.VirtualMachine.State;
  import com.cloud.vm.dao.UserVmDao;
  
  public class AffinityGroupServiceImpl extends ManagerBase implements AffinityGroupService, Manager, StateListener<State, VirtualMachine.Event, VirtualMachine> {
  
      private String _name;
  
      @Inject
      AccountManager _accountMgr;
  
      @Inject
      AffinityGroupDao _affinityGroupDao;
  
      @Inject
      AffinityGroupVMMapDao _affinityGroupVMMapDao;
  
      @Inject
      AffinityGroupDomainMapDao _affinityGroupDomainMapDao;
  
      @Inject
      private UserVmDao _userVmDao;
  
      @Inject
      DomainDao _domainDao;
  
      @Inject
      DomainManager _domainMgr;
  
      @Inject
      MessageBus _messageBus;
  
      protected List<AffinityGroupProcessor> _affinityProcessors;
  
      public List<AffinityGroupProcessor> getAffinityGroupProcessors() {
          return _affinityProcessors;
      }
  
      public void setAffinityGroupProcessors(List<AffinityGroupProcessor> affinityProcessors) {
          _affinityProcessors = affinityProcessors;
      }
  
      @DB
      @Override
      @ActionEvent(eventType = EventTypes.EVENT_AFFINITY_GROUP_CREATE, eventDescription = "Creating Affinity Group", create = true)
      public AffinityGroup createAffinityGroup(CreateAffinityGroupCmd createAffinityGroupCmd) {
          return createAffinityGroup(createAffinityGroupCmd.getAccountName(), createAffinityGroupCmd.getProjectId(), createAffinityGroupCmd.getDomainId(), createAffinityGroupCmd.getAffinityGroupName(), createAffinityGroupCmd.getAffinityGroupType(), createAffinityGroupCmd.getDescription());
      }
  
      @DB
      @Override
      public AffinityGroup createAffinityGroup(final String accountName, final Long projectId, final Long domainId, final String affinityGroupName, final String affinityGroupType,
              final String description) {
  
          // validate the affinityGroupType
          Map<String, AffinityGroupProcessor> typeProcessorMap = getAffinityTypeToProcessorMap();
  
          if (typeProcessorMap == null || typeProcessorMap.isEmpty()) {
              throw new InvalidParameterValueException("Unable to create affinity group, no Affinity Group Types configured");
          }
  
          AffinityGroupProcessor processor = typeProcessorMap.get(affinityGroupType);
  
          if (processor == null) {
              throw new InvalidParameterValueException(String.format("Unable to create affinity group, invalid affinity group type: %s. " +
                      "Valid values are %s", affinityGroupType, String.join(",", typeProcessorMap.keySet())));
          }
  
          Account caller = CallContext.current().getCallingAccount();
          if (processor.isAdminControlledGroup() && !_accountMgr.isRootAdmin(caller.getId())) {
              throw new PermissionDeniedException("Cannot create the affinity group");
          }
  
          ControlledEntity.ACLType aclType = null;
          Account owner = null;
          boolean domainLevel = false;
  
          if (projectId == null && domainId != null && accountName == null) {
              verifyAccessToDomainWideProcessor(caller, processor);
              DomainVO domain = getDomain(domainId);
              _accountMgr.checkAccess(caller, domain);
  
              // domain level group, owner is SYSTEM.
              owner = _accountMgr.getAccount(Account.ACCOUNT_ID_SYSTEM);
              aclType = ControlledEntity.ACLType.Domain;
              domainLevel = true;
          } else {
              owner = _accountMgr.finalizeOwner(caller, accountName, domainId, projectId);
              aclType = ControlledEntity.ACLType.Account;
          }
  
          verifyAffinityGroupNameInUse(owner.getAccountId(), owner.getDomainId(), affinityGroupName);
          verifyDomainLevelAffinityGroupName(domainLevel, owner.getDomainId(), affinityGroupName);
  
          AffinityGroupVO group = createAffinityGroup(processor, owner, aclType, affinityGroupName, affinityGroupType, description, domainLevel, domainId);
  
          if (logger.isDebugEnabled()) {
              logger.debug("Created affinity group {}", group);
          }
          CallContext.current().putContextParameter(AffinityGroup.class, group.getUuid());
  
          return group;
      }
  
      private void verifyAccessToDomainWideProcessor(Account caller, AffinityGroupProcessor processor) {
          if (!_accountMgr.isRootAdmin(caller.getId())) {
              throw new InvalidParameterValueException("Unable to create affinity group, account name must be passed with the domainId");
          }
          if (!processor.canBeSharedDomainWide()) {
              throw new InvalidParameterValueException("Unable to create affinity group, account name is needed. Affinity group type "+ processor.getType() +" cannot be shared domain wide");
          }
      }
  
      private AffinityGroupVO createAffinityGroup(final AffinityGroupProcessor processor, final Account owner, final ACLType aclType, final String affinityGroupName, final String affinityGroupType, final String description, boolean domainLevel, Long domainId) {
  
          final Long affinityGroupDomainId = getDomainIdBasedOnDomainLevel(owner, domainLevel, domainId);
  
          return Transaction.execute(new TransactionCallback<AffinityGroupVO>() {
              @Override
              public AffinityGroupVO doInTransaction(TransactionStatus status) {
                  AffinityGroupVO group = new AffinityGroupVO(affinityGroupName, affinityGroupType, description, affinityGroupDomainId, owner.getId(), aclType);
                  _affinityGroupDao.persist(group);
  
                  if (aclType == ACLType.Domain) {
                      boolean subDomainAccess = false;
                      subDomainAccess = processor.subDomainAccess();
                      AffinityGroupDomainMapVO domainMap = new AffinityGroupDomainMapVO(group.getId(), affinityGroupDomainId,
                              subDomainAccess);
                      _affinityGroupDomainMapDao.persist(domainMap);
                      //send event for storing the domain wide resource access
                      Map<String, Object> params = new HashMap<String, Object>();
                      params.put(ApiConstants.ENTITY_TYPE, AffinityGroup.class);
                      params.put(ApiConstants.ENTITY_ID, group.getId());
                      params.put(ApiConstants.DOMAIN_ID, affinityGroupDomainId);
                      params.put(ApiConstants.SUBDOMAIN_ACCESS, subDomainAccess);
                      _messageBus.publish(_name, EntityManager.MESSAGE_ADD_DOMAIN_WIDE_ENTITY_EVENT, PublishScope.LOCAL,
                              params);
                  }
  
                  return group;
              }
          });
      }
  
      /**
       * If the account is null (domainLevel is true), then returns the domain id passed as a
       * parameter; otherwise (domainLevel is false) it returns the domain id from the owner account.
       *
       * @note: this method fixes a critical bug. More details in JIRA ticket CLOUDSTACK-9432.
       */
      protected Long getDomainIdBasedOnDomainLevel(final Account owner, boolean domainLevel, Long domainId) {
          Long domainIdBasedOnDomainLevel = owner.getDomainId();
          if (domainLevel) {
              domainIdBasedOnDomainLevel = domainId;
          }
          return domainIdBasedOnDomainLevel;
      }
  
      private DomainVO getDomain(Long domainId) {
          DomainVO domain = _domainDao.findById(domainId);
          if (domain == null) {
              throw new InvalidParameterValueException("Unable to find domain by specified id");
          }
          return domain;
      }
  
      private void verifyAffinityGroupNameInUse(long accountId, long domainId, String affinityGroupName) {
          if (_affinityGroupDao.isNameInUse(accountId, domainId, affinityGroupName)) {
              throw new InvalidParameterValueException("Unable to create affinity group, a group with name " + affinityGroupName + " already exists.");
          }
      }
  
      private void verifyDomainLevelAffinityGroupName(boolean domainLevel, long domainId, String affinityGroupName) {
          if (domainLevel && _affinityGroupDao.findDomainLevelGroupByName(domainId, affinityGroupName) != null) {
              throw new InvalidParameterValueException("Unable to create affinity group, a group with name " + affinityGroupName + " already exists under the domain.");
          }
      }
  
      @Override
      @DB
      @ActionEvent(eventType = EventTypes.EVENT_AFFINITY_GROUP_DELETE, eventDescription = "Deleting affinity group")
      public boolean deleteAffinityGroup(Long affinityGroupId, String account, Long projectId, Long domainId, String affinityGroupName) {
  
          AffinityGroupVO group = getAffinityGroup(affinityGroupId, account, projectId, domainId, affinityGroupName);
  
          // check permissions
          Account caller = CallContext.current().getCallingAccount();
          _accountMgr.checkAccess(caller, AccessType.OperateEntry, true, group);
  
          final Long affinityGroupIdFinal = group.getId();
          deleteAffinityGroup(affinityGroupIdFinal);
  
          // remove its related ACL permission
          Pair<Class<?>, Long> params = new Pair<Class<?>, Long>(AffinityGroup.class, affinityGroupIdFinal);
          _messageBus.publish(_name, EntityManager.MESSAGE_REMOVE_ENTITY_EVENT, PublishScope.LOCAL, params);
  
          if (logger.isDebugEnabled()) {
              logger.debug("Deleted affinity group {}", group);
          }
          return true;
      }
  
      private AffinityGroupVO getAffinityGroup(Long affinityGroupId, String account, Long projectId, Long domainId, String affinityGroupName) {
          AffinityGroupVO group = null;
          if (affinityGroupId != null) {
              group = _affinityGroupDao.findById(affinityGroupId);
          } else if (affinityGroupName != null) {
              group = getAffinityGroupByName(account, projectId, domainId, affinityGroupName);
          } else {
              throw new InvalidParameterValueException("Either the affinity group Id or group name must be specified to delete the group");
          }
          if (group == null) {
              throw new InvalidParameterValueException("Unable to find affinity group " + (affinityGroupId == null ? affinityGroupName : affinityGroupId));
          }
          return group;
      }
  
      private AffinityGroupVO getAffinityGroupByName(String account, Long projectId, Long domainId, String affinityGroupName) {
          AffinityGroupVO group = null;
          if(account == null && domainId != null){
              group = _affinityGroupDao.findDomainLevelGroupByName(domainId, affinityGroupName);
          }else{
              Long accountId = _accountMgr.finalyzeAccountId(account, domainId, projectId, true);
              if(accountId == null){
                  Account caller = CallContext.current().getCallingAccount();
                  group = _affinityGroupDao.findByAccountAndName(caller.getAccountId(), affinityGroupName);
              }else{
                  group = _affinityGroupDao.findByAccountAndName(accountId, affinityGroupName);
              }
          }
          return group;
      }
  
      private void deleteAffinityGroup(final Long affinityGroupId) {
          Transaction.execute(new TransactionCallbackNoReturn() {
              @Override
              public void doInTransactionWithoutResult(TransactionStatus status) {
  
                  AffinityGroupVO group = _affinityGroupDao.lockRow(affinityGroupId, true);
                  if (group == null) {
                      throw new InvalidParameterValueException("Unable to find affinity group by id " + affinityGroupId);
                  }
  
                  List<AffinityGroupVMMapVO> affinityGroupVmMap = _affinityGroupVMMapDao.listByAffinityGroup(affinityGroupId);
                  if (!affinityGroupVmMap.isEmpty()) {
                      SearchBuilder<AffinityGroupVMMapVO> listByAffinityGroup = _affinityGroupVMMapDao.createSearchBuilder();
                      listByAffinityGroup.and("affinityGroupId", listByAffinityGroup.entity().getAffinityGroupId(), SearchCriteria.Op.EQ);
                      listByAffinityGroup.done();
                      SearchCriteria<AffinityGroupVMMapVO> sc = listByAffinityGroup.create();
                      sc.setParameters("affinityGroupId", affinityGroupId);
  
                      _affinityGroupVMMapDao.lockRows(sc, null, true);
                      _affinityGroupVMMapDao.remove(sc);
                  }
  
                  // call processor to handle the group delete
                  AffinityGroupProcessor processor = getAffinityGroupProcessorForType(group.getType());
                  if (processor != null) {
                      processor.handleDeleteGroup(group);
                  }
  
                  if(_affinityGroupDao.expunge(affinityGroupId)){
                      AffinityGroupDomainMapVO groupDomain = _affinityGroupDomainMapDao
                              .findByAffinityGroup(affinityGroupId);
                      if (groupDomain != null) {
                          _affinityGroupDomainMapDao.remove(groupDomain.getId());
                      }
                  }
              }
          });
      }
  
      @Override
      public List<String> listAffinityGroupTypes() {
          List<String> types = new ArrayList<String>();
  
          for (AffinityGroupProcessor processor : _affinityProcessors) {
              if (processor.isAdminControlledGroup()) {
                  continue; // we don't list the type if this group can be
                  // created only as an admin/system operation.
              }
              types.add(processor.getType());
          }
  
          return types;
      }
  
      protected Map<String, AffinityGroupProcessor> getAffinityTypeToProcessorMap() {
          Map<String, AffinityGroupProcessor> typeProcessorMap = new HashMap<String, AffinityGroupProcessor>();
  
          for (AffinityGroupProcessor processor : _affinityProcessors) {
              typeProcessorMap.put(processor.getType(), processor);
          }
  
          return typeProcessorMap;
      }
  
      @Override
      public boolean isAdminControlledGroup(AffinityGroup group) {
  
          if (group != null) {
              String affinityGroupType = group.getType();
              Map<String, AffinityGroupProcessor> typeProcessorMap = getAffinityTypeToProcessorMap();
              if (typeProcessorMap != null && !typeProcessorMap.isEmpty()) {
                  AffinityGroupProcessor processor = typeProcessorMap.get(affinityGroupType);
                  if (processor != null) {
                      return processor.isAdminControlledGroup();
                  }
              }
          }
          return false;
  
      }
  
      @Override
      public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
          _name = name;
          VirtualMachine.State.getStateMachine().registerListener(this);
          return true;
      }
  
      @Override
      public boolean start() {
          return true;
      }
  
      @Override
      public boolean stop() {
          return true;
      }
  
      @Override
      public String getName() {
          return _name;
      }
  
      @Override
      public AffinityGroup getAffinityGroup(Long groupId) {
          return _affinityGroupDao.findById(groupId);
      }
  
      @Override
      public boolean preStateTransitionEvent(State oldState, Event event, State newState, VirtualMachine vo, boolean status, Object opaque) {
          return true;
      }
  
      @Override
      public boolean postStateTransitionEvent(StateMachine2.Transition<State, Event> transition, VirtualMachine vo, boolean status, Object opaque) {
          if (!status) {
              return false;
          }
          State newState = transition.getToState();
          if ((newState == State.Expunging) || (newState == State.Error)) {
              // cleanup all affinity groups associations of the Expunged VM
              SearchCriteria<AffinityGroupVMMapVO> sc = _affinityGroupVMMapDao.createSearchCriteria();
              sc.addAnd("instanceId", SearchCriteria.Op.EQ, vo.getId());
              _affinityGroupVMMapDao.expunge(sc);
          }
          return true;
      }
  
      @Override
      public UserVm updateVMAffinityGroups(Long vmId, List<Long> affinityGroupIds) {
          // Verify input parameters
          UserVmVO vmInstance = _userVmDao.findById(vmId);
          if (vmInstance == null) {
              throw new InvalidParameterValueException("Unable to find a virtual machine with id " + vmId);
          }
          if (UserVmManager.SHAREDFSVM.equals(vmInstance.getUserVmType())) {
              throw new InvalidParameterValueException("Operation not supported on Shared FileSystem Instance");
          }
          if (Hypervisor.HypervisorType.External.equals(vmInstance.getHypervisorType())) {
              logger.error("Update VM Affinity Group not supported for {} as it is {} hypervisor instance",
                      vmInstance, Hypervisor.HypervisorType.External.name());
              throw new InvalidParameterValueException(String.format("Operation not supported for instance: %s",
                      vmInstance.getName()));
          }
  
          // Check that the VM is stopped
          if (!vmInstance.getState().equals(State.Stopped)) {
              logger.warn("Unable to update affinity groups of the virtual machine " + vmInstance.toString() + " in state " + vmInstance.getState());
              throw new InvalidParameterValueException("Unable update affinity groups of the virtual machine " + vmInstance.toString() + " " + "in state " +
                      vmInstance.getState() + "; make sure the virtual machine is stopped and not in an error state before updating.");
          }
  
          Account caller = CallContext.current().getCallingAccount();
          Account owner = _accountMgr.getAccount(vmInstance.getAccountId());
  
          List<AffinityGroupVO> affinityGroupList = new ArrayList<>();
          // check that the affinity groups exist
          for (Long affinityGroupId : affinityGroupIds) {
              AffinityGroupVO ag = _affinityGroupDao.findById(affinityGroupId);
              if (ag == null) {
                  throw new InvalidParameterValueException("Unable to find affinity group by id " + affinityGroupId);
              } else {
                  affinityGroupList.add(ag);
                  // verify permissions
                  if (ag.getAclType() == ACLType.Domain) {
                      _accountMgr.checkAccess(caller, null, false, owner, ag);
                      // make sure the affinity group is available in that domain
                      if (caller.getId() == Account.ACCOUNT_ID_SYSTEM || _accountMgr.isRootAdmin(caller.getId())) {
                          if (!isAffinityGroupAvailableInDomain(ag.getId(), owner.getDomainId())) {
                              throw new PermissionDeniedException("Affinity Group " + ag + " does not belong to the VM's domain");
                          }
                      }
                  } else {
                      _accountMgr.checkAccess(caller, null, true, owner, ag);
                      // Root admin has access to both VM and AG by default,
                      // but
                      // make sure the owner of these entities is same
                      if (caller.getId() == Account.ACCOUNT_ID_SYSTEM || _accountMgr.isRootAdmin(caller.getId())) {
                          if (ag.getAccountId() != owner.getAccountId()) {
                              throw new PermissionDeniedException("Affinity Group " + ag + " does not belong to the VM's account");
                          }
                      }
                  }
              }
          }
          _affinityGroupVMMapDao.updateMap(vmId, affinityGroupIds);
          if (logger.isDebugEnabled()) {
              logger.debug("Updated VM {} affinity groups to {}", vmInstance, affinityGroupList);
          }
          // APIResponseHelper will pull out the updated affinitygroups.
          return vmInstance;
  
      }
  
      @Override
      public boolean isAffinityGroupProcessorAvailable(String affinityGroupType) {
          for (AffinityGroupProcessor processor : _affinityProcessors) {
              if (affinityGroupType != null && affinityGroupType.equals(processor.getType())) {
                  return true;
              }
          }
          return false;
      }
  
      private AffinityGroupProcessor getAffinityGroupProcessorForType(String affinityGroupType) {
          for (AffinityGroupProcessor processor : _affinityProcessors) {
              if (affinityGroupType != null && affinityGroupType.equals(processor.getType())) {
                  return processor;
              }
          }
          return null;
      }
  
      @Override
      public boolean isAffinityGroupAvailableInDomain(long affinityGroupId, long domainId) {
          Long groupDomainId = null;
  
          AffinityGroupDomainMapVO domainMap = _affinityGroupDomainMapDao.findByAffinityGroup(affinityGroupId);
          if (domainMap == null) {
              return false;
          } else {
              groupDomainId = domainMap.getDomainId();
          }
  
          if (domainId == groupDomainId.longValue()) {
              return true;
          }
  
          if (domainMap.subdomainAccess) {
              Set<Long> parentDomains = _domainMgr.getDomainParentIds(domainId);
              if (parentDomains.contains(groupDomainId)) {
                  return true;
              }
          }
  
          return false;
      }
  
  }
  