mirror of https://github.com/apache/cloudstack.git
Merge 2aa591bb13 into b7d4df0a11
This commit is contained in:
commit
29a4076ff3
|
|
@ -118,5 +118,6 @@
|
|||
"startDate": "2025-06-01T00:00:00Z",
|
||||
"endDate": "2025-07-16T00:00:00Z"
|
||||
},
|
||||
"defaultLanguage": "en"
|
||||
"defaultLanguage": "en",
|
||||
"advisoriesDisabled": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1178,7 +1178,12 @@
|
|||
"label.go.back": "Go back",
|
||||
"label.go.to.compute.offerings": "Go to Compute Offerings",
|
||||
"label.go.to.global.settings": "Go to Global Settings",
|
||||
"label.go.to.networks": "Go to Networks",
|
||||
"label.go.to.templates": "Go to Templates",
|
||||
"label.go.to.isos": "Go to ISOs",
|
||||
"label.go.to.volumes": "Go to Volumes",
|
||||
"label.go.to.kubernetes.isos": "Go to Kubernetes ISOs",
|
||||
"label.go.to.snapshots": "Go to Volume Snapshots",
|
||||
"label.gpu": "GPU",
|
||||
"label.gpucardid": "GPU Card",
|
||||
"label.gpucardname": "GPU Card",
|
||||
|
|
@ -3151,9 +3156,14 @@
|
|||
"message.add.ip.v6.firewall.rule.failed": "Failed to add IPv6 firewall rule",
|
||||
"message.add.ip.v6.firewall.rule.processing": "Adding IPv6 firewall rule...",
|
||||
"message.add.ip.v6.firewall.rule.success": "Added IPv6 firewall rule",
|
||||
"message.advisory.instance.compute.offering.missing": "No compute offering found for deploying an Instance.",
|
||||
"message.advisory.instance.image.missing": "No suitable Template/ISO/Volume/Volume Snapshot found for deploying an Instance. Please make sure you have a Template/ISO/Volume/Snapshot ready for Instance deployment.",
|
||||
"message.advisory.instance.network.missing": "No suitable Network found for deploying an Instance. Please create a Network to be used by the Instance.",
|
||||
"message.advisory.cks.endpoint.url.not.configured": "Endpoint URL which will be used by Kubernetes clusters is not configured correctly",
|
||||
"message.advisory.cks.min.offering": "No suitable Compute Offering found for Kubernetes cluster nodes with minimum required resources (2 vCPU, 2 GB RAM)",
|
||||
"message.advisory.cks.version.check": "No Kubernetes version found that can be used to deploy a Kubernetes cluster",
|
||||
"message.advisory.vnf.appliance.compute.offering.missing": "No compute offering found for deploying a VNF appliance.",
|
||||
"message.advisory.vnf.appliance.template.missing": "No VNF Template found for deploying a VNF appliance. Please make sure you have a VNF Template for appliance deployment.",
|
||||
"message.redeliver.webhook.delivery": "Redeliver this Webhook delivery",
|
||||
"message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule",
|
||||
"message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ import tools from '@/config/section/tools'
|
|||
import quota from '@/config/section/plugin/quota'
|
||||
import cloudian from '@/config/section/plugin/cloudian'
|
||||
|
||||
const isAdvisoriesDisabled = () => vueProps.$config.advisoriesDisabled ?? false
|
||||
|
||||
function generateRouterMap (section) {
|
||||
var map = {
|
||||
name: section.name,
|
||||
|
|
@ -83,7 +85,7 @@ function generateRouterMap (section) {
|
|||
filters: child.filters,
|
||||
params: child.params ? child.params : {},
|
||||
columns: child.columns,
|
||||
advisories: !vueProps.$config.advisoriesDisabled ? child.advisories : undefined,
|
||||
advisories: !isAdvisoriesDisabled() ? child.advisories : undefined,
|
||||
details: child.details,
|
||||
searchFilters: child.searchFilters,
|
||||
related: child.related,
|
||||
|
|
@ -183,7 +185,7 @@ function generateRouterMap (section) {
|
|||
map.meta.columns = section.columns
|
||||
}
|
||||
|
||||
if (!vueProps.$config.advisoriesDisabled && section.advisories) {
|
||||
if (!isAdvisoriesDisabled() && section.advisories) {
|
||||
map.meta.advisories = section.advisories
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { isZoneCreated } from '@/utils/zone'
|
|||
import { getAPI, postAPI, getBaseUrl } from '@/api'
|
||||
import { getLatestKubernetesIsoParams } from '@/utils/acsrepo'
|
||||
import kubernetesIcon from '@/assets/icons/kubernetes.svg?inline'
|
||||
import { hasNoItems } from '@/utils/advisory'
|
||||
|
||||
const attachedIsoCount = (record) => (record.isos && record.isos.length) || (record.isoid ? 1 : 0)
|
||||
// Server pre-computes the effective cap (cluster-scoped vm.iso.max.count clamped to the
|
||||
|
|
@ -109,6 +110,117 @@ export default {
|
|||
tabs: [{
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/InstanceTab.vue')))
|
||||
}],
|
||||
advisories: [
|
||||
{
|
||||
id: 'instance-image-check',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.instance.image.missing',
|
||||
condition: async (store) => {
|
||||
return await hasNoItems(store, 'listTemplates', { isvnf: false, templatefilter: 'executable', isready: true }, null, 'listtemplatesresponse', 'template') &&
|
||||
await hasNoItems(store, 'listIsos', { isofilter: 'executable', bootable: true, isready: true }) &&
|
||||
await hasNoItems(store, 'listVolumes', { state: 'Ready' }) &&
|
||||
await hasNoItems(store, 'listSnapshots')
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
label: 'label.register.template',
|
||||
show: (store) => { return ('registerTemplate' in store.getters.apis) },
|
||||
primary: true,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'template', query: { action: 'registerTemplate' } })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.templates',
|
||||
show: (store) => { return ('listTemplates' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'template' })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.isos',
|
||||
show: (store) => { return ('listIsos' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'iso' })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.volumes',
|
||||
show: (store) => { return ('listVolumes' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'volume' })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.snapshots',
|
||||
show: (store) => { return ('listSnapshots' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'snapshot' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'instance-compute-offering-check',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.instance.compute.offering.missing',
|
||||
condition: async (store) => {
|
||||
return await hasNoItems(store, 'listServiceOfferings', { issystem: false })
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
label: 'label.add.compute.offering',
|
||||
show: (store) => { return ('createServiceOffering' in store.getters.apis) },
|
||||
primary: true,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'computeoffering', query: { action: 'createServiceOffering' } })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.compute.offerings',
|
||||
show: (store) => { return ('listServiceOfferings' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'computeoffering' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'instance-network-check',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.instance.network.missing',
|
||||
dismissOnConditionFail: true,
|
||||
condition: async (store) => {
|
||||
return await hasNoItems(store, 'listNetworks')
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
label: 'label.add.network',
|
||||
show: (store) => { return ('createNetwork' in store.getters.apis) },
|
||||
primary: true,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'guestnetwork', query: { action: 'createNetwork' } })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.networks',
|
||||
show: (store) => { return ('listNetworks' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'guestnetworks' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
api: 'deployVirtualMachine',
|
||||
|
|
@ -592,23 +704,12 @@ export default {
|
|||
id: 'cks-min-offering',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.cks.min.offering',
|
||||
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
|
||||
dismissOnConditionFail: true,
|
||||
condition: async (store) => {
|
||||
if (!('listServiceOfferings' in store.getters.apis)) {
|
||||
return false
|
||||
}
|
||||
const params = {
|
||||
cpunumber: 2,
|
||||
memory: 2048,
|
||||
issystem: false
|
||||
}
|
||||
try {
|
||||
const json = await getAPI('listServiceOfferings', params)
|
||||
const offerings = json?.listserviceofferingsresponse?.serviceoffering || []
|
||||
return !offerings.some(o => !o.iscustomized)
|
||||
} catch (error) {}
|
||||
return false
|
||||
return await hasNoItems(store,
|
||||
'listServiceOfferings',
|
||||
{ cpunumber: 2, memory: 2048, issystem: false },
|
||||
o => !o.iscustomized
|
||||
)
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
|
|
@ -650,19 +751,8 @@ export default {
|
|||
id: 'cks-version-check',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.cks.version.check',
|
||||
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
|
||||
dismissOnConditionFail: true,
|
||||
condition: async (store) => {
|
||||
const api = 'listKubernetesSupportedVersions'
|
||||
if (!(api in store.getters.apis)) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const json = await getAPI(api, {})
|
||||
const versions = json?.listkubernetessupportedversionsresponse?.kubernetessupportedversion || []
|
||||
return versions.length === 0
|
||||
} catch (error) {}
|
||||
return false
|
||||
return await hasNoItems(store, 'listKubernetesSupportedVersions')
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
|
|
@ -705,7 +795,6 @@ export default {
|
|||
id: 'cks-endpoint-url',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.cks.endpoint.url.not.configured',
|
||||
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
|
||||
dismissOnConditionFail: true,
|
||||
condition: async (store) => {
|
||||
if (!['Admin'].includes(store.getters.userInfo.roletype)) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import store from '@/store'
|
|||
import tungsten from '@/assets/icons/tungsten.svg?inline'
|
||||
import { isAdmin } from '@/role'
|
||||
import { isZoneCreated } from '@/utils/zone'
|
||||
import { hasNoItems } from '@/utils/advisory'
|
||||
import { vueProps } from '@/vue-app'
|
||||
|
||||
export default {
|
||||
|
|
@ -421,6 +422,71 @@ export default {
|
|||
tabs: [{
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/InstanceTab.vue')))
|
||||
}],
|
||||
advisories: [
|
||||
{
|
||||
id: 'vnfapp-image-check',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.vnf.appliance.template.missing',
|
||||
condition: async (store) => {
|
||||
return await hasNoItems(
|
||||
store,
|
||||
'listTemplates',
|
||||
{ isvnf: false, templatefilter: 'executable', isready: true },
|
||||
null,
|
||||
'listtemplatesresponse',
|
||||
'template'
|
||||
)
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
label: 'label.register.template',
|
||||
show: (store) => { return ('registerTemplate' in store.getters.apis) },
|
||||
primary: true,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'template', query: { action: 'registerTemplate' } })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.templates',
|
||||
show: (store) => { return ('listTemplates' in store.getters.apis) },
|
||||
primary: false,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'template' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'vnfapp-compute-offering-check',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.vnf.appliance.compute.offering.missing',
|
||||
condition: async (store) => {
|
||||
return await hasNoItems(store, 'listServiceOfferings', { issystem: false })
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
label: 'label.add.compute.offering',
|
||||
show: (store) => { return ('createServiceOffering' in store.getters.apis) },
|
||||
primary: true,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'computeoffering', query: { action: 'createServiceOffering' } })
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.compute.offerings',
|
||||
show: (store) => { return ('listServiceOfferings' in store.getters.apis) },
|
||||
primary: false,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'computeoffering' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
api: 'deployVnfAppliance',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
// 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 { getAPI } from '@/api'
|
||||
|
||||
/**
|
||||
* Generic helper to check if an API has no items (useful for advisory conditions)
|
||||
* @param {Object} store - Vuex store instance
|
||||
* @param {string} apiName - Name of the API to call (e.g., 'listNetworks')
|
||||
* @param {Object} params - Optional parameters to merge with defaults
|
||||
* @param {Function} filterFunc - Optional function to filter items. If provided, returns true if no items match the filter.
|
||||
* @param {string} itemsKey - Optional key for items array in response. If not provided, will be deduced from apiName
|
||||
* @returns {Promise<boolean>} - Returns true if no items exist (advisory should be shown), false otherwise
|
||||
*/
|
||||
export async function hasNoItems (store, apiName, params = {}, filterFunc = null, responseKey = null, itemsKey = null) {
|
||||
if (!(apiName in store.getters.apis)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If itemsKey not provided, deduce it from apiName
|
||||
if (!itemsKey) {
|
||||
// Remove 'list' prefix: listNetworks -> Networks
|
||||
let key = apiName.replace(/^list/i, '')
|
||||
// Convert to lowercase
|
||||
key = key.toLowerCase()
|
||||
// Handle plural forms: remove trailing 's' or convert 'ies' to 'y'
|
||||
if (key.endsWith('ies')) {
|
||||
key = key.slice(0, -3) + 'y'
|
||||
} else if (key.endsWith('s')) {
|
||||
key = key.slice(0, -1)
|
||||
}
|
||||
itemsKey = key
|
||||
}
|
||||
|
||||
const allParams = {
|
||||
listall: true,
|
||||
...params
|
||||
}
|
||||
|
||||
if (filterFunc == null) {
|
||||
allParams.page = 1
|
||||
allParams.pageSize = 1
|
||||
}
|
||||
|
||||
try {
|
||||
const json = await getAPI(apiName, allParams)
|
||||
// Auto-derive response key: listNetworks -> listnetworksresponse
|
||||
const apiResponseKey = responseKey || `${apiName.toLowerCase()}response`
|
||||
const items = json?.[apiResponseKey]?.[itemsKey] || []
|
||||
if (filterFunc) {
|
||||
return !items.some(filterFunc)
|
||||
}
|
||||
return items.length === 0
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch items for advisory check via API ${apiName}`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue