This commit is contained in:
Abhishek Kumar 2026-07-04 14:36:11 +00:00 committed by GitHub
commit 29a4076ff3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 271 additions and 32 deletions

View File

@ -118,5 +118,6 @@
"startDate": "2025-06-01T00:00:00Z",
"endDate": "2025-07-16T00:00:00Z"
},
"defaultLanguage": "en"
"defaultLanguage": "en",
"advisoriesDisabled": false
}

View File

@ -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...",

View File

@ -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
}

View File

@ -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)) {

View File

@ -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',

View File

@ -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
}
}