mirror of
https://github.com/apache/cloudstack.git
synced 2025-11-03 04:12:31 +01:00
Merge remote-tracking branch 'origin/main' into kvm-backup-plugin-shared-storage
This commit is contained in:
commit
aa90b6de14
@ -593,6 +593,7 @@ public class ApiConstants {
|
||||
public static final String AGGREGATE_NAME = "aggregatename";
|
||||
public static final String POOL_NAME = "poolname";
|
||||
public static final String VOLUME_NAME = "volumename";
|
||||
public static final String VOLUME_STATE = "volumestate";
|
||||
public static final String SNAPSHOT_POLICY = "snapshotpolicy";
|
||||
public static final String SNAPSHOT_RESERVATION = "snapshotreservation";
|
||||
public static final String IP_NETWORK_LIST = "iptonetworklist";
|
||||
|
||||
@ -47,13 +47,13 @@ public class GenerateUsageRecordsCmd extends BaseCmd {
|
||||
|
||||
@Parameter(name = ApiConstants.END_DATE,
|
||||
type = CommandType.DATE,
|
||||
required = true,
|
||||
required = false,
|
||||
description = "End date range for usage record query. Use yyyy-MM-dd as the date format, e.g. startDate=2009-06-03.")
|
||||
private Date endDate;
|
||||
|
||||
@Parameter(name = ApiConstants.START_DATE,
|
||||
type = CommandType.DATE,
|
||||
required = true,
|
||||
required = false,
|
||||
description = "Start date range for usage record query. Use yyyy-MM-dd as the date format, e.g. startDate=2009-06-01.")
|
||||
private Date startDate;
|
||||
|
||||
|
||||
@ -71,6 +71,10 @@ public class SnapshotResponse extends BaseResponseWithTagInformation implements
|
||||
@Param(description = "type of the disk volume")
|
||||
private String volumeType;
|
||||
|
||||
@SerializedName(ApiConstants.VOLUME_STATE)
|
||||
@Param(description = "state of the disk volume")
|
||||
private String volumeState;
|
||||
|
||||
@SerializedName(ApiConstants.CREATED)
|
||||
@Param(description = " the date the snapshot was created")
|
||||
private Date created;
|
||||
@ -199,6 +203,10 @@ public class SnapshotResponse extends BaseResponseWithTagInformation implements
|
||||
this.volumeType = volumeType;
|
||||
}
|
||||
|
||||
public void setVolumeState(String volumeState) {
|
||||
this.volumeState = volumeState;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
3
debian/rules
vendored
3
debian/rules
vendored
@ -4,6 +4,7 @@ VERSION := $(shell grep '<version>' pom.xml | head -2 | tail -1 | cut -d'>' -f2
|
||||
PACKAGE = $(shell dh_listpackages|head -n 1|cut -d '-' -f 1)
|
||||
SYSCONFDIR = "/etc"
|
||||
DESTDIR = "debian/tmp"
|
||||
CMK_REL := $(shell wget -O - "https://api.github.com/repos/apache/cloudstack-cloudmonkey/releases" 2>/dev/null | jq -r '.[0].tag_name')
|
||||
|
||||
%:
|
||||
dh $@ --with systemd
|
||||
@ -85,7 +86,7 @@ override_dh_auto_install:
|
||||
rm -rf $(DESTDIR)/usr/share/$(PACKAGE)-management/templates/systemvm/md5sum.txt
|
||||
|
||||
# Bundle cmk in cloudstack-management
|
||||
wget https://github.com/apache/cloudstack-cloudmonkey/releases/download/6.3.0/cmk.linux.x86-64 -O $(DESTDIR)/usr/bin/cmk
|
||||
wget https://github.com/apache/cloudstack-cloudmonkey/releases/download/$(CMK_REL)/cmk.linux.x86-64 -O $(DESTDIR)/usr/bin/cmk
|
||||
chmod +x $(DESTDIR)/usr/bin/cmk
|
||||
|
||||
# nast hack for a couple of configuration files
|
||||
|
||||
@ -545,16 +545,11 @@ public class UsageDaoImpl extends GenericDaoBase<UsageVO, Long> implements Usage
|
||||
TransactionLegacy txn = TransactionLegacy.currentTxn();
|
||||
PreparedStatement pstmt = null;
|
||||
try {
|
||||
txn.start();
|
||||
pstmt = txn.prepareAutoCloseStatement(DELETE_ALL_BY_INTERVAL);
|
||||
pstmt.setLong(1, days);
|
||||
pstmt.executeUpdate();
|
||||
txn.commit();
|
||||
} catch (Exception ex) {
|
||||
txn.rollback();
|
||||
logger.error("error removing old cloud_usage records for interval: " + days);
|
||||
} finally {
|
||||
txn.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -48,6 +48,7 @@ SELECT
|
||||
`volumes`.`uuid` AS `volume_uuid`,
|
||||
`volumes`.`name` AS `volume_name`,
|
||||
`volumes`.`volume_type` AS `volume_type`,
|
||||
`volumes`.`state` AS `volume_state`,
|
||||
`volumes`.`size` AS `volume_size`,
|
||||
`data_center`.`id` AS `data_center_id`,
|
||||
`data_center`.`uuid` AS `data_center_uuid`,
|
||||
|
||||
@ -260,7 +260,8 @@ install -D client/target/utilities/bin/cloud-setup-baremetal ${RPM_BUILD_ROOT}%{
|
||||
install -D client/target/utilities/bin/cloud-sysvmadm ${RPM_BUILD_ROOT}%{_bindir}/%{name}-sysvmadm
|
||||
install -D client/target/utilities/bin/cloud-update-xenserver-licenses ${RPM_BUILD_ROOT}%{_bindir}/%{name}-update-xenserver-licenses
|
||||
# Bundle cmk in cloudstack-management
|
||||
wget https://github.com/apache/cloudstack-cloudmonkey/releases/download/6.3.0/cmk.linux.x86-64 -O ${RPM_BUILD_ROOT}%{_bindir}/cmk
|
||||
CMK_REL=$(wget -O - "https://api.github.com/repos/apache/cloudstack-cloudmonkey/releases" 2>/dev/null | jq -r '.[0].tag_name')
|
||||
wget https://github.com/apache/cloudstack-cloudmonkey/releases/download/$CMK_REL/cmk.linux.x86-64 -O ${RPM_BUILD_ROOT}%{_bindir}/cmk
|
||||
chmod +x ${RPM_BUILD_ROOT}%{_bindir}/cmk
|
||||
|
||||
cp -r client/target/utilities/scripts/db/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup
|
||||
|
||||
@ -669,6 +669,7 @@ public class ApiResponseHelper implements ResponseGenerator {
|
||||
snapshotResponse.setVolumeId(volume.getUuid());
|
||||
snapshotResponse.setVolumeName(volume.getName());
|
||||
snapshotResponse.setVolumeType(volume.getVolumeType().name());
|
||||
snapshotResponse.setVolumeState(volume.getState().name());
|
||||
snapshotResponse.setVirtualSize(volume.getSize());
|
||||
DataCenter zone = ApiDBUtils.findZoneById(volume.getDataCenterId());
|
||||
if (zone != null) {
|
||||
|
||||
@ -125,6 +125,7 @@ public class SnapshotJoinDaoImpl extends GenericDaoBaseWithTagInformation<Snapsh
|
||||
snapshotResponse.setVolumeId(snapshot.getVolumeUuid());
|
||||
snapshotResponse.setVolumeName(snapshot.getVolumeName());
|
||||
snapshotResponse.setVolumeType(snapshot.getVolumeType().name());
|
||||
snapshotResponse.setVolumeState(snapshot.getVolumeState().name());
|
||||
snapshotResponse.setVirtualSize(snapshot.getVolumeSize());
|
||||
VolumeVO volume = ApiDBUtils.findVolumeById(snapshot.getVolumeId());
|
||||
if (volume != null && volume.getVolumeType() == Type.ROOT && volume.getInstanceId() != null) {
|
||||
|
||||
@ -132,6 +132,10 @@ public class SnapshotJoinVO extends BaseViewWithTagInformationVO implements Cont
|
||||
@Enumerated(EnumType.STRING)
|
||||
Volume.Type volumeType = Volume.Type.UNKNOWN;
|
||||
|
||||
@Column(name = "volume_state")
|
||||
@Enumerated(EnumType.STRING)
|
||||
Volume.State volumeState;
|
||||
|
||||
@Column(name = "volume_size")
|
||||
Long volumeSize;
|
||||
|
||||
@ -299,6 +303,10 @@ public class SnapshotJoinVO extends BaseViewWithTagInformationVO implements Cont
|
||||
return volumeType;
|
||||
}
|
||||
|
||||
public Volume.State getVolumeState() {
|
||||
return volumeState;
|
||||
}
|
||||
|
||||
public Long getVolumeSize() {
|
||||
return volumeSize;
|
||||
}
|
||||
|
||||
@ -630,31 +630,32 @@ class CsVmMetadata(CsDataBag):
|
||||
if os.path.exists(datafile):
|
||||
os.remove(datafile)
|
||||
|
||||
def __writefile(self, dest, data, mode):
|
||||
fh = open(dest, mode)
|
||||
self.__exflock(fh)
|
||||
fh.write(data)
|
||||
self.__unflock(fh)
|
||||
fh.close()
|
||||
os.chmod(dest, 0o644)
|
||||
|
||||
def __createfile(self, ip, folder, file, data):
|
||||
dest = "/var/www/html/" + folder + "/" + ip + "/" + file
|
||||
metamanifestdir = "/var/www/html/" + folder + "/" + ip
|
||||
metamanifest = metamanifestdir + "/meta-data"
|
||||
|
||||
# base64 decode userdata
|
||||
if folder == "userdata" or folder == "user-data":
|
||||
if data is not None:
|
||||
if data is not None:
|
||||
# base64 decode userdata
|
||||
if folder == "userdata" or folder == "user-data":
|
||||
# need to pad data if it is not valid base 64
|
||||
if len(data) % 4 != 0:
|
||||
data += (4 - (len(data) % 4)) * "="
|
||||
data = base64.b64decode(data)
|
||||
|
||||
fh = open(dest, "w")
|
||||
self.__exflock(fh)
|
||||
if data is not None:
|
||||
if isinstance(data, str):
|
||||
fh.write(data)
|
||||
self.__writefile(dest, data, "w")
|
||||
elif isinstance(data, bytes):
|
||||
fh.write(data.decode())
|
||||
self.__writefile(dest, data, "wb")
|
||||
else:
|
||||
fh.write("")
|
||||
self.__unflock(fh)
|
||||
fh.close()
|
||||
os.chmod(dest, 0o644)
|
||||
self.__writefile(dest, "", "w")
|
||||
|
||||
if folder == "metadata" or folder == "meta-data":
|
||||
try:
|
||||
|
||||
@ -34,7 +34,7 @@ import threading
|
||||
import urllib.parse
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from socketserver import ThreadingMixIn #, ForkingMixIn
|
||||
from socketserver import ThreadingMixIn
|
||||
|
||||
|
||||
passMap = {}
|
||||
|
||||
@ -87,14 +87,16 @@ install_package() {
|
||||
if [ "$os" == "" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local package=${package_properties["package_name"]}
|
||||
local file=${package_properties["file_name"]}
|
||||
|
||||
local DEBIAN_RELEASE=$(lsb_release -rs)
|
||||
if [ "$os" != "$DEBIAN_RELEASE" ]; then
|
||||
log_it "Skipped the installation of package $package on Debian $DEBIAN_RELEASE as it can only be installed on Debian $os."
|
||||
return
|
||||
fi
|
||||
|
||||
local package=${package_properties["package_name"]}
|
||||
local file=${package_properties["file_name"]}
|
||||
if [ -z "$package" ] || [ -z "$file" ]; then
|
||||
log_it "Skipped the installation due to empty package of file name (package name: $package, file name: $file)."
|
||||
return
|
||||
|
||||
@ -81,28 +81,33 @@ def deletefile(ip, folder, file):
|
||||
os.remove(datafile)
|
||||
|
||||
|
||||
def writefile(dest, data, mode):
|
||||
fh = open(dest, mode)
|
||||
exflock(fh)
|
||||
fh.write(data)
|
||||
unflock(fh)
|
||||
fh.close()
|
||||
os.chmod(dest, 0o644)
|
||||
|
||||
|
||||
def createfile(ip, folder, file, data):
|
||||
dest = "/var/www/html/" + folder + "/" + ip + "/" + file
|
||||
metamanifestdir = "/var/www/html/" + folder + "/" + ip
|
||||
metamanifest = metamanifestdir + "/meta-data"
|
||||
|
||||
# base64 decode userdata
|
||||
if folder == "userdata" or folder == "user-data":
|
||||
if data is not None:
|
||||
data = base64.b64decode(data)
|
||||
|
||||
fh = open(dest, "w")
|
||||
exflock(fh)
|
||||
if data is not None:
|
||||
# base64 decode userdata
|
||||
if folder == "userdata" or folder == "user-data":
|
||||
# need to pad data if it is not valid base 64
|
||||
if len(data) % 4 != 0:
|
||||
data += (4 - (len(data) % 4)) * "="
|
||||
data = base64.b64decode(data)
|
||||
if isinstance(data, str):
|
||||
fh.write(data)
|
||||
writefile(dest, data, "w")
|
||||
elif isinstance(data, bytes):
|
||||
fh.write(data.decode())
|
||||
writefile(dest, data, "wb")
|
||||
else:
|
||||
fh.write("")
|
||||
unflock(fh)
|
||||
fh.close()
|
||||
os.chmod(dest, 0o644)
|
||||
writefile(dest, "", "w")
|
||||
|
||||
if folder == "metadata" or folder == "meta-data":
|
||||
try:
|
||||
|
||||
@ -273,6 +273,7 @@ class TestPurgeExpungedVms(cloudstackTestCase):
|
||||
return False
|
||||
self.debug("Restarting all management server")
|
||||
for idx, server_ip in enumerate(server_ips):
|
||||
self.debug(f"Restarting management server #{idx} with IP {server_ip}")
|
||||
sshClient = SshClient(
|
||||
server_ip,
|
||||
22,
|
||||
@ -283,6 +284,9 @@ class TestPurgeExpungedVms(cloudstackTestCase):
|
||||
sshClient.execute(command)
|
||||
command = "service cloudstack-management start"
|
||||
sshClient.execute(command)
|
||||
if idx == 0:
|
||||
# Wait before restarting other management servers to make the first as oldest running
|
||||
time.sleep(10)
|
||||
|
||||
# Waits for management to come up in 10 mins, when it's up it will continue
|
||||
timeout = time.time() + (10 * 60)
|
||||
@ -349,15 +353,18 @@ class TestPurgeExpungedVms(cloudstackTestCase):
|
||||
@skipTestIf("hypervisorIsSimulator")
|
||||
@attr(tags=["advanced"], required_hardware="true")
|
||||
def test_06_purge_expunged_vm_background_task(self):
|
||||
purge_task_delay = 60
|
||||
purge_task_delay = 120
|
||||
self.changeConfiguration('expunged.resources.purge.enabled', 'true')
|
||||
self.changeConfiguration('expunged.resources.purge.delay', purge_task_delay)
|
||||
self.changeConfiguration('expunged.resources.purge.interval', int(purge_task_delay/2))
|
||||
self.changeConfiguration('expunged.resources.purge.keep.past.days', 1)
|
||||
if len(self.staticConfigurations) > 0:
|
||||
self.restartAllManagementServers()
|
||||
wait = 2 * purge_task_delay
|
||||
logging.info("Waiting for 2x%d = %d seconds for background task to execute" % (purge_task_delay, wait))
|
||||
wait_multiple = 2
|
||||
wait = wait_multiple * purge_task_delay
|
||||
logging.info(f"Waiting for {wait_multiple}x{purge_task_delay} = {wait} seconds for background task to execute")
|
||||
time.sleep(wait)
|
||||
logging.debug("Validating expunged VMs")
|
||||
self.validatePurgedVmEntriesInDb(
|
||||
[self.vm_ids[self.timestamps[0]], self.vm_ids[self.timestamps[1]], self.vm_ids[self.timestamps[2]]],
|
||||
None
|
||||
|
||||
@ -25,7 +25,8 @@ CentOS based built-in user VM template.
|
||||
|
||||
# Setting up Tools and Environment
|
||||
|
||||
- Install packer and latest KVM, qemu on a Linux machine
|
||||
- Install packer (v1.8.x, v1.9.x tested) and latest KVM, qemu on a Linux x86
|
||||
machine (Ubuntu 20.04 tested)
|
||||
- Install tools for exporting appliances: qemu-img, ovftool, faketime, sharutils
|
||||
- Build and install `vhd-util` as described in build.sh or use pre-built
|
||||
binaries at:
|
||||
@ -33,10 +34,18 @@ CentOS based built-in user VM template.
|
||||
http://packages.shapeblue.com/systemvmtemplate/vhd-util
|
||||
http://packages.shapeblue.com/systemvmtemplate/libvhd.so.1.0
|
||||
|
||||
- For building ARM64 systemvm template on amd64 systems, please also install:
|
||||
qemu-utils qemu-system-arm qemu-efi-aarch64
|
||||
|
||||
# How to build appliances
|
||||
|
||||
Just run build.sh, it will export archived appliances for KVM, XenServer,
|
||||
VMWare and HyperV in `dist` directory:
|
||||
|
||||
bash build.sh systemvmtemplate
|
||||
bash build.sh <name> <version> <arch>
|
||||
bash build.sh systemvmtemplate 4.19.1.0 x86_64
|
||||
bash build.sh systemvmtemplate 4.19.1.0 aarch64
|
||||
|
||||
For building builtin x86_64 template run:
|
||||
|
||||
bash build.sh builtin
|
||||
|
||||
@ -27,6 +27,8 @@ Usage:
|
||||
(or use command line arg, default systemvmtemplate)
|
||||
* Set \$version to provide version to apply to built appliance
|
||||
(or use command line arg, default empty)
|
||||
* Set \$target_arch to provide target architecture
|
||||
(or use command line arg, default to current architecture. Currently x86_64 and aarch64 are implemented)
|
||||
* Set \$BUILD_NUMBER to provide build number to apply to built appliance
|
||||
(or use command line arg, default empty)
|
||||
* Set \$DEBUG=1 to enable debug logging
|
||||
@ -85,12 +87,18 @@ if [[ ! -z "${JENKINS_HOME}" ]]; then
|
||||
DEBUG=1
|
||||
fi
|
||||
|
||||
# get current system architecture
|
||||
base_arch=`arch`
|
||||
|
||||
# which packer definition to use
|
||||
appliance="${1:-${appliance:-systemvmtemplate}}"
|
||||
|
||||
# optional version tag to put into the image filename
|
||||
version="${2:-${version:-}}"
|
||||
|
||||
# which architecture to build the template for
|
||||
target_arch="${3:-${target_arch:-${base_arch}}}"
|
||||
|
||||
# optional (jenkins) build number tag to put into the image filename
|
||||
BUILD_NUMBER="${4:-${BUILD_NUMBER:-}}"
|
||||
|
||||
@ -105,7 +113,7 @@ elif [ ! -z "${BUILD_NUMBER}" ]; then
|
||||
version_tag="-${BUILD_NUMBER}"
|
||||
fi
|
||||
|
||||
appliance_build_name=${appliance}${version_tag}
|
||||
appliance_build_name="${appliance}${version_tag}-${target_arch}"
|
||||
|
||||
###
|
||||
### Generic helper functions
|
||||
@ -218,7 +226,7 @@ function prepare() {
|
||||
|
||||
function packer_build() {
|
||||
log INFO "building new image with packer"
|
||||
cd ${appliance_build_name} && packer build template.json && cd ..
|
||||
cd ${appliance_build_name} && packer build template-base_${base_arch}-target_${target_arch}.json && cd ..
|
||||
}
|
||||
|
||||
function stage_vmx() {
|
||||
@ -349,10 +357,12 @@ function main() {
|
||||
|
||||
# process the disk at dist
|
||||
kvm_export
|
||||
ovm_export
|
||||
xen_server_export
|
||||
vmware_export
|
||||
hyperv_export
|
||||
if [ "${target_arch}" == "x86_64" ]; then
|
||||
ovm_export
|
||||
xen_server_export
|
||||
vmware_export
|
||||
hyperv_export
|
||||
fi
|
||||
rm -f "dist/${appliance}"
|
||||
cd dist && chmod +r * && cd ..
|
||||
cd dist && md5sum * > md5sum.txt && cd ..
|
||||
|
||||
122
tools/appliance/systemvmtemplate/http/preseed_aarch64.cfg
Normal file
122
tools/appliance/systemvmtemplate/http/preseed_aarch64.cfg
Normal file
@ -0,0 +1,122 @@
|
||||
# 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.
|
||||
|
||||
### Localization
|
||||
# Locale sets language and country.
|
||||
d-i debian-installer/locale string en_US.UTF-8
|
||||
d-i debian-installer/country string IN
|
||||
|
||||
# Keyboard selection.
|
||||
d-i keymap select us
|
||||
d-i keyboard-configuration/xkb-keymap select us
|
||||
|
||||
### Network configuration
|
||||
d-i netcfg/choose_interface select auto
|
||||
d-i netcfg/get_hostname string systemvm
|
||||
d-i netcfg/get_domain string apache.org
|
||||
d-i netcfg/wireless_wep string
|
||||
|
||||
d-i hw-detect/load_firmware boolean true
|
||||
|
||||
### Mirror settings
|
||||
d-i mirror/country string manual
|
||||
d-i mirror/http/hostname string deb.debian.org
|
||||
d-i mirror/http/directory string /debian
|
||||
d-i mirror/http/proxy string
|
||||
|
||||
### Apt setup
|
||||
d-i apt-setup/cdrom/set-first false
|
||||
d-i apt-setup/security-updates boolean true
|
||||
d-i apt-setup/services-select multiselect security, updates
|
||||
d-i apt-setup/security_host string security.debian.org
|
||||
d-i apt-setup/local0/source boolean false
|
||||
d-i apt-setup/multiarch string i386
|
||||
d-i apt-setup/backports boolean true
|
||||
d-i apt-setup/contrib boolean true
|
||||
d-i apt-setup/multiverse boolean true
|
||||
d-i apt-setup/universe boolean true
|
||||
|
||||
### Clock and time zone setup
|
||||
d-i clock-setup/utc boolean true
|
||||
d-i time/zone string UTC
|
||||
d-i clock-setup/ntp boolean true
|
||||
|
||||
### Partitioning
|
||||
d-i partman-auto/disk string /dev/vda
|
||||
d-i partman-auto/method string regular
|
||||
d-i partman-auto/expert_recipe string \
|
||||
boot-root :: \
|
||||
538 538 1075 free \
|
||||
$iflabel{ gpt } \
|
||||
$reusemethod{ } \
|
||||
method{ efi } \
|
||||
format{ } \
|
||||
. \
|
||||
400 60 400 ext2 \
|
||||
$primary{ } $bootable{ } \
|
||||
method{ format } format{ } \
|
||||
use_filesystem{ } filesystem{ ext2 } \
|
||||
mountpoint{ /boot } \
|
||||
. \
|
||||
256 1000 256 linux-swap \
|
||||
method{ swap } format{ } \
|
||||
. \
|
||||
2240 40 4000 ext4 \
|
||||
method{ format } format{ } \
|
||||
use_filesystem{ } filesystem{ ext4 } \
|
||||
mountpoint{ / } \
|
||||
.
|
||||
|
||||
d-i partman-md/confirm boolean true
|
||||
d-i partman-partitioning/confirm_write_new_label boolean true
|
||||
d-i partman/choose_partition select finish
|
||||
d-i partman/confirm boolean true
|
||||
d-i partman/confirm_nooverwrite boolean true
|
||||
grub-efi-arm64 grub2/force_efi_extra_removable boolean true
|
||||
d-i partman-partitioning/choose_label select gpt
|
||||
d-i partman-partitioning/default_label string gpt
|
||||
|
||||
### Base system installation
|
||||
# ...
|
||||
|
||||
### Account setup
|
||||
d-i passwd/root-login boolean false
|
||||
d-i passwd/root-password password password
|
||||
d-i passwd/root-password-again password password
|
||||
d-i passwd/user-fullname string Cloud User
|
||||
d-i passwd/username string cloud
|
||||
d-i passwd/user-password password cloud
|
||||
d-i passwd/user-password-again password cloud
|
||||
d-i user-setup/encrypt-home boolean false
|
||||
d-i user-setup/allow-password-weak boolean true
|
||||
d-i passwd/user-default-groups string audio cdrom video admin
|
||||
|
||||
### Package selection
|
||||
tasksel tasksel/first multiselect ssh-server
|
||||
d-i pkgsel/include string openssh-server ntp acpid sudo bzip2 openssl
|
||||
# Allowed values: none, safe-upgrade, full-upgrade
|
||||
d-i pkgsel/upgrade select full-upgrade
|
||||
d-i pkgsel/update-policy select none
|
||||
|
||||
popularity-contest popularity-contest/participate boolean false
|
||||
|
||||
### Boot loader installation
|
||||
d-i grub-installer/only_debian boolean true
|
||||
d-i grub-installer/bootdev string default
|
||||
d-i finish-install/reboot_in_progress note
|
||||
|
||||
#### Advanced options
|
||||
@ -83,7 +83,7 @@ function install_packages() {
|
||||
apt_clean
|
||||
|
||||
# 32 bit architecture support for vhd-util
|
||||
if [ "${arch}" != "i386" ]; then
|
||||
if [[ "${arch}" != "i386" && "${arch}" != "arm64" ]]; then
|
||||
dpkg --add-architecture i386
|
||||
apt-get update
|
||||
${apt_get} install libuuid1:i386 libc6:i386
|
||||
@ -92,17 +92,25 @@ function install_packages() {
|
||||
# Install docker and containerd for CKS
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
|
||||
apt-key fingerprint 0EBFCD88
|
||||
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
|
||||
if [ "${arch}" == "arm64" ]; then
|
||||
add-apt-repository "deb [arch=arm64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
|
||||
elif [ "${arch}" == "amd64" ]; then
|
||||
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
|
||||
else
|
||||
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
|
||||
fi
|
||||
apt-get update
|
||||
${apt_get} install containerd.io
|
||||
|
||||
apt_clean
|
||||
|
||||
install_vhd_util
|
||||
# Install xenserver guest utilities as debian repos don't have it
|
||||
wget --no-check-certificate https://download.cloudstack.org/systemvm/debian/xe-guest-utilities_7.20.2-0ubuntu1_amd64.deb
|
||||
dpkg -i xe-guest-utilities_7.20.2-0ubuntu1_amd64.deb
|
||||
rm -f xe-guest-utilities_7.20.2-0ubuntu1_amd64.deb
|
||||
if [ "${arch}" != "arm64" ]; then
|
||||
install_vhd_util
|
||||
# Install xenserver guest utilities as debian repos don't have it
|
||||
wget --no-check-certificate https://download.cloudstack.org/systemvm/debian/xe-guest-utilities_7.20.2-0ubuntu1_amd64.deb
|
||||
dpkg -i xe-guest-utilities_7.20.2-0ubuntu1_amd64.deb
|
||||
rm -f xe-guest-utilities_7.20.2-0ubuntu1_amd64.deb
|
||||
fi
|
||||
}
|
||||
|
||||
return 2>/dev/null || install_packages
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
{
|
||||
"_license": "Apache License 2.0",
|
||||
"builders": [
|
||||
{
|
||||
"accelerator": "kvm",
|
||||
"boot_command": [
|
||||
"c<wait>",
|
||||
"linux /install.a64/vmlinuz <wait>",
|
||||
"preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed_aarch64.cfg <wait>",
|
||||
"debian-installer=en_US.UTF-8 <wait>",
|
||||
"auto <wait>",
|
||||
"language=en locale=en_US.UTF-8 <wait>",
|
||||
"kbd-chooser/method=us <wait>",
|
||||
"keyboard-configuration/xkb-keymap=us <wait>",
|
||||
"netcfg/get_hostname=systemvm <wait>",
|
||||
"netcfg/get_domain=apache.org <wait>",
|
||||
"country=IN keymap=us <wait>",
|
||||
"fb=false <wait>",
|
||||
"debconf/frontend=noninteractive <wait>",
|
||||
"console-setup/ask_detect=false <wait>",
|
||||
"console-keymaps-at/keymap=us <wait>",
|
||||
"---",
|
||||
"<enter><wait>",
|
||||
"initrd /install.a64/initrd.gz",
|
||||
"<enter><wait>",
|
||||
"boot<enter><wait>"
|
||||
],
|
||||
"boot_wait": "180s",
|
||||
"disk_interface": "virtio",
|
||||
"cdrom_interface": "virtio-scsi",
|
||||
"disk_size": "5000M",
|
||||
"format": "qcow2",
|
||||
"headless": true,
|
||||
"http_directory": "http",
|
||||
"iso_checksum": "sha512:14c2ca243ee7f6e447cc4466296d974ee36645c06d72043236c3fbea78f1948d3af88d65139105a475288f270e4b636e6885143d01bdf69462620d1825e470ae",
|
||||
"iso_url": "https://cdimage.debian.org/mirror/cdimage/archive/12.5.0/arm64/iso-cd/debian-12.5.0-arm64-netinst.iso",
|
||||
"net_device": "virtio-net",
|
||||
"output_directory": "../dist",
|
||||
"qemu_binary": "qemu-system-aarch64",
|
||||
"qemuargs": [
|
||||
[
|
||||
"-m",
|
||||
"500M"
|
||||
],
|
||||
[
|
||||
"-machine",
|
||||
"virt"
|
||||
],
|
||||
[
|
||||
"-cpu",
|
||||
"host"
|
||||
],
|
||||
[
|
||||
"-smp",
|
||||
"1"
|
||||
],
|
||||
[ "-pflash", "/usr/share/AAVMF/AAVMF_CODE.fd" ],
|
||||
[ "-monitor", "none" ],
|
||||
[ "-enable-kvm" ],
|
||||
[ "-boot", "strict=off" ]
|
||||
],
|
||||
"shutdown_command": "sudo halt -p",
|
||||
"ssh_password": "cloud",
|
||||
"ssh_timeout": "120m",
|
||||
"ssh_username": "cloud",
|
||||
"type": "qemu",
|
||||
"vm_name": "systemvmtemplate"
|
||||
}
|
||||
],
|
||||
"description": "CloudStack SystemVM template",
|
||||
"provisioners": [
|
||||
{
|
||||
"execute_command": "echo 'cloud' | sudo -u root -S bash {{.Path}}",
|
||||
"scripts": [
|
||||
"scripts/apt_upgrade.sh",
|
||||
"scripts/configure_grub.sh",
|
||||
"scripts/configure_locale.sh",
|
||||
"scripts/configure_networking.sh",
|
||||
"scripts/configure_acpid.sh",
|
||||
"scripts/install_systemvm_packages.sh",
|
||||
"scripts/configure_conntrack.sh",
|
||||
"scripts/authorized_keys.sh",
|
||||
"scripts/configure_persistent_config.sh",
|
||||
"scripts/configure_login.sh",
|
||||
"../cloud_scripts_shar_archive.sh",
|
||||
"scripts/configure_systemvm_services.sh",
|
||||
"scripts/cleanup.sh",
|
||||
"scripts/finalize.sh"
|
||||
],
|
||||
"type": "shell"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
{
|
||||
"_license": "Apache License 2.0",
|
||||
"builders": [
|
||||
{
|
||||
"boot_command": [
|
||||
"c<wait>",
|
||||
"linux /install.a64/vmlinuz <wait>",
|
||||
"preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed_aarch64.cfg <wait>",
|
||||
"debian-installer=en_US.UTF-8 <wait>",
|
||||
"auto <wait>",
|
||||
"language=en locale=en_US.UTF-8 <wait>",
|
||||
"kbd-chooser/method=us <wait>",
|
||||
"keyboard-configuration/xkb-keymap=us <wait>",
|
||||
"netcfg/get_hostname=systemvm <wait>",
|
||||
"netcfg/get_domain=apache.org <wait>",
|
||||
"country=IN keymap=us <wait>",
|
||||
"fb=false <wait>",
|
||||
"debconf/frontend=noninteractive <wait>",
|
||||
"console-setup/ask_detect=false <wait>",
|
||||
"console-keymaps-at/keymap=us <wait>",
|
||||
"---",
|
||||
"<enter><wait>",
|
||||
"initrd /install.a64/initrd.gz",
|
||||
"<enter><wait>",
|
||||
"boot<enter><wait>"
|
||||
],
|
||||
"boot_wait": "60s",
|
||||
"disk_interface": "virtio",
|
||||
"cdrom_interface": "virtio-scsi",
|
||||
"disk_size": "5000M",
|
||||
"format": "qcow2",
|
||||
"headless": true,
|
||||
"http_directory": "http",
|
||||
"iso_checksum": "sha512:14c2ca243ee7f6e447cc4466296d974ee36645c06d72043236c3fbea78f1948d3af88d65139105a475288f270e4b636e6885143d01bdf69462620d1825e470ae",
|
||||
"iso_url": "https://cdimage.debian.org/mirror/cdimage/archive/12.5.0/arm64/iso-cd/debian-12.5.0-arm64-netinst.iso",
|
||||
"net_device": "virtio-net",
|
||||
"output_directory": "../dist",
|
||||
"qemu_binary": "qemu-system-aarch64",
|
||||
"qemuargs": [
|
||||
[
|
||||
"-m",
|
||||
"500M"
|
||||
],
|
||||
[
|
||||
"-machine",
|
||||
"virt"
|
||||
],
|
||||
[
|
||||
"-cpu",
|
||||
"cortex-a72"
|
||||
],
|
||||
[
|
||||
"-smp",
|
||||
"1"
|
||||
],
|
||||
[ "-bios", "/usr/share/qemu-efi-aarch64/QEMU_EFI.fd" ],
|
||||
[ "-monitor", "none" ],
|
||||
[ "-boot", "strict=off" ]
|
||||
],
|
||||
"shutdown_command": "sudo halt -p",
|
||||
"ssh_password": "cloud",
|
||||
"ssh_timeout": "120m",
|
||||
"ssh_username": "cloud",
|
||||
"type": "qemu",
|
||||
"vm_name": "systemvmtemplate"
|
||||
}
|
||||
],
|
||||
"description": "CloudStack SystemVM template",
|
||||
"provisioners": [
|
||||
{
|
||||
"execute_command": "echo 'cloud' | sudo -u root -S bash {{.Path}}",
|
||||
"scripts": [
|
||||
"scripts/apt_upgrade.sh",
|
||||
"scripts/configure_grub.sh",
|
||||
"scripts/configure_locale.sh",
|
||||
"scripts/configure_networking.sh",
|
||||
"scripts/configure_acpid.sh",
|
||||
"scripts/install_systemvm_packages.sh",
|
||||
"scripts/configure_conntrack.sh",
|
||||
"scripts/authorized_keys.sh",
|
||||
"scripts/configure_persistent_config.sh",
|
||||
"scripts/configure_login.sh",
|
||||
"../cloud_scripts_shar_archive.sh",
|
||||
"scripts/configure_systemvm_services.sh",
|
||||
"scripts/cleanup.sh",
|
||||
"scripts/finalize.sh"
|
||||
],
|
||||
"type": "shell"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
"boot_command": [
|
||||
"<esc><wait>",
|
||||
"install <wait>",
|
||||
"preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg <wait>",
|
||||
"preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed_x86_64.cfg <wait>",
|
||||
"debian-installer=en_US.UTF-8 <wait>",
|
||||
"auto <wait>",
|
||||
"language=en locale=en_US.UTF-8 <wait>",
|
||||
@ -618,6 +618,7 @@
|
||||
"label.datetime.filter.starting": "Starting <b>{startDate}</b>.",
|
||||
"label.datetime.filter.up.to": "Up to <b>{endDate}</b>.",
|
||||
"label.day": "Day",
|
||||
"label.days": "Days",
|
||||
"label.day.of.month": "Day of month",
|
||||
"label.day.of.week": "Day of week",
|
||||
"label.db.usage.metrics": "DB/Usage server",
|
||||
@ -806,6 +807,7 @@
|
||||
"label.done": "Done",
|
||||
"label.down": "Down",
|
||||
"label.download": "Download",
|
||||
"label.download.csv": "Download CSV",
|
||||
"label.download.kubeconfig.cluster": "Download kubeconfig for the cluster <br><br> The <code><b>kubectl</b></code> command-line tool uses kubeconfig files to find the information it needs to choose a cluster and communicate with the API server of a cluster.",
|
||||
"label.download.kubectl": "Download <code><b>kubectl</b></code> tool for cluster's Kubernetes version",
|
||||
"label.download.kubernetes.cluster.config": "Download Kubernetes cluster config",
|
||||
@ -924,6 +926,7 @@
|
||||
"label.fetch.instances": "Fetch Instances",
|
||||
"label.fetch.latest": "Fetch latest",
|
||||
"label.filename": "File Name",
|
||||
"label.fetched": "Fetched",
|
||||
"label.files": "Alternate files to retrieve",
|
||||
"label.filter": "Filter",
|
||||
"label.filter.annotations.all": "All comments",
|
||||
@ -1229,6 +1232,8 @@
|
||||
"label.label": "Label",
|
||||
"label.last.updated": "Last update",
|
||||
"label.lastannotated": "Last annotation date",
|
||||
"label.lastheartbeat": "Last heartbeat",
|
||||
"label.lastsuccessfuljob": "Last successful job",
|
||||
"label.lastboottime": "Boot time of the management server machine",
|
||||
"label.lastname": "Last name",
|
||||
"label.lastname.lower": "lastname",
|
||||
@ -1483,6 +1488,7 @@
|
||||
"label.no.items": "No available Items",
|
||||
"label.no.matching.offering": "No matching offering found",
|
||||
"label.no.matching.network": "No matching Networks found",
|
||||
"label.no.usage.records": "No usage records found",
|
||||
"label.noderootdisksize": "Node root disk size (in GB)",
|
||||
"label.nodiskcache": "No disk cache",
|
||||
"label.none": "None",
|
||||
@ -1523,6 +1529,7 @@
|
||||
"label.of": "of",
|
||||
"label.of.month": "of month",
|
||||
"label.offerha": "Offer HA",
|
||||
"label.offeringid": "Offering ID",
|
||||
"label.offeringtype": "Compute offering type",
|
||||
"label.ok": "OK",
|
||||
"label.only.end.date.and.time": "Only end date and time",
|
||||
@ -1700,6 +1707,8 @@
|
||||
"label.publicnetwork": "Public Network",
|
||||
"label.publicport": "Public port",
|
||||
"label.purgeresources": "Purge Resources",
|
||||
"label.purge.usage.records.success": "Successfuly purged usage records",
|
||||
"label.purge.usage.records.error": "Failed while purging usage records",
|
||||
"label.purpose": "Purpose",
|
||||
"label.qostype": "QoS type",
|
||||
"label.quickview": "Quick view",
|
||||
@ -1731,7 +1740,14 @@
|
||||
"label.rados.secret": "RADOS secret",
|
||||
"label.rados.user": "RADOS user",
|
||||
"label.ram": "RAM",
|
||||
"label.range.today": "Today",
|
||||
"label.range.yesterday": "Yesterday",
|
||||
"label.range.last.1week": "Last 1 week",
|
||||
"label.range.last.2week": "Last 2 weeks",
|
||||
"label.range.last.1month": "Last 1 month",
|
||||
"label.range.last.3month": "Last 3 months",
|
||||
"label.raw.data": "Raw data",
|
||||
"label.rawusage": "Raw usage (in hours)",
|
||||
"label.rbd": "RBD",
|
||||
"label.rbdid": "Cephx user",
|
||||
"label.rbdmonitor": "Ceph monitor",
|
||||
@ -1975,6 +1991,7 @@
|
||||
"label.sharedrouteripv6": "IPv6 address for the VR in this shared Network.",
|
||||
"label.sharewith": "Share with",
|
||||
"label.showing": "Showing",
|
||||
"label.show.usage.records": "Show usage records",
|
||||
"label.shrinkok": "Shrink OK",
|
||||
"label.shutdown": "Shutdown",
|
||||
"label.shutdown.provider": "Shutdown provider",
|
||||
@ -2283,8 +2300,22 @@
|
||||
"label.upload.volume.from.url": "Upload volume from URL",
|
||||
"label.url": "URL",
|
||||
"label.usage.explanation": "Note: Only the usage server that owns the active usage job is shown here.",
|
||||
"label.usage": "Usage",
|
||||
"label.usage.records.downloading": "Downloading usage records",
|
||||
"label.usage.records.fetch.child.domains": "Fetch usage records for child domains",
|
||||
"label.usage.records.usagetype.required": "Usage type is required with resource ID",
|
||||
"label.usage.records.generate": "Generate usage records",
|
||||
"label.usage.records.generate.after": "Usage records will be created for the period after ",
|
||||
"label.usage.records.generated": "A job has been created to generate usage records.",
|
||||
"label.usage.records.generate.description": "If the scheduled usage job was not run or failed, this will generate records(only if there any records to be generated)",
|
||||
"label.usage.records.purge": "Purge usage records",
|
||||
"label.usage.records.purge.days": "Purge records older than",
|
||||
"label.usage.records.purge.days.description": "Purge records older than the specified number of days.",
|
||||
"label.usage.records.purge.alert": "Purging usage records will permanently delete the records from the database. Depending on the data being deleted, this can increase load on the database and may take a while. Are you sure you want to continue?",
|
||||
"label.usageid": "Resource ID",
|
||||
"label.usageinterface": "Usage interface",
|
||||
"label.usagename": "Usage type",
|
||||
"label.usagetype": "Usage type",
|
||||
"label.usageunit": "Unit",
|
||||
"label.usageislocal": "A Usage Server is installed locally",
|
||||
"label.usagetypedescription": "Usage description",
|
||||
|
||||
@ -448,7 +448,8 @@
|
||||
<div class="resource-detail-item__label">{{ $t('label.volume') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<hdd-outlined />
|
||||
<router-link :to="{ path: '/volume/' + resource.volumeid }">{{ resource.volumename || resource.volume || resource.volumeid }} </router-link>
|
||||
<router-link v-if="validLinks.volume" :to="{ path: '/volume/' + resource.volumeid }">{{ resource.volumename || resource.volume || resource.volumeid }} </router-link>
|
||||
<span v-else>{{ resource.volumename || resource.volume || resource.volumeid }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.associatednetworkid">
|
||||
@ -812,6 +813,7 @@
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import { createPathBasedOnVmType } from '@/utils/plugins'
|
||||
import { validateLinks } from '@/utils/links'
|
||||
import Console from '@/components/widgets/Console'
|
||||
import OsLogo from '@/components/widgets/OsLogo'
|
||||
import Status from '@/components/widgets/Status'
|
||||
@ -877,7 +879,8 @@ export default {
|
||||
vpc: '',
|
||||
network: ''
|
||||
},
|
||||
newResource: {}
|
||||
newResource: {},
|
||||
validLinks: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -894,6 +897,7 @@ export default {
|
||||
this.newResource = newData
|
||||
this.showKeys = false
|
||||
this.setData()
|
||||
this.validLinks = validateLinks(this.$router, this.isStatic, this.resource)
|
||||
|
||||
if ('apikey' in this.resource) {
|
||||
this.getUserKeys()
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
:pagination="false"
|
||||
:rowSelection="explicitlyAllowRowSelection || enableGroupAction() || $route.name === 'event' ? {selectedRowKeys: selectedRowKeys, onChange: onSelectChange, columnWidth: 30} : null"
|
||||
:rowClassName="getRowClassName"
|
||||
@resizeColumn="handleResizeColumn"
|
||||
style="overflow-y: auto"
|
||||
>
|
||||
<template #customFilterDropdown>
|
||||
@ -98,6 +99,9 @@
|
||||
<template v-if="column.key === 'templatetype'">
|
||||
<span>{{ text }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'templateid'">
|
||||
<router-link :to="{ path: '/template/' + record.templateid }">{{ text }}</router-link>
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">
|
||||
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(text)">{{ $t(text.toLowerCase()) }}</span>
|
||||
<span v-else>{{ text }}</span>
|
||||
@ -167,7 +171,8 @@
|
||||
<router-link :to="{ path: getVmRouteUsingType(record) + record.virtualmachineid }">{{ text }}</router-link>
|
||||
</template>
|
||||
<template v-if="column.key === 'volumename'">
|
||||
<router-link :to="{ path: '/volume/' + record.volumeid }">{{ text }}</router-link>
|
||||
<router-link v-if="resourceIdToValidLinksMap[record.id]?.volume" :to="{ path: '/volume/' + record.volumeid }">{{ text }}</router-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'size'">
|
||||
<span v-if="text && $route.path === '/kubernetes'">
|
||||
@ -244,7 +249,7 @@
|
||||
</template>
|
||||
<template v-if="column.key === 'vpcname'">
|
||||
<a v-if="record.vpcid">
|
||||
<router-link :to="{ path: '/vpc/' + record.vpcid }">{{ text }}</router-link>
|
||||
<router-link :to="{ path: '/vpc/' + record.vpcid }">{{ text || record.vpcid }}</router-link>
|
||||
</a>
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
@ -275,6 +280,9 @@
|
||||
<template v-if="column.key === 'level'">
|
||||
<router-link :to="{ path: '/event/' + record.id }">{{ text }}</router-link>
|
||||
</template>
|
||||
<template v-if="column.key === 'usageType'">
|
||||
{{ usageTypeMap[record.usagetype] }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'clustername'">
|
||||
<router-link :to="{ path: '/cluster/' + record.clusterid }">{{ text }}</router-link>
|
||||
@ -318,7 +326,7 @@
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'zone'">
|
||||
<router-link v-if="record.zoneid && !record.zoneid.includes(',') && $router.resolve('/zone/' + record.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
|
||||
<router-link v-if="record.zoneid && !record.zoneid.includes(',') && $router.resolve('/zone/' + record.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zone/' + record.zoneid }">{{ text || record.zoneid }}</router-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'zonename'">
|
||||
@ -373,6 +381,9 @@
|
||||
<template v-if="column.key === 'payloadurl'">
|
||||
<copy-label :label="text" />
|
||||
</template>
|
||||
<template v-if="column.key === 'usageid'">
|
||||
<copy-label :label="text" />
|
||||
</template>
|
||||
<template v-if="column.key === 'eventtype'">
|
||||
<router-link v-if="$router.resolve('/event/' + record.eventid).matched[0].redirect !== '/exception/404'" :to="{ path: '/event/' + record.eventid }">{{ text }}</router-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
@ -405,6 +416,9 @@
|
||||
<template v-if="column.key === 'duration' && ['webhook', 'webhookdeliveries'].includes($route.path.split('/')[1])">
|
||||
<span> {{ getDuration(record.startdate, record.enddate) }} </span>
|
||||
</template>
|
||||
<template v-if="['startdate', 'enddate'].includes(column.key) && ['usage'].includes($route.path.split('/')[1])">
|
||||
{{ $toLocaleDate(text.replace('\'T\'', ' ')) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'order'">
|
||||
<div class="shift-btns">
|
||||
<a-tooltip :name="text" placement="top">
|
||||
@ -481,6 +495,13 @@
|
||||
icon="reload-outlined"
|
||||
:disabled="!('updateConfiguration' in $store.getters.apis)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'usageActions'">
|
||||
<tooltip-button
|
||||
:tooltip="$t('label.view')"
|
||||
icon="search-outlined"
|
||||
@onClick="$emit('view-usage-record', record)" />
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template v-if="column.key === 'tariffActions'">
|
||||
<tooltip-button
|
||||
:tooltip="$t('label.edit')"
|
||||
@ -523,6 +544,7 @@ import TooltipButton from '@/components/widgets/TooltipButton'
|
||||
import ResourceIcon from '@/components/view/ResourceIcon'
|
||||
import ResourceLabel from '@/components/widgets/ResourceLabel'
|
||||
import { createPathBasedOnVmType } from '@/utils/plugins'
|
||||
import { validateLinks } from '@/utils/links'
|
||||
import cronstrue from 'cronstrue/i18n'
|
||||
import moment from 'moment-timezone'
|
||||
|
||||
@ -615,9 +637,25 @@ export default {
|
||||
notification: 'storageallocatedthreshold',
|
||||
disable: 'storageallocateddisablethreshold'
|
||||
}
|
||||
},
|
||||
usageTypeMap: {},
|
||||
resourceIdToValidLinksMap: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
items: {
|
||||
deep: true,
|
||||
handler (newData, oldData) {
|
||||
if (newData === oldData) return
|
||||
this.items.forEach(record => {
|
||||
this.resourceIdToValidLinksMap[record.id] = validateLinks(this.$router, false, record)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getUsageTypes()
|
||||
},
|
||||
computed: {
|
||||
hasSelected () {
|
||||
return this.selectedRowKeys.length > 0
|
||||
@ -928,6 +966,9 @@ export default {
|
||||
}
|
||||
return name
|
||||
},
|
||||
handleResizeColumn (w, col) {
|
||||
col.width = w
|
||||
},
|
||||
updateSelectedColumns (name) {
|
||||
this.$emit('update-selected-columns', name)
|
||||
},
|
||||
@ -951,6 +992,24 @@ export default {
|
||||
}
|
||||
var duration = Date.parse(enddate) - Date.parse(startdate)
|
||||
return (duration > 0 ? duration / 1000.0 : 0) + ''
|
||||
},
|
||||
getUsageTypes () {
|
||||
if (this.$route.path.split('/')[1] === 'usage') {
|
||||
api('listUsageTypes').then(json => {
|
||||
if (json && json.listusagetypesresponse && json.listusagetypesresponse.usagetype) {
|
||||
this.usageTypes = json.listusagetypesresponse.usagetype.map(x => {
|
||||
return {
|
||||
id: x.usagetypeid,
|
||||
value: x.description
|
||||
}
|
||||
})
|
||||
this.usageTypeMap = {}
|
||||
for (var usageType of this.usageTypes) {
|
||||
this.usageTypeMap[usageType.id] = usageType.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +87,12 @@ export default {
|
||||
case 'InProgress':
|
||||
state = this.$t('state.inprogress')
|
||||
break
|
||||
case 'Down':
|
||||
state = this.$t('state.down')
|
||||
break
|
||||
case 'Up':
|
||||
state = this.$t('state.up')
|
||||
break
|
||||
}
|
||||
return state.charAt(0).toUpperCase() + state.slice(1)
|
||||
}
|
||||
|
||||
@ -224,7 +224,6 @@ export function asyncRouterMap () {
|
||||
generateRouterMap(tools),
|
||||
generateRouterMap(quota),
|
||||
generateRouterMap(cloudian),
|
||||
|
||||
{
|
||||
path: '/exception',
|
||||
name: 'exception',
|
||||
|
||||
@ -61,6 +61,14 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'usage',
|
||||
title: 'label.usage',
|
||||
icon: 'ContainerOutlined',
|
||||
permission: ['listUsageRecords'],
|
||||
meta: { title: 'label.usage', icon: 'ContainerOutlined' },
|
||||
component: () => import('@/views/infra/UsageRecords.vue')
|
||||
},
|
||||
{
|
||||
name: 'manageinstances',
|
||||
title: 'label.action.import.export.instances',
|
||||
|
||||
@ -56,6 +56,7 @@ import {
|
||||
ClusterOutlined,
|
||||
CodeOutlined,
|
||||
CompassOutlined,
|
||||
ContainerOutlined,
|
||||
ControlOutlined,
|
||||
CopyOutlined,
|
||||
CreditCardOutlined,
|
||||
@ -220,6 +221,7 @@ export default {
|
||||
app.component('CloudUploadOutlined', CloudUploadOutlined)
|
||||
app.component('ClusterOutlined', ClusterOutlined)
|
||||
app.component('CodeOutlined', CodeOutlined)
|
||||
app.component('ContainerOutlined', ContainerOutlined)
|
||||
app.component('ControlOutlined', ControlOutlined)
|
||||
app.component('CompassOutlined', CompassOutlined)
|
||||
app.component('CopyOutlined', CopyOutlined)
|
||||
|
||||
36
ui/src/utils/links.js
Normal file
36
ui/src/utils/links.js
Normal file
@ -0,0 +1,36 @@
|
||||
// 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.
|
||||
|
||||
export function validateLinks (router, isStatic, resource) {
|
||||
const validLinks = {
|
||||
volume: false
|
||||
}
|
||||
|
||||
if (isStatic) {
|
||||
return validLinks
|
||||
}
|
||||
|
||||
if (resource.volumeid && router.resolve('/volume/' + resource.volumeid).matched[0].redirect !== '/exception/404') {
|
||||
if (resource.volumestate) {
|
||||
validLinks.volume = resource.volumestate !== 'Expunged'
|
||||
} else {
|
||||
validLinks.volume = true
|
||||
}
|
||||
}
|
||||
|
||||
return validLinks
|
||||
}
|
||||
@ -68,3 +68,27 @@ export function sanitizeReverse (value) {
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
}
|
||||
|
||||
export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
|
||||
if (data === null || !data.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
let result = ''
|
||||
result += keys.join(columnDelimiter)
|
||||
result += lineDelimiter
|
||||
|
||||
data.forEach(item => {
|
||||
keys.forEach(key => {
|
||||
if (item[key] === undefined) {
|
||||
item[key] = ''
|
||||
}
|
||||
result += typeof item[key] === 'string' && item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key]
|
||||
result += columnDelimiter
|
||||
})
|
||||
result = result.slice(0, -1)
|
||||
result += lineDelimiter
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -738,7 +738,7 @@ export default {
|
||||
})
|
||||
},
|
||||
fetchData (params = {}) {
|
||||
if (this.$route.name === 'deployVirtualMachine') {
|
||||
if (['deployVirtualMachine', 'usage'].includes(this.$route.name)) {
|
||||
return
|
||||
}
|
||||
if (this.routeName !== this.$route.name) {
|
||||
|
||||
@ -402,6 +402,7 @@ export default {
|
||||
if (this.arrayHasItems(networks)) {
|
||||
this.network = networks[0]
|
||||
}
|
||||
resolve(this.network)
|
||||
})
|
||||
this.networkLoading = false
|
||||
})
|
||||
|
||||
@ -111,6 +111,7 @@ import draggable from 'vuedraggable'
|
||||
import PermissionEditable from './PermissionEditable'
|
||||
import RuleDelete from './RuleDelete'
|
||||
import TooltipButton from '@/components/widgets/TooltipButton'
|
||||
import { toCsv } from '@/utils/util.js'
|
||||
|
||||
export default {
|
||||
name: 'RolePermissionTab',
|
||||
@ -249,32 +250,8 @@ export default {
|
||||
this.updateTable = false
|
||||
})
|
||||
},
|
||||
rulesDataToCsv ({ data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
|
||||
if (data === null || !data.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const keys = ['rule', 'permission', 'description']
|
||||
let result = ''
|
||||
result += keys.join(columnDelimiter)
|
||||
result += lineDelimiter
|
||||
|
||||
data.forEach(item => {
|
||||
keys.forEach(key => {
|
||||
if (item[key] === undefined) {
|
||||
item[key] = ''
|
||||
}
|
||||
result += typeof item[key] === 'string' && item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key]
|
||||
result += columnDelimiter
|
||||
})
|
||||
result = result.slice(0, -1)
|
||||
result += lineDelimiter
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
exportRolePermissions () {
|
||||
const rulesCsvData = this.rulesDataToCsv({ data: this.rules })
|
||||
const rulesCsvData = toCsv({ keys: ['rule', 'permission', 'description'], data: this.rules })
|
||||
const hiddenElement = document.createElement('a')
|
||||
hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(rulesCsvData)
|
||||
hiddenElement.target = '_blank'
|
||||
|
||||
834
ui/src/views/infra/UsageRecords.vue
Normal file
834
ui/src/views/infra/UsageRecords.vue
Normal file
@ -0,0 +1,834 @@
|
||||
// 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-affix :offsetTop="this.$store.getters.shutdownTriggered ? 103 : 78">
|
||||
<a-card class="breadcrumb-card">
|
||||
<a-row>
|
||||
<a-col
|
||||
:span="device === 'mobile' ? 24 : 12"
|
||||
style="padding-left: 12px; margin-top: 10px"
|
||||
>
|
||||
<breadcrumb :resource="resource">
|
||||
<template #end>
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>{{ $t('label.refresh') }}</template>
|
||||
<a-button
|
||||
style="margin-top: 4px"
|
||||
:loading="serverMetricsLoading"
|
||||
shape="round"
|
||||
size="small"
|
||||
@click="fetchData(); listUsageRecords()"
|
||||
>
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
{{ $t('label.refresh') }}
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</breadcrumb>
|
||||
</a-col>
|
||||
<a-col
|
||||
:span="device === 'mobile' ? 24 : 12"
|
||||
:style="device === 'mobile' ? { float: 'right', 'margin-top': '12px', 'margin-bottom': '-6px', display: 'table' } : { float: 'right', display: 'table', 'margin-top': '6px' }"
|
||||
>
|
||||
<a-row justify="end">
|
||||
<a-col>
|
||||
<tooltip-button
|
||||
type="primary"
|
||||
icon="hdd-outlined"
|
||||
:tooltip="$t('label.usage.records.generate')"
|
||||
@onClick="generateModal = true"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<tooltip-button
|
||||
type="danger"
|
||||
icon="delete-outlined"
|
||||
:tooltip="$t('label.usage.records.purge')"
|
||||
@onClick="() => purgeModal = true"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-affix>
|
||||
<a-col>
|
||||
<a-card size="small" :loading="serverMetricsLoading">
|
||||
<a-row justify="space-around">
|
||||
<a-card-grid style="width: 30%; text-align: center; font-size: small;">
|
||||
<a-statistic
|
||||
:title="$t('label.server')"
|
||||
:value="serverStats.hostname"
|
||||
valueStyle="font-size: medium"
|
||||
>
|
||||
<template #prefix>
|
||||
<status :text="serverStats.state || ''" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card-grid>
|
||||
<a-card-grid style="width: 35%; text-align: center; font-size: small;">
|
||||
<a-statistic
|
||||
:title="$t('label.lastheartbeat')"
|
||||
:value="$toLocaleDate(serverStats.lastheartbeat)"
|
||||
valueStyle="font-size: medium"
|
||||
/>
|
||||
<a-card-meta :description="getTimeSince(serverStats.collectiontime)" />
|
||||
</a-card-grid>
|
||||
<a-card-grid style="width: 35%; text-align: center; font-size: small;">
|
||||
<a-statistic
|
||||
:title="$t('label.lastsuccessfuljob')"
|
||||
:value="$toLocaleDate(serverStats.lastsuccessfuljob)"
|
||||
valueStyle="font-size: medium"
|
||||
/>
|
||||
<a-card-meta :description="getTimeSince(serverStats.lastsuccessfuljob)" />
|
||||
</a-card-grid>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-row justify="space-between">
|
||||
<a-col :span="24">
|
||||
<a-card>
|
||||
<a-form
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-col :span="4">
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
ref="domain"
|
||||
name="domain"
|
||||
>
|
||||
<a-auto-complete
|
||||
v-model:value="form.domain"
|
||||
:options="domains"
|
||||
:placeholder="$t('label.domain')"
|
||||
:filter-option="filterOption"
|
||||
style="width: 100%;"
|
||||
@select="getAccounts"
|
||||
:dropdownMatchSelectWidth="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
ref="isRecursive"
|
||||
name="isRecursive"
|
||||
>
|
||||
<a-checkbox v-model:checked="form.isRecursive">{{ $t('label.usage.records.fetch.child.domains')
|
||||
}}</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<a-form-item
|
||||
ref="account"
|
||||
name="account"
|
||||
>
|
||||
<a-auto-complete
|
||||
v-model:value="form.account"
|
||||
:options="accounts"
|
||||
:placeholder="$t('label.account')"
|
||||
:filter-option="filterOption"
|
||||
:disabled="form.isRecursive"
|
||||
:dropdownMatchSelectWidth="false"
|
||||
@select="selectAccount"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<a-form-item
|
||||
ref="type"
|
||||
name="type"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.type"
|
||||
:options="usageTypes"
|
||||
:placeholder="$t('label.usagetype')"
|
||||
:filterOption="filterOption"
|
||||
@select="selectUsageType"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<a-form-item
|
||||
ref="id"
|
||||
name="id"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.id"
|
||||
:placeholder="$t('label.resourceid')"
|
||||
:allowClear="true"
|
||||
@change="handleResourceIdChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item
|
||||
ref="dateRange"
|
||||
name="dateRange"
|
||||
>
|
||||
<a-range-picker
|
||||
:ranges="rangePresets"
|
||||
v-model:value="form.dateRange"
|
||||
:disabled-date="disabledDate"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<a-row justify="space-between">
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
@click="handleSearch"
|
||||
:loading="loading"
|
||||
>
|
||||
<search-outlined />
|
||||
{{ $t('label.show.usage.records') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="downloadRecords"
|
||||
:loading="loading"
|
||||
>
|
||||
<download-outlined />
|
||||
{{ $t('label.download.csv') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button @click="clearFilters">
|
||||
{{ $t('label.clear') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row justify="space-around">
|
||||
<a-col :span="24">
|
||||
<list-view
|
||||
:loading="tableLoading"
|
||||
:columns="columns"
|
||||
:items="usageRecords"
|
||||
:columnKeys="columnKeys"
|
||||
:selectedColumns="selectedColumnKeys"
|
||||
ref="listview"
|
||||
@update-selected-columns="updateSelectedColumns"
|
||||
@view-usage-record="viewUsageRecord"
|
||||
@refresh="this.fetchData"
|
||||
/>
|
||||
<a-pagination
|
||||
:current="page"
|
||||
:pageSize="pageSize"
|
||||
:total="totalUsageRecords"
|
||||
:showTotal="total => `${$t('label.showing')} ${Math.min(total, 1 + ((page - 1) * pageSize))}-${Math.min(page * pageSize, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
|
||||
:pageSizeOptions="['20', '50', '100']"
|
||||
@change="handleTableChange"
|
||||
:showSizeChanger="true"
|
||||
:showQuickJumper="true"
|
||||
>
|
||||
</a-pagination>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-modal
|
||||
:title="$t('label.usage.records.generate')"
|
||||
:cancelText="$t('label.cancel')"
|
||||
:closable="true"
|
||||
:maskClosable="true"
|
||||
:destroyOnClose="true"
|
||||
:visible="generateModal"
|
||||
@ok="generateUsageRecords"
|
||||
@cancel="generateModal = false"
|
||||
>
|
||||
<a-alert
|
||||
:message="$t('label.usage.records.generate.description')"
|
||||
type="info"
|
||||
show-icon
|
||||
>
|
||||
</a-alert>
|
||||
<br/>
|
||||
{{ $t('label.usage.records.generate.after') + $toLocaleDate(serverStats.lastsuccessfuljob) }}
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
:title="$t('label.usage.records.purge')"
|
||||
:visible="purgeModal"
|
||||
:okText="$t('label.usage.records.purge')"
|
||||
:okButtonProps="{ type: 'danger' }"
|
||||
:cancelText="$t('label.cancel')"
|
||||
:closable="true"
|
||||
:maskClosable="true"
|
||||
:destroyOnClose="true"
|
||||
@ok="purgeUsageRecords"
|
||||
@cancel="purgeModal = false"
|
||||
>
|
||||
<a-row>
|
||||
<a-alert
|
||||
:description="$t('label.usage.records.purge.alert')"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
</a-row>
|
||||
<br />
|
||||
<a-row justify="space-between">
|
||||
<tooltip-label
|
||||
bold
|
||||
:title="$t('label.usage.records.purge.days')"
|
||||
:tooltip="$t('label.usage.records.purge.days.description')"
|
||||
/>
|
||||
<a-input-number
|
||||
:min="0"
|
||||
v-model:value="purgeDays"
|
||||
style="width: 128px;"
|
||||
>
|
||||
<template #addonAfter>{{ $t('label.days') }}</template>
|
||||
</a-input-number>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
<a-modal
|
||||
:title="$t('label.usage.records.downloading')"
|
||||
:visible="downloadModal"
|
||||
:closable="false"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
:footer="null"
|
||||
>
|
||||
<a-progress
|
||||
:percent="downloadPercent"
|
||||
:status="downloadStatus"
|
||||
/>
|
||||
<a-spin size="small" /> {{ [$t('label.fetched'), downloadedRecords, $t('label.of'), downloadTotalRecords,
|
||||
$t('label.items')].join(' ') }}
|
||||
</a-modal>
|
||||
<a-modal
|
||||
:visible="viewModal"
|
||||
:cancelText="$t('label.close')"
|
||||
:closable="true"
|
||||
:maskClosable="true"
|
||||
:okButtonProps="{ style: { display: 'none' } }"
|
||||
:destroyOnClose="true"
|
||||
width="50%"
|
||||
@cancel="viewModal = false"
|
||||
>
|
||||
<pre style="text-align: start; white-space: break-spaces;">{{ JSON.stringify(recordView, null, 2) }}</pre>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { api } from '@/api'
|
||||
import { toCsv } from '@/utils/util.js'
|
||||
import { mixinForm } from '@/utils/mixin'
|
||||
|
||||
import Breadcrumb from '@/components/widgets/Breadcrumb'
|
||||
import ChartCard from '@/components/widgets/ChartCard'
|
||||
import ListView from '@/components/view/ListView'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import TooltipButton from '@/components/widgets/TooltipButton'
|
||||
import Status from '@/components/widgets/Status'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(utc)
|
||||
|
||||
export default {
|
||||
name: 'UsageRecords',
|
||||
mixins: [mixinForm],
|
||||
components: {
|
||||
Breadcrumb,
|
||||
ChartCard,
|
||||
ListView,
|
||||
Status,
|
||||
TooltipLabel,
|
||||
TooltipButton
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
var selectedColumnKeys = ['account', 'domain', 'usageType', 'usageid', 'startdate', 'enddate', 'rawusage', 'description']
|
||||
return {
|
||||
serverMetricsLoading: true,
|
||||
serverStats: {},
|
||||
loading: false,
|
||||
tableLoading: false,
|
||||
usageRecords: [],
|
||||
totalUsageRecords: 0,
|
||||
columnKeys: [...selectedColumnKeys,
|
||||
'zone', 'virtualmachinename', 'cpunumber', 'cpuspeed', 'memory', 'project', 'templateid', 'offeringid', 'size', 'type', 'vpcname'
|
||||
],
|
||||
selectedColumnKeys: selectedColumnKeys,
|
||||
selectedColumns: [],
|
||||
columns: [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
usageTypes: [],
|
||||
domains: [],
|
||||
accounts: [],
|
||||
account: null,
|
||||
domain: null,
|
||||
usageType: null,
|
||||
usageTypeMap: {},
|
||||
usageRecordKeys: {},
|
||||
generateModal: false,
|
||||
downloadModal: false,
|
||||
viewModal: false,
|
||||
purgeModal: false,
|
||||
purgeDays: ref(365),
|
||||
downloadPercent: 0,
|
||||
downloadedRecords: 0,
|
||||
downloadTotalRecords: 0,
|
||||
downloadStatus: 'active',
|
||||
rangePresets: {},
|
||||
recordView: {}
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.apiParams = this.$getApiParams('listUsageRecords')
|
||||
},
|
||||
created () {
|
||||
this.rangePresets[this.$t('label.range.today')] = [dayjs(), dayjs()]
|
||||
this.rangePresets[this.$t('label.range.yesterday')] = [dayjs().add(-1, 'd'), dayjs().add(-1, 'd')]
|
||||
this.rangePresets[this.$t('label.range.last.1week')] = [dayjs().add(-1, 'w'), dayjs()]
|
||||
this.rangePresets[this.$t('label.range.last.2week')] = [dayjs().add(-2, 'w'), dayjs()]
|
||||
this.rangePresets[this.$t('label.range.last.1month')] = [dayjs().add(-1, 'M'), dayjs()]
|
||||
this.rangePresets[this.$t('label.range.last.3month')] = [dayjs().add(-90, 'M'), dayjs()]
|
||||
this.initForm()
|
||||
this.fetchData()
|
||||
this.updateColumns()
|
||||
},
|
||||
methods: {
|
||||
clearFilters () {
|
||||
this.formRef.value.resetFields()
|
||||
this.rules.type = {}
|
||||
this.domain = null
|
||||
this.account = null
|
||||
this.usageType = null
|
||||
this.page = 1
|
||||
this.pageSize = 20
|
||||
|
||||
this.getAccounts()
|
||||
},
|
||||
disabledDate (current) {
|
||||
return current && current > dayjs().endOf('day')
|
||||
},
|
||||
filterOption (input, option) {
|
||||
return option.value.toUpperCase().indexOf(input.toUpperCase()) >= 0
|
||||
},
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({
|
||||
domain: null,
|
||||
account: null,
|
||||
type: null,
|
||||
id: null,
|
||||
dateRange: [],
|
||||
isRecursive: false
|
||||
})
|
||||
this.rules = reactive({
|
||||
dateRange: [{ type: 'array', required: true, message: this.$t('label.required') }],
|
||||
type: { type: 'string', required: false, message: this.$t('label.usage.records.usagetype.required') }
|
||||
})
|
||||
},
|
||||
fetchData () {
|
||||
this.listUsageServerMetrics()
|
||||
this.getUsageTypes()
|
||||
this.getAllUsageRecordColumns()
|
||||
this.getDomains()
|
||||
this.getAccounts()
|
||||
if (!this.$store.getters.customColumns[this.$store.getters.userInfo.id]) {
|
||||
this.$store.getters.customColumns[this.$store.getters.userInfo.id] = {}
|
||||
this.$store.getters.customColumns[this.$store.getters.userInfo.id][this.$route.path] = this.selectedColumnKeys
|
||||
} else {
|
||||
this.selectedColumnKeys = this.$store.getters.customColumns[this.$store.getters.userInfo.id][this.$route.path] || this.selectedColumnKeys
|
||||
this.updateSelectedColumns()
|
||||
}
|
||||
this.updateSelectedColumns()
|
||||
},
|
||||
viewUsageRecord (record) {
|
||||
this.viewModal = true
|
||||
this.recordView = record
|
||||
},
|
||||
handleResourceIdChange () {
|
||||
this.rules.type.required = this.form.id && this.form.id.trim()
|
||||
},
|
||||
handleTableChange (page, pageSize) {
|
||||
if (this.pageSize !== pageSize) {
|
||||
page = 1
|
||||
}
|
||||
if (this.page !== page || this.pageSize !== pageSize) {
|
||||
this.page = page
|
||||
this.pageSize = pageSize
|
||||
this.listUsageRecords()
|
||||
document.documentElement.scrollIntoView()
|
||||
}
|
||||
},
|
||||
listUsageServerMetrics () {
|
||||
this.serverMetricsLoading = true
|
||||
api('listUsageServerMetrics').then(json => {
|
||||
this.stats = []
|
||||
if (json && json.listusageservermetricsresponse && json.listusageservermetricsresponse.usageMetrics) {
|
||||
this.serverStats = json.listusageservermetricsresponse.usageMetrics
|
||||
}
|
||||
}).finally(f => {
|
||||
this.serverMetricsLoading = false
|
||||
})
|
||||
},
|
||||
handleSearch () {
|
||||
if (this.loading) return
|
||||
this.formRef.value.clearValidate()
|
||||
this.formRef.value.validate().then(() => {
|
||||
this.page = 1
|
||||
this.listUsageRecords()
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
},
|
||||
selectAccount (value, option) {
|
||||
if (option && option.id) {
|
||||
this.account = option
|
||||
} else {
|
||||
this.account = null
|
||||
if (this.formRef?.value) {
|
||||
this.formRef.value.resetFields('account')
|
||||
}
|
||||
}
|
||||
},
|
||||
selectUsageType (value, option) {
|
||||
if (option && option.id) {
|
||||
this.usageType = option
|
||||
} else {
|
||||
this.usageType = null
|
||||
if (this.formRef?.value) {
|
||||
this.formRef.value.resetFields('type')
|
||||
}
|
||||
}
|
||||
},
|
||||
getDomains () {
|
||||
api('listDomains', { listAll: true }).then(json => {
|
||||
if (json && json.listdomainsresponse && json.listdomainsresponse.domain) {
|
||||
this.domains = [{ id: null, value: '' }, ...json.listdomainsresponse.domain.map(x => {
|
||||
return {
|
||||
id: x.id,
|
||||
value: x.path
|
||||
}
|
||||
})]
|
||||
}
|
||||
})
|
||||
},
|
||||
getAccounts (value, option) {
|
||||
var params = {
|
||||
listAll: true
|
||||
}
|
||||
if (option && option.id) {
|
||||
params.domainid = option.id
|
||||
this.domain = option
|
||||
} else {
|
||||
this.domain = null
|
||||
if (this.formRef?.value) {
|
||||
this.formRef.value.resetFields('domain')
|
||||
}
|
||||
}
|
||||
api('listAccounts', params).then(json => {
|
||||
if (json && json.listaccountsresponse && json.listaccountsresponse.account) {
|
||||
this.accounts = [{ id: null, value: '' }, ...json.listaccountsresponse.account.map(x => {
|
||||
return {
|
||||
id: x.id,
|
||||
value: x.name
|
||||
}
|
||||
})]
|
||||
}
|
||||
})
|
||||
},
|
||||
getParams (page, pageSize) {
|
||||
const formRaw = toRaw(this.form)
|
||||
const values = this.handleRemoveFields(formRaw)
|
||||
var params = {
|
||||
page: page || this.page,
|
||||
pagesize: pageSize || this.pageSize
|
||||
}
|
||||
if (values.dateRange) {
|
||||
if (this.$store.getters.usebrowsertimezone) {
|
||||
params.startdate = dayjs.utc(dayjs(values.dateRange[0]).startOf('day')).format('YYYY-MM-DD HH:mm:ss')
|
||||
params.enddate = dayjs.utc(dayjs(values.dateRange[0]).endOf('day')).format('YYYY-MM-DD HH:mm:ss')
|
||||
} else {
|
||||
params.startdate = dayjs(values.dateRange[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
params.enddate = dayjs(values.dateRange[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
}
|
||||
if (values.domain) {
|
||||
params.domainid = this.domain.id
|
||||
}
|
||||
if (values.account) {
|
||||
params.accountid = this.account.id
|
||||
}
|
||||
if (values.type) {
|
||||
params.type = this.usageType.id
|
||||
}
|
||||
if (values.isRecursive) {
|
||||
params.isrecursive = true
|
||||
}
|
||||
if (values.id) {
|
||||
params.usageid = values.id
|
||||
}
|
||||
return params
|
||||
},
|
||||
listUsageRecords () {
|
||||
this.tableLoading = true
|
||||
this.loading = true
|
||||
var params = this.getParams()
|
||||
if (!(params.startdate && params.enddate)) {
|
||||
this.tableLoading = false
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
api('listUsageRecords', params).then(json => {
|
||||
if (json && json.listusagerecordsresponse) {
|
||||
this.usageRecords = json?.listusagerecordsresponse?.usagerecord || []
|
||||
this.totalUsageRecords = json?.listusagerecordsresponse?.count || 0
|
||||
let count = 1
|
||||
for (var record of this.usageRecords) {
|
||||
// Set id to ensure a unique value of rowKey to avoid duplicates
|
||||
record.id = count++
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
}).finally(f => {
|
||||
this.tableLoading = false
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
getUsageTypes () {
|
||||
api('listUsageTypes').then(json => {
|
||||
if (json && json.listusagetypesresponse && json.listusagetypesresponse.usagetype) {
|
||||
this.usageTypes = [{ id: null, value: '' }, ...json.listusagetypesresponse.usagetype.map(x => {
|
||||
return {
|
||||
id: x.usagetypeid,
|
||||
value: x.description
|
||||
}
|
||||
})]
|
||||
this.usageTypeMap = {}
|
||||
for (var usageType of this.usageTypes) {
|
||||
this.usageTypeMap[usageType.id] = usageType.value
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
getTimeSince (date) {
|
||||
if (date === undefined || date === null) {
|
||||
return ''
|
||||
}
|
||||
return dayjs(date).fromNow()
|
||||
},
|
||||
updateSelectedColumns (key) {
|
||||
if (this.selectedColumnKeys.includes(key)) {
|
||||
this.selectedColumnKeys = this.selectedColumnKeys.filter(x => x !== key)
|
||||
} else {
|
||||
this.selectedColumnKeys.push(key)
|
||||
}
|
||||
this.updateColumns()
|
||||
if (!this.$store.getters.customColumns[this.$store.getters.userInfo.id]) {
|
||||
this.$store.getters.customColumns[this.$store.getters.userInfo.id] = {}
|
||||
}
|
||||
this.$store.getters.customColumns[this.$store.getters.userInfo.id][this.$route.path] = this.selectedColumnKeys
|
||||
this.$store.dispatch('SetCustomColumns', this.$store.getters.customColumns)
|
||||
},
|
||||
updateColumns () {
|
||||
this.columns = []
|
||||
for (var columnKey of this.columnKeys) {
|
||||
if (!this.selectedColumnKeys.includes(columnKey)) continue
|
||||
var title
|
||||
var dataIndex = columnKey
|
||||
var resizable = true
|
||||
switch (columnKey) {
|
||||
case 'templateid':
|
||||
title = this.$t('label.templatename')
|
||||
break
|
||||
case 'startdate':
|
||||
title = this.$t('label.start.date.and.time')
|
||||
break
|
||||
case 'enddate':
|
||||
title = this.$t('label.end.date.and.time')
|
||||
break
|
||||
case 'usageActions':
|
||||
title = this.$t('label.view')
|
||||
break
|
||||
case 'virtualmachinename':
|
||||
dataIndex = 'name'
|
||||
break
|
||||
default:
|
||||
title = this.$t('label.' + String(columnKey).toLowerCase())
|
||||
}
|
||||
this.columns.push({
|
||||
key: columnKey,
|
||||
title: title,
|
||||
dataIndex: dataIndex,
|
||||
resizable: resizable
|
||||
})
|
||||
}
|
||||
this.columns.push({
|
||||
key: 'usageActions',
|
||||
title: this.$t('label.view'),
|
||||
dataIndex: 'usageActions',
|
||||
resizable: false
|
||||
})
|
||||
if (this.columns.length > 0) {
|
||||
this.columns[this.columns.length - 1].customFilterDropdown = true
|
||||
}
|
||||
},
|
||||
downloadRecords () {
|
||||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
this.downloadModal = true
|
||||
this.downloadPercent = 0
|
||||
this.downloadStatus = 'active'
|
||||
this.loading = true
|
||||
var params = this.getParams(1, 0) // to get count
|
||||
api('listUsageRecords', params).then(json => {
|
||||
if (Object.getOwnPropertyNames(json.listusagerecordsresponse).length === 0 || json.listusagerecordsresponse.count === 0) {
|
||||
this.$notifyError({
|
||||
response: { data: null },
|
||||
message: this.$t('label.no.usage.records')
|
||||
})
|
||||
this.loading = false
|
||||
this.downloadStatus = 'exception'
|
||||
this.downloadModal = false
|
||||
} else {
|
||||
var totalRecords = json.listusagerecordsresponse.count
|
||||
this.downloadTotalRecords = totalRecords
|
||||
var pageSize = 500
|
||||
var totalPages = Math.ceil(totalRecords / pageSize)
|
||||
var records = []
|
||||
var promises = []
|
||||
for (var i = 1; i <= totalPages; i++) {
|
||||
var p = this.fetchUsageRecords({ ...params, page: i, pagesize: pageSize }).then(data => {
|
||||
records = records.concat(data)
|
||||
this.downloadPercent = Math.round((records.length / totalRecords) * 100)
|
||||
this.downloadedRecords += records.length
|
||||
})
|
||||
promises.push(p)
|
||||
}
|
||||
return Promise.allSettled(promises).then(() => {
|
||||
this.downloadPercent = 100
|
||||
this.downloadStatus = 'success'
|
||||
this.downloadCsv(records, 'usage-records.csv')
|
||||
this.loading = false
|
||||
this.downloadModal = false
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
this.loading = false
|
||||
this.downloadStatus = 'exception'
|
||||
this.downloadModal = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
},
|
||||
downloadCsv (records, filename) {
|
||||
var csv = toCsv({ keys: this.usageRecordKeys, data: records })
|
||||
const hiddenElement = document.createElement('a')
|
||||
hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(csv)
|
||||
hiddenElement.target = '_blank'
|
||||
hiddenElement.download = filename
|
||||
hiddenElement.click()
|
||||
hiddenElement.remove()
|
||||
},
|
||||
fetchUsageRecords (params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
api('listUsageRecords', params).then(json => {
|
||||
return resolve(json.listusagerecordsresponse.usagerecord)
|
||||
}).catch(error => {
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
getAllUsageRecordColumns () {
|
||||
api('listApis', { name: 'listUsageRecords' }).then(json => {
|
||||
if (json && json.listapisresponse && json.listapisresponse.api) {
|
||||
var apiResponse = json.listapisresponse.api.filter(x => x.name === 'listUsageRecords')[0].response
|
||||
this.usageRecordKeys = []
|
||||
apiResponse.forEach(x => {
|
||||
if (x && x.name) {
|
||||
this.usageRecordKeys.push(x.name)
|
||||
}
|
||||
})
|
||||
this.usageRecordKeys.sort()
|
||||
}
|
||||
})
|
||||
},
|
||||
parseDates (date) {
|
||||
return this.$toLocaleDate(dayjs(date))
|
||||
},
|
||||
generateUsageRecords () {
|
||||
api('generateUsageRecords').then(json => {
|
||||
this.$message.success(this.$t('label.usage.records.generated'))
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
}).finally(f => {
|
||||
this.generateModal = false
|
||||
})
|
||||
},
|
||||
purgeUsageRecords () {
|
||||
var params = {
|
||||
interval: this.purgeDays
|
||||
}
|
||||
api('removeRawUsageRecords', params).then(json => {
|
||||
this.$message.success(this.$t('label.purge.usage.records.success'))
|
||||
}).catch(error => {
|
||||
this.$message.error(this.$t('label.purge.usage.records.error') + ': ' + error.message)
|
||||
}).finally(f => {
|
||||
this.purgeModal = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.breadcrumb-card {
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
margin-top: -16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -19,19 +19,33 @@
|
||||
<a-row :gutter="12" v-if="isPageAllowed">
|
||||
<a-col :md="24">
|
||||
<a-card class="breadcrumb-card">
|
||||
<a-col :md="24" style="display: flex">
|
||||
<breadcrumb style="padding-top: 6px; padding-left: 8px" />
|
||||
<a-button
|
||||
style="margin-left: 12px; margin-top: 4px"
|
||||
:loading="viewLoading"
|
||||
size="small"
|
||||
shape="round"
|
||||
@click="fetchData()" >
|
||||
<template #icon><reload-outlined /></template>
|
||||
{{ $t('label.refresh') }}
|
||||
</a-button>
|
||||
<a-row>
|
||||
<a-col
|
||||
:span="device === 'mobile' ? 24 : 12"
|
||||
style="padding-left: 12px; margin-top: 10px"
|
||||
>
|
||||
<breadcrumb :resource="resource">
|
||||
<template #end>
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>{{ $t('label.refresh') }}</template>
|
||||
<a-button
|
||||
style="margin-top: 4px"
|
||||
:loading="viewLoading"
|
||||
shape="round"
|
||||
size="small"
|
||||
@click="fetchData()"
|
||||
>
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
{{ $t('label.refresh') }}
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</breadcrumb>
|
||||
</a-col>
|
||||
</a-card>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col
|
||||
:md="24">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user