Merge remote-tracking branch 'origin/4.15'

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Rohit Yadav 2021-05-07 16:37:42 +05:30
commit cb167072a1
61 changed files with 1188 additions and 536 deletions

View File

@ -374,6 +374,7 @@ public class EventTypes {
// Primary storage pool
public static final String EVENT_ENABLE_PRIMARY_STORAGE = "ENABLE.PS";
public static final String EVENT_DISABLE_PRIMARY_STORAGE = "DISABLE.PS";
public static final String EVENT_SYNC_STORAGE_POOL = "SYNC.STORAGE.POOL";
// VPN
public static final String EVENT_REMOTE_ACCESS_VPN_CREATE = "VPN.REMOTE.ACCESS.CREATE";

View File

@ -27,6 +27,7 @@ import org.apache.cloudstack.api.command.admin.storage.DeleteImageStoreCmd;
import org.apache.cloudstack.api.command.admin.storage.DeletePoolCmd;
import org.apache.cloudstack.api.command.admin.storage.DeleteSecondaryStagingStoreCmd;
import org.apache.cloudstack.api.command.admin.storage.UpdateStoragePoolCmd;
import org.apache.cloudstack.api.command.admin.storage.SyncStoragePoolCmd;
import com.cloud.exception.DiscoveryException;
import com.cloud.exception.InsufficientCapacityException;
@ -104,4 +105,6 @@ public interface StorageService {
ImageStore updateImageStoreStatus(Long id, Boolean readonly);
StoragePool syncStoragePool(SyncStoragePoolCmd cmd);
}

View File

@ -0,0 +1,97 @@
// 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.storage;
import com.cloud.event.EventTypes;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.storage.StoragePool;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.StoragePoolResponse;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.context.CallContext;
import java.util.logging.Logger;
@APICommand(name = SyncStoragePoolCmd.APINAME,
description = "Sync storage pool with management server (currently supported for Datastore Cluster in VMware and syncs the datastores in it)",
responseObject = StoragePoolResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.15.1",
authorized = {RoleType.Admin}
)
public class SyncStoragePoolCmd extends BaseAsyncCmd {
public static final String APINAME = "syncStoragePool";
public static final Logger LOGGER = Logger.getLogger(SyncStoragePoolCmd.class.getName());
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = StoragePoolResponse.class, required = true, description = "Storage pool id")
private Long poolId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getPoolId() {
return poolId;
}
@Override
public String getEventType() {
return EventTypes.EVENT_SYNC_STORAGE_POOL;
}
@Override
public String getEventDescription() {
return "Attempting to synchronise storage pool with management server";
}
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
StoragePool result = _storageService.syncStoragePool(this);
if (result != null) {
StoragePoolResponse response = _responseGenerator.createStoragePoolResponse(result);
response.setResponseName("storagepool");
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to synchronise storage pool");
}
}
@Override
public String getCommandName() {
return APINAME.toLowerCase() + BaseAsyncCmd.RESPONSE_SUFFIX;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccountId();
}
}

View File

@ -30,6 +30,7 @@ import org.apache.cloudstack.storage.command.ForgetObjectCmd;
import org.apache.cloudstack.storage.command.IntroduceObjectCmd;
import org.apache.cloudstack.storage.command.ResignatureCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import com.cloud.agent.api.Answer;
@ -81,5 +82,7 @@ public interface StorageProcessor {
Answer copyVolumeFromPrimaryToPrimary(CopyCommand cmd);
public Answer CheckDataStoreStoragePolicyComplaince(CheckDataStoreStoragePolicyComplainceCommand cmd);
public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplainceCommand cmd);
public Answer syncVolumePath(SyncVolumePathCommand cmd);
}

View File

@ -34,6 +34,7 @@ import org.apache.cloudstack.storage.command.IntroduceObjectCmd;
import org.apache.cloudstack.storage.command.ResignatureCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand;
import org.apache.cloudstack.storage.command.StorageSubSystemCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
@ -73,7 +74,9 @@ public class StorageSubsystemCommandHandlerBase implements StorageSubsystemComma
} else if (command instanceof DirectDownloadCommand) {
return processor.handleDownloadTemplateToPrimaryStorage((DirectDownloadCommand) command);
} else if (command instanceof CheckDataStoreStoragePolicyComplainceCommand) {
return processor.CheckDataStoreStoragePolicyComplaince((CheckDataStoreStoragePolicyComplainceCommand) command);
return processor.checkDataStoreStoragePolicyCompliance((CheckDataStoreStoragePolicyComplainceCommand) command);
} else if (command instanceof SyncVolumePathCommand) {
return processor.syncVolumePath((SyncVolumePathCommand) command);
}
return new Answer((Command)command, false, "not implemented yet");

View File

@ -0,0 +1,49 @@
//
// 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.storage.command;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.to.DiskTO;
public class SyncVolumePathAnswer extends Answer {
private DiskTO disk;
public SyncVolumePathAnswer() {
super(null);
}
public SyncVolumePathAnswer(DiskTO disk) {
super(null);
setDisk(disk);
}
public SyncVolumePathAnswer(String errMsg) {
super(null, false, errMsg);
}
public DiskTO getDisk() {
return disk;
}
public void setDisk(DiskTO disk) {
this.disk = disk;
}
}

View File

@ -0,0 +1,49 @@
//
// 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.storage.command;
import com.cloud.agent.api.to.DiskTO;
public class SyncVolumePathCommand extends StorageSubSystemCommand {
private DiskTO disk;
public SyncVolumePathCommand(final DiskTO disk) {
super();
this.disk = disk;
}
public DiskTO getDisk() {
return disk;
}
public void setDisk(final DiskTO disk) {
this.disk = disk;
}
@Override
public boolean executeInSequence() {
return false;
}
@Override
public void setExecuteInSequence(boolean inSeq) {
}
}

View File

@ -19,6 +19,7 @@ package com.cloud.storage;
import java.math.BigDecimal;
import java.util.List;
import com.cloud.agent.api.ModifyStoragePoolAnswer;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener;
import org.apache.cloudstack.framework.config.ConfigKey;
@ -272,4 +273,6 @@ public interface StorageManager extends StorageService {
boolean isStoragePoolDatastoreClusterParent(StoragePool pool);
void syncDatastoreClusterStoragePool(long datastoreClusterPoolId, List<ModifyStoragePoolAnswer> childDatastoreAnswerList, long hostId);
}

View File

@ -23,7 +23,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;

View File

@ -130,4 +130,6 @@ public interface PrimaryDataStoreDao extends GenericDao<StoragePoolVO, Long> {
Integer countAll();
List<StoragePoolVO> findPoolsByStorageType(String storageType);
}

View File

@ -93,6 +93,7 @@ public class PrimaryDataStoreDaoImpl extends GenericDaoBase<StoragePoolVO, Long>
AllFieldSearch.and("podId", AllFieldSearch.entity().getPodId(), Op.EQ);
AllFieldSearch.and("clusterId", AllFieldSearch.entity().getClusterId(), Op.EQ);
AllFieldSearch.and("storage_provider_name", AllFieldSearch.entity().getStorageProviderName(), Op.EQ);
AllFieldSearch.and("poolType", AllFieldSearch.entity().getPoolType(), Op.EQ);
AllFieldSearch.done();
DcPodSearch = createSearchBuilder();
@ -581,4 +582,11 @@ public class PrimaryDataStoreDaoImpl extends GenericDaoBase<StoragePoolVO, Long>
sc.setParameters("status", StoragePoolStatus.Up);
return listBy(sc);
}
@Override
public List<StoragePoolVO> findPoolsByStorageType(String storageType) {
SearchCriteria<StoragePoolVO> sc = AllFieldSearch.create();
sc.setParameters("poolType", storageType);
return listBy(sc);
}
}

View File

@ -22,16 +22,14 @@ import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ModifyStoragePoolAnswer;
import com.cloud.agent.api.ModifyStoragePoolCommand;
import com.cloud.agent.api.StoragePoolInfo;
import com.cloud.alert.AlertManager;
import com.cloud.exception.StorageConflictException;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage;
import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.StoragePoolStatus;
import com.cloud.storage.dao.StoragePoolHostDao;
import com.cloud.storage.dao.StoragePoolTagsDao;
import com.cloud.storage.StorageManager;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener;
@ -43,9 +41,7 @@ import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DefaultHostListener implements HypervisorHostListener {
private static final Logger s_logger = Logger.getLogger(DefaultHostListener.class);
@ -62,7 +58,7 @@ public class DefaultHostListener implements HypervisorHostListener {
@Inject
StoragePoolDetailsDao storagePoolDetailsDao;
@Inject
StoragePoolTagsDao storagePoolTagsDao;
StorageManager storageManager;
@Override
public boolean hostAdded(long hostId) {
@ -104,43 +100,7 @@ public class DefaultHostListener implements HypervisorHostListener {
updateStoragePoolHostVOAndDetails(poolVO, hostId, mspAnswer);
if (pool.getPoolType() == Storage.StoragePoolType.DatastoreCluster) {
for (ModifyStoragePoolAnswer childDataStoreAnswer : ((ModifyStoragePoolAnswer) answer).getDatastoreClusterChildren()) {
StoragePoolInfo childStoragePoolInfo = childDataStoreAnswer.getPoolInfo();
StoragePoolVO dataStoreVO = primaryStoreDao.findPoolByUUID(childStoragePoolInfo.getUuid());
if (dataStoreVO != null) {
continue;
}
dataStoreVO = new StoragePoolVO();
dataStoreVO.setStorageProviderName(poolVO.getStorageProviderName());
dataStoreVO.setHostAddress(childStoragePoolInfo.getHost());
dataStoreVO.setPoolType(Storage.StoragePoolType.PreSetup);
dataStoreVO.setPath(childStoragePoolInfo.getHostPath());
dataStoreVO.setPort(poolVO.getPort());
dataStoreVO.setName(childStoragePoolInfo.getName());
dataStoreVO.setUuid(childStoragePoolInfo.getUuid());
dataStoreVO.setDataCenterId(poolVO.getDataCenterId());
dataStoreVO.setPodId(poolVO.getPodId());
dataStoreVO.setClusterId(poolVO.getClusterId());
dataStoreVO.setStatus(StoragePoolStatus.Up);
dataStoreVO.setUserInfo(poolVO.getUserInfo());
dataStoreVO.setManaged(poolVO.isManaged());
dataStoreVO.setCapacityIops(poolVO.getCapacityIops());
dataStoreVO.setCapacityBytes(childDataStoreAnswer.getPoolInfo().getCapacityBytes());
dataStoreVO.setUsedBytes(childDataStoreAnswer.getPoolInfo().getCapacityBytes() - childDataStoreAnswer.getPoolInfo().getAvailableBytes());
dataStoreVO.setHypervisor(poolVO.getHypervisor());
dataStoreVO.setScope(poolVO.getScope());
dataStoreVO.setParent(poolVO.getId());
Map<String, String> details = new HashMap<>();
if(StringUtils.isNotEmpty(childDataStoreAnswer.getPoolType())) {
details.put("pool_type", childDataStoreAnswer.getPoolType());
}
List<String> storageTags = storagePoolTagsDao.getStoragePoolTags(poolId);
primaryStoreDao.persist(dataStoreVO, details, storageTags);
updateStoragePoolHostVOAndDetails(dataStoreVO, hostId, childDataStoreAnswer);
}
storageManager.syncDatastoreClusterStoragePool(poolId, ((ModifyStoragePoolAnswer) answer).getDatastoreClusterChildren(), hostId);
}
s_logger.info("Connection established between storage pool " + pool + " and host " + hostId);

View File

@ -59,6 +59,7 @@ import org.apache.cloudstack.storage.command.ResignatureAnswer;
import org.apache.cloudstack.storage.command.ResignatureCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer;
import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
@ -1922,8 +1923,14 @@ public class KVMStorageProcessor implements StorageProcessor {
}
@Override
public Answer CheckDataStoreStoragePolicyComplaince(CheckDataStoreStoragePolicyComplainceCommand cmd) {
public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplainceCommand cmd) {
s_logger.info("'CheckDataStoreStoragePolicyComplainceCommand' not currently applicable for KVMStorageProcessor");
return new Answer(cmd,false,"Not currently applicable for KVMStorageProcessor");
}
@Override
public Answer syncVolumePath(SyncVolumePathCommand cmd) {
s_logger.info("SyncVolumePathCommand not currently applicable for KVMStorageProcessor");
return new Answer(cmd, false, "Not currently applicable for KVMStorageProcessor");
}
}

View File

@ -35,8 +35,9 @@ import org.apache.cloudstack.storage.command.ForgetObjectCmd;
import org.apache.cloudstack.storage.command.IntroduceObjectCmd;
import org.apache.cloudstack.storage.command.ResignatureAnswer;
import org.apache.cloudstack.storage.command.ResignatureCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer;
import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
@ -828,11 +829,17 @@ public class Ovm3StorageProcessor implements StorageProcessor {
}
@Override
public Answer CheckDataStoreStoragePolicyComplaince(CheckDataStoreStoragePolicyComplainceCommand cmd) {
public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplainceCommand cmd) {
LOGGER.info("'CheckDataStoreStoragePolicyComplainceCommand' not applicable used for Ovm3StorageProcessor");
return new Answer(cmd,false,"Not applicable used for Ovm3StorageProcessor");
}
@Override
public Answer syncVolumePath(SyncVolumePathCommand cmd) {
LOGGER.info("SyncVolumePathCommand not currently applicable for Ovm3StorageProcessor");
return new Answer(cmd, false, "Not currently applicable for Ovm3StorageProcessor");
}
@Override
public Answer copyVolumeFromPrimaryToPrimary(CopyCommand cmd) {
return null;

View File

@ -41,6 +41,7 @@ import org.apache.cloudstack.storage.command.ResignatureAnswer;
import org.apache.cloudstack.storage.command.ResignatureCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer;
import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
@ -272,7 +273,12 @@ public class SimulatorStorageProcessor implements StorageProcessor {
}
@Override
public Answer CheckDataStoreStoragePolicyComplaince(CheckDataStoreStoragePolicyComplainceCommand cmd) {
public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplainceCommand cmd) {
return new Answer(cmd, true, null);
}
@Override
public Answer syncVolumePath(SyncVolumePathCommand cmd) {
return new Answer(cmd, true, null);
}
}

View File

@ -5037,10 +5037,12 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa
String datacenterName = datastoreClusterPath.substring(0, pathstartPosition+1);
String childPath = datacenterName + summary.getName();
poolInfo.setHostPath(childPath);
String uuid = UUID.nameUUIDFromBytes(((pool.getHost() + childPath)).getBytes()).toString();
String uuid = childDsMo.getCustomFieldValue(CustomFieldConstants.CLOUD_UUID);
if (uuid == null) {
uuid = UUID.nameUUIDFromBytes(((pool.getHost() + childPath)).getBytes()).toString();
}
poolInfo.setUuid(uuid);
poolInfo.setLocalPath(cmd.LOCAL_PATH_PREFIX + File.separator + uuid);
answer.setPoolInfo(poolInfo);
answer.setPoolType(summary.getType());
answer.setLocalDatastoreName(morDatastore.getValue());

View File

@ -50,6 +50,8 @@ import org.apache.cloudstack.storage.command.ResignatureAnswer;
import org.apache.cloudstack.storage.command.ResignatureCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer;
import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathAnswer;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
@ -3892,7 +3894,7 @@ public class VmwareStorageProcessor implements StorageProcessor {
}
@Override
public Answer CheckDataStoreStoragePolicyComplaince(CheckDataStoreStoragePolicyComplainceCommand cmd) {
public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplainceCommand cmd) {
String primaryStorageNameLabel = cmd.getStoragePool().getUuid();
String storagePolicyId = cmd.getStoragePolicyId();
VmwareContext context = hostService.getServiceContext(cmd);
@ -3962,4 +3964,81 @@ public class VmwareStorageProcessor implements StorageProcessor {
throw new CloudRuntimeException(msg, e);
}
}
@Override
public Answer syncVolumePath(SyncVolumePathCommand cmd) {
DiskTO disk = cmd.getDisk();
VolumeObjectTO volumeTO = (VolumeObjectTO)disk.getData();
DataStoreTO primaryStore = volumeTO.getDataStore();
String volumePath = volumeTO.getPath();
String vmName = volumeTO.getVmName();
boolean datastoreChangeObserved = false;
boolean volumePathChangeObserved = false;
String chainInfo = null;
try {
VmwareContext context = hostService.getServiceContext(null);
VmwareHypervisorHost hyperHost = hostService.getHyperHost(context, null);
VirtualMachineMO vmMo = hyperHost.findVmOnHyperHost(vmName);
if (vmMo == null) {
vmMo = hyperHost.findVmOnPeerHyperHost(vmName);
if (vmMo == null) {
String msg = "Unable to find the VM to execute SyncVolumePathCommand, vmName: " + vmName;
s_logger.error(msg);
throw new Exception(msg);
}
}
String datastoreUUID = primaryStore.getUuid();
if (disk.getDetails().get(DiskTO.PROTOCOL_TYPE) != null && disk.getDetails().get(DiskTO.PROTOCOL_TYPE).equalsIgnoreCase("DatastoreCluster")) {
VirtualMachineDiskInfo matchingExistingDisk = getMatchingExistingDisk(hyperHost, context, vmMo, disk);
VirtualMachineDiskInfoBuilder diskInfoBuilder = vmMo.getDiskInfoBuilder();
if (diskInfoBuilder != null && matchingExistingDisk != null) {
String[] diskChain = matchingExistingDisk.getDiskChain();
assert (diskChain.length > 0);
DatastoreFile file = new DatastoreFile(diskChain[0]);
if (!file.getFileBaseName().equalsIgnoreCase(volumePath)) {
if (s_logger.isInfoEnabled())
s_logger.info("Detected disk-chain top file change on volume: " + volumeTO.getId() + " " + volumePath + " -> " + file.getFileBaseName());
volumePathChangeObserved = true;
volumePath = file.getFileBaseName();
volumeTO.setPath(volumePath);
chainInfo = _gson.toJson(matchingExistingDisk);
}
DatastoreMO diskDatastoreMofromVM = getDiskDatastoreMofromVM(hyperHost, context, vmMo, disk, diskInfoBuilder);
if (diskDatastoreMofromVM != null) {
String actualPoolUuid = diskDatastoreMofromVM.getCustomFieldValue(CustomFieldConstants.CLOUD_UUID);
if (!actualPoolUuid.equalsIgnoreCase(primaryStore.getUuid())) {
s_logger.warn(String.format("Volume %s found to be in a different storage pool %s", volumePath, actualPoolUuid));
datastoreChangeObserved = true;
datastoreUUID = actualPoolUuid;
chainInfo = _gson.toJson(matchingExistingDisk);
}
}
}
}
SyncVolumePathAnswer answer = new SyncVolumePathAnswer(disk);
if (datastoreChangeObserved) {
answer.setContextParam("datastoreName", datastoreUUID);
}
if (volumePathChangeObserved) {
answer.setContextParam("volumePath", volumePath);
}
if (chainInfo != null && !chainInfo.isEmpty()) {
answer.setContextParam("chainInfo", chainInfo);
}
return answer;
} catch (Throwable e) {
if (e instanceof RemoteException) {
s_logger.warn("Encounter remote exception to vCenter, invalidate VMware session context");
hostService.invalidateServiceContext(null);
}
return new SyncVolumePathAnswer("Failed to process SyncVolumePathCommand due to " + e.getMessage());
}
}
}

View File

@ -50,6 +50,7 @@ import org.apache.cloudstack.storage.command.ResignatureAnswer;
import org.apache.cloudstack.storage.command.ResignatureCommand;
import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer;
import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
@ -217,11 +218,17 @@ public class XenServerStorageProcessor implements StorageProcessor {
}
@Override
public Answer CheckDataStoreStoragePolicyComplaince(CheckDataStoreStoragePolicyComplainceCommand cmd) {
public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplainceCommand cmd) {
s_logger.info("'CheckDataStoreStoragePolicyComplainceCommand' not applicable used for XenServerStorageProcessor");
return new Answer(cmd,false,"Not applicable used for XenServerStorageProcessor");
}
@Override
public Answer syncVolumePath(SyncVolumePathCommand cmd) {
s_logger.info("SyncVolumePathCommand not currently applicable for XenServerStorageProcessor");
return new Answer(cmd, false, "Not currently applicable for XenServerStorageProcessor");
}
@Override
public AttachAnswer attachIso(final AttachCommand cmd) {
final DiskTO disk = cmd.getDisk();

View File

@ -30,6 +30,7 @@ import java.util.UUID;
import org.apache.cloudstack.storage.command.CheckDataStoreStoragePolicyComplainceCommand;
import org.apache.cloudstack.storage.command.CopyCmdAnswer;
import org.apache.cloudstack.storage.command.CopyCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
@ -915,11 +916,17 @@ public class Xenserver625StorageProcessor extends XenServerStorageProcessor {
}
@Override
public Answer CheckDataStoreStoragePolicyComplaince(CheckDataStoreStoragePolicyComplainceCommand cmd) {
public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplainceCommand cmd) {
s_logger.info("'CheckDataStoreStoragePolicyComplainceCommand' not applicable used for XenServerStorageProcessor");
return new Answer(cmd,false,"Not applicable used for XenServerStorageProcessor");
}
@Override
public Answer syncVolumePath(SyncVolumePathCommand cmd) {
s_logger.info("SyncVolumePathCommand not currently applicable for XenServerStorageProcessor");
return new Answer(cmd, false, "Not currently applicable for XenServerStorageProcessor");
}
@Override
public Answer copyVolumeFromPrimaryToSecondary(final CopyCommand cmd) {
final Connection conn = hypervisorResource.getConnection();

View File

@ -207,6 +207,7 @@ import org.apache.cloudstack.api.command.admin.storage.PreparePrimaryStorageForM
import org.apache.cloudstack.api.command.admin.storage.UpdateCloudToUseObjectStoreCmd;
import org.apache.cloudstack.api.command.admin.storage.UpdateImageStoreCmd;
import org.apache.cloudstack.api.command.admin.storage.UpdateStoragePoolCmd;
import org.apache.cloudstack.api.command.admin.storage.SyncStoragePoolCmd;
import org.apache.cloudstack.api.command.admin.swift.AddSwiftCmd;
import org.apache.cloudstack.api.command.admin.swift.ListSwiftsCmd;
import org.apache.cloudstack.api.command.admin.systemvm.DestroySystemVmCmd;
@ -3039,6 +3040,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(FindStoragePoolsForMigrationCmd.class);
cmdList.add(PreparePrimaryStorageForMaintenanceCmd.class);
cmdList.add(UpdateStoragePoolCmd.class);
cmdList.add(SyncStoragePoolCmd.class);
cmdList.add(UpdateImageStoreCmd.class);
cmdList.add(DestroySystemVmCmd.class);
cmdList.add(ListSystemVMsCmd.class);

View File

@ -32,6 +32,8 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -52,6 +54,7 @@ import org.apache.cloudstack.api.command.admin.storage.DeleteImageStoreCmd;
import org.apache.cloudstack.api.command.admin.storage.DeletePoolCmd;
import org.apache.cloudstack.api.command.admin.storage.DeleteSecondaryStagingStoreCmd;
import org.apache.cloudstack.api.command.admin.storage.UpdateStoragePoolCmd;
import org.apache.cloudstack.api.command.admin.storage.SyncStoragePoolCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
@ -90,6 +93,8 @@ import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
import org.apache.cloudstack.storage.command.CheckDataStoreStoragePolicyComplainceCommand;
import org.apache.cloudstack.storage.command.DettachCommand;
import org.apache.cloudstack.storage.command.SyncVolumePathAnswer;
import org.apache.cloudstack.storage.command.SyncVolumePathCommand;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailsDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
@ -104,6 +109,7 @@ import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
@ -119,6 +125,8 @@ import com.cloud.agent.api.StoragePoolInfo;
import com.cloud.agent.api.VolumeStatsEntry;
import com.cloud.agent.api.to.DataTO;
import com.cloud.agent.api.to.DiskTO;
import com.cloud.agent.api.ModifyStoragePoolCommand;
import com.cloud.agent.api.ModifyStoragePoolAnswer;
import com.cloud.agent.manager.Commands;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.query.dao.TemplateJoinDao;
@ -1624,14 +1632,14 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
if (primaryStorage.getStatus() == StoragePoolStatus.PrepareForMaintenance) {
throw new CloudRuntimeException(String.format("There is already a job running for preparation for maintenance of the storage pool %s", primaryStorage.getUuid()));
}
handlePrepareDatastoreCluserMaintenance(lifeCycle, primaryStorageId);
handlePrepareDatastoreClusterMaintenance(lifeCycle, primaryStorageId);
}
lifeCycle.maintain(store);
return (PrimaryDataStoreInfo)_dataStoreMgr.getDataStore(primaryStorage.getId(), DataStoreRole.Primary);
}
private void handlePrepareDatastoreCluserMaintenance(DataStoreLifeCycle lifeCycle, Long primaryStorageId) {
private void handlePrepareDatastoreClusterMaintenance(DataStoreLifeCycle lifeCycle, Long primaryStorageId) {
StoragePoolVO datastoreCluster = _storagePoolDao.findById(primaryStorageId);
datastoreCluster.setStatus(StoragePoolStatus.PrepareForMaintenance);
_storagePoolDao.update(datastoreCluster.getId(), datastoreCluster);
@ -1705,6 +1713,261 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
return (PrimaryDataStoreInfo)_dataStoreMgr.getDataStore(primaryStorage.getId(), DataStoreRole.Primary);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_SYNC_STORAGE_POOL, eventDescription = "synchronising storage pool with management server", async = true)
public StoragePool syncStoragePool(SyncStoragePoolCmd cmd) {
Long poolId = cmd.getPoolId();
StoragePoolVO pool = _storagePoolDao.findById(poolId);
if (pool == null) {
String msg = String.format("Unable to obtain lock on the storage pool record while syncing storage pool [%s] with management server", pool.getUuid());
s_logger.error(msg);
throw new InvalidParameterValueException(msg);
}
if (!pool.getPoolType().equals(StoragePoolType.DatastoreCluster)) {
throw new InvalidParameterValueException("SyncStoragePool API is currently supported only for storage type of datastore cluster");
}
if (!pool.getStatus().equals(StoragePoolStatus.Up)) {
throw new InvalidParameterValueException(String.format("Primary storage with id %s is not ready for syncing, as the status is %s", pool.getUuid(), pool.getStatus().toString()));
}
// find the host
List<Long> poolIds = new ArrayList<>();
poolIds.add(poolId);
List<Long> hosts = _storagePoolHostDao.findHostsConnectedToPools(poolIds);
if (hosts.size() > 0) {
Long hostId = hosts.get(0);
ModifyStoragePoolCommand modifyStoragePoolCommand = new ModifyStoragePoolCommand(true, pool);
final Answer answer = _agentMgr.easySend(hostId, modifyStoragePoolCommand);
if (answer == null) {
throw new CloudRuntimeException(String.format("Unable to get an answer to the modify storage pool command %s", pool.getUuid()));
}
if (!answer.getResult()) {
throw new CloudRuntimeException(String.format("Unable to process ModifyStoragePoolCommand for pool %s on the host %s due to ", pool.getUuid(), hostId, answer.getDetails()));
}
assert (answer instanceof ModifyStoragePoolAnswer) : "Well, now why won't you actually return the ModifyStoragePoolAnswer when it's ModifyStoragePoolCommand? Pool=" +
pool.getId() + "Host=" + hostId;
ModifyStoragePoolAnswer mspAnswer = (ModifyStoragePoolAnswer) answer;
StoragePoolVO poolVO = _storagePoolDao.findById(poolId);
updateStoragePoolHostVOAndBytes(poolVO, hostId, mspAnswer);
validateChildDatastoresToBeAddedInUpState(poolVO, mspAnswer.getDatastoreClusterChildren());
syncDatastoreClusterStoragePool(poolId, mspAnswer.getDatastoreClusterChildren(), hostId);
for (ModifyStoragePoolAnswer childDataStoreAnswer : mspAnswer.getDatastoreClusterChildren()) {
StoragePoolInfo childStoragePoolInfo = childDataStoreAnswer.getPoolInfo();
StoragePoolVO dataStoreVO = _storagePoolDao.findPoolByUUID(childStoragePoolInfo.getUuid());
for (Long host : hosts) {
updateStoragePoolHostVOAndBytes(dataStoreVO, host, childDataStoreAnswer);
}
}
} else {
throw new CloudRuntimeException(String.format("Unable to sync storage pool [%s] as there no connected hosts to the storage pool", pool.getUuid()));
}
return (PrimaryDataStoreInfo) _dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary);
}
public void syncDatastoreClusterStoragePool(long datastoreClusterPoolId, List<ModifyStoragePoolAnswer> childDatastoreAnswerList, long hostId) {
StoragePoolVO datastoreClusterPool = _storagePoolDao.findById(datastoreClusterPoolId);
List<String> storageTags = _storagePoolTagsDao.getStoragePoolTags(datastoreClusterPoolId);
List<StoragePoolVO> childDatastores = _storagePoolDao.listChildStoragePoolsInDatastoreCluster(datastoreClusterPoolId);
Set<String> childDatastoreUUIDs = new HashSet<>();
for (StoragePoolVO childDatastore : childDatastores) {
childDatastoreUUIDs.add(childDatastore.getUuid());
}
for (ModifyStoragePoolAnswer childDataStoreAnswer : childDatastoreAnswerList) {
StoragePoolInfo childStoragePoolInfo = childDataStoreAnswer.getPoolInfo();
StoragePoolVO dataStoreVO = _storagePoolDao.findPoolByUUID(childStoragePoolInfo.getUuid());
if (dataStoreVO == null && childDataStoreAnswer.getPoolType().equalsIgnoreCase("NFS")) {
List<StoragePoolVO> nfsStoragePools = _storagePoolDao.findPoolsByStorageType(StoragePoolType.NetworkFilesystem.toString());
for (StoragePoolVO storagePool : nfsStoragePools) {
String storagePoolUUID = storagePool.getUuid();
if (childStoragePoolInfo.getName().equalsIgnoreCase(storagePoolUUID.replaceAll("-", ""))) {
dataStoreVO = storagePool;
break;
}
}
}
if (dataStoreVO != null) {
if (dataStoreVO.getParent() != datastoreClusterPoolId) {
s_logger.debug(String.format("Storage pool %s with uuid %s is found to be under datastore cluster %s at vCenter, " +
"so moving the storage pool to be a child storage pool under the datastore cluster in CloudStack management server",
childStoragePoolInfo.getName(), childStoragePoolInfo.getUuid(), datastoreClusterPool.getName()));
dataStoreVO.setParent(datastoreClusterPoolId);
_storagePoolDao.update(dataStoreVO.getId(), dataStoreVO);
if (CollectionUtils.isNotEmpty(storageTags)) {
storageTags.addAll(_storagePoolTagsDao.getStoragePoolTags(dataStoreVO.getId()));
} else {
storageTags = _storagePoolTagsDao.getStoragePoolTags(dataStoreVO.getId());
}
if (CollectionUtils.isNotEmpty(storageTags)) {
Set<String> set = new LinkedHashSet<>(storageTags);
storageTags.clear();
storageTags.addAll(set);
if (s_logger.isDebugEnabled()) {
s_logger.debug("Updating Storage Pool Tags to :" + storageTags);
}
_storagePoolTagsDao.persist(dataStoreVO.getId(), storageTags);
}
} else {
// This is to find datastores which are removed from datastore cluster.
// The final set childDatastoreUUIDs contains the UUIDs of child datastores which needs to be removed from datastore cluster
childDatastoreUUIDs.remove(childStoragePoolInfo.getUuid());
}
} else {
dataStoreVO = createChildDatastoreVO(datastoreClusterPool, childDataStoreAnswer);
}
updateStoragePoolHostVOAndBytes(dataStoreVO, hostId, childDataStoreAnswer);
}
handleRemoveChildStoragePoolFromDatastoreCluster(childDatastoreUUIDs);
}
private void validateChildDatastoresToBeAddedInUpState(StoragePoolVO datastoreClusterPool, List<ModifyStoragePoolAnswer> childDatastoreAnswerList) {
for (ModifyStoragePoolAnswer childDataStoreAnswer : childDatastoreAnswerList) {
StoragePoolInfo childStoragePoolInfo = childDataStoreAnswer.getPoolInfo();
StoragePoolVO dataStoreVO = _storagePoolDao.findPoolByUUID(childStoragePoolInfo.getUuid());
if (dataStoreVO == null && childDataStoreAnswer.getPoolType().equalsIgnoreCase("NFS")) {
List<StoragePoolVO> nfsStoragePools = _storagePoolDao.findPoolsByStorageType(StoragePoolType.NetworkFilesystem.toString());
for (StoragePoolVO storagePool : nfsStoragePools) {
String storagePoolUUID = storagePool.getUuid();
if (childStoragePoolInfo.getName().equalsIgnoreCase(storagePoolUUID.replaceAll("-", ""))) {
dataStoreVO = storagePool;
break;
}
}
}
if (dataStoreVO != null && !dataStoreVO.getStatus().equals(StoragePoolStatus.Up)) {
String msg = String.format("Cannot synchronise datastore cluster %s because primary storage with id %s is not ready for syncing, " +
"as the status is %s", datastoreClusterPool.getUuid(), dataStoreVO.getUuid(), dataStoreVO.getStatus().toString());
throw new CloudRuntimeException(msg);
}
}
}
private StoragePoolVO createChildDatastoreVO(StoragePoolVO datastoreClusterPool, ModifyStoragePoolAnswer childDataStoreAnswer) {
StoragePoolInfo childStoragePoolInfo = childDataStoreAnswer.getPoolInfo();
List<String> storageTags = _storagePoolTagsDao.getStoragePoolTags(datastoreClusterPool.getId());
StoragePoolVO dataStoreVO = new StoragePoolVO();
dataStoreVO.setStorageProviderName(datastoreClusterPool.getStorageProviderName());
dataStoreVO.setHostAddress(childStoragePoolInfo.getHost());
dataStoreVO.setPoolType(Storage.StoragePoolType.PreSetup);
dataStoreVO.setPath(childStoragePoolInfo.getHostPath());
dataStoreVO.setPort(datastoreClusterPool.getPort());
dataStoreVO.setName(childStoragePoolInfo.getName());
dataStoreVO.setUuid(childStoragePoolInfo.getUuid());
dataStoreVO.setDataCenterId(datastoreClusterPool.getDataCenterId());
dataStoreVO.setPodId(datastoreClusterPool.getPodId());
dataStoreVO.setClusterId(datastoreClusterPool.getClusterId());
dataStoreVO.setStatus(StoragePoolStatus.Up);
dataStoreVO.setUserInfo(datastoreClusterPool.getUserInfo());
dataStoreVO.setManaged(datastoreClusterPool.isManaged());
dataStoreVO.setCapacityIops(datastoreClusterPool.getCapacityIops());
dataStoreVO.setCapacityBytes(childDataStoreAnswer.getPoolInfo().getCapacityBytes());
dataStoreVO.setUsedBytes(childDataStoreAnswer.getPoolInfo().getCapacityBytes() - childDataStoreAnswer.getPoolInfo().getAvailableBytes());
dataStoreVO.setHypervisor(datastoreClusterPool.getHypervisor());
dataStoreVO.setScope(datastoreClusterPool.getScope());
dataStoreVO.setParent(datastoreClusterPool.getId());
Map<String, String> details = new HashMap<>();
if(org.apache.commons.lang.StringUtils.isNotEmpty(childDataStoreAnswer.getPoolType())) {
details.put("pool_type", childDataStoreAnswer.getPoolType());
}
_storagePoolDao.persist(dataStoreVO, details, storageTags);
return dataStoreVO;
}
private void handleRemoveChildStoragePoolFromDatastoreCluster(Set<String> childDatastoreUUIDs) {
for (String childDatastoreUUID : childDatastoreUUIDs) {
StoragePoolVO dataStoreVO = _storagePoolDao.findPoolByUUID(childDatastoreUUID);
List<VolumeVO> allVolumes = _volumeDao.findByPoolId(dataStoreVO.getId());
allVolumes.removeIf(volumeVO -> volumeVO.getInstanceId() == null);
allVolumes.removeIf(volumeVO -> volumeVO.getState() != Volume.State.Ready);
for (VolumeVO volume : allVolumes) {
VMInstanceVO vmInstance = _vmInstanceDao.findById(volume.getInstanceId());
if (vmInstance == null) {
continue;
}
long volumeId = volume.getId();
Long hostId = vmInstance.getHostId();
if (hostId == null) {
hostId = vmInstance.getLastHostId();
}
HostVO hostVO = _hostDao.findById(hostId);
// Prepare for the syncvolumepath command
DataTO volTO = volFactory.getVolume(volume.getId()).getTO();
DiskTO disk = new DiskTO(volTO, volume.getDeviceId(), volume.getPath(), volume.getVolumeType());
Map<String, String> details = new HashMap<String, String>();
details.put(DiskTO.PROTOCOL_TYPE, Storage.StoragePoolType.DatastoreCluster.toString());
disk.setDetails(details);
s_logger.debug(String.format("Attempting to process SyncVolumePathCommand for the volume %d on the host %d with state %s", volumeId, hostId, hostVO.getResourceState()));
SyncVolumePathCommand cmd = new SyncVolumePathCommand(disk);
final Answer answer = _agentMgr.easySend(hostId, cmd);
// validate answer
if (answer == null) {
throw new CloudRuntimeException("Unable to get an answer to the SyncVolumePath command for volume " + volumeId);
}
if (!answer.getResult()) {
throw new CloudRuntimeException("Unable to process SyncVolumePathCommand for the volume" + volumeId + " to the host " + hostId + " due to " + answer.getDetails());
}
assert (answer instanceof SyncVolumePathAnswer) : "Well, now why won't you actually return the SyncVolumePathAnswer when it's SyncVolumePathCommand? volume=" +
volume.getUuid() + "Host=" + hostId;
// check for the changed details of volume and update database
VolumeVO volumeVO = _volumeDao.findById(volumeId);
String datastoreName = answer.getContextParam("datastoreName");
if (datastoreName != null) {
StoragePoolVO storagePoolVO = _storagePoolDao.findByUuid(datastoreName);
if (storagePoolVO != null) {
volumeVO.setPoolId(storagePoolVO.getId());
} else {
s_logger.warn(String.format("Unable to find datastore %s while updating the new datastore of the volume %d", datastoreName, volumeId));
}
}
String volumePath = answer.getContextParam("volumePath");
if (volumePath != null) {
volumeVO.setPath(volumePath);
}
String chainInfo = answer.getContextParam("chainInfo");
if (chainInfo != null) {
volumeVO.setChainInfo(chainInfo);
}
_volumeDao.update(volumeVO.getId(), volumeVO);
}
dataStoreVO.setParent(0L);
_storagePoolDao.update(dataStoreVO.getId(), dataStoreVO);
}
}
private void updateStoragePoolHostVOAndBytes(StoragePool pool, long hostId, ModifyStoragePoolAnswer mspAnswer) {
StoragePoolHostVO poolHost = _storagePoolHostDao.findByPoolHost(pool.getId(), hostId);
if (poolHost == null) {
poolHost = new StoragePoolHostVO(pool.getId(), hostId, mspAnswer.getPoolInfo().getLocalPath().replaceAll("//", "/"));
_storagePoolHostDao.persist(poolHost);
} else {
poolHost.setLocalPath(mspAnswer.getPoolInfo().getLocalPath().replaceAll("//", "/"));
}
StoragePoolVO poolVO = _storagePoolDao.findById(pool.getId());
poolVO.setUsedBytes(mspAnswer.getPoolInfo().getCapacityBytes() - mspAnswer.getPoolInfo().getAvailableBytes());
poolVO.setCapacityBytes(mspAnswer.getPoolInfo().getCapacityBytes());
_storagePoolDao.update(pool.getId(), poolVO);
}
protected class StorageGarbageCollector extends ManagedContextRunnable {
public StorageGarbageCollector() {

View File

@ -35,6 +35,7 @@ import org.springframework.stereotype.Component;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ModifyStoragePoolCommand;
import com.cloud.agent.api.ModifyStoragePoolAnswer;
import com.cloud.alert.AlertManager;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
@ -100,6 +101,8 @@ public class StoragePoolAutomationImpl implements StoragePoolAutomation {
ManagementServer server;
@Inject
DataStoreProviderManager providerMgr;
@Inject
StorageManager storageManager;
@Override
public boolean maintain(DataStore store) {
@ -162,6 +165,10 @@ public class StoragePoolAutomationImpl implements StoragePoolAutomation {
if (s_logger.isDebugEnabled()) {
s_logger.debug("ModifyStoragePool false succeeded");
}
if (pool.getPoolType() == Storage.StoragePoolType.DatastoreCluster) {
s_logger.debug(String.format("Started synchronising datastore cluster storage pool %s with vCenter", pool.getUuid()));
storageManager.syncDatastoreClusterStoragePool(pool.getId(), ((ModifyStoragePoolAnswer) answer).getDatastoreClusterChildren(), host.getId());
}
}
}
// check to see if other ps exist
@ -323,6 +330,10 @@ public class StoragePoolAutomationImpl implements StoragePoolAutomation {
if (s_logger.isDebugEnabled()) {
s_logger.debug("ModifyStoragePool add succeeded");
}
if (pool.getPoolType() == Storage.StoragePoolType.DatastoreCluster) {
s_logger.debug(String.format("Started synchronising datastore cluster storage pool %s with vCenter", pool.getUuid()));
storageManager.syncDatastoreClusterStoragePool(pool.getId(), ((ModifyStoragePoolAnswer) answer).getDatastoreClusterChildren(), host.getId());
}
}
}

View File

@ -2265,7 +2265,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
VMInstanceVO vmInstance = _vmInstanceDao.findById(vmId);
// only load running vms. For stopped vms get loaded on starting
if (vmInstance.getState() == State.Running) {
if (vmInstance != null && vmInstance.getState() == State.Running) {
VmAndCountDetails vmAndCount = new VmAndCountDetails(vmId, VmIpFetchTrialMax.value());
vmIdCountMap.put(nicId, vmAndCount);
}

View File

@ -95,6 +95,7 @@ known_categories = {
'StorageMaintenance': 'Storage Pool',
'StoragePool': 'Storage Pool',
'StorageProvider': 'Storage Pool',
'syncStoragePool': 'Storage Pool',
'SecurityGroup': 'Security Group',
'SSH': 'SSH',
'register': 'Registration',

View File

@ -52,18 +52,8 @@
:dataSource="detailOptions[newKey]"
:placeholder="$t('label.value')"
@change="e => onAddInputChange(e, 'newValue')" />
<a-tooltip arrowPointAtCenter placement="topRight">
<template slot="title">
{{ $t('label.add.setting') }}
</template>
<a-button icon="check" @click="addDetail" class="detail-button"></a-button>
</a-tooltip>
<a-tooltip arrowPointAtCenter placement="topRight">
<template slot="title">
{{ $t('label.cancel') }}
</template>
<a-button icon="close" @click="closeDetail" class="detail-button"></a-button>
</a-tooltip>
<tooltip-button :tooltip="$t('label.add.setting')" icon="check" @click="addDetail" buttonClass="detail-button" />
<tooltip-button :tooltip="$t('label.cancel')" icon="close" @click="closeDetail" buttonClass="detail-button" />
</a-input-group>
<p v-if="error" style="color: red"> {{ $t(error) }} </p>
</div>
@ -90,14 +80,10 @@
slot="actions"
v-if="!disableSettings && 'updateTemplate' in $store.getters.apis &&
'updateVirtualMachine' in $store.getters.apis && isAdminOrOwner() && allowEditOfDetail(item.name)">
<a-button shape="circle" size="default" @click="updateDetail(index)" v-if="item.edit">
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
</a-button>
<a-button shape="circle" size="default" @click="hideEditDetail(index)" v-if="item.edit">
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
<a-button
shape="circle"
<tootip-button :tooltip="$t('label.cancel')" @click="hideEditDetail(index)" v-if="item.edit" iconType="close-circle" iconTwoToneColor="#f5222d" />
<tootip-button :tooltip="$t('label.ok')" @click="updateDetail(index)" v-if="item.edit" iconType="check-circle" iconTwoToneColor="#52c41a" />
<tooltip-button
:tooltip="$t('label.edit')"
icon="edit"
v-if="!item.edit"
@click="showEditDetail(index)" />
@ -113,7 +99,7 @@
:cancelText="$t('label.no')"
placement="left"
>
<a-button shape="circle" type="danger" icon="delete" />
<tooltip-button :tooltip="$t('label.delete')" type="danger" icon="delete" />
</a-popconfirm>
</div>
</a-list-item>
@ -123,8 +109,10 @@
<script>
import { api } from '@/api'
import TooltipButton from './TooltipButton.vue'
export default {
components: { TooltipButton },
name: 'DetailSettings',
props: {
resource: {

View File

@ -106,19 +106,15 @@
<div class="resource-detail-item" v-if="resource.id">
<div class="resource-detail-item__label">{{ $t('label.id') }}</div>
<div class="resource-detail-item__details">
<a-tooltip placement="right" >
<template slot="title">
<span>{{ $t('label.copyid') }}</span>
</template>
<a-button
style="margin-left: -5px"
shape="circle"
icon="barcode"
type="dashed"
size="small"
@click="$message.success($t('label.copied.clipboard'))"
v-clipboard:copy="resource.id" />
</a-tooltip>
<tooltip-button
tooltipPlacement="right"
:tooltip="$t('label.copyid')"
style="margin-left: -5px"
icon="barcode"
type="dashed"
size="small"
@click="$message.success($t('label.copied.clipboard'))"
v-clipboard:copy="resource.id" />
<span style="margin-left: 10px;">{{ resource.id }}</span>
</div>
</div>
@ -573,14 +569,14 @@
<a-icon type="key" />
<strong>
{{ $t('label.apikey') }}
<a-tooltip placement="right" >
<template slot="title">
<span>{{ $t('label.copy') + ' ' + $t('label.apikey') }}</span>
</template>
<a-button shape="circle" type="dashed" size="small" @click="$message.success($t('label.copied.clipboard'))" v-clipboard:copy="resource.apikey">
<a-icon type="copy"/>
</a-button>
</a-tooltip>
<tooltip-button
tooltipPlacement="right"
:tooltip="$t('label.copy') + ' ' + $t('label.apikey')"
icon="copy"
type="dashed"
size="small"
@click="$message.success($t('label.copied.clipboard'))"
v-clipboard:copy="resource.apikey" />
</strong>
<div>
{{ resource.apikey.substring(0, 20) }}...
@ -590,14 +586,14 @@
<a-icon type="lock" />
<strong>
{{ $t('label.secretkey') }}
<a-tooltip placement="right" >
<template slot="title">
<span>{{ $t('label.copy') + ' ' + $t('label.secretkey') }}</span>
</template>
<a-button shape="circle" type="dashed" size="small" @click="$message.success($t('label.copied.clipboard'))" v-clipboard:copy="resource.secretkey">
<a-icon type="copy"/>
</a-button>
</a-tooltip>
<tooltip-button
tooltipPlacement="right"
:tooltip="$t('label.copy') + ' ' + $t('label.secretkey')"
icon="copy"
type="dashed"
size="small"
@click="$message.success($t('label.copied.clipboard'))"
v-clipboard:copy="resource.secretkey" />
</strong>
<div>
{{ resource.secretkey.substring(0, 20) }}...
@ -626,12 +622,8 @@
<a-input ref="input" :value="inputKey" @change="handleKeyChange" style="width: 30%; text-align: center" :placeholder="$t('label.key')" />
<a-input style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
<a-input :value="inputValue" @change="handleValueChange" style="width: 30%; text-align: center; border-left: 0" :placeholder="$t('label.value')" />
<a-button shape="circle" size="small" @click="handleInputConfirm">
<a-icon type="check"/>
</a-button>
<a-button shape="circle" size="small" @click="inputVisible=false">
<a-icon type="close"/>
</a-button>
<tooltip-button :tooltip="$t('label.ok')" icon="check" size="small" @click="handleInputConfirm" />
<tooltip-button :tooltip="$t('label.cancel')" icon="close" size="small" @click="inputVisible=false" />
</a-input-group>
</div>
<a-tag @click="showInput" style="background: #fff; borderStyle: dashed;" v-else-if="isAdminOrOwner() && 'createTags' in $store.getters.apis">
@ -701,13 +693,15 @@ import { api } from '@/api'
import Console from '@/components/widgets/Console'
import OsLogo from '@/components/widgets/OsLogo'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'InfoCard',
components: {
Console,
OsLogo,
Status
Status,
TooltipButton
},
props: {
resource: {

View File

@ -70,7 +70,7 @@
:enabled="quickViewEnabled() && actions.length > 0 && columns && columns[0].dataIndex === 'name' "
@exec-action="$parent.execAction"/>
<span v-if="$route.path.startsWith('/project')" style="margin-right: 5px">
<a-button type="dashed" size="small" shape="circle" icon="login" @click="changeProject(record)" />
<tooltip-button type="dashed" size="small" icon="login" @click="changeProject(record)" />
</span>
<os-logo v-if="record.ostypename" :osName="record.ostypename" size="1x" style="margin-right: 5px" />
@ -287,30 +287,29 @@
</div>
</template>
<template slot="actions" slot-scope="text, record">
<a-button
shape="circle"
<tooltip-button
:tooltip="$t('label.edit')"
:disabled="!('updateConfiguration' in $store.getters.apis)"
v-if="editableValueKey !== record.key"
icon="edit"
@click="editValue(record)" />
<a-button
shape="circle"
<tooltip-button
:tooltip="$t('label.cancel')"
@click="editableValueKey = null"
v-if="editableValueKey === record.key"
iconType="close-circle"
iconTwoToneColor="#f5222d" />
<tooltip-button
:tooltip="$t('label.ok')"
:disabled="!('updateConfiguration' in $store.getters.apis)"
@click="saveValue(record)"
v-if="editableValueKey === record.key" >
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
</a-button>
<a-button
shape="circle"
size="default"
@click="editableValueKey = null"
v-if="editableValueKey === record.key" >
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
v-if="editableValueKey === record.key"
iconType="check-circle"
iconTwoToneColor="#52c41a" />
</template>
<template slot="tariffActions" slot-scope="text, record">
<a-button
shape="circle"
<tooltip-button
:tooltip="$t('label.edit')"
v-if="editableValueKey !== record.key"
:disabled="!('quotaTariffUpdate' in $store.getters.apis)"
icon="edit"
@ -327,6 +326,7 @@ import OsLogo from '@/components/widgets/OsLogo'
import Status from '@/components/widgets/Status'
import InfoCard from '@/components/view/InfoCard'
import QuickView from '@/components/view/QuickView'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'ListView',
@ -335,7 +335,8 @@ export default {
OsLogo,
Status,
InfoCard,
QuickView
QuickView,
TooltipButton
},
props: {
columns: {

View File

@ -76,9 +76,7 @@
<a-input ref="input" :value="inputKey" @change="e => inputKey = e.target.value" style="width: 50px; text-align: center" :placeholder="$t('label.key')" />
<a-input style=" width: 20px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
<a-input :value="inputValue" @change="handleValueChange" style="width: 50px; text-align: center; border-left: 0" :placeholder="$t('label.value')" />
<a-button shape="circle" size="small" @click="inputKey = inputValue = ''">
<a-icon type="close"/>
</a-button>
<tooltip-button icon="close" size="small" @click="inputKey = inputValue = ''" />
</a-input-group>
</div>
</div>

View File

@ -46,25 +46,24 @@
</div>
<div slot="actions" class="action">
<a-button
shape="circle"
<tooltip-button
:tooltip="$t('label.edit')"
:disabled="!('updateConfiguration' in $store.getters.apis)"
v-if="editableValueKey !== index"
icon="edit"
@click="setEditableSetting(item, index)" />
<a-button
shape="circle"
size="default"
<tooltip-button
:tooltip="$t('label.cancel')"
@click="editableValueKey = null"
v-if="editableValueKey === index" >
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
<a-button
shape="circle"
v-if="editableValueKey === index"
iconType="close-circle"
iconTwoToneColor="#f5222d" />
<tooltip-button
:tooltip="$t('label.ok')"
@click="updateData(item)"
v-if="editableValueKey === index" >
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
</a-button>
v-if="editableValueKey === index"
iconType="check-circle"
iconTwoToneColor="#52c41a" />
</div>
</a-list-item>
</a-list>
@ -73,8 +72,12 @@
<script>
import { api } from '@/api'
import TooltipButton from './TooltipButton.vue'
export default {
components: {
TooltipButton
},
name: 'SettingsTab',
props: {
resource: {

View File

@ -0,0 +1,100 @@
// 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.
<template>
<a-tooltip arrowPointAtCenter :placement="tooltipPlacement">
<template slot="title" v-if="tooltip">
{{ tooltip }}
</template>
<a-button
shape="circle"
:size="size"
:type="type"
:disabled="disabled"
:icon="icon"
:class="buttonClass"
:loading="loading"
@click="handleClicked()" >
<a-icon
v-if="iconType && iconTwoToneColor"
:type="iconType"
theme="twoTone"
:twoToneColor="iconTwoToneColor" />
</a-button>
</a-tooltip>
</template>
<script>
export default {
name: 'TooltipButton',
props: {
tooltip: {
type: String,
default: null
},
tooltipPlacement: {
type: String,
default: 'bottomRight'
},
disabled: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'default'
},
size: {
type: String,
default: 'default'
},
icon: {
type: String,
default: null
},
iconType: {
type: String,
default: null
},
iconTwoToneColor: {
type: String,
default: null
},
buttonClass: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
}
},
methods: {
handleClicked () {
this.$emit('click')
}
}
}
</script>
<style scoped lang="scss">
</style>

View File

@ -67,61 +67,47 @@
</a-button>
<NicsTable :resource="vm" :loading="loading">
<span slot="actions" slot-scope="record">
<a-tooltip placement="bottom">
<template slot="title">
{{ $t('label.set.default.nic') }}
</template>
<a-popconfirm
:title="$t('label.set.default.nic')"
@confirm="setAsDefault(record.nic)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
v-if="!record.nic.isdefault"
>
<a-button
:disabled="!('updateDefaultNicForVirtualMachine' in $store.getters.apis)"
icon="check-square"
shape="circle" />
</a-popconfirm>
</a-tooltip>
<a-tooltip placement="bottom" v-if="record.nic.type !== 'L2'">
<template slot="title">
{{ $t('label.change.ip.addess') }}
</template>
<a-button
icon="swap"
shape="circle"
:disabled="!('updateVmNicIp' in $store.getters.apis)"
@click="onChangeIPAddress(record)" />
</a-tooltip>
<a-tooltip placement="bottom" v-if="record.nic.type !== 'L2'">
<template slot="title">
{{ $t('label.edit.secondary.ips') }}
</template>
<a-button
icon="environment"
shape="circle"
:disabled="(!('addIpToNic' in $store.getters.apis) && !('addIpToNic' in $store.getters.apis))"
@click="onAcquireSecondaryIPAddress(record)" />
</a-tooltip>
<a-tooltip placement="bottom">
<template slot="title">
{{ $t('label.action.delete.nic') }}
</template>
<a-popconfirm
:title="$t('message.network.removenic')"
@confirm="removeNIC(record.nic)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
v-if="!record.nic.isdefault"
>
<a-button
:disabled="!('removeNicFromVirtualMachine' in $store.getters.apis)"
type="danger"
icon="delete"
shape="circle" />
</a-popconfirm>
</a-tooltip>
<a-popconfirm
:title="$t('label.set.default.nic')"
@confirm="setAsDefault(record.nic)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
v-if="!record.nic.isdefault"
>
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.set.default.nic')"
:disabled="!('updateDefaultNicForVirtualMachine' in $store.getters.apis)"
icon="check-square" />
</a-popconfirm>
<tooltip-button
v-if="record.nic.type !== 'L2'"
tooltipPlacement="bottom"
:tooltip="$t('label.change.ip.addess')"
icon="swap"
:disabled="!('updateVmNicIp' in $store.getters.apis)"
@click="onChangeIPAddress(record)" />
<tooltip-button
v-if="record.nic.type !== 'L2'"
tooltipPlacement="bottom"
:tooltip="$t('label.edit.secondary.ips')"
icon="environment"
:disabled="(!('addIpToNic' in $store.getters.apis) && !('addIpToNic' in $store.getters.apis))"
@click="onAcquireSecondaryIPAddress(record)" />
<a-popconfirm
:title="$t('message.network.removenic')"
@confirm="removeNIC(record.nic)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
v-if="!record.nic.isdefault"
>
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.action.delete.nic')"
:disabled="!('removeNicFromVirtualMachine' in $store.getters.apis)"
type="danger"
icon="delete" />
</a-popconfirm>
</span>
</NicsTable>
</a-tab-pane>
@ -251,9 +237,10 @@
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
>
<a-button
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.action.release.ip')"
type="danger"
shape="circle"
icon="delete" />
{{ ip.ipaddress }}
</a-popconfirm>
@ -274,6 +261,7 @@ import DetailsTab from '@/components/view/DetailsTab'
import DetailSettings from '@/components/view/DetailSettings'
import NicsTable from '@/views/network/NicsTable'
import ListResourceTable from '@/components/view/ListResourceTable'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'InstanceTab',
@ -283,7 +271,8 @@ export default {
DetailSettings,
NicsTable,
Status,
ListResourceTable
ListResourceTable,
TooltipButton
},
mixins: [mixinDevice],
props: {

View File

@ -58,18 +58,14 @@
<label>{{ getTimeZone(record.timezone) }}</label>
</div>
<div slot="action" class="account-button-action" slot-scope="text, record">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.delete') }}
</template>
<a-button
type="danger"
shape="circle"
icon="close"
size="small"
:loading="actionLoading"
@click="handleClickDelete(record)"/>
</a-tooltip>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.delete')"
type="danger"
icon="close"
size="small"
:loading="actionLoading"
@click="handleClickDelete(record)"/>
</div>
</a-table>
</div>
@ -78,9 +74,13 @@
<script>
import { api } from '@/api'
import { timeZoneName } from '@/utils/timezone'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'BackupSchedule',
components: {
TooltipButton
},
props: {
loading: {
type: Boolean,

View File

@ -51,20 +51,13 @@
<a-input v-model="newRuleDescription" :placeholder="$t('label.description')"></a-input>
</div>
<div class="rules-table__col rules-table__col--actions">
<a-tooltip
placement="bottom">
<template slot="title">
{{ $t('label.save.new.rule') }}
</template>
<a-button
:disabled="!('createRolePermission' in $store.getters.apis)"
icon="plus"
type="primary"
shape="circle"
@click="onRuleSave"
>
</a-button>
</a-tooltip>
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.save.new.rule')"
:disabled="!('createRolePermission' in $store.getters.apis)"
icon="plus"
type="primary"
@click="onRuleSave" />
</div>
</div>
@ -117,13 +110,15 @@ import { api } from '@/api'
import draggable from 'vuedraggable'
import PermissionEditable from './PermissionEditable'
import RuleDelete from './RuleDelete'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'RolePermissionTab',
components: {
RuleDelete,
PermissionEditable,
draggable
draggable,
TooltipButton
},
props: {
resource: {

View File

@ -16,24 +16,22 @@
// under the License.
<template>
<a-tooltip placement="bottom">
<template slot="title">
{{ $t('label.delete.rule') }}
</template>
<a-popconfirm
:title="`${$t('label.delete.rule')}?`"
@confirm="handleDelete"
:disabled="disabled">
<a-button type="danger" shape="circle" :disabled="disabled">
<a-icon type="delete" />
</a-button>
</a-popconfirm>
</a-tooltip>
<a-popconfirm
:title="`${$t('label.delete.rule')}?`"
@confirm="handleDelete"
:disabled="disabled">
<tooltip-button :tooltip="$t('label.delete.rule')" tooltipPlacement="bottom" type="danger" icon="delete" :disabled="disabled" />
</a-popconfirm>
</template>
<script>
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'RuleDelete',
components: {
TooltipButton
},
props: {
record: {
type: Object,

View File

@ -29,24 +29,21 @@
v-if="!quickview"
>
<span slot="action" slot-scope="text, record" class="cert-button-action">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.quickview') }}
</template>
<a-button type="primary" shape="circle" icon="eye" size="small" @click="onQuickView(record.id)" />
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.delete.sslcertificate') }}
</template>
<a-button
:disabled="!('deleteSslCert' in $store.getters.apis)"
type="danger"
shape="circle"
icon="delete"
size="small"
@click="onShowConfirm(record)"/>
</a-tooltip>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.quickview')"
type="primary"
icon="eye"
size="small"
@click="onQuickView(record.id)" />
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.delete.sslcertificate')"
:disabled="!('deleteSslCert' in $store.getters.apis)"
type="danger"
icon="delete"
size="small"
@click="onShowConfirm(record)" />
</span>
</a-table>
@ -71,9 +68,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'SSLCertificate',
components: {
TooltipButton
},
data () {
return {
columns: [],

View File

@ -31,10 +31,10 @@
</div>
<template slot="action" slot-scope="text, record">
<span style="margin-right: 5px">
<a-button
<tooltip-button
:tooltip="$t('label.action.copy.iso')"
:disabled="!('copyIso' in $store.getters.apis && record.isready)"
icon="copy"
shape="circle"
:loading="copyLoading"
@click="showCopyIso(record)" />
</span>
@ -48,10 +48,10 @@
:loading="deleteLoading"
@confirm="deleteIso(record)"
>
<a-button
<tooltip-button
:tooltip="$t('label.action.delete.iso')"
type="danger"
icon="delete"
shape="circle" />
icon="delete" />
</a-popconfirm>
</span>
</template>
@ -123,9 +123,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'IsoZones',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -30,22 +30,20 @@
<span v-else>{{ $t('label.no') }}</span>
</div>
<template slot="action" slot-scope="text, record">
<span style="margin-right: 5px">
<a-button
:disabled="!('copyTemplate' in $store.getters.apis && record.isready)"
icon="copy"
shape="circle"
:loading="copyLoading"
@click="showCopyTemplate(record)" />
</span>
<span style="margin-right: 5px">
<a-button
:disabled="!('deleteTemplate' in $store.getters.apis)"
type="danger"
icon="delete"
shape="circle"
@click="onShowDeleteModal(record)"/>
</span>
<tooltip-button
style="margin-right: 5px"
:disabled="!('copyTemplate' in $store.getters.apis && record.isready)"
:title="$t('label.action.copy.template')"
icon="copy"
:loading="copyLoading"
@click="showCopyTemplate(record)" />
<tooltip-button
style="margin-right: 5px"
:disabled="!('deleteTemplate' in $store.getters.apis)"
:title="$t('label.action.delete.template')"
type="danger"
icon="delete"
@click="onShowDeleteModal(record)"/>
</template>
</a-table>
<a-pagination
@ -134,9 +132,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'TemplateZones',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -39,7 +39,7 @@
:cancelText="$t('label.no')"
placement="top"
>
<a-button :disabled="!('releaseDedicatedGuestVlanRange' in $store.getters.apis)" icon="delete" type="danger" shape="circle"></a-button>
<tooltip-button :tooltip="$t('label.delete')" :disabled="!('releaseDedicatedGuestVlanRange' in $store.getters.apis)" icon="delete" type="danger" />
</a-popconfirm>
</template>
</a-table>
@ -134,9 +134,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'DedicatedVLANTab',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -39,15 +39,13 @@
</template>
<template slot="actions" slot-scope="record">
<div class="actions">
<a-popover placement="bottom">
<template slot="content">{{ $t('label.remove.ip.range') }}</template>
<a-button
:disabled="!('deleteManagementNetworkIpRange' in $store.getters.apis)"
icon="delete"
shape="circle"
type="danger"
@click="handleDeleteIpRange(record)"></a-button>
</a-popover>
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.remove.ip.range')"
:disabled="!('deleteManagementNetworkIpRange' in $store.getters.apis)"
icon="delete"
type="danger"
@click="handleDeleteIpRange(record)" />
</div>
</template>
</a-table>
@ -125,9 +123,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'IpRangesTabManagement',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -39,34 +39,28 @@
</template>
<template slot="actions" slot-scope="record">
<div class="actions">
<a-popover v-if="record.account === 'system'" placement="bottom">
<template slot="content">{{ $t('label.add.account') }}</template>
<a-button
icon="user-add"
shape="circle"
@click="() => handleOpenAddAccountModal(record)"
:disabled="!('dedicatePublicIpRange' in $store.getters.apis)"></a-button>
</a-popover>
<a-popover
<tooltip-button
v-if="record.account === 'system'"
tooltipPlacement="bottom"
:tooltip="$t('label.add.account')"
icon="user-add"
@click="() => handleOpenAddAccountModal(record)"
:disabled="!('dedicatePublicIpRange' in $store.getters.apis)" />
<tooltip-button
v-else
placement="bottom">
<template slot="content">{{ $t('label.release.account') }}</template>
<a-button
icon="user-delete"
shape="circle"
type="danger"
@click="() => handleRemoveAccount(record.id)"
:disabled="!('releasePublicIpRange' in $store.getters.apis)"></a-button>
</a-popover>
<a-popover placement="bottom">
<template slot="content">{{ $t('label.remove.ip.range') }}</template>
<a-button
icon="delete"
shape="circle"
type="danger"
@click="handleDeleteIpRange(record.id)"
:disabled="!('deleteVlanIpRange' in $store.getters.apis)"></a-button>
</a-popover>
tooltipPlacement="bottom"
:tooltip="$t('label.release.account')"
icon="user-delete"
type="danger"
@click="() => handleRemoveAccount(record.id)"
:disabled="!('releasePublicIpRange' in $store.getters.apis)" />
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.remove.ip.range')"
icon="delete"
type="danger"
@click="handleDeleteIpRange(record.id)"
:disabled="!('deleteVlanIpRange' in $store.getters.apis)" />
</div>
</template>
</a-table>
@ -201,9 +195,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'IpRangesTabPublic',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -38,15 +38,12 @@
<div>{{ returnPodName(record.podid) }}</div>
</template>
<template slot="actions" slot-scope="record">
<a-popover placement="bottom">
<template slot="content">{{ $t('label.remove.ip.range') }}</template>
<a-button
:disabled="!('deleteStorageNetworkIpRange' in $store.getters.apis)"
icon="delete"
shape="circle"
type="danger"
@click="handleDeleteIpRange(record.id)"></a-button>
</a-popover>
<tooltip-button
:tooltip="$t('label.remove.ip.range')"
:disabled="!('deleteStorageNetworkIpRange' in $store.getters.apis)"
icon="delete"
type="danger"
@click="handleDeleteIpRange(record.id)" />
</template>
</a-table>
<a-pagination
@ -120,9 +117,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'IpRangesTabStorage',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -67,18 +67,17 @@
{{ $t('label.delete.ciscoasa1000v') }}
</span>
</template>
<a-button
<tooltip-button
v-if="resource.name==='Ovs'"
type="default"
shape="circle"
:tooltip="$t('label.configure')"
icon="setting"
size="small"
:loading="actionLoading"
@click="onConfigureOvs(record)"/>
<a-button
<tooltip-button
v-else
:tooltip="$t('label.delete')"
type="danger"
shape="circle"
icon="close"
size="small"
:loading="actionLoading"
@ -117,10 +116,11 @@
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'ProviderListView',
components: { Status },
components: { Status, TooltipButton },
props: {
title: {
type: String,

View File

@ -30,7 +30,7 @@
:pagination="false"
style="margin-bottom: 24px; width: 100%" >
<template slot="actions" slot-scope="text, record">
<a-button type="danger" shape="circle" icon="delete" @click="onDelete(record.key)" />
<tooltip-button :tooltip="$t('label.delete')" type="danger" icon="delete" @click="onDelete(record.key)" />
</template>
<template slot="footer">
<a-form
@ -141,9 +141,14 @@
</div>
</template>
<script>
import TooltipButton from '@/components/view/TooltipButton'
import { mixinDevice } from '@/utils/mixin.js'
export default {
components: {
TooltipButton
},
mixins: [mixinDevice],
props: {
traffic: {

View File

@ -76,16 +76,16 @@
{{ traffic.toUpperCase() }}
</a-select-option>
</a-select>
<a-button
class="icon-button"
shape="circle"
<tooltip-button
:tooltip="$t('label.add')"
buttonClass="icon-button"
icon="plus"
size="small"
@click="trafficAdded" />
<a-button
class="icon-button"
<tooltip-button
:tooltip="$t('label.cancel')"
buttonClass="icon-button"
type="danger"
shape="circle"
icon="close"
size="small"
@click="() => { addingTrafficForKey = null }" />
@ -102,7 +102,7 @@
</div>
</template>
<template slot="actions" slot-scope="text, record">
<a-button v-if="physicalNetworks.indexOf(record) > 0" type="danger" shape="circle" icon="delete" @click="onDelete(record)" />
<tooltip-button :tooltip="$t('label.delete')" v-if="physicalNetworks.indexOf(record) > 0" type="danger" icon="delete" @click="onDelete(record)" />
</template>
<template slot="footer" v-if="isAdvancedZone">
<a-button
@ -166,7 +166,12 @@
</template>
<script>
import TooltipButton from '@/components/view/TooltipButton'
export default {
components: {
TooltipButton
},
props: {
prefillContent: {
type: Object,

View File

@ -87,9 +87,9 @@
</div>
</div>
<div class="list__actions">
<a-button shape="circle" icon="tag" @click="() => openTagsModal(acl)"></a-button>
<a-button shape="circle" icon="edit" @click="() => openEditRuleModal(acl)"></a-button>
<a-button shape="circle" icon="delete" type="danger" :disabled="!('deleteNetworkACL' in $store.getters.apis)" @click="() => handleDeleteRule(acl.id)"></a-button>
<tooltip-button :tooltip="$t('label.tags')" icon="tag" @click="() => openTagsModal(acl)" />
<tooltip-button :tooltip="$t('label.edit')" icon="edit" @click="() => openEditRuleModal(acl)" />
<tooltip-button :tooltip="$t('label.delete')" icon="delete" type="danger" :disabled="!('deleteNetworkACL' in $store.getters.apis)" @click="() => handleDeleteRule(acl.id)" />
</div>
</div>
</transition-group>
@ -198,11 +198,13 @@
<script>
import { api } from '@/api'
import draggable from 'vuedraggable'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'AclListRulesTab',
components: {
draggable
draggable,
TooltipButton
},
props: {
resource: {

View File

@ -78,7 +78,7 @@
{{ record.icmpcode || record.endport >= 0 ? record.icmpcode || record.endport : 'All' }}
</template>
<template slot="actions" slot-scope="record">
<a-button :disabled="!('deleteEgressFirewallRule' in $store.getters.apis)" shape="circle" type="danger" icon="delete" @click="deleteRule(record)" />
<tooltip-button :tooltip="$t('label.delete')" :disabled="!('deleteEgressFirewallRule' in $store.getters.apis)" type="danger" icon="delete" @click="deleteRule(record)" />
</template>
</a-table>
<a-pagination
@ -102,9 +102,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'EgressRulesTab',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -74,12 +74,12 @@
</template>
<template slot="actions" slot-scope="record">
<div class="actions">
<a-button shape="circle" icon="tag" class="rule-action" @click="() => openTagsModal(record.id)" />
<a-button
shape="circle"
<tooltip-button :tooltip="$t('label.edit.tags')" icon="tag" buttonClass="rule-action" @click="() => openTagsModal(record.id)" />
<tooltip-button
:tooltip="$t('label.delete')"
type="danger"
icon="delete"
class="rule-action"
buttonClass="rule-action"
:disabled="!('deleteFirewallRule' in $store.getters.apis)"
@click="deleteRule(record)" />
</div>
@ -137,8 +137,12 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -37,21 +37,17 @@
:pagination="false" >
<template slot="action" slot-scope="text, record">
<a-tooltip placement="bottom">
<template slot="title">
{{ $t('label.action.delete.ip.range') }}
</template>
<a-popconfirm
:title="$t('message.confirm.remove.ip.range')"
@confirm="removeIpRange(record.id)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')" >
<a-button
type="danger"
icon="delete"
shape="circle" />
</a-popconfirm>
</a-tooltip>
<a-popconfirm
:title="$t('message.confirm.remove.ip.range')"
@confirm="removeIpRange(record.id)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')" >
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.action.delete.ip.range')"
type="danger"
icon="delete" />
</a-popconfirm>
</template>
</a-table>
@ -92,10 +88,12 @@
<script>
import { api } from '@/api'
import CreateVlanIpRange from '@/views/network/CreateVlanIpRange'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'GuestIpRanges',
components: {
CreateVlanIpRange
CreateVlanIpRange,
TooltipButton
},
props: {
resource: {

View File

@ -106,14 +106,19 @@
<div v-else>{{ text }}</div>
</template>
<template slot="actions" slot-scope="record">
<a-button shape="circle" icon="tag" class="rule-action" @click="() => openTagsModal(record)" />
<tooltip-button :tooltip="$t('label.edit.tags')" icon="tag" buttonClass="rule-action" @click="() => openTagsModal(record)" />
<a-popconfirm
:title="$t('label.delete') + '?'"
@confirm="handleDeleteRule(record)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
>
<a-button :disabled="!('revokeSecurityGroupIngress' in $store.getters.apis) && !('revokeSecurityGroupEgress' in $store.getters.apis)" shape="circle" type="danger" icon="delete" class="rule-action" />
<tooltip-button
:disabled="!('revokeSecurityGroupIngress' in $store.getters.apis) && !('revokeSecurityGroupEgress' in $store.getters.apis)"
:tooltip="$t('label.delete')"
type="danger"
icon="delete"
buttonClass="rule-action" />
</a-popconfirm>
</template>
</a-table>
@ -165,8 +170,12 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -36,10 +36,10 @@
</span>
</template>
<template slot="remove" slot-scope="text, record">
<a-button
<tooltip-button
:tooltip="$t('label.remove.vm.from.lb')"
type="danger"
icon="delete"
shape="circle"
@click="removeVmFromLB(record)" />
</template>
<a-divider />
@ -63,9 +63,13 @@
</template>
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'InternalLBAssignedVmTab',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -72,11 +72,11 @@
</template>
<template slot="action" slot-scope="text, record">
<a-button
<tooltip-button
v-if="record.issourcenat !== true && record.forvirtualnetwork === true"
:tooltip="$t('label.action.release.ip')"
type="danger"
icon="delete"
shape="circle"
:disabled="!('disassociateIpAddress' in $store.getters.apis)"
@click="releaseIpAddress(record)" />
</template>
@ -129,11 +129,13 @@
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'IpAddressesTab',
components: {
Status
Status,
TooltipButton
},
props: {
resource: {

View File

@ -99,8 +99,8 @@
</router-link>
</div>
<div>{{ ip }}</div>
<a-button
shape="circle"
<tooltip-button
:tooltip="$t('label.action.delete.load.balancer')"
type="danger"
icon="delete"
@click="() => handleDeleteInstanceFromRule(instance, record, ip)" />
@ -110,15 +110,15 @@
</template>
<template slot="actions" slot-scope="record">
<div class="actions">
<a-button shape="circle" icon="edit" @click="() => openEditRuleModal(record)"></a-button>
<a-button :disabled="!('updateLoadBalancerRule' in $store.getters.apis)" shape="circle" icon="tag" @click="() => openTagsModal(record.id)" />
<tooltip-button :tooltip="$t('label.edit')" icon="edit" @click="() => openEditRuleModal(record)" />
<tooltip-button :tooltip="$t('label.edit.tags')" :disabled="!('updateLoadBalancerRule' in $store.getters.apis)" icon="tag" @click="() => openTagsModal(record.id)" />
<a-popconfirm
:title="$t('label.delete') + '?'"
@confirm="handleDeleteRule(record)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
>
<a-button :disabled="!('deleteLoadBalancerRule' in $store.getters.apis)" shape="circle" type="danger" icon="delete" />
<tooltip-button :tooltip="$t('label.delete')" :disabled="!('deleteLoadBalancerRule' in $store.getters.apis)" type="danger" icon="delete" />
</a-popconfirm>
</div>
</template>
@ -384,11 +384,13 @@
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'LoadBalancing',
components: {
Status
Status,
TooltipButton
},
props: {
resource: {

View File

@ -97,12 +97,12 @@
</template>
<template slot="actions" slot-scope="record">
<div class="actions">
<a-button shape="circle" icon="tag" class="rule-action" @click="() => openTagsModal(record.id)" />
<a-button
shape="circle"
<tooltip-button :tooltip="$t('label.tags')" icon="tag" buttonClass="rule-action" @click="() => openTagsModal(record.id)" />
<tooltip-button
:tooltip="$t('label.remove.rule')"
type="danger"
icon="delete"
class="rule-action"
buttonClass="rule-action"
:disabled="!('deletePortForwardingRule' in $store.getters.apis)"
@click="deleteRule(record)" />
</div>
@ -252,10 +252,12 @@
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/view/TooltipButton'
export default {
components: {
Status
Status,
TooltipButton
},
props: {
resource: {

View File

@ -29,8 +29,8 @@
<div>{{ route.cidr }}</div>
</div>
<div class="actions">
<a-button shape="circle" icon="tag" @click="() => openTagsModal(route)"></a-button>
<a-button :disabled="!('deleteStaticRoute' in $store.getters.apis)" shape="circle" icon="delete" type="danger" @click="() => handleDelete(route)"></a-button>
<tooltip-button :tooltip="$t('label.edit.tags')" icon="tag" @click="() => openTagsModal(route)" />
<tooltip-button :tooltip="$t('label.delete')" :disabled="!('deleteStaticRoute' in $store.getters.apis)" icon="delete" type="danger" @click="() => handleDelete(route)" />
</div>
</div>
</div>
@ -76,9 +76,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'StaticRoutesTab',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -33,43 +33,30 @@
{{ getProjectRole(record) }}
</span>
<span v-if="imProjectAdmin && dataSource.length > 1" slot="action" slot-scope="text, record" class="account-button-action">
<a-tooltip
slot="title"
placement="top"
:title="record.userid ? $t('label.make.user.project.owner') : $t('label.make.project.owner')">
<a-button
v-if="record.role !== owner"
type="default"
shape="circle"
icon="arrow-up"
size="small"
@click="promoteAccount(record)" />
</a-tooltip>
<a-tooltip
slot="title"
placement="top"
:title="record.userid ? $t('label.demote.project.owner.user') : $t('label.demote.project.owner')"
v-if="updateProjectApi.params.filter(x => x.name === 'swapowner').length > 0">
<a-button
v-if="record.role === owner"
type="default"
shape="circle"
icon="arrow-down"
size="small"
@click="demoteAccount(record)" />
</a-tooltip>
<a-tooltip
slot="title"
placement="top"
:title="record.userid ? $t('label.remove.project.user') : $t('label.remove.project.account')">
<a-button
type="danger"
shape="circle"
icon="delete"
size="small"
:disabled="!('deleteAccountFromProject' in $store.getters.apis)"
@click="onShowConfirmDelete(record)"/>
</a-tooltip>
<tooltip-button
tooltipPlacement="top"
:tooltip="record.userid ? $t('label.make.user.project.owner') : $t('label.make.project.owner')"
v-if="record.role !== owner"
type="default"
icon="arrow-up"
size="small"
@click="promoteAccount(record)" />
<tooltip-button
tooltipPlacement="top"
:tooltip="record.userid ? $t('label.demote.project.owner.user') : $t('label.demote.project.owner')"
v-if="updateProjectApi.params.filter(x => x.name === 'swapowner').length > 0 && record.role === owner"
type="default"
icon="arrow-down"
size="small"
@click="demoteAccount(record)" />
<tooltip-button
tooltipPlacement="top"
:tooltip="record.userid ? $t('label.remove.project.user') : $t('label.remove.project.account')"
type="danger"
icon="delete"
size="small"
:disabled="!('deleteAccountFromProject' in $store.getters.apis)"
@click="onShowConfirmDelete(record)" />
</span>
</a-table>
<a-pagination
@ -94,9 +81,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'AccountsTab',
components: {
TooltipButton
},
props: {
resource: {
type: Object,

View File

@ -40,28 +40,20 @@
<status :text="text ? text : ''" displayText />
</template>
<span slot="action" v-if="record.state===stateAllow" slot-scope="text, record" class="account-button-action">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.accept.project.invitation') }}
</template>
<a-button
type="success"
shape="circle"
icon="check"
size="small"
@click="onShowConfirmAcceptInvitation(record)"/>
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.decline.invitation') }}
</template>
<a-button
type="danger"
shape="circle"
icon="close"
size="small"
@click="onShowConfirmRevokeInvitation(record)"/>
</a-tooltip>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.accept.project.invitation')"
type="success"
icon="check"
size="small"
@click="onShowConfirmAcceptInvitation(record)"/>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.decline.invitation')"
type="danger"
icon="close"
size="small"
@click="onShowConfirmRevokeInvitation(record)"/>
</span>
</a-table>
<a-pagination
@ -87,11 +79,13 @@
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'InvitationsTemplate',
components: {
Status
Status,
TooltipButton
},
data () {
return {

View File

@ -46,19 +46,12 @@
<a-input v-model="newRuleDescription" placeholder="Description"></a-input>
</div>
<div class="rules-table__col rules-table__col--actions">
<a-tooltip
placement="bottom">
<template slot="title">
Save new Rule
</template>
<a-button
icon="plus"
type="primary"
shape="circle"
@click="onRuleSave"
>
</a-button>
</a-tooltip>
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.save.new.rule')"
icon="plus"
type="primary"
@click="onRuleSave" />
</div>
</div>
@ -109,13 +102,15 @@ import { api } from '@/api'
import draggable from 'vuedraggable'
import PermissionEditable from '@/views/iam/PermissionEditable'
import RuleDelete from '@/views/iam/RuleDelete'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'ProjectRolePermissionTab',
components: {
RuleDelete,
PermissionEditable,
draggable
draggable,
TooltipButton
},
props: {
resource: {

View File

@ -36,29 +36,20 @@
{{ record }}
</template>
<span slot="action" slot-scope="text, record">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.update.project.role') }}
</template>
<a-button
type="default"
shape="circle"
icon="edit"
size="small"
style="margin:10px"
@click="openUpdateModal(record)" />
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.remove.project.role') }}
</template>
<a-button
type="danger"
shape="circle"
icon="delete"
size="small"
@click="deleteProjectRole(record)"/>
</a-tooltip>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.update.project.role')"
icon="edit"
size="small"
style="margin:10px"
@click="openUpdateModal(record)" />
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.remove.project.role')"
type="danger"
icon="delete"
size="small"
@click="deleteProjectRole(record)" />
</span>
</a-table>
<a-modal
@ -82,29 +73,20 @@
<a-button type="primary" @click="updateProjectRole" :loading="loading">{{ $t('label.ok') }}</a-button>
</div>
<span slot="action" slot-scope="text, record">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.update.project.role') }}
</template>
<a-button
type="default"
shape="circle"
icon="edit"
size="small"
style="margin:10px"
@click="openUpdateModal(record)" />
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.remove.project.role') }}
</template>
<a-button
type="danger"
shape="circle"
icon="delete"
size="small"
@click="deleteProjectRole(record)"/>
</a-tooltip>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.update.project.role')"
icon="edit"
size="small"
style="margin:10px"
@click="openUpdateModal(record)" />
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.remove.project.role')"
type="danger"
icon="edit"
size="small"
@click="deleteProjectRole(record)" />
</span>
</a-form>
</a-modal>
@ -139,6 +121,7 @@
<script>
import { api } from '@/api'
import ProjectRolePermissionTab from '@/views/project/iam/ProjectRolePermissionTab'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'ProjectRoleTab',
props: {
@ -148,7 +131,8 @@ export default {
}
},
components: {
ProjectRolePermissionTab
ProjectRolePermissionTab,
TooltipButton
},
data () {
return {

View File

@ -163,12 +163,8 @@
<a-input ref="input" :value="inputKey" @change="handleKeyChange" style="width: 100px; text-align: center" :placeholder="$t('label.key')" />
<a-input style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
<a-input :value="inputValue" @change="handleValueChange" style="width: 100px; text-align: center; border-left: 0" :placeholder="$t('label.value')" />
<a-button shape="circle" size="small" @click="handleInputConfirm">
<a-icon type="check"/>
</a-button>
<a-button shape="circle" size="small" @click="inputVisible=false">
<a-icon type="close"/>
</a-button>
<tooltip-button :tooltip="$t('label.ok')" icon="check" size="small" @click="handleInputConfirm" />
<tooltip-button :tooltip="$t('label.cancel')" icon="close" size="small" @click="inputVisible=false" />
</a-input-group>
</div>
<a-tag v-else @click="showInput" style="background: #fff; borderStyle: dashed;">
@ -197,11 +193,15 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
import { timeZone } from '@/utils/timezone'
import debounce from 'lodash/debounce'
export default {
name: 'FormSchedule',
components: {
TooltipButton
},
props: {
loading: {
type: Boolean,

View File

@ -61,18 +61,14 @@
<a-tag v-for="(tag, index) in record.tags" :key="index">{{ tag.key + '=' + tag.value }}</a-tag>
</div>
<div slot="action" class="account-button-action" slot-scope="text, record">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.delete') }}
</template>
<a-button
type="danger"
shape="circle"
icon="close"
size="small"
:loading="actionLoading"
@click="handleClickDelete(record)"/>
</a-tooltip>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.delete')"
type="danger"
icon="close"
size="small"
:loading="actionLoading"
@click="handleClickDelete(record)" />
</div>
</a-table>
</div>
@ -80,10 +76,14 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
import { timeZoneName } from '@/utils/timezone'
export default {
name: 'ScheduledSnapshots',
components: {
TooltipButton
},
props: {
loading: {
type: Boolean,

View File

@ -64,12 +64,8 @@
<a-input ref="input" :value="inputKey" @change="handleKeyChange" style="width: 100px; text-align: center" :placeholder="$t('label.key')" />
<a-input style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
<a-input :value="inputValue" @change="handleValueChange" style="width: 100px; text-align: center; border-left: 0" :placeholder="$t('label.value')" />
<a-button shape="circle" size="small" @click="handleInputConfirm">
<a-icon type="check"/>
</a-button>
<a-button shape="circle" size="small" @click="inputVisible=false">
<a-icon type="close"/>
</a-button>
<tooltip-button :tooltip="$t('label.ok')" icon="check" size="small" @click="handleInputConfirm" />
<tooltip-button :tooltip="$t('label.cancel')" icon="close" size="small" @click="inputVisible=false" />
</a-input-group>
</div>
<a-tag v-else @click="showInput" style="background: #fff; borderStyle: dashed;">
@ -97,9 +93,13 @@
<script>
import { api } from '@/api'
import TooltipButton from '@/components/view/TooltipButton'
export default {
name: 'TakeSnapshot',
components: {
TooltipButton
},
props: {
loading: {
type: Boolean,