diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 8fca652518f2..0f3b307b3f99 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -302,6 +302,7 @@ public class ApiConstants { public static final String NEXT_ACL_RULE_ID = "nextaclruleid"; public static final String NEXT_HOP = "nexthop"; public static final String MOVE_ACL_CONSISTENCY_HASH = "aclconsistencyhash"; + public static final String IMAGE_CACHE_STORES = "imagecachestores"; public static final String IMAGE_PATH = "imagepath"; public static final String INSTANCE_CONVERSION_SUPPORTED = "instanceconversionsupported"; public static final String INTERNAL_DNS1 = "internaldns1"; diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDao.java index eda4bcfdaa1f..9c5589f53396 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDao.java @@ -40,6 +40,8 @@ public interface ImageStoreDao extends GenericDao { List listImageStores(); + Integer countAllImageCacheStores(); + List listImageCacheStores(); List listStoresByZoneId(long zoneId); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java index 4cb40b5eaf63..cf028505bdb7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java @@ -174,6 +174,13 @@ public List listImageStores() { return listBy(sc); } + @Override + public Integer countAllImageCacheStores() { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("role", SearchCriteria.Op.EQ, DataStoreRole.Image); + return getCount(sc); + } + @Override public List listImageCacheStores() { SearchCriteria sc = createSearchCriteria(); diff --git a/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java b/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java index f20c4484e85b..87e1d91185c5 100644 --- a/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java +++ b/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java @@ -628,6 +628,7 @@ public InfrastructureResponse listInfrastructure() { response.setHosts(hostCountAndCpuSockets.first()); response.setStoragePools(storagePoolDao.countAll()); response.setImageStores(imageStoreDao.countAllImageStores()); + response.setImageCacheStores(imageStoreDao.countAllImageCacheStores()); response.setBackupRepositories(backupRepositoryDao.countAll()); response.setObjectStores(objectStoreDao.countAllObjectStores()); response.setSystemvms(vmInstanceDao.countByTypes(VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.SecondaryStorageVm)); diff --git a/plugins/metrics/src/main/java/org/apache/cloudstack/response/InfrastructureResponse.java b/plugins/metrics/src/main/java/org/apache/cloudstack/response/InfrastructureResponse.java index 6fda3ff59534..13a1d5d1cc1a 100644 --- a/plugins/metrics/src/main/java/org/apache/cloudstack/response/InfrastructureResponse.java +++ b/plugins/metrics/src/main/java/org/apache/cloudstack/response/InfrastructureResponse.java @@ -19,6 +19,8 @@ import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; public class InfrastructureResponse extends BaseResponse { @@ -79,6 +81,10 @@ public class InfrastructureResponse extends BaseResponse { @Param(description = "Number of Alerts") private Integer alerts; + @SerializedName(ApiConstants.IMAGE_CACHE_STORES) + @Param(description = "Number of image cache stores", since = "4.22.0") + private Integer imageCacheStores; + public InfrastructureResponse() { setObjectName("infrastructure"); } @@ -134,4 +140,8 @@ public void setManagementServers(Integer managementServers) { public void setObjectStores(Integer objectStores) { this.objectStores = objectStores; } + + public void setImageCacheStores(Integer imageCacheStores) { + this.imageCacheStores = imageCacheStores; + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 805ea1adae94..c841f1ae3211 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -107,6 +107,7 @@ "label.action.delete.pod": "Delete Pod", "label.action.delete.primary.storage": "Delete Primary Storage", "label.action.delete.routing.firewall.rule": "Delete IPv4 Routing firewall rule", +"label.action.delete.secondary.staging.storage": "Delete Secondary Staging Storage", "label.action.delete.secondary.storage": "Delete Secondary Storage", "label.action.delete.security.group": "Delete Security Group", "label.action.delete.snapshot": "Delete Snapshot", @@ -318,6 +319,7 @@ "label.add.routing.policy": "Add Routing Policy", "label.add.rule": "Add Rule", "label.add.secondary.ip": "Add Secondary IP", +"label.add.secondary.staging.storage": "Add Secondary Staging Storage", "label.add.secondary.storage": "Add Secondary Storage", "label.add.security.group": "Add Security Group", "label.add.setting": "Add setting", @@ -2195,6 +2197,7 @@ "label.search": "Search", "label.secondary.isolated.vlan.type.isolated": "Isolated", "label.secondary.isolated.vlan.type.promiscuous": "Promiscuous", +"label.secondary.staging.storage": "Secondary Staging Storage", "label.secondary.storage": "Secondary Storage", "label.secondary.storage.vm": "Secondary Storage VM", "label.secondaryips": "Secondary IPs", @@ -2924,6 +2927,7 @@ "message.action.delete.oauth.provider": "Please confirm that you want to delete the OAuth provider.", "message.action.delete.physical.network": "Please confirm that you want to delete this physical Network.", "message.action.delete.pod": "Please confirm that you want to delete this Pod.", +"message.action.delete.secondary.staging.storage": "Please confirm that you want to delete this secondary staging storage.", "message.action.delete.secondary.storage": "Please confirm that you want to delete this secondary storage.", "message.action.delete.security.group": "Please confirm that you want to delete this security group.", "message.action.delete.snapshot": "Please confirm that you want to delete this Snapshot.", diff --git a/ui/src/config/router.js b/ui/src/config/router.js index 582fbaaf2f35..bceb59e88e68 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -66,6 +66,7 @@ function generateRouterMap (section) { if ('show' in child && !child.show()) { continue } + console.log('Generating route for child:', child.name) var component = child.component ? child.component : shallowRef(AutogenView) var route = { name: child.name, diff --git a/ui/src/config/section/infra.js b/ui/src/config/section/infra.js index dc365b74c930..99067edfecd9 100644 --- a/ui/src/config/section/infra.js +++ b/ui/src/config/section/infra.js @@ -23,6 +23,7 @@ import clusters from '@/config/section/infra/clusters' import hosts from '@/config/section/infra/hosts' import primaryStorages from '@/config/section/infra/primaryStorages' import secondaryStorages from '@/config/section/infra/secondaryStorages' +import secondaryStagingStorages from '@/config/section/infra/secondaryStagingStorages' import backupRepositories from '@/config/section/infra/backupRepositories' import objectStorages from '@/config/section/infra/objectStorages' import systemVms from '@/config/section/infra/systemVms' @@ -51,6 +52,7 @@ export default { hosts, primaryStorages, secondaryStorages, + secondaryStagingStorages, backupRepositories, objectStorages, systemVms, diff --git a/ui/src/config/section/infra/secondaryStagingStorages.js b/ui/src/config/section/infra/secondaryStagingStorages.js new file mode 100644 index 000000000000..43741afd9f97 --- /dev/null +++ b/ui/src/config/section/infra/secondaryStagingStorages.js @@ -0,0 +1,64 @@ +// 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. + +import store from '@/store' + +export default { + name: 'imagecachestore', + title: 'label.secondary.staging.storage', + icon: 'file-image-outlined', + docHelp: 'adminguide/storage.html#secondary-storage', + permission: ['listSecondaryStagingStores'], + searchFilters: ['name', 'zoneid', 'provider'], + hidden: true, + columns: () => { + var fields = ['name', 'url', 'protocol', 'scope', 'zonename'] + if (store.getters.apis.listSecondaryStagingStores.params.filter(x => x.name === 'readonly').length > 0) { + fields.push({ + field: 'readonly', + customTitle: 'access' + }) + } + return fields + }, + details: () => { + var fields = ['name', 'id', 'url', 'protocol', 'provider', 'scope', 'zonename'] + if (store.getters.apis.listSecondaryStagingStores.params.filter(x => x.name === 'readonly').length > 0) { + fields.push('readonly') + } + return fields + }, + resourceType: 'SecondaryStagingStorage', + actions: [ + { + api: 'createSecondaryStagingStore', + icon: 'plus-outlined', + docHelp: 'installguide/configuration.html#add-secondary-storage', + label: 'label.add.secondary.staging.storage', + listView: true, + args: ['url', 'zoneid', 'scope', 'provider'] + }, + { + api: 'deleteSecondaryStagingStore', + icon: 'delete-outlined', + label: 'label.action.delete.secondary.staging.storage', + message: 'message.action.delete.secondary.staging.storage', + dataView: true, + displayName: (record) => { return record.name || record.displayName || record.id } + } + ] +} diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js index 502eb5de0b63..cf2f61ed5d2e 100644 --- a/ui/src/core/lazy_lib/icons_use.js +++ b/ui/src/core/lazy_lib/icons_use.js @@ -82,6 +82,7 @@ import { EyeOutlined, FieldTimeOutlined, FileDoneOutlined, + FileImageOutlined, FileProtectOutlined, FileSyncOutlined, FileTextOutlined, @@ -257,6 +258,7 @@ export default { app.component('EyeOutlined', EyeOutlined) app.component('FieldTimeOutlined', FieldTimeOutlined) app.component('FileDoneOutlined', FileDoneOutlined) + app.component('FileImageOutlined', FileImageOutlined) app.component('FileProtectOutlined', FileProtectOutlined) app.component('FileSyncOutlined', FileSyncOutlined) app.component('FileTextOutlined', FileTextOutlined) diff --git a/ui/src/utils/util.js b/ui/src/utils/util.js index 3c51096ac53e..f34ae453d801 100644 --- a/ui/src/utils/util.js +++ b/ui/src/utils/util.js @@ -123,3 +123,16 @@ export function isValidIPv4Cidr (rule, value) { resolve() }) } + +export function getSingularIfPluralWord(word) { + if (word.endsWith('ies') && word.length > 3) { + return word.slice(0, -3) + 'y' + } + if (word.endsWith('sses') || word.endsWith('shes') || word.endsWith('ches') || word.endsWith('xes') || word.endsWith('zes')) { + return word.slice(0, -2) + } + if (word.endsWith('s') && !word.endsWith('ss')) { + return word.slice(0, -1) + } + return word +} diff --git a/ui/src/views/infra/InfraSummary.vue b/ui/src/views/infra/InfraSummary.vue index 9d4c06abd120..c34b8c1dd1fc 100644 --- a/ui/src/views/infra/InfraSummary.vue +++ b/ui/src/views/infra/InfraSummary.vue @@ -156,8 +156,8 @@ v-if="routes[section]">
- -

{{ $t(routes[section].title) }}

+ +

{{ $t(routes[section].title) }}

{{ stats[section] }}

@@ -175,6 +175,7 @@ import router from '@/router' import Breadcrumb from '@/components/widgets/Breadcrumb' import ChartCard from '@/components/widgets/ChartCard' import TooltipLabel from '@/components/widgets/TooltipLabel' +import { getSingularIfPluralWord } from '@/utils/util.js' export default { name: 'InfraSummary', @@ -187,7 +188,7 @@ export default { return { loading: true, routes: {}, - sections: ['zones', 'pods', 'clusters', 'hosts', 'storagepools', 'imagestores', 'backuprepositories', 'objectstores', 'systemvms', 'routers', 'cpusockets', 'managementservers', 'alerts', 'ilbvms', 'metrics'], + sections: ['zones', 'pods', 'clusters', 'hosts', 'storagepools', 'imagestores', 'imagecachestores', 'backuprepositories', 'objectstores', 'systemvms', 'routers', 'cpusockets', 'managementservers', 'alerts', 'ilbvms', 'metrics'], sslFormVisible: false, stats: {}, intermediateCertificates: [], @@ -216,12 +217,14 @@ export default { fetchData () { this.routes = {} for (const section of this.sections) { - const route = section === 'backuprepositories' ? 'backuprepository' : section.substring(0, section.length - 1) - if (router.resolve('/' + route).matched[0].redirect === '/exception/404') { + const sectionRouteName = getSingularIfPluralWord(section) + const sectionKey = section + if (router.resolve('/' + sectionRouteName).matched[0].redirect === '/exception/404') { continue } - const node = router.resolve({ name: route }) - this.routes[section] = { + const node = router.resolve({ name: sectionRouteName }) + this.routes[sectionKey] = { + routename: sectionRouteName, title: node.meta.title, icon: node.meta.icon }