mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 01:32:18 +02:00
Notify users when upgrades are available or restart is required for network or VPC (#7610)
Co-authored-by: Harikrishna <harikrishna.patnala@gmail.com> Co-authored-by: dahn <daan.hoogland@gmail.com>
This commit is contained in:
parent
39152323e3
commit
f9451fce3a
@ -60,6 +60,7 @@
|
||||
"npm-check-updates": "^6.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"qrious": "^4.0.2",
|
||||
"semver": "^7.6.3",
|
||||
"vue": "^3.2.31",
|
||||
"vue-chartjs": "^4.0.7",
|
||||
"vue-clipboard2": "^0.3.1",
|
||||
|
||||
@ -1446,6 +1446,7 @@
|
||||
"label.network.offering": "Network offering",
|
||||
"label.network.offerings": "Network offerings",
|
||||
"label.network.policy": "Network Policy",
|
||||
"label.network.restart.required": "Network restart required",
|
||||
"label.network.route.table": "Network route table",
|
||||
"label.network.routing.policy": "Network routing policy",
|
||||
"label.network.permissions": "Network permissions",
|
||||
@ -1476,6 +1477,7 @@
|
||||
"label.new.secondaryip.description": "Enter new secondary IP address",
|
||||
"label.new.tag": "New tag",
|
||||
"label.new.vm": "New Instance",
|
||||
"label.new.version.available": "New version available",
|
||||
"label.newdiskoffering": "New offering",
|
||||
"label.newinstance": "New Instance",
|
||||
"label.newname": "New name",
|
||||
@ -2463,6 +2465,7 @@
|
||||
"label.vpc.id": "VPC ID",
|
||||
"label.vpc.offerings": "VPC offerings",
|
||||
"label.vpc.virtual.router": "VPC virtual router",
|
||||
"label.vpc.restart.required": "VPC restart required",
|
||||
"label.vpcid": "VPC",
|
||||
"label.vpclimit": "VPC limits",
|
||||
"label.vpcname": "VPC",
|
||||
@ -3185,6 +3188,7 @@
|
||||
"message.network.offering.mac.learning.warning": "WARNING: In order to use MAC Learning you must ensure your hypervisor hosts are running ESXi 6.7+ and the Network uses distributed vSwitch 6.6.0+.",
|
||||
"message.network.offering.promiscuous.mode": "Applicable for guest Networks on VMware hypervisor only.\nReject - The switch drops any outbound frame from a virtual machine adapter with a source MAC address that is different from the one in the .vmx configuration file.\nAccept - The switch does not perform filtering, and permits all outbound frames.\nNone - Default to value from global setting.",
|
||||
"message.network.removenic": "Please confirm that want to remove this NIC, which will also remove the associated Network from the Instance.",
|
||||
"message.network.restart.required": "Restart is required for network(s). Click here to view network(s) which require restart.",
|
||||
"message.network.secondaryip": "Please confirm that you would like to acquire a new secondary IP for this NIC. \n NOTE: You need to manually configure the newly-acquired secondary IP inside the virtual machine.",
|
||||
"message.network.selection": "Choose one or more Networks to attach the Instance to.",
|
||||
"message.network.selection.new.network": "A new Network can also be created here.",
|
||||
@ -3192,6 +3196,7 @@
|
||||
"message.network.usage.info.data.points": "Each data point represents the difference in data traffic since the last data point.",
|
||||
"message.network.usage.info.sum.of.vnics": "The Network usage shown is made up of the sum of data traffic from all the vNICs in the Instance.",
|
||||
"message.nfs.mount.options.description": "Comma separated list of NFS mount options for KVM hosts. Supported options : vers=[3,4.0,4.1,4.2], nconnect=[1...16]",
|
||||
"message.new.version.available": "A new version of CloudStack is available. Click here to check the details",
|
||||
"message.no.data.to.show.for.period": "No data to show for the selected period.",
|
||||
"message.no.description": "No description entered.",
|
||||
"message.offering.internet.protocol.warning": "WARNING: IPv6 supported Networks use static routing and will require upstream routes to be configured manually.",
|
||||
@ -3526,6 +3531,7 @@
|
||||
"message.volume.state.primary.storage.suitability": "The suitability of a primary storage for a volume depends on the disk offering of the volume and on the virtual machine allocation (if the volume is attached to a virtual machine).",
|
||||
"message.volumes.managed": "Volumes controlled by CloudStack.",
|
||||
"message.volumes.unmanaged": "Volumes not controlled by CloudStack.",
|
||||
"message.vpc.restart.required": "Restart is required for VPC(s). Click here to view VPC(s) which require restart.",
|
||||
"message.vr.alert.upon.network.offering.creation.l2": "As virtual routers are not created for L2 Networks, the compute offering will not be used.",
|
||||
"message.vr.alert.upon.network.offering.creation.others": "As none of the obligatory services for creating a virtual router (VPN, DHCP, DNS, Firewall, LB, UserData, SourceNat, StaticNat, PortForwarding) are enabled, the virtual router will not be created and the compute offering will not be used.",
|
||||
"message.warn.change.primary.storage.scope": "This feature is tested and supported for the following configurations:<br>KVM - NFS/Ceph - DefaultPrimary<br>VMware - NFS - DefaultPrimary<br>*There might be extra steps involved to make it work for other configurations.",
|
||||
|
||||
@ -47,10 +47,12 @@
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #description>
|
||||
<span v-if="getResourceName(notice.description, 'name') && notice.path">
|
||||
<router-link :to="{ path: notice.path}"> {{ getResourceName(notice.description, "name") + ' - ' }}</router-link>
|
||||
<span v-if="getResourceName(notice.description, 'name') && notice.path && !['VPC_RESTART_REQUIRED', 'NETWORK_RESTART_REQUIRED'].includes(notice.key)">
|
||||
<router-link :to="{ path: notice.path}">{{ getResourceName(notice.description, "name") + ' - ' }}</router-link>
|
||||
{{ getResourceName(notice.description, "msg") }}</span>
|
||||
<span v-else-if="notice.path && ['VPC_RESTART_REQUIRED', 'NETWORK_RESTART_REQUIRED'].includes(notice.key)">
|
||||
<router-link :to="{ path: notice.path, query: notice.query }">{{ notice.description }}</router-link>
|
||||
</span>
|
||||
<span v-if="getResourceName(notice.description, 'name') && notice.path"> {{ getResourceName(notice.description, "msg") }}</span>
|
||||
<span v-else>{{ notice.description }}</span>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
|
||||
@ -22,6 +22,15 @@
|
||||
</div>
|
||||
<div class="line" v-if="$store.getters.userInfo.roletype === 'Admin'">
|
||||
CloudStack {{ $store.getters.features.cloudstackversion }}
|
||||
<span v-if="showVersionUpdate()">
|
||||
<a-divider type="vertical" />
|
||||
<a
|
||||
:href="'https://github.com/apache/cloudstack/releases/tag/' + $store.getters.latestVersion.version"
|
||||
target="_blank">
|
||||
<info-circle-outlined />
|
||||
{{ $t('label.new.version.available') + ': ' + $store.getters.latestVersion.version }}
|
||||
</a>
|
||||
</span>
|
||||
<a-divider type="vertical" />
|
||||
<a href="https://github.com/apache/cloudstack/discussions" target="_blank">
|
||||
<github-outlined />
|
||||
@ -32,11 +41,24 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import semver from 'semver'
|
||||
import { getParsedVersion } from '@/utils/util'
|
||||
|
||||
export default {
|
||||
name: 'LayoutFooter',
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showVersionUpdate () {
|
||||
if (this.$store.getters?.features?.cloudstackversion && this.$store.getters?.latestVersion?.version) {
|
||||
const currentVersion = getParsedVersion(this.$store.getters?.features?.cloudstackversion)
|
||||
const latestVersion = getParsedVersion(this.$store.getters?.latestVersion?.version)
|
||||
return semver.valid(currentVersion) && semver.valid(latestVersion) && semver.gt(latestVersion, currentVersion)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -90,6 +90,13 @@
|
||||
<span v-else>
|
||||
<router-link :to="{ path: $route.path + '/' + record.id }" v-if="record.id">{{ text }}</router-link>
|
||||
<router-link :to="{ path: $route.path + '/' + record.name }" v-else>{{ text }}</router-link>
|
||||
<span v-if="['guestnetwork','vpc'].includes($route.path.split('/')[1]) && record.restartrequired && !record.vpcid">
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>{{ $t('label.restartrequired') }}</template>
|
||||
<warning-outlined style="color: #f5222d"/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@ -303,7 +303,7 @@ export default {
|
||||
}
|
||||
if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level',
|
||||
'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider',
|
||||
'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'usagetype'].includes(item)
|
||||
'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'usagetype', 'restartrequired'].includes(item)
|
||||
) {
|
||||
type = 'list'
|
||||
} else if (item === 'tags') {
|
||||
@ -395,6 +395,16 @@ export default {
|
||||
this.fields[providerIndex].loading = false
|
||||
}
|
||||
|
||||
if (arrayField.includes('restartrequired')) {
|
||||
const restartRequiredIndex = this.fields.findIndex(item => item.name === 'restartrequired')
|
||||
this.fields[restartRequiredIndex].loading = true
|
||||
this.fields[restartRequiredIndex].opts = [
|
||||
{ id: 'true', name: 'label.yes' },
|
||||
{ id: 'false', name: 'label.no' }
|
||||
]
|
||||
this.fields[restartRequiredIndex].loading = false
|
||||
}
|
||||
|
||||
if (arrayField.includes('resourcetype')) {
|
||||
const resourceTypeIndex = this.fields.findIndex(item => item.name === 'resourcetype')
|
||||
this.fields[resourceTypeIndex].loading = true
|
||||
|
||||
@ -54,7 +54,7 @@ export default {
|
||||
return fields
|
||||
},
|
||||
filters: ['all', 'account', 'domainpath', 'shared'],
|
||||
searchFilters: ['keyword', 'zoneid', 'domainid', 'account', 'type', 'tags'],
|
||||
searchFilters: ['keyword', 'zoneid', 'domainid', 'account', 'type', 'restartrequired', 'tags'],
|
||||
related: [{
|
||||
name: 'vm',
|
||||
title: 'label.instances',
|
||||
@ -218,7 +218,7 @@ export default {
|
||||
return fields
|
||||
},
|
||||
details: ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ip6routes', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu'],
|
||||
searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
|
||||
searchFilters: ['name', 'zoneid', 'domainid', 'account', 'restartrequired', 'tags'],
|
||||
related: [{
|
||||
name: 'vm',
|
||||
title: 'label.instances',
|
||||
|
||||
@ -28,6 +28,7 @@ const getters = {
|
||||
apis: state => state.user.apis,
|
||||
features: state => state.user.features,
|
||||
userInfo: state => state.user.info,
|
||||
latestVersion: state => state.user.latestVersion,
|
||||
addRouters: state => state.permission.addRouters,
|
||||
multiTab: state => state.app.multiTab,
|
||||
listAllProjects: state => state.app.listAllProjects,
|
||||
|
||||
@ -18,12 +18,15 @@
|
||||
import Cookies from 'js-cookie'
|
||||
import message from 'ant-design-vue/es/message'
|
||||
import notification from 'ant-design-vue/es/notification'
|
||||
import semver from 'semver'
|
||||
|
||||
import { vueProps } from '@/vue-app'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import { oauthlogin, login, logout, api } from '@/api'
|
||||
import { i18n } from '@/locales'
|
||||
import { axios } from '../../utils/request'
|
||||
import { getParsedVersion } from '@/utils/util'
|
||||
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
@ -38,7 +41,8 @@ import {
|
||||
DARK_MODE,
|
||||
CUSTOM_COLUMNS,
|
||||
OAUTH_DOMAIN,
|
||||
OAUTH_PROVIDER
|
||||
OAUTH_PROVIDER,
|
||||
LATEST_CS_VERSION
|
||||
} from '@/store/mutation-types'
|
||||
|
||||
const user = {
|
||||
@ -167,6 +171,12 @@ const user = {
|
||||
},
|
||||
SET_OAUTH_PROVIDER_USED_TO_LOGIN: (state, provider) => {
|
||||
vueProps.$localStorage.set(OAUTH_PROVIDER, provider)
|
||||
},
|
||||
SET_LATEST_VERSION: (state, version) => {
|
||||
if (version?.fetchedTs > 0) {
|
||||
vueProps.$localStorage.set(LATEST_CS_VERSION, version)
|
||||
state.latestVersion = version
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -212,6 +222,8 @@ const user = {
|
||||
commit('SET_2FA_PROVIDER', result.providerfor2fa)
|
||||
commit('SET_2FA_ISSUER', result.issuerfor2fa)
|
||||
commit('SET_LOGIN_FLAG', false)
|
||||
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
|
||||
commit('SET_LATEST_VERSION', latestVersion)
|
||||
notification.destroy()
|
||||
|
||||
resolve()
|
||||
@ -259,6 +271,8 @@ const user = {
|
||||
commit('SET_2FA_PROVIDER', result.providerfor2fa)
|
||||
commit('SET_2FA_ISSUER', result.issuerfor2fa)
|
||||
commit('SET_LOGIN_FLAG', false)
|
||||
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
|
||||
commit('SET_LATEST_VERSION', latestVersion)
|
||||
notification.destroy()
|
||||
|
||||
resolve()
|
||||
@ -277,10 +291,12 @@ const user = {
|
||||
const cachedCustomColumns = vueProps.$localStorage.get(CUSTOM_COLUMNS, {})
|
||||
const domainStore = vueProps.$localStorage.get(DOMAIN_STORE, {})
|
||||
const darkMode = vueProps.$localStorage.get(DARK_MODE, false)
|
||||
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
|
||||
const hasAuth = Object.keys(cachedApis).length > 0
|
||||
|
||||
commit('SET_DOMAIN_STORE', domainStore)
|
||||
commit('SET_DARK_MODE', darkMode)
|
||||
commit('SET_LATEST_VERSION', latestVersion)
|
||||
if (hasAuth) {
|
||||
console.log('Login detected, using cached APIs')
|
||||
commit('SET_ZONES', cachedZones)
|
||||
@ -294,6 +310,7 @@ const user = {
|
||||
const result = response.listusersresponse.user[0]
|
||||
commit('SET_INFO', result)
|
||||
commit('SET_NAME', result.firstname + ' ' + result.lastname)
|
||||
store.dispatch('SetCsLatestVersion', result.rolename)
|
||||
resolve(cachedApis)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
@ -332,12 +349,41 @@ const user = {
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
api('listNetworks', { restartrequired: true, forvpc: false }).then(response => {
|
||||
if (response.listnetworksresponse.count > 0) {
|
||||
store.dispatch('AddHeaderNotice', {
|
||||
key: 'NETWORK_RESTART_REQUIRED',
|
||||
title: i18n.global.t('label.network.restart.required'),
|
||||
description: i18n.global.t('message.network.restart.required'),
|
||||
path: '/guestnetwork/',
|
||||
query: { restartrequired: true, forvpc: false },
|
||||
status: 'done',
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
}).catch(ignored => {})
|
||||
|
||||
api('listVPCs', { restartrequired: true }).then(response => {
|
||||
if (response.listvpcsresponse.count > 0) {
|
||||
store.dispatch('AddHeaderNotice', {
|
||||
key: 'VPC_RESTART_REQUIRED',
|
||||
title: i18n.global.t('label.vpc.restart.required'),
|
||||
description: i18n.global.t('message.vpc.restart.required'),
|
||||
path: '/vpc/',
|
||||
query: { restartrequired: true },
|
||||
status: 'done',
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
}).catch(ignored => {})
|
||||
}
|
||||
|
||||
api('listUsers', { username: Cookies.get('username') }).then(response => {
|
||||
const result = response.listusersresponse.user[0]
|
||||
commit('SET_INFO', result)
|
||||
commit('SET_NAME', result.firstname + ' ' + result.lastname)
|
||||
store.dispatch('SetCsLatestVersion', result.rolename)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
@ -367,6 +413,8 @@ const user = {
|
||||
commit('SET_CLOUDIAN', cloudian)
|
||||
}).catch(ignored => {
|
||||
})
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
},
|
||||
|
||||
@ -488,6 +536,29 @@ const user = {
|
||||
SetDomainStore ({ commit }, domainStore) {
|
||||
commit('SET_DOMAIN_STORE', domainStore)
|
||||
},
|
||||
SetCsLatestVersion ({ commit }, rolename) {
|
||||
const lastFetchTs = store.getters.latestVersion?.fetchedTs ? store.getters.latestVersion.fetchedTs : 0
|
||||
if (rolename === 'Root Admin' && (+new Date() - lastFetchTs) > 24 * 60 * 60 * 1000) {
|
||||
axios.get(
|
||||
'https://api.github.com/repos/apache/cloudstack/releases'
|
||||
).then(response => {
|
||||
let latestReleaseVersion = getParsedVersion(response[0].tag_name)
|
||||
let latestTag = response[0].tag_name
|
||||
|
||||
for (const release of response) {
|
||||
if (release.tag_name.toLowerCase().includes('rc')) {
|
||||
continue
|
||||
}
|
||||
const parsedVersion = getParsedVersion(release.tag_name)
|
||||
if (semver.gte(parsedVersion, latestReleaseVersion)) {
|
||||
latestReleaseVersion = parsedVersion
|
||||
latestTag = release.tag_name
|
||||
commit('SET_LATEST_VERSION', { version: latestTag, fetchedTs: (+new Date()) })
|
||||
}
|
||||
}
|
||||
}).catch(ignored => {})
|
||||
}
|
||||
},
|
||||
SetDarkMode ({ commit }, darkMode) {
|
||||
commit('SET_DARK_MODE', darkMode)
|
||||
},
|
||||
|
||||
@ -35,6 +35,7 @@ export const USE_BROWSER_TIMEZONE = 'USE_BROWSER_TIMEZONE'
|
||||
export const SERVER_MANAGER = 'SERVER_MANAGER'
|
||||
export const DOMAIN_STORE = 'DOMAIN_STORE'
|
||||
export const DARK_MODE = 'DARK_MODE'
|
||||
export const LATEST_CS_VERSION = 'LATEST_CS_VERSION'
|
||||
export const VUE_VERSION = 'VUE_VERSION'
|
||||
export const CUSTOM_COLUMNS = 'CUSTOM_COLUMNS'
|
||||
export const RELOAD_ALL_PROJECTS = 'RELOAD_ALL_PROJECTS'
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import semver from 'semver'
|
||||
|
||||
export function timeFix () {
|
||||
const time = new Date()
|
||||
const hour = time.getHours()
|
||||
@ -69,6 +71,14 @@ export function sanitizeReverse (value) {
|
||||
.replace(/>/g, '>')
|
||||
}
|
||||
|
||||
export function getParsedVersion (version) {
|
||||
version = version.split('-')[0]
|
||||
if (semver.valid(version) === null) {
|
||||
version = version.split('.').slice(1).join('.')
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
|
||||
if (data === null || !data.length) {
|
||||
return null
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user