mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
Merge branch 'main' into nsx-integration
This commit is contained in:
commit
5ec455228d
@ -22,6 +22,7 @@ This PR...
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] Enhancement (improves an existing feature and functionality)
|
||||
- [ ] Cleanup (Code refactoring and cleanup, that may add test cases)
|
||||
- [ ] build/CI
|
||||
|
||||
### Feature/Enhancement Scale or Bug Severity
|
||||
|
||||
@ -43,8 +44,12 @@ This PR...
|
||||
|
||||
|
||||
### How Has This Been Tested?
|
||||
|
||||
<!-- Please describe in detail how you tested your changes. -->
|
||||
<!-- Include details of your testing environment, and the tests you ran to -->
|
||||
|
||||
#### How did you try to break this feature and the system with this change?
|
||||
|
||||
<!-- see how your change affects other areas of the code, etc. -->
|
||||
|
||||
|
||||
|
||||
@ -279,6 +279,11 @@ hypervisor.type=kvm
|
||||
# If this parameter is used, property host.overcommit.mem.mb must be set to 0.
|
||||
#host.reserved.mem.mb=1024
|
||||
|
||||
# Number of CPU cores to subtract from advertised available cores.
|
||||
# These are reserved for system activity, or otherwise share host CPU resources with
|
||||
# CloudStack VM allocation.
|
||||
# host.reserved.cpu.count = 0
|
||||
|
||||
# The model of Watchdog timer to present to the Guest.
|
||||
# For all models refer to the libvirt documentation.
|
||||
#vm.watchdog.model=i6300esb
|
||||
|
||||
@ -502,6 +502,15 @@ public class AgentProperties{
|
||||
*/
|
||||
public static final Property<Integer> HOST_RESERVED_MEM_MB = new Property<>("host.reserved.mem.mb", 1024);
|
||||
|
||||
/**
|
||||
* How many host CPUs to reserve for non-allocation.<br>
|
||||
* This can be used to set aside CPU cores on the host for other tasks, such as running hyperconverged storage<br>
|
||||
* processes, etc.
|
||||
* Data type: Integer.<br>
|
||||
* Default value: <code>0</code>
|
||||
*/
|
||||
public static final Property<Integer> HOST_RESERVED_CPU_CORE_COUNT = new Property<>("host.reserved.cpu.count", 0);
|
||||
|
||||
/**
|
||||
* The model of Watchdog timer to present to the Guest.<br>
|
||||
* For all models refer to the libvirt documentation.<br>
|
||||
|
||||
@ -149,7 +149,7 @@ public class ConfigKey<T> {
|
||||
|
||||
public ConfigKey(Class<T> type, String name, String category, String defaultValue, String description, boolean isDynamic, Scope scope, T multiplier,
|
||||
String displayText, String parent, Ternary<String, String, Long> group, Pair<String, Long> subGroup) {
|
||||
this(type, name, category, defaultValue, description, isDynamic, scope, multiplier, null, parent, null, null, null, null);
|
||||
this(type, name, category, defaultValue, description, isDynamic, scope, multiplier, displayText, parent, group, subGroup, null, null);
|
||||
}
|
||||
|
||||
public ConfigKey(Class<T> type, String name, String category, String defaultValue, String description, boolean isDynamic, Scope scope, T multiplier,
|
||||
|
||||
@ -98,6 +98,8 @@ public class ConfigDepotAdminTest extends TestCase {
|
||||
ConfigurationVO staticIntCV = new ConfigurationVO("UnitTestComponent", StaticIntCK);
|
||||
dynamicIntCV.setValue("200");
|
||||
ConfigurationVO testCV = new ConfigurationVO("UnitTestComponent", TestCK);
|
||||
ConfigurationGroupVO groupVO = new ConfigurationGroupVO();
|
||||
ConfigurationSubGroupVO subGroupVO = new ConfigurationSubGroupVO();
|
||||
|
||||
when(_configurable.getConfigComponentName()).thenReturn("UnitTestComponent");
|
||||
when(_configurable.getConfigKeys()).thenReturn(new ConfigKey<?>[] {DynamicIntCK, StaticIntCK, TestCK});
|
||||
@ -105,6 +107,8 @@ public class ConfigDepotAdminTest extends TestCase {
|
||||
when(_configDao.findById(DynamicIntCK.key())).thenReturn(dynamicIntCV);
|
||||
when(_configDao.findById(TestCK.key())).thenReturn(testCV);
|
||||
when(_configDao.persist(any(ConfigurationVO.class))).thenReturn(dynamicIntCV);
|
||||
when(_configGroupDao.persist(any(ConfigurationGroupVO.class))).thenReturn(groupVO);
|
||||
when(_configSubGroupDao.persist(any(ConfigurationSubGroupVO.class))).thenReturn(subGroupVO);
|
||||
_depotAdmin.populateConfigurations();
|
||||
|
||||
// This is once because DynamicIntCK is returned.
|
||||
|
||||
@ -86,7 +86,7 @@ public class EntityManagerImpl extends ManagerBase implements EntityManager {
|
||||
|
||||
public <T, K> GenericSearchBuilder<T, K> createGenericSearchBuilder(Class<T> entityType, Class<K> resultType) {
|
||||
GenericDao<T, ? extends Serializable> dao = (GenericDao<T, ? extends Serializable>)GenericDaoBase.getDao(entityType);
|
||||
return dao.createSearchBuilder((Class<K>)resultType.getClass());
|
||||
return dao.createSearchBuilder((Class<K>)resultType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -124,7 +124,7 @@ public class DbUtil {
|
||||
public static Field findField(Class<?> clazz, String columnName) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getAnnotation(Embedded.class) != null || field.getAnnotation(EmbeddedId.class) != null) {
|
||||
findField(field.getClass(), columnName);
|
||||
findField(field.getType(), columnName);
|
||||
} else {
|
||||
if (columnName.equals(DbUtil.getColumnName(field))) {
|
||||
return field;
|
||||
@ -170,7 +170,7 @@ public class DbUtil {
|
||||
}
|
||||
|
||||
if (field.getAnnotation(EmbeddedId.class) != null) {
|
||||
assert (field.getClass().getAnnotation(Embeddable.class) != null) : "Class " + field.getClass().getName() + " must be Embeddable to be used as Embedded Id";
|
||||
assert (field.getType().getAnnotation(Embeddable.class) != null) : "Class " + field.getType().getName() + " must be Embeddable to be used as Embedded Id";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
// under the License.
|
||||
package com.cloud.utils.db;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.sql.Driver;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -56,12 +58,13 @@ public class DriverLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
Class.forName(driverClass).newInstance();
|
||||
Class<Driver> klazz = (Class<Driver>) Class.forName(driverClass);
|
||||
klazz.getDeclaredConstructor().newInstance();
|
||||
LOADED_DRIVERS.add(dbDriver);
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Successfully loaded DB driver " + driverClass);
|
||||
}
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
|
||||
LOGGER.error("Failed to load DB driver " + driverClass);
|
||||
throw new CloudRuntimeException("Failed to load DB driver " + driverClass, e);
|
||||
}
|
||||
|
||||
@ -57,10 +57,10 @@ public class EcInfo {
|
||||
rawClass = HashSet.class;
|
||||
} else if (List.class == rawClazz) {
|
||||
rawClass = ArrayList.class;
|
||||
} else if (Collection.class == Collection.class) {
|
||||
} else if (Collection.class == rawClazz) {
|
||||
rawClass = ArrayList.class;
|
||||
} else {
|
||||
assert (false) : " We don't know how to create this calss " + rawType.toString() + " for " + attr.field.getName();
|
||||
assert (false) : " We don't know how to create this class " + rawType.toString() + " for " + attr.field.getName();
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new CloudRuntimeException("Write your own support for " + rawClazz + " defined by " + attr.field.getName());
|
||||
|
||||
@ -874,7 +874,8 @@ public abstract class GenericDaoBase<T, ID extends Serializable> extends Compone
|
||||
if (_idField.getAnnotation(EmbeddedId.class) == null) {
|
||||
sql.append(_table).append(".").append(DbUtil.getColumnName(_idField, null)).append(" = ? ");
|
||||
} else {
|
||||
final Class<?> clazz = _idField.getClass();
|
||||
s_logger.debug(String.format("field type vs declarator : %s vs %s", _idField.getType(), _idField.getDeclaringClass()));
|
||||
final Class<?> clazz = _idField.getType();
|
||||
final AttributeOverride[] overrides = DbUtil.getAttributeOverrides(_idField);
|
||||
for (final Field field : clazz.getDeclaredFields()) {
|
||||
sql.append(_table).append(".").append(DbUtil.getColumnName(field, overrides)).append(" = ? AND ");
|
||||
|
||||
@ -460,6 +460,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
||||
|
||||
private long dom0OvercommitMem;
|
||||
|
||||
private int dom0MinCpuCores;
|
||||
|
||||
protected int cmdsTimeout;
|
||||
protected int stopTimeout;
|
||||
protected CPUStat cpuStat = new CPUStat();
|
||||
@ -1063,6 +1065,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
||||
// Reserve 1GB unless admin overrides
|
||||
dom0MinMem = ByteScaleUtils.mebibytesToBytes(AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_RESERVED_MEM_MB));
|
||||
|
||||
dom0MinCpuCores = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_RESERVED_CPU_CORE_COUNT);
|
||||
|
||||
// Support overcommit memory for host if host uses ZSWAP, KSM and other memory
|
||||
// compressing technologies
|
||||
dom0OvercommitMem = ByteScaleUtils.mebibytesToBytes(AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_OVERCOMMIT_MEM_MB));
|
||||
@ -3540,7 +3544,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
||||
@Override
|
||||
public StartupCommand[] initialize() {
|
||||
|
||||
final KVMHostInfo info = new KVMHostInfo(dom0MinMem, dom0OvercommitMem, manualCpuSpeed);
|
||||
final KVMHostInfo info = new KVMHostInfo(dom0MinMem, dom0OvercommitMem, manualCpuSpeed, dom0MinCpuCores);
|
||||
|
||||
String capabilities = String.join(",", info.getCapabilities());
|
||||
if (dpdkSupport) {
|
||||
@ -3548,7 +3552,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
||||
}
|
||||
|
||||
final StartupRoutingCommand cmd =
|
||||
new StartupRoutingCommand(info.getCpus(), info.getCpuSpeed(), info.getTotalMemory(), info.getReservedMemory(), capabilities, hypervisorType,
|
||||
new StartupRoutingCommand(info.getAllocatableCpus(), info.getCpuSpeed(), info.getTotalMemory(), info.getReservedMemory(), capabilities, hypervisorType,
|
||||
RouterPrivateIpStrategy.HostLocal);
|
||||
cmd.setCpuSockets(info.getCpuSockets());
|
||||
fillNetworkInformation(cmd);
|
||||
|
||||
@ -1715,11 +1715,11 @@ public class KVMStorageProcessor implements StorageProcessor {
|
||||
snapshotPath = getSnapshotPathInPrimaryStorage(primaryPool.getLocalPath(), snapshotName);
|
||||
|
||||
String diskLabel = takeVolumeSnapshot(resource.getDisks(conn, vmName), snapshotName, diskPath, vm);
|
||||
String copyResult = copySnapshotToPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume);
|
||||
String convertResult = convertBaseFileToSnapshotFileInPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume, cmd.getWait());
|
||||
|
||||
mergeSnapshotIntoBaseFile(vm, diskLabel, diskPath, snapshotName, volume, conn);
|
||||
|
||||
validateCopyResult(copyResult, snapshotPath);
|
||||
validateConvertResult(convertResult, snapshotPath);
|
||||
} catch (LibvirtException e) {
|
||||
if (!e.getMessage().contains(LIBVIRT_OPERATION_NOT_SUPPORTED_MESSAGE)) {
|
||||
throw e;
|
||||
@ -1784,8 +1784,8 @@ public class KVMStorageProcessor implements StorageProcessor {
|
||||
}
|
||||
} else {
|
||||
snapshotPath = getSnapshotPathInPrimaryStorage(primaryPool.getLocalPath(), snapshotName);
|
||||
String copyResult = copySnapshotToPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume);
|
||||
validateCopyResult(copyResult, snapshotPath);
|
||||
String convertResult = convertBaseFileToSnapshotFileInPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume, cmd.getWait());
|
||||
validateConvertResult(convertResult, snapshotPath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1838,13 +1838,13 @@ public class KVMStorageProcessor implements StorageProcessor {
|
||||
s_logger.debug(String.format("Full VM Snapshot [%s] of VM [%s] took [%s] seconds to finish.", snapshotName, vmName, (System.currentTimeMillis() - start)/1000));
|
||||
}
|
||||
|
||||
protected void validateCopyResult(String copyResult, String snapshotPath) throws CloudRuntimeException, IOException {
|
||||
if (copyResult == null) {
|
||||
protected void validateConvertResult(String convertResult, String snapshotPath) throws CloudRuntimeException, IOException {
|
||||
if (convertResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Files.deleteIfExists(Paths.get(snapshotPath));
|
||||
throw new CloudRuntimeException(copyResult);
|
||||
throw new CloudRuntimeException(convertResult);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1901,20 +1901,31 @@ public class KVMStorageProcessor implements StorageProcessor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the snapshot directory in the primary storage, if it does not exist; then copies the base file (VM's old writing file) to the snapshot dir..
|
||||
* Creates the snapshot directory in the primary storage, if it does not exist; then, converts the base file (VM's old writing file) to the snapshot directory.
|
||||
* @param primaryPool Storage to create folder, if not exists;
|
||||
* @param baseFile Base file of VM, which will be copied;
|
||||
* @param snapshotPath Path to copy the base file;
|
||||
* @return null if copies successfully or a error message.
|
||||
* @param baseFile Base file of VM, which will be converted;
|
||||
* @param snapshotPath Path to convert the base file;
|
||||
* @return null if the conversion occurs successfully or an error message that must be handled.
|
||||
*/
|
||||
protected String copySnapshotToPrimaryStorageDir(KVMStoragePool primaryPool, String baseFile, String snapshotPath, VolumeObjectTO volume) {
|
||||
protected String convertBaseFileToSnapshotFileInPrimaryStorageDir(KVMStoragePool primaryPool, String baseFile, String snapshotPath, VolumeObjectTO volume, int wait) {
|
||||
try {
|
||||
s_logger.debug(String.format("Trying to convert volume [%s] (%s) to snapshot [%s].", volume, baseFile, snapshotPath));
|
||||
|
||||
primaryPool.createFolder(TemplateConstants.DEFAULT_SNAPSHOT_ROOT_DIR);
|
||||
Files.copy(Paths.get(baseFile), Paths.get(snapshotPath));
|
||||
s_logger.debug(String.format("Copied %s snapshot from [%s] to [%s].", volume, baseFile, snapshotPath));
|
||||
|
||||
QemuImgFile srcFile = new QemuImgFile(baseFile);
|
||||
srcFile.setFormat(PhysicalDiskFormat.QCOW2);
|
||||
|
||||
QemuImgFile destFile = new QemuImgFile(snapshotPath);
|
||||
destFile.setFormat(PhysicalDiskFormat.QCOW2);
|
||||
|
||||
QemuImg q = new QemuImg(wait);
|
||||
q.convert(srcFile, destFile);
|
||||
|
||||
s_logger.debug(String.format("Converted volume [%s] (from path \"%s\") to snapshot [%s].", volume, baseFile, snapshotPath));
|
||||
return null;
|
||||
} catch (IOException ex) {
|
||||
return String.format("Unable to copy %s snapshot [%s] to [%s] due to [%s].", volume, baseFile, snapshotPath, ex.getMessage());
|
||||
} catch (QemuImgException | LibvirtException ex) {
|
||||
return String.format("Failed to convert %s snapshot of volume [%s] to [%s] due to [%s].", volume, baseFile, snapshotPath, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -48,7 +48,8 @@ public class KVMHostInfo {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(KVMHostInfo.class);
|
||||
|
||||
private int cpus;
|
||||
private int totalCpus;
|
||||
private int allocatableCpus;
|
||||
private int cpusockets;
|
||||
private long cpuSpeed;
|
||||
private long totalMemory;
|
||||
@ -58,16 +59,25 @@ public class KVMHostInfo {
|
||||
|
||||
private static String cpuInfoFreqFileName = "/sys/devices/system/cpu/cpu0/cpufreq/base_frequency";
|
||||
|
||||
public KVMHostInfo(long reservedMemory, long overCommitMemory, long manualSpeed) {
|
||||
public KVMHostInfo(long reservedMemory, long overCommitMemory, long manualSpeed, int reservedCpus) {
|
||||
this.cpuSpeed = manualSpeed;
|
||||
this.reservedMemory = reservedMemory;
|
||||
this.overCommitMemory = overCommitMemory;
|
||||
this.getHostInfoFromLibvirt();
|
||||
this.totalMemory = new MemStat(this.getReservedMemory(), this.getOverCommitMemory()).getTotal();
|
||||
this.allocatableCpus = totalCpus - reservedCpus;
|
||||
if (allocatableCpus < 1) {
|
||||
LOGGER.warn(String.format("Aggressive reserved CPU config leaves no usable CPUs for VMs! Total system CPUs: %d, Reserved: %d, Allocatable: %d", totalCpus, reservedCpus, allocatableCpus));
|
||||
allocatableCpus = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int getCpus() {
|
||||
return this.cpus;
|
||||
public int getTotalCpus() {
|
||||
return this.totalCpus;
|
||||
}
|
||||
|
||||
public int getAllocatableCpus() {
|
||||
return this.allocatableCpus;
|
||||
}
|
||||
|
||||
public int getCpuSockets() {
|
||||
@ -189,7 +199,7 @@ public class KVMHostInfo {
|
||||
if (hosts.nodes > 0) {
|
||||
this.cpusockets = hosts.sockets * hosts.nodes;
|
||||
}
|
||||
this.cpus = hosts.cpus;
|
||||
this.totalCpus = hosts.cpus;
|
||||
|
||||
final LibvirtCapXMLParser parser = new LibvirtCapXMLParser();
|
||||
parser.parseCapabilitiesXML(capabilities);
|
||||
|
||||
@ -39,6 +39,9 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
|
||||
import org.apache.cloudstack.storage.to.VolumeObjectTO;
|
||||
import org.apache.cloudstack.utils.qemu.QemuImg;
|
||||
import org.apache.cloudstack.utils.qemu.QemuImgException;
|
||||
import org.apache.cloudstack.utils.qemu.QemuImgFile;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
@ -88,6 +91,11 @@ public class KVMStorageProcessorTest {
|
||||
@Mock
|
||||
Connect connectMock;
|
||||
|
||||
@Mock
|
||||
QemuImg qemuImgMock;
|
||||
|
||||
@Mock
|
||||
LibvirtDomainXMLParser libvirtDomainXMLParserMock;
|
||||
@Mock
|
||||
LibvirtVMDef.DiskDef diskDefMock;
|
||||
|
||||
@ -251,36 +259,47 @@ public class KVMStorageProcessorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateCopySnapshotToPrimaryStorageDirFailToCopyReturnErrorMessage() throws Exception {
|
||||
public void convertBaseFileToSnapshotFileInPrimaryStorageDirTestFailToConvertWithQemuImgExceptionReturnErrorMessage() throws Exception {
|
||||
String baseFile = "baseFile";
|
||||
String snapshotPath = "snapshotPath";
|
||||
String errorMessage = "error";
|
||||
String expectedResult = String.format("Unable to copy %s snapshot [%s] to [%s] due to [%s].", volumeObjectToMock, baseFile, snapshotPath, errorMessage);
|
||||
String expectedResult = String.format("Failed to convert %s snapshot of volume [%s] to [%s] due to [%s].", volumeObjectToMock, baseFile, snapshotPath, errorMessage);
|
||||
|
||||
Mockito.doReturn(true).when(kvmStoragePoolMock).createFolder(Mockito.anyString());
|
||||
try (MockedStatic<Files> ignored = Mockito.mockStatic(Files.class)) {
|
||||
Mockito.when(Files.copy(Mockito.any(Path.class), Mockito.any(Path.class), Mockito.any())).thenThrow(
|
||||
new IOException(errorMessage));
|
||||
|
||||
String result = storageProcessorSpy.copySnapshotToPrimaryStorageDir(kvmStoragePoolMock, baseFile,
|
||||
snapshotPath, volumeObjectToMock);
|
||||
|
||||
try (MockedConstruction<QemuImg> ignored = Mockito.mockConstruction(QemuImg.class, (mock,context) -> {
|
||||
Mockito.doThrow(new QemuImgException(errorMessage)).when(mock).convert(Mockito.any(QemuImgFile.class), Mockito.any(QemuImgFile.class));
|
||||
})) {
|
||||
String result = storageProcessorSpy.convertBaseFileToSnapshotFileInPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock, 1);
|
||||
Assert.assertEquals(expectedResult, result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateCopySnapshotToPrimaryStorageDirCopySuccessReturnNull() throws Exception {
|
||||
public void convertBaseFileToSnapshotFileInPrimaryStorageDirTestFailToConvertWithLibvirtExceptionReturnErrorMessage() throws Exception {
|
||||
String baseFile = "baseFile";
|
||||
String snapshotPath = "snapshotPath";
|
||||
String errorMessage = "null";
|
||||
String expectedResult = String.format("Failed to convert %s snapshot of volume [%s] to [%s] due to [%s].", volumeObjectToMock, baseFile, snapshotPath, errorMessage);
|
||||
|
||||
Mockito.doReturn(true).when(kvmStoragePoolMock).createFolder(Mockito.anyString());
|
||||
try (MockedConstruction<QemuImg> ignored = Mockito.mockConstruction(QemuImg.class, (mock,context) -> {
|
||||
Mockito.doThrow(LibvirtException.class).when(mock).convert(Mockito.any(QemuImgFile.class), Mockito.any(QemuImgFile.class));
|
||||
})) {
|
||||
String result = storageProcessorSpy.convertBaseFileToSnapshotFileInPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock, 1);
|
||||
Assert.assertEquals(expectedResult, result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertBaseFileToSnapshotFileInPrimaryStorageDirTestConvertSuccessReturnNull() throws Exception {
|
||||
String baseFile = "baseFile";
|
||||
String snapshotPath = "snapshotPath";
|
||||
|
||||
Mockito.doReturn(true).when(kvmStoragePoolMock).createFolder(Mockito.anyString());
|
||||
try (MockedStatic<Files> ignored = Mockito.mockStatic(Files.class)) {
|
||||
Mockito.when(Files.copy(Mockito.any(Path.class), Mockito.any(Path.class), Mockito.any())).thenReturn(null);
|
||||
|
||||
String result = storageProcessorSpy.copySnapshotToPrimaryStorageDir(kvmStoragePoolMock, baseFile,
|
||||
snapshotPath, volumeObjectToMock);
|
||||
|
||||
try (MockedConstruction<QemuImg> ignored = Mockito.mockConstruction(QemuImg.class, (mock, context) -> {
|
||||
Mockito.doNothing().when(mock).convert(Mockito.any(QemuImgFile.class), Mockito.any(QemuImgFile.class));
|
||||
})) {
|
||||
String result = storageProcessorSpy.convertBaseFileToSnapshotFileInPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock, 1);
|
||||
Assert.assertNull(result);
|
||||
}
|
||||
}
|
||||
@ -334,14 +353,14 @@ public class KVMStorageProcessorTest {
|
||||
|
||||
@Test
|
||||
public void validateValidateCopyResultResultIsNullReturn() throws CloudRuntimeException, IOException{
|
||||
storageProcessorSpy.validateCopyResult(null, "");
|
||||
storageProcessorSpy.validateConvertResult(null, "");
|
||||
}
|
||||
|
||||
@Test (expected = IOException.class)
|
||||
public void validateValidateCopyResultFailToDeleteThrowIOException() throws CloudRuntimeException, IOException{
|
||||
try (MockedStatic<Files> ignored = Mockito.mockStatic(Files.class)) {
|
||||
Mockito.when(Files.deleteIfExists(Mockito.any())).thenThrow(new IOException(""));
|
||||
storageProcessorSpy.validateCopyResult("", "");
|
||||
storageProcessorSpy.validateConvertResult("", "");
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,7 +368,7 @@ public class KVMStorageProcessorTest {
|
||||
public void validateValidateCopyResulResultNotNullThrowCloudRuntimeException() throws CloudRuntimeException, IOException{
|
||||
try (MockedStatic<Files> ignored = Mockito.mockStatic(Files.class)) {
|
||||
Mockito.when(Files.deleteIfExists(Mockito.any())).thenReturn(true);
|
||||
storageProcessorSpy.validateCopyResult("", "");
|
||||
storageProcessorSpy.validateConvertResult("", "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -73,8 +73,36 @@ public class KVMHostInfoTest {
|
||||
Mockito.when(conn.close()).thenReturn(0);
|
||||
int manualSpeed = 500;
|
||||
|
||||
KVMHostInfo kvmHostInfo = new KVMHostInfo(10, 10, manualSpeed);
|
||||
KVMHostInfo kvmHostInfo = new KVMHostInfo(10, 10, manualSpeed, 0);
|
||||
Assert.assertEquals(kvmHostInfo.getCpuSpeed(), manualSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reservedCpuCoresTest() throws Exception {
|
||||
if (!System.getProperty("os.name").equals("Linux")) {
|
||||
return;
|
||||
}
|
||||
try (MockedStatic<LibvirtConnection> ignored = Mockito.mockStatic(LibvirtConnection.class)) {
|
||||
Connect conn = Mockito.mock(Connect.class);
|
||||
NodeInfo nodeInfo = Mockito.mock(NodeInfo.class);
|
||||
nodeInfo.cpus = 10;
|
||||
String capabilitiesXml = "<capabilities></capabilities>";
|
||||
|
||||
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
|
||||
Mockito.when(conn.nodeInfo()).thenReturn(nodeInfo);
|
||||
Mockito.when(conn.getCapabilities()).thenReturn(capabilitiesXml);
|
||||
Mockito.when(conn.close()).thenReturn(0);
|
||||
int manualSpeed = 500;
|
||||
|
||||
KVMHostInfo kvmHostInfo = new KVMHostInfo(10, 10, 100, 2);
|
||||
Assert.assertEquals("reserve two CPU cores", 8, kvmHostInfo.getAllocatableCpus());
|
||||
|
||||
kvmHostInfo = new KVMHostInfo(10, 10, 100, 0);
|
||||
Assert.assertEquals("no reserve CPU core setting", 10, kvmHostInfo.getAllocatableCpus());
|
||||
|
||||
kvmHostInfo = new KVMHostInfo(10, 10, 100, 12);
|
||||
Assert.assertEquals("Misconfigured/too large CPU reserve", 0, kvmHostInfo.getAllocatableCpus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +163,12 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp
|
||||
final CapacityVO cpuCapacity = capacityDao.findByHostIdType(host.getId(), Capacity.CAPACITY_TYPE_CPU);
|
||||
final double cpuUsedMhz = hostStats.getCpuUtilization() * host.getCpus() * host.getSpeed() / 100.0 ;
|
||||
|
||||
if (cpuCapacity != null && cpuCapacity.getCapacityState() == CapacityState.Enabled) {
|
||||
if (host.isInMaintenanceStates()) {
|
||||
metricsList.add(new ItemHostCpu(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), cpuFactor, ALLOCATED, 0L, isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostCpu(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), cpuFactor, USED, 0L, isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostCpu(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), cpuFactor, TOTAL, 0L, isDedicated, hostTags));
|
||||
}
|
||||
else if (cpuCapacity != null && cpuCapacity.getCapacityState() == CapacityState.Enabled) {
|
||||
metricsList.add(new ItemHostCpu(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), cpuFactor, ALLOCATED, cpuCapacity.getUsedCapacity(), isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostCpu(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), cpuFactor, USED, cpuUsedMhz, isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostCpu(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), cpuFactor, TOTAL, cpuCapacity.getTotalCapacity(), isDedicated, hostTags));
|
||||
@ -175,7 +180,12 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp
|
||||
|
||||
final String memoryFactor = String.valueOf(CapacityManager.MemOverprovisioningFactor.valueIn(host.getClusterId()));
|
||||
final CapacityVO memCapacity = capacityDao.findByHostIdType(host.getId(), Capacity.CAPACITY_TYPE_MEMORY);
|
||||
if (memCapacity != null && memCapacity.getCapacityState() == CapacityState.Enabled) {
|
||||
if (host.isInMaintenanceStates()) {
|
||||
metricsList.add(new ItemHostMemory(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), memoryFactor, ALLOCATED, 0L, isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostMemory(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), memoryFactor, USED, 0, isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostMemory(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), memoryFactor, TOTAL, 0L, isDedicated, hostTags));
|
||||
}
|
||||
else if (memCapacity != null && memCapacity.getCapacityState() == CapacityState.Enabled) {
|
||||
metricsList.add(new ItemHostMemory(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), memoryFactor, ALLOCATED, memCapacity.getUsedCapacity(), isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostMemory(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), memoryFactor, USED, hostStats.getUsedMemory(), isDedicated, hostTags));
|
||||
metricsList.add(new ItemHostMemory(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), memoryFactor, TOTAL, memCapacity.getTotalCapacity(), isDedicated, hostTags));
|
||||
@ -188,7 +198,11 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp
|
||||
metricsList.add(new ItemHostVM(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), vmDao.listByHostId(host.getId()).size()));
|
||||
|
||||
final CapacityVO coreCapacity = capacityDao.findByHostIdType(host.getId(), Capacity.CAPACITY_TYPE_CPU_CORE);
|
||||
if (coreCapacity != null && coreCapacity.getCapacityState() == CapacityState.Enabled) {
|
||||
if (host.isInMaintenanceStates()) {
|
||||
metricsList.add(new ItemVMCore(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), USED, 0L, isDedicated, hostTags));
|
||||
metricsList.add(new ItemVMCore(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), TOTAL, 0L, isDedicated, hostTags));
|
||||
}
|
||||
else if (coreCapacity != null && coreCapacity.getCapacityState() == CapacityState.Enabled) {
|
||||
metricsList.add(new ItemVMCore(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), USED, coreCapacity.getUsedCapacity(), isDedicated, hostTags));
|
||||
metricsList.add(new ItemVMCore(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), TOTAL, coreCapacity.getTotalCapacity(), isDedicated, hostTags));
|
||||
} else {
|
||||
|
||||
@ -1171,28 +1171,40 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
|
||||
}
|
||||
|
||||
final Map<VirtualMachine, Backup.Metric> metrics = backupProvider.getBackupMetrics(dataCenter.getId(), new ArrayList<>(vms));
|
||||
try {
|
||||
for (final VirtualMachine vm : metrics.keySet()) {
|
||||
final Backup.Metric metric = metrics.get(vm);
|
||||
if (metric != null) {
|
||||
// Sync out-of-band backups
|
||||
backupProvider.syncBackups(vm, metric);
|
||||
// Emit a usage event, update usage metric for the VM by the usage server
|
||||
UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_BACKUP_USAGE_METRIC, vm.getAccountId(),
|
||||
vm.getDataCenterId(), vm.getId(), "Backup-" + vm.getHostName() + "-" + vm.getUuid(),
|
||||
vm.getBackupOfferingId(), null, metric.getBackupSize(), metric.getDataSize(),
|
||||
Backup.class.getSimpleName(), vm.getUuid());
|
||||
}
|
||||
}
|
||||
} catch (final Throwable e) {
|
||||
LOG.error(String.format("Failed to sync backup usage metrics and out-of-band backups due to: [%s].", e.getMessage()), e);
|
||||
}
|
||||
syncBackupMetrics(backupProvider, metrics);
|
||||
}
|
||||
} catch (final Throwable t) {
|
||||
LOG.error(String.format("Error trying to run backup-sync background task due to: [%s].", t.getMessage()), t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to sync the VM backups. If one backup synchronization fails, only this VM backups are skipped, and the entire process does not stop.
|
||||
*/
|
||||
private void syncBackupMetrics(final BackupProvider backupProvider, final Map<VirtualMachine, Backup.Metric> metrics) {
|
||||
for (final VirtualMachine vm : metrics.keySet()) {
|
||||
tryToSyncVMBackups(backupProvider, metrics, vm);
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToSyncVMBackups(BackupProvider backupProvider, Map<VirtualMachine, Backup.Metric> metrics, VirtualMachine vm) {
|
||||
try {
|
||||
final Backup.Metric metric = metrics.get(vm);
|
||||
if (metric != null) {
|
||||
LOG.debug(String.format("Trying to sync backups of VM [%s] using backup provider [%s].", vm.getUuid(), backupProvider.getName()));
|
||||
// Sync out-of-band backups
|
||||
backupProvider.syncBackups(vm, metric);
|
||||
// Emit a usage event, update usage metric for the VM by the usage server
|
||||
UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_BACKUP_USAGE_METRIC, vm.getAccountId(),
|
||||
vm.getDataCenterId(), vm.getId(), "Backup-" + vm.getHostName() + "-" + vm.getUuid(),
|
||||
vm.getBackupOfferingId(), null, metric.getBackupSize(), metric.getDataSize(),
|
||||
Backup.class.getSimpleName(), vm.getUuid());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error(String.format("Failed to sync backup usage metrics and out-of-band backups of VM [%s] due to: [%s].", vm.getUuid(), e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getDelay() {
|
||||
return BackupSyncPollingInterval.value() * 1000L;
|
||||
|
||||
@ -66,7 +66,7 @@ patch() {
|
||||
if [ "$TYPE" != "cksnode" ]; then
|
||||
while [ $retry -gt 0 ]
|
||||
do
|
||||
if [ -f $patchfile ]; then
|
||||
if tar tf $patchfile &> /dev/null; then
|
||||
eval $(validate_checksums $md5file $patchfile)
|
||||
if [ "$oldmd5" != "$newmd5" ] && [ -f ${patchfile} ] && [ "$newmd5" != "" ]
|
||||
then
|
||||
|
||||
@ -596,6 +596,10 @@ class TestVMSchedule(cloudstackTestCase):
|
||||
time.sleep(30)
|
||||
current_state = self.virtual_machine.update(self.apiclient).state
|
||||
if previous_state != current_state:
|
||||
# Add these checks because VMs can take some time to start or stop
|
||||
if (previous_state == 'Starting' and current_state in ('Starting', 'Running')) or (
|
||||
previous_state == 'Stopping' and current_state in ('Stopping', 'Stopped')):
|
||||
continue
|
||||
self.debug(
|
||||
"VM changed state from %s to %s" % (previous_state, current_state)
|
||||
)
|
||||
|
||||
688
ui/package-lock.json
generated
688
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -34,13 +34,13 @@
|
||||
"test:unit": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@vue-js-cron/ant": "^1.1.3",
|
||||
"@vue-js-cron/core": "^3.7.1",
|
||||
"ant-design-vue": "^3.2.9",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"antd": "^4.21.4",
|
||||
"antd-theme-webpack-plugin": "^1.3.9",
|
||||
"axios": "^0.21.1",
|
||||
|
||||
30
ui/public/config.json
vendored
30
ui/public/config.json
vendored
@ -59,6 +59,36 @@
|
||||
"jp": "label.japanese.keyboard",
|
||||
"sc": "label.simplified.chinese.keyboard"
|
||||
},
|
||||
"userCard": {
|
||||
"title": "label.help",
|
||||
"icon": "question-circle-outlined",
|
||||
"links": [
|
||||
{
|
||||
"title": "Documentation",
|
||||
"text": "CloudStack documentation website",
|
||||
"link": "https://docs.cloudstack.apache.org/en/latest/",
|
||||
"icon": "read-outlined"
|
||||
},
|
||||
{
|
||||
"title": "API Documentation",
|
||||
"text": "Refer to API documentation",
|
||||
"link": "https://cloudstack.apache.org/api.html",
|
||||
"icon": "api-outlined"
|
||||
},
|
||||
{
|
||||
"title": "Email Support",
|
||||
"text": "Join CloudStack users mailing list to seek and provide support",
|
||||
"link": "mailto:users-subscribe@cloudstack.apache.org",
|
||||
"icon": "mail-outlined"
|
||||
},
|
||||
{
|
||||
"title": "Report Issue",
|
||||
"text": "Submit a bug or improvement request",
|
||||
"link": "https://github.com/apache/cloudstack/issues/new",
|
||||
"icon": "bug-outlined"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": [],
|
||||
"basicZoneEnabled": true,
|
||||
"multipleServer": false,
|
||||
|
||||
@ -538,6 +538,7 @@
|
||||
"label.cpuused": "CPU utilized",
|
||||
"label.cpuusedghz": "CPU used",
|
||||
"label.create": "Create",
|
||||
"label.create.instance": "Create cloud server",
|
||||
"label.create.account": "Create account",
|
||||
"label.create.backup": "Start backup",
|
||||
"label.create.network": "Create new network",
|
||||
@ -2202,6 +2203,7 @@
|
||||
"label.volumetotal": "Volume",
|
||||
"label.volumetype": "Volume Type",
|
||||
"label.vpc": "VPC",
|
||||
"label.vpcs": "VPCs",
|
||||
"label.vpc.id": "VPC ID",
|
||||
"label.vpc.offerings": "VPC offerings",
|
||||
"label.vpc.virtual.router": "VPC virtual router",
|
||||
@ -2371,6 +2373,8 @@
|
||||
"message.add.network.failed": "Adding network failed.",
|
||||
"message.add.network.processing": "Adding network...",
|
||||
"message.add.new.gateway.to.vpc": "Please specify the information to add a new gateway to this VPC.",
|
||||
"message.add.physical.network.failed": "Adding physical network failed",
|
||||
"message.add.physical.network.processing": "Adding a new physical network...",
|
||||
"message.add.pod": "Add a new pod for zone <b><span id=\"add_pod_zone_name\"></span></b>",
|
||||
"message.add.pod.during.zone.creation": "Each zone must contain one or more pods. We will add the first pod now. A pod contains hosts and primary storage servers, which you will add in a later step. First, configure a range of reserved IP addresses for CloudStack's internal management traffic. The reserved IP range must be unique for each zone in the cloud.",
|
||||
"message.add.port.forward.failed": "Adding new port forwarding rule failed.",
|
||||
@ -2972,6 +2976,7 @@
|
||||
"message.success.add.network.acl": "Successfully added Network ACL list",
|
||||
"message.success.add.network.static.route": "Successfully added network Static Route",
|
||||
"message.success.add.network.permissions": "Successfully added network permissions",
|
||||
"message.success.add.physical.network": "Successfully added Physical network",
|
||||
"message.success.add.policy.rule": "Successfully added Policy rule",
|
||||
"message.success.add.port.forward": "Successfully added new port forwarding rule",
|
||||
"message.success.add.private.gateway": "Successfully added private gateway",
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:ns4="http://ns.adobe.com/SaveForWeb/1.0/"
|
||||
xmlns:ns3="http://ns.adobe.com/Variables/1.0/"
|
||||
xmlns:ns2="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
ns2:viewOrigin="262 450"
|
||||
ns2:rulerOrigin="0 0"
|
||||
ns2:pageBounds="0 792 612 0"
|
||||
viewBox="0 0 55.999999 56.000069"
|
||||
overflow="visible"
|
||||
enable-background="new 0 0 87.041 108.445"
|
||||
xml:space="preserve"
|
||||
version="1.1"
|
||||
id="svg31"
|
||||
sodipodi:docname="debian.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
style="overflow:visible"><defs
|
||||
id="defs35" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1866"
|
||||
inkscape:window-height="1017"
|
||||
id="namedview33"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.1762184"
|
||||
inkscape:cx="-62.475298"
|
||||
inkscape:cy="28.002047"
|
||||
inkscape:window-x="54"
|
||||
inkscape:window-y="26"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g28"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata2">
|
||||
<ns3:variableSets>
|
||||
<ns3:variableSet
|
||||
varSetName="binding1"
|
||||
locked="none">
|
||||
<ns3:variables />
|
||||
<ns3:sampleDataSets />
|
||||
</ns3:variableSet>
|
||||
</ns3:variableSets>
|
||||
<ns4:sfw>
|
||||
<ns4:slices />
|
||||
<ns4:sliceSourceBounds
|
||||
y="341.555"
|
||||
x="262"
|
||||
width="87.041"
|
||||
height="108.445"
|
||||
bottomLeftOrigin="true" />
|
||||
</ns4:sfw>
|
||||
<rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||
<g
|
||||
id="Layer_1"
|
||||
ns2:layer="yes"
|
||||
ns2:dimmedPercent="50"
|
||||
ns2:rgbTrio="#4F008000FFFF"
|
||||
transform="translate(-20.985947,-26.22447)">
|
||||
<g
|
||||
id="g28">
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 53.872479,55.811055 c -0.927921,0.01291 0.175567,0.478161 1.386977,0.664571 0.334609,-0.261284 0.638236,-0.525667 0.908815,-0.78282 -0.75442,0.184861 -1.522266,0.188992 -2.295792,0.118249"
|
||||
id="path4"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 58.852891,54.569696 c 0.552518,-0.762682 0.955289,-1.597656 1.097291,-2.461031 -0.123929,0.615516 -0.458022,1.146863 -0.772493,1.707644 -1.734495,1.092127 -0.163174,-0.648564 -10e-4,-1.310037 -1.865137,2.347429 -0.256121,1.407631 -0.323766,2.063424"
|
||||
id="path6"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 60.691176,49.786022 c 0.112053,-1.670981 -0.328929,-1.142732 -0.477128,-0.505012 0.172985,0.08985 0.309824,1.177846 0.477128,0.505012"
|
||||
id="path8"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 50.353918,26.946873 c 0.495201,0.08882 1.069924,0.156977 0.98937,0.275226 0.541674,-0.118765 0.664571,-0.228236 -0.98937,-0.275226"
|
||||
id="path10"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 51.343288,27.222099 -0.350101,0.07229 0.325831,-0.02892 0.02427,-0.04338"
|
||||
id="path12"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 66.784887,50.419611 c 0.05525,1.500578 -0.438917,2.228663 -0.884546,3.517529 l -0.801926,0.400705 c -0.65631,1.274407 0.06351,0.809155 -0.406386,1.822794 -1.024482,0.910881 -3.109077,2.850376 -3.776231,3.027491 -0.486939,-0.01085 0.329962,-0.574722 0.436851,-0.79573 -1.371485,0.941864 -1.100389,1.413828 -3.197894,1.985969 l -0.06145,-0.136323 c -5.173018,2.433663 -12.358856,-2.389255 -12.26436,-8.969904 -0.05525,0.417745 -0.156977,0.313438 -0.271612,0.482292 -0.266964,-3.385854 1.563576,-6.786682 4.650966,-8.175207 3.019746,-1.494898 6.559995,-0.881448 8.723078,1.13447 -1.188172,-1.556347 -3.553158,-3.206156 -6.356027,-3.051761 -2.745552,0.04337 -5.313988,1.788197 -6.171166,3.682251 -1.406598,0.885579 -1.569772,3.413738 -2.182706,3.876408 -0.824647,6.060662 1.551183,8.679186 5.570109,11.759347 0.632556,0.426524 0.178148,0.49107 0.263866,0.815869 -1.335339,-0.625327 -2.558109,-1.569256 -3.563486,-2.724897 0.533413,0.780755 1.109168,1.539822 1.853261,2.136232 -1.258916,-0.426523 -2.940741,-3.050728 -3.431811,-3.157617 2.170313,3.885702 8.805182,6.814566 12.279335,5.361494 -1.607467,0.05938 -3.64972,0.03305 -5.455991,-0.634621 -0.758551,-0.390378 -1.790263,-1.199017 -1.605918,-1.350314 4.741331,1.771157 9.639123,1.341535 13.741702,-1.94724 1.043588,-0.81277 2.183738,-2.195615 2.513184,-2.214721 -0.496234,0.746158 0.08469,0.358879 -0.296398,1.01777 1.039974,-1.677178 -0.451826,-0.682645 1.075087,-2.896333 l 0.563879,0.776624 c -0.209647,-1.39214 1.728815,-3.082743 1.532077,-5.284555 0.444596,-0.673349 0.496234,0.724471 0.02427,2.273588 0.654761,-1.718487 0.172469,-1.994746 0.340806,-3.412705 0.181763,0.476612 0.420327,0.983173 0.542707,1.48612 -0.426523,-1.660654 0.437884,-2.796673 0.651662,-3.761773 -0.21068,-0.09346 -0.658374,0.734282 -0.760616,-1.227417 0.01497,-0.852014 0.237015,-0.446662 0.322733,-0.656309 -0.167305,-0.09605 -0.606222,-0.749257 -0.873186,-2.001976 0.19364,-0.294332 0.517405,0.763198 0.780755,0.806574 -0.16937,-0.996083 -0.461121,-1.755666 -0.472997,-2.519897 -0.769395,-1.607984 -0.272128,0.214294 -0.896423,-0.69039 -0.818966,-2.554494 0.679547,-0.592796 0.780755,-1.753601 1.24136,1.798525 1.949306,4.585903 2.274104,5.740512 -0.247858,-1.407631 -0.648563,-2.771371 -1.137568,-4.090702 0.376952,0.158526 -0.607254,-2.896333 0.490037,-0.873186 -1.172165,-4.312742 -5.016557,-8.342512 -8.553191,-10.233467 0.43272,0.396057 0.979042,0.893324 0.78282,0.971296 -1.758764,-1.047203 -1.449457,-1.12879 -1.701447,-1.571321 -1.432933,-0.582984 -1.526913,0.04699 -2.476005,0.001 -2.70063,-1.432419 -3.221133,-1.280089 -5.706433,-2.177544 l 0.113085,0.528249 c -1.78923,-0.595894 -2.084595,0.226171 -4.018409,0.0021 -0.117733,-0.09191 0.619646,-0.332544 1.226384,-0.420843 -1.729847,0.228236 -1.648777,-0.340806 -3.341446,0.063 0.417229,-0.292783 0.858211,-0.486423 1.303324,-0.735314 -1.410729,0.08572 -3.36778,0.821032 -2.763625,0.15233 -2.300955,1.026548 -6.387526,2.467743 -8.680735,4.617918 l -0.07229,-0.481776 c -1.050818,1.261498 -4.582289,3.767453 -4.863712,5.401255 l -0.280906,0.06558 c -0.546839,0.925856 -0.900553,1.975125 -1.334306,2.927832 -0.715176,1.218638 -1.048236,0.468866 -0.946511,0.659924 -1.406598,2.851924 -2.10525,5.248408 -2.708889,7.213721 0.430138,0.642884 0.01033,3.870211 0.172985,6.453106 -0.706398,12.756463 8.952863,25.142168 19.511129,28.001841 1.547568,0.553548 3.849039,0.532381 5.806607,0.589182 -2.309733,-0.660445 -2.608197,-0.350104 -4.858031,-1.134472 -1.622958,-0.764231 -1.978739,-1.636901 -3.128184,-2.634532 l 0.454924,0.803992 c -2.25448,-0.7978 -1.311068,-0.987308 -3.145223,-1.568227 l 0.485907,-0.634622 c -0.730667,-0.05525 -1.935364,-1.231548 -2.26481,-1.882693 l -0.799344,0.0315 c -0.960453,-1.185074 -1.472178,-2.039154 -1.434999,-2.700627 l -0.258186,0.460088 C 37.555113,72.462514 34.31436,68.520528 35.995668,69.438122 35.683263,69.152568 35.2681,68.973386 34.817823,68.155453 l 0.342355,-0.391411 c -0.809156,-1.041006 -1.489218,-2.375312 -1.437581,-2.819909 0.431688,0.582984 0.731184,0.691939 1.027581,0.791599 -2.043285,-5.069744 -2.15792,-0.279358 -3.705488,-5.160626 l 0.32738,-0.02634 c -0.250956,-0.377984 -0.403286,-0.7885 -0.605188,-1.191271 l 0.142519,-1.420024 c -1.471145,-1.70093 -0.411549,-7.232311 -0.19932,-10.265999 0.147166,-1.233613 1.227933,-2.546748 2.049998,-4.606041 l -0.500881,-0.08623 c 0.957354,-1.669948 5.466318,-6.706644 7.554528,-6.447425 1.011573,-1.270793 -0.200869,-0.0046 -0.39864,-0.324799 2.22195,-2.299406 2.920602,-1.624507 4.420148,-2.038121 1.617278,-0.959937 -1.388009,0.37437 -0.621196,-0.366108 2.79564,-0.714143 1.98132,-1.623475 5.628458,-1.985968 0.384698,0.218941 -0.892807,0.338223 -1.213475,0.622228 2.329356,-1.139634 7.371216,-0.880415 10.646049,0.632556 3.799984,1.775805 8.069351,7.025246 8.237688,11.964348 l 0.191575,0.05164 c -0.09708,1.963248 0.300528,4.233737 -0.388312,6.319365 l 0.468866,-0.987304"
|
||||
id="path14"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 43.744352,57.084946 -0.130126,0.650629 c 0.609836,0.828261 1.093677,1.725716 1.872366,2.373247 -0.560264,-1.093677 -0.97646,-1.545502 -1.74224,-3.023876"
|
||||
id="path16"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 45.186064,57.028145 c -0.322733,-0.356814 -0.513791,-0.786435 -0.727569,-1.214508 0.204483,0.752354 0.623261,1.398853 1.013123,2.056195 l -0.285554,-0.841687"
|
||||
id="path18"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 70.696924,51.483338 -0.136323,0.341839 c -0.249924,1.775288 -0.789533,3.531987 -1.616762,5.160625 0.91398,-1.718487 1.505226,-3.598082 1.753085,-5.502464"
|
||||
id="path20"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 50.53723,26.50176 c 0.627393,-0.229786 1.542405,-0.125995 2.208009,-0.277292 -0.867506,0.07281 -1.730881,0.116184 -2.583411,0.226171 l 0.375402,0.05112"
|
||||
id="path22"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 28.511368,38.214118 c 0.144584,1.338437 -1.006926,1.857908 0.255088,0.975427 0.676447,-1.523815 -0.264383,-0.420843 -0.255088,-0.975427"
|
||||
id="path24"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
ns2:knockout="Off"
|
||||
d="m 27.028346,44.408521 c 0.290718,-0.892292 0.343388,-1.428286 0.454408,-1.944659 -0.803476,1.027065 -0.369723,1.246007 -0.454408,1.944659"
|
||||
id="path26"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 10 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.6 KiB |
152
ui/src/components/header/CreateMenu.vue
Normal file
152
ui/src/components/header/CreateMenu.vue
Normal file
@ -0,0 +1,152 @@
|
||||
// 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-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item style="width: 100%; padding: 12px">
|
||||
<router-link :to="{ path: '/action/deployVirtualMachine'}">
|
||||
<a-row>
|
||||
<a-col style="margin-right: 12px">
|
||||
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
|
||||
<template #icon>
|
||||
<cloud-server-outlined/>
|
||||
</template>
|
||||
</a-avatar>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<h3 style="margin-bottom: 0px;">
|
||||
{{ $t('label.instance') }}
|
||||
</h3>
|
||||
<small>{{ $t('label.create.instance') }}</small>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item style="width: 100%; padding: 12px" v-if="'listKubernetesClusters' in $store.getters.apis">
|
||||
<router-link :to="{ path: '/kubernetes', query: { action: 'createKubernetesCluster' } }">
|
||||
<a-row>
|
||||
<a-col style="margin-right: 12px">
|
||||
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
|
||||
<template #icon>
|
||||
<font-awesome-icon :icon="['fa-solid', 'fa-dharmachakra']" />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<h3 style="margin-bottom: 0px;">
|
||||
{{ $t('label.kubernetes') }}
|
||||
</h3>
|
||||
<small>{{ $t('label.kubernetes.cluster.create') }}</small>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item style="width: 100%; padding: 12px">
|
||||
<router-link :to="{ path: '/volume', query: { action: 'createVolume' } }">
|
||||
<a-row>
|
||||
<a-col style="margin-right: 12px">
|
||||
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
|
||||
<template #icon>
|
||||
<hdd-outlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<h3 style="margin-bottom: 0px;">
|
||||
{{ $t('label.volume') }}
|
||||
</h3>
|
||||
<small>{{ $t('label.action.create.volume') }}</small>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item style="width: 100%; padding: 12px">
|
||||
<router-link :to="{ path: '/guestnetwork', query: { action: 'createNetwork' } }">
|
||||
<a-row>
|
||||
<a-col style="margin-right: 12px">
|
||||
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
|
||||
<template #icon>
|
||||
<apartment-outlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<h3 style="margin-bottom: 0px;">
|
||||
{{ $t('label.network') }}
|
||||
</h3>
|
||||
<small>{{ $t('label.add.network') }}</small>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item style="width: 100%; padding: 12px">
|
||||
<router-link :to="{ path: '/vpc', query: { action: 'createVPC' } }">
|
||||
<a-row>
|
||||
<a-col style="margin-right: 12px">
|
||||
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
|
||||
<template #icon>
|
||||
<deployment-unit-outlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<h3 style="margin-bottom: 0px;">
|
||||
{{ $t('label.vpc') }}
|
||||
</h3>
|
||||
<small>{{ $t('label.add.vpc') }}</small>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item style="width: 100%; padding: 12px">
|
||||
<router-link :to="{ path: '/template', query: { action: 'registerTemplate' } }">
|
||||
<a-row>
|
||||
<a-col style="margin-right: 12px">
|
||||
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
|
||||
<template #icon>
|
||||
<picture-outlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<h3 style="margin-bottom: 0px;">
|
||||
{{ $t('label.templatename') }}
|
||||
</h3>
|
||||
<small>{{ $t('label.action.register.template') }}</small>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="primary">
|
||||
{{ $t('label.create') }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'CreateMenu',
|
||||
components: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -81,7 +81,7 @@ export default {
|
||||
const projects = []
|
||||
const getNextPage = () => {
|
||||
this.loading = true
|
||||
api('listProjects', { listAll: true, details: 'min', page: page, pageSize: 500, showIcon: true }).then(json => {
|
||||
api('listProjects', { listAll: true, page: page, pageSize: 500, showIcon: true }).then(json => {
|
||||
if (json?.listprojectsresponse?.project) {
|
||||
projects.push(...json.listprojectsresponse.project)
|
||||
}
|
||||
@ -130,7 +130,7 @@ export default {
|
||||
<style lang="less" scoped>
|
||||
.project {
|
||||
&-select {
|
||||
width: 30vw;
|
||||
width: 27vw;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
|
||||
<template>
|
||||
<div class="user-menu">
|
||||
<span class="action">
|
||||
<create-menu v-if="device === 'desktop'" />
|
||||
</span>
|
||||
<external-link class="action"/>
|
||||
<translation-menu class="action"/>
|
||||
<header-notice class="action"/>
|
||||
@ -69,6 +72,7 @@
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import CreateMenu from './CreateMenu'
|
||||
import ExternalLink from './ExternalLink'
|
||||
import HeaderNotice from './HeaderNotice'
|
||||
import TranslationMenu from './TranslationMenu'
|
||||
@ -80,11 +84,19 @@ import { SERVER_MANAGER } from '@/store/mutation-types'
|
||||
export default {
|
||||
name: 'UserMenu',
|
||||
components: {
|
||||
CreateMenu,
|
||||
ExternalLink,
|
||||
TranslationMenu,
|
||||
HeaderNotice,
|
||||
ResourceIcon
|
||||
},
|
||||
props: {
|
||||
device: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'desktop'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
image: '',
|
||||
|
||||
@ -41,6 +41,11 @@
|
||||
<render-icon
|
||||
v-if="children.meta.icon && typeof (children.meta.icon) === 'string'"
|
||||
:icon="children.meta.icon" />
|
||||
<font-awesome-icon
|
||||
v-else-if="children.meta.icon && Array.isArray(children.meta.icon)"
|
||||
:icon="children.meta.icon"
|
||||
class="anticon"
|
||||
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' }]" />
|
||||
<render-icon v-else :svgIcon="children.meta.icon" />
|
||||
<span>{{ $t(children.meta.title) }}</span>
|
||||
</router-link>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<template>
|
||||
<a-layout-header v-if="!headerBarFixed" :class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', theme ]" :style="{ padding: '0' }">
|
||||
<div v-if="mode === 'sidemenu'" class="header">
|
||||
<template v-if="device==='mobile'">
|
||||
<template v-if="device === 'mobile'">
|
||||
<menu-fold-outlined class="trigger" v-if="collapsed" @click="toggle" />
|
||||
<menu-unfold-outlined class="trigger" v-else @click="toggle" />
|
||||
</template>
|
||||
@ -28,7 +28,7 @@
|
||||
</template>
|
||||
<project-menu v-if="device !== 'mobile'" />
|
||||
<saml-domain-switcher style="margin-left: 20px" />
|
||||
<user-menu></user-menu>
|
||||
<user-menu :device="device"></user-menu>
|
||||
</div>
|
||||
<div v-else :class="['top-nav-header-index', theme]">
|
||||
<div class="header-index-wide">
|
||||
|
||||
@ -279,6 +279,11 @@ export default {
|
||||
&.dark {
|
||||
.ant-drawer-content {
|
||||
background-color: rgb(0, 21, 41);
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.ant-drawer-content-wrapper {
|
||||
width: 256px !important;;
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,11 +292,16 @@ export default {
|
||||
|
||||
.ant-drawer-content {
|
||||
background-color: #fff;
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.ant-drawer-content-wrapper {
|
||||
width: 256px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 0
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,12 @@
|
||||
<span v-else>
|
||||
<os-logo v-if="resource.ostypeid || resource.ostypename" :osId="resource.ostypeid" :osName="resource.ostypename" size="4x" @update-osname="setResourceOsType"/>
|
||||
<render-icon v-else-if="typeof $route.meta.icon ==='string'" style="font-size: 36px" :icon="$route.meta.icon" />
|
||||
<font-awesome-icon
|
||||
v-else-if="$route.meta.icon && Array.isArray($route.meta.icon)"
|
||||
:icon="$route.meta.icon"
|
||||
size="4x"
|
||||
class="anticon"
|
||||
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' }]" />
|
||||
<render-icon v-else style="font-size: 36px" :svgIcon="$route.meta.icon" />
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
</keep-alive>
|
||||
<a-tabs
|
||||
v-else
|
||||
style="width: 100%"
|
||||
style="width: 100%; margin-top: -12px"
|
||||
:animated="false"
|
||||
:activeKey="activeTab || tabs[0].name"
|
||||
@change="onTabChange" >
|
||||
|
||||
@ -93,7 +93,6 @@ export default {
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding-top: 9px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@ export default {
|
||||
text-align: center;
|
||||
transition: all 0.5s;
|
||||
cursor: pointer;
|
||||
top: calc(50% - 45px);
|
||||
top: calc(100% - 45px);
|
||||
z-index: 100;
|
||||
|
||||
&.left{
|
||||
|
||||
@ -24,29 +24,15 @@
|
||||
:icon="['fab', logo]"
|
||||
:size="size"
|
||||
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#666' }]"
|
||||
v-if="logo !== 'debian'" />
|
||||
<debian-icon
|
||||
v-else-if="logo === 'debian'"
|
||||
:width="size === '4x' ? 56 : 16"
|
||||
:height="size === '4x' ? 56 : 16"
|
||||
:style="{
|
||||
height: size === '4x' ? '56px' : '16px',
|
||||
width: size === '4x' ? '56px' : '16px',
|
||||
marginBottom: '-4px',
|
||||
background: $store.getters.darkMode ? 'rgba(255, 255, 255, 0.65)' : ''
|
||||
}" />
|
||||
/>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import DebianIcon from '@/assets/icons/debian.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'OsLogo',
|
||||
components: {
|
||||
DebianIcon
|
||||
},
|
||||
props: {
|
||||
osId: {
|
||||
type: String,
|
||||
|
||||
@ -20,7 +20,7 @@ import { UserLayout, BasicLayout, RouteView } from '@/layouts'
|
||||
import AutogenView from '@/views/AutogenView.vue'
|
||||
import IFramePlugin from '@/views/plugins/IFramePlugin.vue'
|
||||
|
||||
import { shallowRef, defineAsyncComponent } from 'vue'
|
||||
import { shallowRef } from 'vue'
|
||||
import { vueProps } from '@/vue-app'
|
||||
|
||||
import compute from '@/config/section/compute'
|
||||
@ -201,26 +201,7 @@ export function asyncRouterMap () {
|
||||
name: 'dashboard',
|
||||
meta: {
|
||||
title: 'label.dashboard',
|
||||
icon: 'DashboardOutlined',
|
||||
tabs: [
|
||||
{
|
||||
name: 'dashboard',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/dashboard/UsageDashboardChart')))
|
||||
},
|
||||
{
|
||||
name: 'accounts',
|
||||
show: (record, route, user) => { return record.account === user.account || ['Admin', 'DomainAdmin'].includes(user.roletype) },
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/project/AccountsTab')))
|
||||
},
|
||||
{
|
||||
name: 'limits',
|
||||
params: {
|
||||
projectid: 'id'
|
||||
},
|
||||
show: (record, route, user) => { return ['Admin'].includes(user.roletype) },
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue')))
|
||||
}
|
||||
]
|
||||
icon: 'DashboardOutlined'
|
||||
},
|
||||
component: () => import('@/views/dashboard/Dashboard')
|
||||
},
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
// under the License.
|
||||
|
||||
import { shallowRef, defineAsyncComponent } from 'vue'
|
||||
import kubernetes from '@/assets/icons/kubernetes.svg?inline'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
@ -27,7 +26,7 @@ export default {
|
||||
{
|
||||
name: 'vm',
|
||||
title: 'label.instances',
|
||||
icon: 'desktop-outlined',
|
||||
icon: 'cloud-server-outlined',
|
||||
docHelp: 'adminguide/virtual_machines.html',
|
||||
permission: ['listVirtualMachinesMetrics'],
|
||||
resourceType: 'UserVm',
|
||||
@ -456,7 +455,7 @@ export default {
|
||||
{
|
||||
name: 'kubernetes',
|
||||
title: 'label.kubernetes',
|
||||
icon: shallowRef(kubernetes),
|
||||
icon: ['fa-solid', 'fa-dharmachakra'],
|
||||
docHelp: 'plugins/cloudstack-kubernetes-service.html',
|
||||
permission: ['listKubernetesClusters'],
|
||||
columns: (store) => {
|
||||
@ -557,7 +556,7 @@ export default {
|
||||
{
|
||||
name: 'autoscalevmgroup',
|
||||
title: 'label.autoscale.vm.groups',
|
||||
icon: 'ordered-list-outlined',
|
||||
icon: 'fullscreen-outlined',
|
||||
docHelp: 'adminguide/autoscale_without_netscaler.html',
|
||||
resourceType: 'AutoScaleVmGroup',
|
||||
permission: ['listAutoScaleVmGroups'],
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
// under the License.
|
||||
|
||||
import { shallowRef, defineAsyncComponent } from 'vue'
|
||||
import kubernetes from '@/assets/icons/kubernetes.svg?inline'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
@ -340,7 +339,7 @@ export default {
|
||||
{
|
||||
name: 'kubernetesiso',
|
||||
title: 'label.kubernetes.isos',
|
||||
icon: shallowRef(kubernetes),
|
||||
icon: ['fa-solid', 'fa-dharmachakra'],
|
||||
docHelp: 'plugins/cloudstack-kubernetes-service.html#kubernetes-supported-versions',
|
||||
permission: ['listKubernetesSupportedVersions'],
|
||||
columns: ['name', 'state', 'semanticversion', 'isostate', 'mincpunumber', 'minmemory', 'zonename'],
|
||||
|
||||
@ -21,7 +21,7 @@ import store from '@/store'
|
||||
export default {
|
||||
name: 'host',
|
||||
title: 'label.hosts',
|
||||
icon: 'desktop-outlined',
|
||||
icon: 'database-outlined',
|
||||
docHelp: 'conceptsandterminology/concepts.html#about-hosts',
|
||||
permission: ['listHostsMetrics'],
|
||||
resourceType: 'Host',
|
||||
|
||||
@ -21,7 +21,7 @@ import store from '@/store'
|
||||
export default {
|
||||
name: 'storagepool',
|
||||
title: 'label.primary.storage',
|
||||
icon: 'database-outlined',
|
||||
icon: 'hdd-outlined',
|
||||
docHelp: 'adminguide/storage.html#primary-storage',
|
||||
permission: ['listStoragePoolsMetrics'],
|
||||
columns: () => {
|
||||
|
||||
@ -21,7 +21,7 @@ import store from '@/store'
|
||||
export default {
|
||||
name: 'storage',
|
||||
title: 'label.storage',
|
||||
icon: 'database-outlined',
|
||||
icon: 'hdd-outlined',
|
||||
children: [
|
||||
{
|
||||
name: 'volume',
|
||||
|
||||
@ -22,11 +22,11 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
// import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
// import { far } from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
import { faCentos, faUbuntu, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faCompactDisc, faCameraRetro } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCentos, faUbuntu, faDebian, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faCompactDisc, faCameraRetro, faDharmachakra } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faCentos, faUbuntu, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava)
|
||||
library.add(faCompactDisc, faCameraRetro)
|
||||
library.add(faCentos, faUbuntu, faDebian, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava)
|
||||
library.add(faCompactDisc, faCameraRetro, faDharmachakra)
|
||||
|
||||
export default {
|
||||
install: (app) => {
|
||||
|
||||
@ -63,7 +63,8 @@ import {
|
||||
Slider,
|
||||
AutoComplete,
|
||||
Collapse,
|
||||
Space
|
||||
Space,
|
||||
Statistic
|
||||
} from 'ant-design-vue'
|
||||
import VueClipboard from 'vue3-clipboard'
|
||||
import VueCropper from 'vue-cropper'
|
||||
@ -131,5 +132,6 @@ export default {
|
||||
app.use(Collapse)
|
||||
app.use(Descriptions)
|
||||
app.use(Space)
|
||||
app.use(Statistic)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
.dark-mode {
|
||||
background: @dark-bgColor;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1, h2, h3, h4, h5, h6, .ant-statistic-title, .ant-statistic-content {
|
||||
color: @dark-text-color-3;
|
||||
}
|
||||
|
||||
|
||||
@ -519,6 +519,11 @@ a {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-left>.ant-tabs-nav .ant-tabs-tab {
|
||||
padding: 8px 24px 8px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ant-steps {
|
||||
&-item-container {
|
||||
&:hover {
|
||||
|
||||
@ -43,7 +43,7 @@ export const deviceEnquire = function (callback) {
|
||||
}
|
||||
|
||||
enquireJs
|
||||
.register('screen and (max-width: 800px)', matchMobile)
|
||||
.register('screen and (min-width: 800px) and (max-width: 1366px)', matchTablet)
|
||||
.register('screen and (min-width: 1367px)', matchDesktop)
|
||||
.register('screen and (max-width: 765px)', matchMobile)
|
||||
.register('screen and (min-width: 766px) and (max-width: 1279px)', matchTablet)
|
||||
.register('screen and (min-width: 1280px)', matchDesktop)
|
||||
}
|
||||
|
||||
@ -393,8 +393,8 @@
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
<div :style="this.$store.getters.shutdownTriggered ? 'margin-top: 25px;' : null">
|
||||
<div v-if="dataView" style="margin-top: -10px">
|
||||
<div :style="this.$store.getters.shutdownTriggered ? 'margin-top: 24px; margin-bottom: 12px' : null">
|
||||
<div v-if="dataView">
|
||||
<slot name="resource" v-if="$route.path.startsWith('/quotasummary') || $route.path.startsWith('/publicip')"></slot>
|
||||
<resource-view
|
||||
v-else
|
||||
@ -1059,6 +1059,19 @@ export default {
|
||||
this.loading = false
|
||||
this.searchParams = params
|
||||
})
|
||||
|
||||
if ('action' in this.$route.query) {
|
||||
const actionName = this.$route.query.action
|
||||
for (const action of this.actions) {
|
||||
if (action.listView && action.api === actionName) {
|
||||
this.execAction(action, false)
|
||||
const query = Object.assign({}, this.$route.query)
|
||||
delete query.action
|
||||
this.$router.replace({ query })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
closeAction () {
|
||||
this.actionLoading = false
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<a-row class="capacity-dashboard" :gutter="12">
|
||||
<a-col :xl="18">
|
||||
<a-row class="capacity-dashboard" :gutter="[12,12]">
|
||||
<a-col :span="24">
|
||||
<div class="capacity-dashboard-wrapper">
|
||||
<div class="capacity-dashboard-select">
|
||||
<a-select
|
||||
@ -41,91 +41,282 @@
|
||||
<div class="capacity-dashboard-button">
|
||||
<a-button
|
||||
shape="round"
|
||||
@click="() => { listCapacity(zoneSelected, true); listEvents() }">
|
||||
@click="() => { updateData(zoneSelected); listAlerts(); listEvents(); }">
|
||||
<reload-outlined/>
|
||||
{{ $t('label.fetch.latest') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-row :gutter="12">
|
||||
<a-col
|
||||
:xs="12"
|
||||
:sm="8"
|
||||
:md="6"
|
||||
:style="{ marginBottom: '12px' }"
|
||||
v-for="stat in stats"
|
||||
:key="stat.type">
|
||||
<chart-card :loading="loading">
|
||||
<router-link :to="{ path: '/zone/' + zoneSelected.id }">
|
||||
<div class="capacity-dashboard-chart-card-inner">
|
||||
<h3>{{ $t(ts[stat.name]) }}</h3>
|
||||
<a-progress
|
||||
type="dashboard"
|
||||
:status="getStatus(parseFloat(stat.percentused))"
|
||||
:percent="parseFloat(stat.percentused)"
|
||||
:format="percent => `${parseFloat(stat.percentused).toFixed(2)}%`"
|
||||
:strokeColor="getStrokeColour(parseFloat(stat.percentused))"
|
||||
:width="100" />
|
||||
</div>
|
||||
</router-link>
|
||||
<template #footer>
|
||||
<div class="center">{{ displayData(stat.name, stat.capacityused) }} / {{ displayData(stat.name, stat.capacitytotal) }}</div>
|
||||
</template>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
|
||||
<a-col :xl="6" class="dashboard-event">
|
||||
<chart-card :loading="loading">
|
||||
<div style="text-align: center">
|
||||
<a-tooltip placement="bottom" class="capacity-dashboard-button-wrapper">
|
||||
<template #title>
|
||||
{{ $t('label.view') + ' ' + $t('label.host.alerts') }}
|
||||
</template>
|
||||
<a-button type="primary" danger shape="circle">
|
||||
<router-link :to="{ name: 'host', query: {'state': 'Alert'} }">
|
||||
<desktop-outlined class="capacity-dashboard-button-icon" />
|
||||
</router-link>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="bottom" class="capacity-dashboard-button-wrapper">
|
||||
<template #title>
|
||||
{{ $t('label.view') + ' ' + $t('label.alerts') }}
|
||||
</template>
|
||||
<a-button shape="circle">
|
||||
<router-link :to="{ name: 'alert' }">
|
||||
<flag-outlined class="capacity-dashboard-button-icon" />
|
||||
</router-link>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="bottom" class="capacity-dashboard-button-wrapper">
|
||||
<template #title>
|
||||
{{ $t('label.view') + ' ' + $t('label.events') }}
|
||||
</template>
|
||||
<a-button shape="circle">
|
||||
<router-link :to="{ name: 'event' }">
|
||||
<schedule-outlined class="capacity-dashboard-button-icon" />
|
||||
</router-link>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="capacity-dashboard-footer">
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
:color="getEventColour(event)">
|
||||
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span><br/>
|
||||
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
|
||||
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
|
||||
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<router-link :to="{ path: '/infrasummary' }" v-if="!zoneSelected.id">
|
||||
<h3>
|
||||
<bank-outlined />
|
||||
{{ $t('label.infrastructure') }}
|
||||
</h3>
|
||||
</router-link>
|
||||
<router-link :to="{ path: '/zone/' + zoneSelected.id }" v-else>
|
||||
<h3>
|
||||
<global-outlined />
|
||||
{{ $t('label.zone') }}
|
||||
</h3>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider style="margin: 0px 0px; border-width: 0px"/>
|
||||
<a-row :gutter="[12, 12]">
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/pod', query: { zoneid: zoneSelected.id } }">
|
||||
<a-statistic
|
||||
:title="$t('label.pods')"
|
||||
:value="data.pods"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<appstore-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/cluster', query: { zoneid: zoneSelected.id } }">
|
||||
<a-statistic
|
||||
:title="$t('label.clusters')"
|
||||
:value="data.clusters"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<cluster-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/host', query: { zoneid: zoneSelected.id } }">
|
||||
<a-statistic
|
||||
:title="$t('label.hosts')"
|
||||
:value="data.totalHosts"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<database-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/host', query: { zoneid: zoneSelected.id, state: 'alert' } }">
|
||||
<a-statistic
|
||||
:title="$t('label.host.alerts')"
|
||||
:value="data.alertHosts"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<database-outlined/>
|
||||
<status class="status" text="Alert" style="margin-left: -10px"/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/storagepool', query: { zoneid: zoneSelected.id } }">
|
||||
<a-statistic
|
||||
:title="$t('label.primary.storage')"
|
||||
:value="data.pools"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<hdd-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/systemvm', query: { zoneid: zoneSelected.id } }">
|
||||
<a-statistic
|
||||
:title="$t('label.system.vms')"
|
||||
:value="data.systemvms"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<thunderbolt-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/router', query: { zoneid: zoneSelected.id } }">
|
||||
<a-statistic
|
||||
:title="$t('label.virtual.routers')"
|
||||
:value="data.routers"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<fork-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/vm', query: { zoneid: zoneSelected.id, projectid: '-1' } }">
|
||||
<a-statistic
|
||||
:title="$t('label.instances')"
|
||||
:value="data.instances"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<cloud-server-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3><cloud-outlined /> {{ $t('label.compute') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div v-for="ctype in ['MEMORY', 'CPU', 'CPU_CORE', 'GPU']" :key="ctype" >
|
||||
<div v-if="statsMap[ctype]">
|
||||
<div>
|
||||
<strong>{{ $t(ts[ctype]) }}</strong>
|
||||
</div>
|
||||
<a-progress
|
||||
status="active"
|
||||
:percent="statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) : 0"
|
||||
:format="p => statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) + '%' : '0%'"
|
||||
stroke-color="#52c41a"
|
||||
size="small"
|
||||
style="width:95%; float: left"
|
||||
/>
|
||||
<br/>
|
||||
<div style="text-align: center">
|
||||
{{ displayData(ctype, statsMap[ctype]?.capacityused) }} {{ $t('label.allocated') }} | {{ displayData(ctype, statsMap[ctype]?.capacitytotal) }} {{ $t('label.total') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3><hdd-outlined /> {{ $t('label.storage') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div v-for="ctype in ['STORAGE', 'STORAGE_ALLOCATED', 'LOCAL_STORAGE', 'SECONDARY_STORAGE']" :key="ctype" >
|
||||
<div v-if="statsMap[ctype]">
|
||||
<div>
|
||||
<strong>{{ $t(ts[ctype]) }}</strong>
|
||||
</div>
|
||||
<a-progress
|
||||
status="active"
|
||||
:percent="statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) : 0"
|
||||
:format="p => statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) + '%' : '0%'"
|
||||
stroke-color="#52c41a"
|
||||
size="small"
|
||||
style="width:95%; float: left"
|
||||
/>
|
||||
<br/>
|
||||
<div style="text-align: center">
|
||||
{{ displayData(ctype, statsMap[ctype]?.capacityused) }} <span v-if="ctype !== 'STORAGE'">{{ $t('label.allocated') }}</span><span v-else>{{ $t('label.used') }}</span> | {{ displayData(ctype, statsMap[ctype]?.capacitytotal) }} {{ $t('label.total') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3><apartment-outlined /> {{ $t('label.network') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div v-for="ctype in ['VLAN', 'VIRTUAL_NETWORK_PUBLIC_IP', 'VIRTUAL_NETWORK_IPV6_SUBNET', 'DIRECT_ATTACHED_PUBLIC_IP', 'PRIVATE_IP']" :key="ctype" >
|
||||
<div v-if="statsMap[ctype]">
|
||||
<div>
|
||||
<strong>{{ $t(ts[ctype]) }}</strong>
|
||||
</div>
|
||||
<a-progress
|
||||
status="active"
|
||||
:percent="statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) : 0"
|
||||
:format="p => statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) + '%' : '0%'"
|
||||
stroke-color="#52c41a"
|
||||
size="small"
|
||||
style="width:95%; float: left"
|
||||
/>
|
||||
<br/>
|
||||
<div style="text-align: center">
|
||||
{{ displayData(ctype, statsMap[ctype]?.capacityused) }} {{ $t('label.allocated') }} | {{ displayData(ctype, statsMap[ctype]?.capacitytotal) }} {{ $t('label.total') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<router-link :to="{ path: '/alert' }">
|
||||
<a-card :loading="loading" :bordered="false" class="dashboard-card dashboard-event">
|
||||
<div class="center" style="margin-top: -8px">
|
||||
<h3>
|
||||
<flag-outlined />
|
||||
{{ $t('label.alerts') }}
|
||||
</h3>
|
||||
</div>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="alert in alerts"
|
||||
:key="alert.id"
|
||||
color="red">
|
||||
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(alert.sent) }}</small></span>
|
||||
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/alert/' + alert.id }">{{ alert.name }}</router-link></small></span><br/>
|
||||
<span :style="{ color: '#aaa' }">{{ alert.description }}</span>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<router-link :to="{ path: '/alert' }">
|
||||
<a-button>
|
||||
{{ $t('label.view') }} {{ $t('label.alerts') }}
|
||||
</a-button>
|
||||
</router-link>
|
||||
</a-card>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<router-link :to="{ path: '/event' }">
|
||||
<a-card :loading="loading" :bordered="false" class="dashboard-card dashboard-event">
|
||||
<div class="center" style="margin-top: -8px">
|
||||
<h3>
|
||||
<schedule-outlined />
|
||||
{{ $t('label.events') }}
|
||||
</h3>
|
||||
</div>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
:color="getEventColour(event)">
|
||||
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span>
|
||||
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
|
||||
<span>
|
||||
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
|
||||
</span>
|
||||
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<router-link :to="{ path: '/event' }">
|
||||
<a-button>
|
||||
{{ $t('label.view') }} {{ $t('label.events') }}
|
||||
</a-button>
|
||||
</router-link>
|
||||
</a-card>
|
||||
</router-link>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
@ -135,21 +326,35 @@ import { api } from '@/api'
|
||||
import ChartCard from '@/components/widgets/ChartCard'
|
||||
import ResourceIcon from '@/components/view/ResourceIcon'
|
||||
import ResourceLabel from '@/components/widgets/ResourceLabel'
|
||||
import Status from '@/components/widgets/Status'
|
||||
|
||||
export default {
|
||||
name: 'CapacityDashboard',
|
||||
components: {
|
||||
ChartCard,
|
||||
ResourceIcon,
|
||||
ResourceLabel
|
||||
ResourceLabel,
|
||||
Status
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: true,
|
||||
tabKey: 'alerts',
|
||||
alerts: [],
|
||||
events: [],
|
||||
zones: [],
|
||||
zoneSelected: {},
|
||||
stats: [],
|
||||
statsMap: {},
|
||||
data: {
|
||||
pods: 0,
|
||||
clusters: 0,
|
||||
totalHosts: 0,
|
||||
alertHosts: 0,
|
||||
pools: 0,
|
||||
instances: 0,
|
||||
systemvms: 0,
|
||||
routers: 0
|
||||
},
|
||||
ts: {
|
||||
CPU: 'label.cpu',
|
||||
CPU_CORE: 'label.cpunumber',
|
||||
@ -159,8 +364,8 @@ export default {
|
||||
MEMORY: 'label.memory',
|
||||
PRIVATE_IP: 'label.management.ips',
|
||||
SECONDARY_STORAGE: 'label.secondary.storage',
|
||||
STORAGE: 'label.storage',
|
||||
STORAGE_ALLOCATED: 'label.primary.storage',
|
||||
STORAGE: 'label.primary.storage.used',
|
||||
STORAGE_ALLOCATED: 'label.primary.storage.allocated',
|
||||
VIRTUAL_NETWORK_PUBLIC_IP: 'label.public.ips',
|
||||
VLAN: 'label.vlan',
|
||||
VIRTUAL_NETWORK_IPV6_SUBNET: 'label.ipv6.subnets'
|
||||
@ -196,13 +401,10 @@ export default {
|
||||
}
|
||||
return 'normal'
|
||||
},
|
||||
getStrokeColour (value) {
|
||||
if (value >= 80) {
|
||||
return this.$config.theme['@graph-exception-color'] || 'red'
|
||||
}
|
||||
return this.$config.theme['@graph-normal-color'] || 'primary'
|
||||
},
|
||||
displayData (dataType, value) {
|
||||
if (!value) {
|
||||
value = 0
|
||||
}
|
||||
switch (dataType) {
|
||||
case 'CPU':
|
||||
value = parseFloat(value / 1000.0, 10).toFixed(2) + ' GHz'
|
||||
@ -214,9 +416,9 @@ export default {
|
||||
case 'LOCAL_STORAGE':
|
||||
value = parseFloat(value / (1024 * 1024 * 1024.0), 10).toFixed(2)
|
||||
if (value >= 1024.0) {
|
||||
value = parseFloat(value / 1024.0).toFixed(2) + ' TB'
|
||||
value = parseFloat(value / 1024.0).toFixed(2) + ' TiB'
|
||||
} else {
|
||||
value = value + ' GB'
|
||||
value = value + ' GiB'
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -224,26 +426,134 @@ export default {
|
||||
},
|
||||
fetchData () {
|
||||
this.listZones()
|
||||
this.listAlerts()
|
||||
this.listEvents()
|
||||
},
|
||||
listCapacity (zone, latest = false) {
|
||||
const params = {
|
||||
zoneid: zone.id,
|
||||
fetchlatest: latest
|
||||
listCapacity (zone, latest = false, additive = false) {
|
||||
this.loading = true
|
||||
api('listCapacity', { zoneid: zone.id, fetchlatest: latest }).then(json => {
|
||||
this.loading = false
|
||||
let stats = []
|
||||
if (json && json.listcapacityresponse && json.listcapacityresponse.capacity) {
|
||||
stats = json.listcapacityresponse.capacity
|
||||
}
|
||||
for (const stat of stats) {
|
||||
if (additive) {
|
||||
for (const [key, value] of Object.entries(stat)) {
|
||||
if (stat.name in this.statsMap) {
|
||||
if (key in this.statsMap[stat.name]) {
|
||||
this.statsMap[stat.name][key] += value
|
||||
} else {
|
||||
this.statsMap[stat.name][key] = value
|
||||
}
|
||||
} else {
|
||||
this.statsMap[stat.name] = { key: value }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.statsMap[stat.name] = stat
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateData (zone) {
|
||||
if (!zone.id) {
|
||||
this.statsMap = {}
|
||||
for (const zone of this.zones.slice(1)) {
|
||||
this.listCapacity(zone, true, true)
|
||||
}
|
||||
} else {
|
||||
this.statsMap = {}
|
||||
this.listCapacity(this.zoneSelected, true)
|
||||
}
|
||||
|
||||
this.data = {
|
||||
pods: 0,
|
||||
clusters: 0,
|
||||
totalHosts: 0,
|
||||
alertHosts: 0,
|
||||
pools: 0,
|
||||
instances: 0,
|
||||
systemvms: 0,
|
||||
routers: 0
|
||||
}
|
||||
this.loading = true
|
||||
api('listCapacity', params).then(json => {
|
||||
this.stats = []
|
||||
api('listPods', { zoneid: zone.id }).then(json => {
|
||||
this.loading = false
|
||||
if (json && json.listcapacityresponse && json.listcapacityresponse.capacity) {
|
||||
this.stats = json.listcapacityresponse.capacity
|
||||
this.data.pods = json?.listpodsresponse?.count
|
||||
if (!this.data.pods) {
|
||||
this.data.pods = 0
|
||||
}
|
||||
})
|
||||
api('listClusters', { zoneid: zone.id }).then(json => {
|
||||
this.loading = false
|
||||
this.data.clusters = json?.listclustersresponse?.count
|
||||
if (!this.data.clusters) {
|
||||
this.data.clusters = 0
|
||||
}
|
||||
})
|
||||
api('listHosts', { zoneid: zone.id, listall: true, details: 'min', type: 'routing', page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.totalHosts = json?.listhostsresponse?.count
|
||||
if (!this.data.totalHosts) {
|
||||
this.data.totalHosts = 0
|
||||
}
|
||||
})
|
||||
api('listHosts', { zoneid: zone.id, listall: true, details: 'min', type: 'routing', state: 'alert', page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.alertHosts = json?.listhostsresponse?.count
|
||||
if (!this.data.alertHosts) {
|
||||
this.data.alertHosts = 0
|
||||
}
|
||||
})
|
||||
api('listStoragePools', { zoneid: zone.id }).then(json => {
|
||||
this.loading = false
|
||||
this.data.pools = json?.liststoragepoolsresponse?.count
|
||||
if (!this.data.pools) {
|
||||
this.data.pools = 0
|
||||
}
|
||||
})
|
||||
api('listSystemVms', { zoneid: zone.id }).then(json => {
|
||||
this.loading = false
|
||||
this.data.systemvms = json?.listsystemvmsresponse?.count
|
||||
if (!this.data.systemvms) {
|
||||
this.data.systemvms = 0
|
||||
}
|
||||
})
|
||||
api('listRouters', { zoneid: zone.id, listall: true }).then(json => {
|
||||
this.loading = false
|
||||
this.data.routers = json?.listroutersresponse?.count
|
||||
if (!this.data.routers) {
|
||||
this.data.routers = 0
|
||||
}
|
||||
})
|
||||
api('listVirtualMachines', { zoneid: zone.id, listall: true, projectid: '-1', details: 'min', page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.instances = json?.listvirtualmachinesresponse?.count
|
||||
if (!this.data.instances) {
|
||||
this.data.instances = 0
|
||||
}
|
||||
})
|
||||
},
|
||||
listAlerts () {
|
||||
const params = {
|
||||
page: 1,
|
||||
pagesize: 8,
|
||||
listall: true
|
||||
}
|
||||
this.loading = true
|
||||
api('listAlerts', params).then(json => {
|
||||
this.alerts = []
|
||||
this.loading = false
|
||||
if (json && json.listalertsresponse && json.listalertsresponse.alert) {
|
||||
this.alerts = json.listalertsresponse.alert
|
||||
}
|
||||
})
|
||||
},
|
||||
listEvents () {
|
||||
const params = {
|
||||
page: 1,
|
||||
pagesize: 6,
|
||||
pagesize: 8,
|
||||
listall: true
|
||||
}
|
||||
this.loading = true
|
||||
@ -269,15 +579,16 @@ export default {
|
||||
if (json && json.listzonesresponse && json.listzonesresponse.zone) {
|
||||
this.zones = json.listzonesresponse.zone
|
||||
if (this.zones.length > 0) {
|
||||
this.zones.splice(0, 0, { name: this.$t('label.all.zone') })
|
||||
this.zoneSelected = this.zones[0]
|
||||
this.listCapacity(this.zones[0])
|
||||
this.updateData(this.zones[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
changeZone (index) {
|
||||
this.zoneSelected = this.zones[index]
|
||||
this.listCapacity(this.zoneSelected)
|
||||
this.updateData(this.zoneSelected)
|
||||
},
|
||||
filterZone (input, option) {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
@ -290,7 +601,6 @@ export default {
|
||||
.capacity-dashboard {
|
||||
&-wrapper {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&-chart-card-inner {
|
||||
@ -313,7 +623,7 @@ export default {
|
||||
|
||||
&-button {
|
||||
width: auto;
|
||||
padding-left: 12px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
&-button-icon {
|
||||
@ -321,21 +631,28 @@ export default {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
&-title {
|
||||
padding-top: 12px;
|
||||
padding-left: 3px;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
width: 100%;
|
||||
min-height: 370px;
|
||||
}
|
||||
|
||||
.dashboard-event {
|
||||
width: 100%;
|
||||
overflow-x:hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 370px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-event {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -16,82 +16,331 @@
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<a-row class="usage-dashboard" :gutter="12">
|
||||
<a-col :xl="16" style="padding-left: 0; padding-right: 0;">
|
||||
<a-row>
|
||||
<a-card style="width: 100%">
|
||||
<a-tabs
|
||||
v-if="showProject"
|
||||
:animated="false"
|
||||
@change="onTabChange">
|
||||
<template v-for="tab in $route.meta.tabs" :key="tab.name">
|
||||
<a-tab-pane
|
||||
v-if="'show' in tab ? tab.show(project, $route, $store.getters.userInfo) : true"
|
||||
:tab="$t('label.' + tab.name)"
|
||||
:key="tab.name">
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="tab.component"
|
||||
:resource="project"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:stats="stats" />
|
||||
</keep-alive>
|
||||
</a-tab-pane>
|
||||
</template>
|
||||
</a-tabs>
|
||||
<a-row :gutter="24" v-else>
|
||||
<a-col
|
||||
class="usage-dashboard-chart-tile"
|
||||
:xs="12"
|
||||
:md="8"
|
||||
v-for="stat in stats"
|
||||
:key="stat.type">
|
||||
<a-card
|
||||
class="usage-dashboard-chart-card"
|
||||
:bordered="false"
|
||||
:loading="loading"
|
||||
:style="stat.bgcolor ? { 'background': stat.bgcolor } : {}">
|
||||
<router-link v-if="stat.path" :to="{ path: stat.path, query: stat.query }">
|
||||
<div
|
||||
class="usage-dashboard-chart-card-inner">
|
||||
<h3>{{ stat.name }}</h3>
|
||||
<h2>
|
||||
<render-icon :icon="stat.icon" />
|
||||
{{ stat.count == undefined ? 0 : stat.count }}
|
||||
</h2>
|
||||
</div>
|
||||
</router-link>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<a-col :xl="8">
|
||||
<chart-card :loading="loading" >
|
||||
<div class="usage-dashboard-chart-card-inner">
|
||||
<a-button>
|
||||
<router-link :to="{ name: 'event' }">
|
||||
{{ $t('label.view') + ' ' + $t('label.events') }}
|
||||
</router-link>
|
||||
</a-button>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="usage-dashboard-chart-footer">
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
:color="getEventColour(event)">
|
||||
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span><br/>
|
||||
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
|
||||
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
|
||||
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-row class="capacity-dashboard" :gutter="[12,12]">
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3>
|
||||
<dashboard-outlined /> {{ $t('label.resources') }}
|
||||
<span style="float: right" v-if="showProject">
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<router-link :to="{ path: '/project/' + project.id }">
|
||||
<project-outlined/>
|
||||
{{ $t('label.view') }} {{ $t('label.project') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="showProject && ['Admin'].includes($store.getters.userInfo.roletype)">
|
||||
<router-link :to="{ path: '/project/' + project.id, query: { tab: 'limits.configure' } }">
|
||||
<setting-outlined/>
|
||||
{{ $t('label.configure') }} {{ $t('label.project') }} {{ $t('label.limits') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button size="small" type="text">
|
||||
<more-outlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<a-row :gutter="[10, 10]">
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/vm' }">
|
||||
<a-statistic
|
||||
:title="$t('label.instances')"
|
||||
:value="data.instances"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<cloud-server-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/kubernetes' }">
|
||||
<a-statistic
|
||||
:title="$t('label.kubernetes.cluster')"
|
||||
:value="data.kubernetes"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<cluster-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/volume' }">
|
||||
<a-statistic
|
||||
:title="$t('label.volumes')"
|
||||
:value="data.volumes"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<hdd-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/snapshot' }">
|
||||
<a-statistic
|
||||
:title="$t('label.snapshots')"
|
||||
:value="data.snapshots"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<build-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/guestnetwork' }">
|
||||
<a-statistic
|
||||
:title="$t('label.guest.networks')"
|
||||
:value="data.networks"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<apartment-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/vpc' }">
|
||||
<a-statistic
|
||||
:title="$t('label.vpcs')"
|
||||
:value="data.vpcs"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<deployment-unit-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/publicip' }">
|
||||
<a-statistic
|
||||
:title="$t('label.public.ips')"
|
||||
:value="data.ips"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<environment-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/template', query: { templatefilter: 'self', filter: 'self' } }">
|
||||
<a-statistic
|
||||
:title="$t('label.templates')"
|
||||
:value="data.templates"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<picture-outlined/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3>
|
||||
<cloud-outlined /> {{ $t('label.compute') }}
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/vm', query: { state: 'running', filter: 'running' } }">
|
||||
<a-statistic
|
||||
:title="$t('label.running') + ' ' + $t('label.instances')"
|
||||
:value="data.running"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<status class="status" text="Running"/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<router-link :to="{ path: '/vm', query: { state: 'stopped', filter: 'stopped' } }">
|
||||
<a-statistic
|
||||
:title="$t('label.stopped') + ' ' + $t('label.instances')"
|
||||
:value="data.stopped"
|
||||
:value-style="{ color: $config.theme['@primary-color'] }">
|
||||
<template #prefix>
|
||||
<status class="status" text="Stopped"/>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</router-link>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider style="margin: 1px 0px; border-width: 0px;"/>
|
||||
<div
|
||||
v-for="usageType in ['vm', 'cpu', 'memory', 'project']"
|
||||
:key="usageType">
|
||||
<div v-if="usageType + 'total' in entity">
|
||||
<div>
|
||||
<strong>
|
||||
{{ $t(getLabel(usageType)) }}
|
||||
</strong>
|
||||
<span style="float: right">
|
||||
{{ getValue(usageType, entity[usageType + 'total']) }} {{ $t('label.used') }}
|
||||
</span>
|
||||
</div>
|
||||
<a-progress
|
||||
status="active"
|
||||
:percent="parseFloat(getPercentUsed(entity[usageType + 'total'], entity[usageType + 'limit']))"
|
||||
:format="p => resource[item + 'limit'] !== '-1' && resource[item + 'limit'] !== 'Unlimited' ? p.toFixed(0) + '%' : ''"
|
||||
stroke-color="#52c41a"
|
||||
size="small"
|
||||
/>
|
||||
<br/>
|
||||
<div style="text-align: center">
|
||||
{{ entity[usageType + 'available'] === 'Unlimited' ? $t('label.unlimited') : getValue(usageType, entity[usageType + 'available']) }} {{ $t('label.available') }}
|
||||
{{ entity[usageType + 'limit'] === 'Unlimited' ? '' : (' | ' + getValue(usageType, entity[usageType + 'limit']) + ' ' + $t('label.limit')) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3><hdd-outlined /> {{ $t('label.storage') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<div
|
||||
v-for="usageType in ['volume', 'snapshot', 'template', 'primarystorage', 'secondarystorage']"
|
||||
:key="usageType">
|
||||
<div>
|
||||
<div>
|
||||
<strong>
|
||||
{{ $t(getLabel(usageType)) }}
|
||||
</strong>
|
||||
<span style="float: right">
|
||||
{{ getValue(usageType, entity[usageType + 'total']) }} {{ $t('label.used') }}
|
||||
</span>
|
||||
</div>
|
||||
<a-progress
|
||||
status="active"
|
||||
:percent="parseFloat(getPercentUsed(entity[usageType + 'total'], entity[usageType + 'limit']))"
|
||||
:format="p => resource[item + 'limit'] !== '-1' && resource[item + 'limit'] !== 'Unlimited' ? p.toFixed(0) + '%' : ''"
|
||||
stroke-color="#52c41a"
|
||||
size="small"
|
||||
/>
|
||||
<br/>
|
||||
<div style="text-align: center">
|
||||
{{ entity[usageType + 'available'] === 'Unlimited' ? $t('label.unlimited') : getValue(usageType, entity[usageType + 'available']) }} {{ $t('label.available') }}
|
||||
{{ entity[usageType + 'limit'] === 'Unlimited' ? '' : (' | ' + getValue(usageType, entity[usageType + 'limit']) + ' ' + $t('label.limit')) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }" class="dashboard-card">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3><apartment-outlined /> {{ $t('label.network') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<div
|
||||
v-for="usageType in ['ip', 'network', 'vpc']"
|
||||
:key="usageType">
|
||||
<div>
|
||||
<div>
|
||||
<strong>
|
||||
{{ $t(getLabel(usageType)) }}
|
||||
</strong>
|
||||
<span style="float: right">
|
||||
{{ getValue(usageType, entity[usageType + 'total']) }} {{ $t('label.used') }}
|
||||
</span>
|
||||
</div>
|
||||
<a-progress
|
||||
status="active"
|
||||
:percent="parseFloat(getPercentUsed(entity[usageType + 'total'], entity[usageType + 'limit']))"
|
||||
:format="p => resource[item + 'limit'] !== '-1' && resource[item + 'limit'] !== 'Unlimited' ? p.toFixed(0) + '%' : ''"
|
||||
stroke-color="#52c41a"
|
||||
size="small"
|
||||
/>
|
||||
<br/>
|
||||
<div style="text-align: center">
|
||||
{{ entity[usageType + 'available'] === 'Unlimited' ? $t('label.unlimited') : getValue(usageType, entity[usageType + 'available']) }} {{ $t('label.available') }}
|
||||
{{ entity[usageType + 'limit'] === 'Unlimited' ? '' : (' | ' + getValue(usageType, entity[usageType + 'limit']) + ' ' + $t('label.limit')) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }" class="dashboard-card">
|
||||
<chart-card :loading="loading" class="dashboard-card">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3><render-icon :icon="$config.userCard.icon" /> {{ $t($config.userCard.title) }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<a-list item-layout="horizontal" :data-source="$config.userCard.links">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta :description="item.text">
|
||||
<template #title>
|
||||
<a :href="item.link" target="_blank"><h4>{{ item.title }}</h4></a>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
|
||||
<template #icon>
|
||||
<render-icon :icon="item.icon" />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
|
||||
<chart-card :loading="loading" class="dashboard-card dashboard-event">
|
||||
<template #title>
|
||||
<div class="center">
|
||||
<h3><schedule-outlined /> {{ $t('label.events') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider style="margin: 6px 0px; border-width: 0px"/>
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
:color="getEventColour(event)">
|
||||
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span>
|
||||
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
|
||||
<span>
|
||||
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
|
||||
</span>
|
||||
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<router-link :to="{ path: '/event' }">
|
||||
<a-button>
|
||||
{{ $t('label.view') }} {{ $t('label.events') }}
|
||||
</a-button>
|
||||
</router-link>
|
||||
</chart-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@ -104,13 +353,15 @@ import store from '@/store'
|
||||
import ChartCard from '@/components/widgets/ChartCard'
|
||||
import UsageDashboardChart from '@/views/dashboard/UsageDashboardChart'
|
||||
import ResourceLabel from '@/components/widgets/ResourceLabel'
|
||||
import Status from '@/components/widgets/Status'
|
||||
|
||||
export default {
|
||||
name: 'UsageDashboard',
|
||||
components: {
|
||||
ChartCard,
|
||||
UsageDashboardChart,
|
||||
ResourceLabel
|
||||
ResourceLabel,
|
||||
Status
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
@ -129,9 +380,29 @@ export default {
|
||||
loading: false,
|
||||
showAction: false,
|
||||
showAddAccount: false,
|
||||
project: {},
|
||||
account: {},
|
||||
events: [],
|
||||
stats: [],
|
||||
project: {}
|
||||
data: {
|
||||
running: 0,
|
||||
stopped: 0,
|
||||
instances: 0,
|
||||
kubernetes: 0,
|
||||
volumes: 0,
|
||||
snapshots: 0,
|
||||
networks: 0,
|
||||
vpcs: 0,
|
||||
ips: 0,
|
||||
templates: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
entity: function () {
|
||||
if (this.showProject) {
|
||||
return this.project
|
||||
}
|
||||
return this.account
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@ -158,6 +429,9 @@ export default {
|
||||
deep: true,
|
||||
handler (newData, oldData) {
|
||||
this.project = newData
|
||||
if (newData.id) {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
},
|
||||
'$i18n.global.locale' (to, from) {
|
||||
@ -168,61 +442,95 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
this.stats = [{}, {}, {}, {}, {}, {}]
|
||||
api('listVirtualMachines', { state: 'Running', listall: true, retrieveonlyresourcecount: true }).then(json => {
|
||||
var count = 0
|
||||
if (json && json.listvirtualmachinesresponse) {
|
||||
count = json.listvirtualmachinesresponse.count
|
||||
if (store.getters.project.id) {
|
||||
this.listProject()
|
||||
} else {
|
||||
this.listAccount()
|
||||
}
|
||||
this.updateData()
|
||||
},
|
||||
listAccount () {
|
||||
this.loading = true
|
||||
api('listAccounts', { id: this.$store.getters.userInfo.accountid }).then(json => {
|
||||
this.loading = false
|
||||
if (json && json.listaccountsresponse && json.listaccountsresponse.account) {
|
||||
this.account = json.listaccountsresponse.account[0]
|
||||
}
|
||||
var tileColor = this.$config.theme['@dashboard-tile-runningvms-bg'] || '#dfe9cc'
|
||||
this.stats.splice(0, 1, { name: this.$t('label.running.vms'), count: count, icon: 'desktop-outlined', bgcolor: tileColor, path: '/vm', query: { state: 'running', filter: 'running' } })
|
||||
})
|
||||
api('listVirtualMachines', { state: 'Stopped', listall: true, retrieveonlyresourcecount: true }).then(json => {
|
||||
var count = 0
|
||||
if (json && json.listvirtualmachinesresponse) {
|
||||
count = json.listvirtualmachinesresponse.count
|
||||
},
|
||||
listProject () {
|
||||
this.loading = true
|
||||
api('listProjects', { id: store.getters.project.id }).then(json => {
|
||||
this.loading = false
|
||||
if (json && json.listprojectsresponse && json.listprojectsresponse.project) {
|
||||
this.project = json.listprojectsresponse.project[0]
|
||||
}
|
||||
var tileColor = this.$config.theme['@dashboard-tile-stoppedvms-bg'] || '#edcbce'
|
||||
this.stats.splice(1, 1, { name: this.$t('label.stopped.vms'), count: count, icon: 'poweroff-outlined', bgcolor: tileColor, path: '/vm', query: { state: 'stopped', filter: 'stopped' } })
|
||||
})
|
||||
api('listVirtualMachines', { listall: true, retrieveonlyresourcecount: true }).then(json => {
|
||||
var count = 0
|
||||
if (json && json.listvirtualmachinesresponse) {
|
||||
count = json.listvirtualmachinesresponse.count
|
||||
}
|
||||
var tileColor = this.$config.theme['@dashboard-tile-totalvms-bg'] || '#ffffff'
|
||||
this.stats.splice(2, 1, { name: this.$t('label.total.vms'), count: count, icon: 'number-outlined', bgcolor: tileColor, path: '/vm' })
|
||||
})
|
||||
api('listVolumes', { listall: true, retrieveonlyresourcecount: true }).then(json => {
|
||||
var count = 0
|
||||
if (json && json.listvolumesresponse) {
|
||||
count = json.listvolumesresponse.count
|
||||
}
|
||||
var tileColor = this.$config.theme['@dashboard-tile-totalvolumes-bg'] || '#ffffff'
|
||||
this.stats.splice(3, 1, { name: this.$t('label.total.volume'), count: count, icon: 'database-outlined', bgcolor: tileColor, path: '/volume' })
|
||||
})
|
||||
api('listNetworks', { listall: true, retrieveonlyresourcecount: true }).then(json => {
|
||||
var count = 0
|
||||
if (json && json.listnetworksresponse) {
|
||||
count = json.listnetworksresponse.count
|
||||
}
|
||||
var tileColor = this.$config.theme['@dashboard-tile-totalnetworks-bg'] || '#ffffff'
|
||||
this.stats.splice(4, 1, { name: this.$t('label.total.network'), count: count, icon: 'apartment-outlined', bgcolor: tileColor, path: '/guestnetwork' })
|
||||
})
|
||||
api('listPublicIpAddresses', { listall: true, retrieveonlyresourcecount: true }).then(json => {
|
||||
var count = 0
|
||||
if (json && json.listpublicipaddressesresponse) {
|
||||
count = json.listpublicipaddressesresponse.count
|
||||
}
|
||||
var tileColor = this.$config.theme['@dashboard-tile-totalips-bg'] || '#ffffff'
|
||||
this.stats.splice(5, 1, { name: this.$t('label.public.ip.addresses'), count: count, icon: 'environment-outlined', bgcolor: tileColor, path: '/publicip' })
|
||||
})
|
||||
},
|
||||
updateData () {
|
||||
this.data = {
|
||||
running: 0,
|
||||
stopped: 0,
|
||||
instances: 0,
|
||||
kubernetes: 0,
|
||||
volumes: 0,
|
||||
snapshots: 0,
|
||||
networks: 0,
|
||||
vpcs: 0,
|
||||
ips: 0,
|
||||
templates: 0
|
||||
}
|
||||
this.listInstances()
|
||||
this.listEvents()
|
||||
this.loading = true
|
||||
api('listKubernetesClusters', { listall: true, page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.kubernetes = json?.listkubernetesclustersresponse?.count
|
||||
})
|
||||
api('listVolumes', { listall: true, page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.volumes = json?.listvolumesresponse?.count
|
||||
})
|
||||
api('listSnapshots', { listall: true, page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.snapshots = json?.listsnapshotsresponse?.count
|
||||
})
|
||||
api('listNetworks', { listall: true, page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.networks = json?.listnetworksresponse?.count
|
||||
})
|
||||
api('listVPCs', { listall: true, page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.vpcs = json?.listvpcsresponse?.count
|
||||
})
|
||||
api('listPublicIpAddresses', { listall: true, page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.ips = json?.listpublicipaddressesresponse?.count
|
||||
})
|
||||
api('listTemplates', { templatefilter: 'self', listall: true, page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.templates = json?.listtemplatesresponse?.count
|
||||
})
|
||||
},
|
||||
listInstances (zone) {
|
||||
this.loading = true
|
||||
api('listVirtualMachines', { listall: true, details: 'min', page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.instances = json?.listvirtualmachinesresponse?.count
|
||||
})
|
||||
api('listVirtualMachines', { listall: true, details: 'min', state: 'running', page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.running = json?.listvirtualmachinesresponse?.count
|
||||
})
|
||||
api('listVirtualMachines', { listall: true, details: 'min', state: 'stopped', page: 1, pagesize: 1 }).then(json => {
|
||||
this.loading = false
|
||||
this.data.stopped = json?.listvirtualmachinesresponse?.count
|
||||
})
|
||||
},
|
||||
listEvents () {
|
||||
const params = {
|
||||
page: 1,
|
||||
pagesize: 6,
|
||||
pagesize: 8,
|
||||
listall: true
|
||||
}
|
||||
this.loading = true
|
||||
@ -234,6 +542,37 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
getLabel (usageType) {
|
||||
switch (usageType) {
|
||||
case 'vm':
|
||||
return 'label.instances'
|
||||
case 'cpu':
|
||||
return 'label.cpunumber'
|
||||
case 'memory':
|
||||
return 'label.memory'
|
||||
case 'primarystorage':
|
||||
return 'label.primary.storage'
|
||||
case 'secondarystorage':
|
||||
return 'label.secondary.storage'
|
||||
case 'ip':
|
||||
return 'label.public.ips'
|
||||
}
|
||||
return 'label.' + usageType + 's'
|
||||
},
|
||||
getValue (usageType, value) {
|
||||
switch (usageType) {
|
||||
case 'memory':
|
||||
return parseFloat(value / 1024.0).toFixed(2) + ' GiB'
|
||||
case 'primarystorage':
|
||||
return parseFloat(value).toFixed(2) + ' GiB'
|
||||
case 'secondarystorage':
|
||||
return parseFloat(value).toFixed(2) + ' GiB'
|
||||
}
|
||||
return value
|
||||
},
|
||||
getPercentUsed (total, limit) {
|
||||
return (limit === 'Unlimited') ? 0 : (total / limit) * 100
|
||||
},
|
||||
getEventColour (event) {
|
||||
if (event.level === 'ERROR') {
|
||||
return 'red'
|
||||
@ -242,13 +581,6 @@ export default {
|
||||
return 'green'
|
||||
}
|
||||
return 'blue'
|
||||
},
|
||||
onTabChange (key) {
|
||||
this.showAddAccount = false
|
||||
|
||||
if (key !== 'Dashboard') {
|
||||
this.showAddAccount = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -276,6 +608,23 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
width: 100%;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.dashboard-event {
|
||||
width: 100%;
|
||||
overflow-x:hidden;
|
||||
overflow-y: scroll;
|
||||
max-height: 420px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.ant-col-xl-8 {
|
||||
width: 100%;
|
||||
|
||||
@ -17,6 +17,14 @@
|
||||
|
||||
<template>
|
||||
<a-spin :spinning="fetchLoading">
|
||||
<a-button
|
||||
type="primary"
|
||||
style="width: 100%; margin-bottom: 10px"
|
||||
@click="showAddPhyNetModal"
|
||||
:loading="loading"
|
||||
:disabled="!('createPhysicalNetwork' in $store.getters.apis)">
|
||||
<template #icon><plus-outlined /></template> {{ $t('label.add.physical.network') }}
|
||||
</a-button>
|
||||
<a-list class="list">
|
||||
<a-list-item v-for="network in networks" :key="network.id" class="list__item">
|
||||
<div class="list__item-outer-container">
|
||||
@ -65,17 +73,94 @@
|
||||
</div>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<a-modal
|
||||
:visible="addPhyNetModal"
|
||||
:title="$t('label.add.physical.network')"
|
||||
:maskClosable="false"
|
||||
:closable="true"
|
||||
:footer="null"
|
||||
@cancel="closeModals">
|
||||
<a-form
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
@finish="handleAddPhyNet"
|
||||
v-ctrl-enter="handleAddPhyNet"
|
||||
layout="vertical"
|
||||
class="form"
|
||||
|
||||
>
|
||||
<a-form-item name="name" ref="name">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
|
||||
</template>
|
||||
<a-input v-model:value="form.name" :placeholder="apiParams.name.description" />
|
||||
</a-form-item>
|
||||
<a-form-item name="isolationmethods" ref="isolationmethods">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.isolationmethods')" :tooltip="apiParams.isolationmethods.description"/>
|
||||
</template>
|
||||
<a-select
|
||||
v-model:value="form.isolationmethods"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
:filterOption="(input, option) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}"
|
||||
v-focus="true"
|
||||
:placeholder="apiParams.isolationmethods.description">
|
||||
<a-select-option v-for="i in isolationMethods" :key="i" :value="i" :label="i">{{ i }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item name="vlan" ref="vlan">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.vlan')" :tooltip="apiParams.vlan.description"/>
|
||||
</template>
|
||||
<a-input v-model:value="form.vlan" :placeholder="apiParams.vlan.description" />
|
||||
</a-form-item>
|
||||
<a-form-item name="tags" ref="tags">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.tags')" :tooltip="apiParams.tags.description"/>
|
||||
</template>
|
||||
<a-select
|
||||
mode="tags"
|
||||
v-model:value="form.tags"
|
||||
:placeholder="apiParams.tags.description">
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item name="networkspeed" ref="networkspeed">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.networkspeed')" :tooltip="apiParams.networkspeed.description"/>
|
||||
</template>
|
||||
<a-input v-model:value="form.networkspeed" :placeholder="apiParams.networkspeed.description" />
|
||||
</a-form-item>
|
||||
<a-form-item name="broadcastdomainrange" ref="broadcastdomainrange">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.broadcastdomainrange')" :tooltip="apiParams.broadcastdomainrange.description"/>
|
||||
</template>
|
||||
<a-input v-model:value="form.broadcastdomainrange" :placeholder="apiParams.broadcastdomainrange.description" />
|
||||
</a-form-item>
|
||||
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeModals">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" ref="submit" @click="handleAddPhyNet">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { api } from '@/api'
|
||||
import Status from '@/components/widgets/Status'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
|
||||
export default {
|
||||
name: 'PhysicalNetworksTab',
|
||||
components: {
|
||||
Status
|
||||
Status,
|
||||
TooltipLabel
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
@ -90,11 +175,17 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
networks: [],
|
||||
fetchLoading: false
|
||||
fetchLoading: false,
|
||||
addPhyNetModal: false
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.apiParams = this.$getApiParams('createPhysicalNetwork')
|
||||
console.log(this.apiParams)
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
this.initAddPhyNetForm()
|
||||
},
|
||||
watch: {
|
||||
resource: {
|
||||
@ -107,6 +198,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isolationMethods () {
|
||||
return ['VLAN', 'VXLAN', 'GRE', 'STT', 'BCF_SEGMENT', 'SSP', 'ODL', 'L3VPN', 'VCS']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
this.fetchLoading = true
|
||||
@ -122,7 +218,9 @@ export default {
|
||||
for (const network of this.networks) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
api('listTrafficTypes', { physicalnetworkid: network.id }).then(json => {
|
||||
network.traffictype = json.listtraffictypesresponse.traffictype.filter(e => { return e.traffictype }).map(e => { return e.traffictype }).join(', ')
|
||||
if (json.listtraffictypesresponse.traffictype) {
|
||||
network.traffictype = json.listtraffictypesresponse.traffictype.filter(e => { return e.traffictype }).map(e => { return e.traffictype }).join(', ')
|
||||
}
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
@ -133,6 +231,52 @@ export default {
|
||||
Promise.all(promises).finally(() => {
|
||||
this.fetchLoading = false
|
||||
})
|
||||
},
|
||||
showAddPhyNetModal () {
|
||||
this.addPhyNetModal = true
|
||||
},
|
||||
initAddPhyNetForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({})
|
||||
this.rules = reactive({
|
||||
name: [{ required: true, message: this.$t('label.required') }]
|
||||
})
|
||||
},
|
||||
handleAddPhyNet () {
|
||||
this.formRef.value.validate().then(() => {
|
||||
const values = toRaw(this.form)
|
||||
values.zoneid = this.resource.id
|
||||
if (values.tags) {
|
||||
values.tags = values.tags.join()
|
||||
}
|
||||
console.log(values)
|
||||
api('createPhysicalNetwork', values).then(response => {
|
||||
this.$pollJob({
|
||||
jobId: response.createphysicalnetworkresponse.jobid,
|
||||
successMessage: this.$t('message.success.add.physical.network'),
|
||||
successMethod: () => {
|
||||
this.fetchData()
|
||||
this.closeModals()
|
||||
},
|
||||
errorMessage: this.$t('message.add.physical.network.failed'),
|
||||
errorMethod: () => {
|
||||
this.fetchData()
|
||||
this.closeModals()
|
||||
},
|
||||
loadingMessage: this.$t('message.add.physical.network.processing'),
|
||||
catchMessage: this.$t('error.fetching.async.job.result'),
|
||||
catchMethod: () => {
|
||||
this.fetchData()
|
||||
this.closeModals()
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
closeModals () {
|
||||
this.addPhyNetModal = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,11 +293,11 @@
|
||||
</div>
|
||||
<a-form-item v-if="selectedNetworkOfferingSupportsSourceNat" name="sourcenatipaddress" ref="sourcenatipaddress">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress.description"/>
|
||||
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress?.description"/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="form.sourcenatipaddress"
|
||||
:placeholder="apiParams.sourcenatipaddress.description"/>
|
||||
:placeholder="apiParams.sourcenatipaddress?.description"/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
ref="networkdomain"
|
||||
|
||||
@ -106,7 +106,8 @@ export default {
|
||||
fetchActionZoneData () {
|
||||
this.loading = true
|
||||
const params = {}
|
||||
if (this.resource.zoneid && this.$route.name === 'deployVirtualMachine') {
|
||||
console.log(this.resource)
|
||||
if (this.$route.name === 'deployVirtualMachine' && this.resource.zoneid) {
|
||||
params.id = this.resource.zoneid
|
||||
}
|
||||
this.actionZoneLoading = true
|
||||
|
||||
@ -157,11 +157,11 @@
|
||||
</a-row>
|
||||
<a-form-item v-if="selectedNetworkOfferingSupportsSourceNat" name="sourcenatipaddress" ref="sourcenatipaddress">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress.description"/>
|
||||
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress?.description"/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="form.sourcenatipaddress"
|
||||
:placeholder="apiParams.sourcenatipaddress.description"/>
|
||||
:placeholder="apiParams.sourcenatipaddress?.description"/>
|
||||
</a-form-item>
|
||||
<a-form-item name="start" ref="start">
|
||||
<template #label>
|
||||
|
||||
@ -23,7 +23,7 @@ package com.cloud.utils;
|
||||
* purposes. This is purely on an honor system though. You should always
|
||||
**/
|
||||
public interface SerialVersionUID {
|
||||
public static final long Base = 0x564D4F70 << 32; // 100 brownie points if you guess what this is and tell me.
|
||||
public static final long Base = 0x564D4F70L << 32; // 100 brownie points if you guess what this is and tell me.
|
||||
|
||||
public static final long UUID = Base | 0x1;
|
||||
public static final long CloudRuntimeException = Base | 0x2;
|
||||
|
||||
@ -25,6 +25,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
@ -160,5 +161,10 @@ public class SubscriptionMgr {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.clazz, this.subscriber, this.methodName, this.method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -716,6 +716,7 @@ public class NetUtilsTest {
|
||||
NetUtils.isIpv4("2001:db8:300::/64");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllIpsOfDefaultNic() {
|
||||
final String defaultHostIp = NetUtils.getDefaultHostIp();
|
||||
if (defaultHostIp != null) {
|
||||
|
||||
@ -87,7 +87,7 @@ public class RedfishClientTest {
|
||||
public void buildRequestUrlTestHttpsGetSystemId() {
|
||||
RedfishClient redfishclient = new RedfishClient(USERNAME, PASSWORD, true, false, REDFISHT_REQUEST_RETRIES);
|
||||
String result = redfishclient.buildRequestUrl(oobAddress, RedfishClient.RedfishCmdType.GetSystemId, systemId);
|
||||
String expected = String.format("https://%s/redfish/v1/Systems/", oobAddress, systemId);
|
||||
String expected = String.format("https://%s/redfish/v1/Systems/", oobAddress);
|
||||
Assert.assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ public class RedfishClientTest {
|
||||
public void buildRequestUrlTestGetSystemId() {
|
||||
RedfishClient redfishclient = new RedfishClient(USERNAME, PASSWORD, false, false, REDFISHT_REQUEST_RETRIES);
|
||||
String result = redfishclient.buildRequestUrl(oobAddress, RedfishClient.RedfishCmdType.GetSystemId, systemId);
|
||||
String expected = String.format("http://%s/redfish/v1/Systems/", oobAddress, systemId);
|
||||
String expected = String.format("http://%s/redfish/v1/Systems/", oobAddress);
|
||||
Assert.assertEquals(expected, result);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user