Skip to content

Commit b849fa1

Browse files
vishesh92harikrishna-patnalaDaanHoogland
authored andcommitted
Notify users when upgrades are available or restart is required for network or VPC (apache#7610)
Co-authored-by: Harikrishna <harikrishna.patnala@gmail.com> Co-authored-by: dahn <daan.hoogland@gmail.com>
1 parent 4c3c42b commit b849fa1

File tree

11 files changed

+129
-7
lines changed

11 files changed

+129
-7
lines changed

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"npm-check-updates": "^6.0.1",
6161
"nprogress": "^0.2.0",
6262
"qrious": "^4.0.2",
63+
"semver": "^7.6.3",
6364
"vue": "^3.2.31",
6465
"vue-chartjs": "^4.0.7",
6566
"vue-clipboard2": "^0.3.1",

ui/public/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,7 @@
14951495
"label.network.offering": "Network offering",
14961496
"label.network.offerings": "Network offerings",
14971497
"label.network.policy": "Network Policy",
1498+
"label.network.restart.required": "Network restart required",
14981499
"label.network.route.table": "Network route table",
14991500
"label.network.routing.policy": "Network routing policy",
15001501
"label.network.permissions": "Network permissions",
@@ -1526,6 +1527,7 @@
15261527
"label.new.secondaryip.description": "Enter new secondary IP address",
15271528
"label.new.tag": "New tag",
15281529
"label.new.vm": "New Instance",
1530+
"label.new.version.available": "New version available",
15291531
"label.newdiskoffering": "New offering",
15301532
"label.newinstance": "New Instance",
15311533
"label.newname": "New name",
@@ -2519,6 +2521,7 @@
25192521
"label.vpc.id": "VPC ID",
25202522
"label.vpc.offerings": "VPC offerings",
25212523
"label.vpc.virtual.router": "VPC virtual router",
2524+
"label.vpc.restart.required": "VPC restart required",
25222525
"label.vpcid": "VPC",
25232526
"label.vpclimit": "VPC limits",
25242527
"label.vpcname": "VPC",
@@ -3300,6 +3303,7 @@
33003303
"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+.",
33013304
"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.",
33023305
"message.network.removenic": "Please confirm that want to remove this NIC, which will also remove the associated Network from the Instance.",
3306+
"message.network.restart.required": "Restart is required for network(s). Click here to view network(s) which require restart.",
33033307
"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.",
33043308
"message.network.selection": "Choose one or more Networks to attach the Instance to.",
33053309
"message.network.selection.new.network": "A new Network can also be created here.",
@@ -3310,6 +3314,7 @@
33103314
"message.network.usage.info.data.points": "Each data point represents the difference in data traffic since the last data point.",
33113315
"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.",
33123316
"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]",
3317+
"message.new.version.available": "A new version of CloudStack is available. Click here to check the details",
33133318
"message.no.data.to.show.for.period": "No data to show for the selected period.",
33143319
"message.no.description": "No description entered.",
33153320
"message.offering.internet.protocol.warning": "WARNING: IPv6 supported Networks use static routing and will require upstream routes to be configured manually.",
@@ -3663,6 +3668,7 @@
36633668
"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).",
36643669
"message.volumes.managed": "Volumes controlled by CloudStack.",
36653670
"message.volumes.unmanaged": "Volumes not controlled by CloudStack.",
3671+
"message.vpc.restart.required": "Restart is required for VPC(s). Click here to view VPC(s) which require restart.",
36663672
"message.vr.alert.upon.network.offering.creation.l2": "As virtual routers are not created for L2 Networks, the compute offering will not be used.",
36673673
"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.",
36683674
"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.",

ui/src/components/header/HeaderNotice.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@
4747
</a-avatar>
4848
</template>
4949
<template #description>
50-
<span v-if="getResourceName(notice.description, 'name') && notice.path">
51-
<router-link :to="{ path: notice.path}"> {{ getResourceName(notice.description, "name") + ' - ' }}</router-link>
50+
<span v-if="getResourceName(notice.description, 'name') && notice.path && !['VPC_RESTART_REQUIRED', 'NETWORK_RESTART_REQUIRED'].includes(notice.key)">
51+
<router-link :to="{ path: notice.path}">{{ getResourceName(notice.description, "name") + ' - ' }}</router-link>
52+
{{ getResourceName(notice.description, "msg") }}</span>
53+
<span v-else-if="notice.path && ['VPC_RESTART_REQUIRED', 'NETWORK_RESTART_REQUIRED'].includes(notice.key)">
54+
<router-link :to="{ path: notice.path, query: notice.query }">{{ notice.description }}</router-link>
5255
</span>
53-
<span v-if="getResourceName(notice.description, 'name') && notice.path"> {{ getResourceName(notice.description, "msg") }}</span>
5456
<span v-else>{{ notice.description }}</span>
5557
</template>
5658
</a-list-item-meta>

ui/src/components/page/GlobalFooter.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
</template>
3333

3434
<script>
35+
import semver from 'semver'
36+
import { getParsedVersion } from '@/utils/util'
37+
3538
export default {
3639
name: 'LayoutFooter',
3740
data () {
@@ -47,6 +50,16 @@ export default {
4750
// const date = m.getFullYear() + ('0' + (m.getMonth() + 1)).slice(-2) + ('0' + m.getDate()).slice(-2)
4851
// this.buildVersion = version + '-' + date + '-dev'
4952
// }
53+
},
54+
methods: {
55+
showVersionUpdate () {
56+
if (this.$store.getters?.features?.cloudstackversion && this.$store.getters?.latestVersion?.version) {
57+
const currentVersion = getParsedVersion(this.$store.getters?.features?.cloudstackversion)
58+
const latestVersion = getParsedVersion(this.$store.getters?.latestVersion?.version)
59+
return semver.valid(currentVersion) && semver.valid(latestVersion) && semver.gt(latestVersion, currentVersion)
60+
}
61+
return false
62+
}
5063
}
5164
}
5265
</script>

ui/src/components/view/ListView.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@
9393
<span v-else>
9494
<router-link :to="{ path: $route.path + '/' + record.id }" v-if="record.id">{{ text }}</router-link>
9595
<router-link :to="{ path: $route.path + '/' + record.name }" v-else>{{ text }}</router-link>
96+
<span v-if="['guestnetwork','vpc'].includes($route.path.split('/')[1]) && record.restartrequired && !record.vpcid">
97+
&nbsp;
98+
<a-tooltip>
99+
<template #title>{{ $t('label.restartrequired') }}</template>
100+
<warning-outlined style="color: #f5222d"/>
101+
</a-tooltip>
102+
</span>
96103
</span>
97104
</span>
98105
</template>

ui/src/components/view/SearchView.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export default {
303303
}
304304
if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level',
305305
'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider',
306-
'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'usagetype'].includes(item)
306+
'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'usagetype', 'restartrequired'].includes(item)
307307
) {
308308
type = 'list'
309309
} else if (item === 'tags') {
@@ -395,6 +395,16 @@ export default {
395395
this.fields[providerIndex].loading = false
396396
}
397397
398+
if (arrayField.includes('restartrequired')) {
399+
const restartRequiredIndex = this.fields.findIndex(item => item.name === 'restartrequired')
400+
this.fields[restartRequiredIndex].loading = true
401+
this.fields[restartRequiredIndex].opts = [
402+
{ id: 'true', name: 'label.yes' },
403+
{ id: 'false', name: 'label.no' }
404+
]
405+
this.fields[restartRequiredIndex].loading = false
406+
}
407+
398408
if (arrayField.includes('resourcetype')) {
399409
const resourceTypeIndex = this.fields.findIndex(item => item.name === 'resourcetype')
400410
this.fields[resourceTypeIndex].loading = true

ui/src/config/section/network.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default {
5454
return fields
5555
},
5656
filters: ['all', 'account', 'domainpath', 'shared'],
57-
searchFilters: ['keyword', 'zoneid', 'domainid', 'account', 'type', 'tags'],
57+
searchFilters: ['keyword', 'zoneid', 'domainid', 'account', 'type', 'restartrequired', 'tags'],
5858
related: [{
5959
name: 'vm',
6060
title: 'label.instances',
@@ -218,7 +218,7 @@ export default {
218218
return fields
219219
},
220220
details: ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ip6routes', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu'],
221-
searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
221+
searchFilters: ['name', 'zoneid', 'domainid', 'account', 'restartrequired', 'tags'],
222222
related: [{
223223
name: 'vm',
224224
title: 'label.instances',

ui/src/store/getters.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const getters = {
2828
apis: state => state.user.apis,
2929
features: state => state.user.features,
3030
userInfo: state => state.user.info,
31+
latestVersion: state => state.user.latestVersion,
3132
addRouters: state => state.permission.addRouters,
3233
multiTab: state => state.app.multiTab,
3334
listAllProjects: state => state.app.listAllProjects,

ui/src/store/modules/user.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
import Cookies from 'js-cookie'
1919
import message from 'ant-design-vue/es/message'
2020
import notification from 'ant-design-vue/es/notification'
21+
import semver from 'semver'
2122

2223
import { vueProps } from '@/vue-app'
2324
import router from '@/router'
2425
import store from '@/store'
2526
import { oauthlogin, login, logout, api } from '@/api'
2627
import { i18n } from '@/locales'
28+
import { axios } from '../../utils/request'
29+
import { getParsedVersion } from '@/utils/util'
2730

2831
import {
2932
ACCESS_TOKEN,
@@ -38,7 +41,8 @@ import {
3841
DARK_MODE,
3942
CUSTOM_COLUMNS,
4043
OAUTH_DOMAIN,
41-
OAUTH_PROVIDER
44+
OAUTH_PROVIDER,
45+
LATEST_CS_VERSION
4246
} from '@/store/mutation-types'
4347

4448
const user = {
@@ -171,6 +175,12 @@ const user = {
171175
},
172176
SET_OAUTH_PROVIDER_USED_TO_LOGIN: (state, provider) => {
173177
vueProps.$localStorage.set(OAUTH_PROVIDER, provider)
178+
},
179+
SET_LATEST_VERSION: (state, version) => {
180+
if (version?.fetchedTs > 0) {
181+
vueProps.$localStorage.set(LATEST_CS_VERSION, version)
182+
state.latestVersion = version
183+
}
174184
}
175185
},
176186

@@ -218,6 +228,8 @@ const user = {
218228
commit('SET_2FA_ISSUER', result.issuerfor2fa)
219229
commit('SET_FIRST_LOGIN', (result.firstlogin === 'true'))
220230
commit('SET_LOGIN_FLAG', false)
231+
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
232+
commit('SET_LATEST_VERSION', latestVersion)
221233
notification.destroy()
222234

223235
resolve()
@@ -265,6 +277,8 @@ const user = {
265277
commit('SET_2FA_PROVIDER', result.providerfor2fa)
266278
commit('SET_2FA_ISSUER', result.issuerfor2fa)
267279
commit('SET_LOGIN_FLAG', false)
280+
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
281+
commit('SET_LATEST_VERSION', latestVersion)
268282
notification.destroy()
269283

270284
resolve()
@@ -283,10 +297,12 @@ const user = {
283297
const cachedCustomColumns = vueProps.$localStorage.get(CUSTOM_COLUMNS, {})
284298
const domainStore = vueProps.$localStorage.get(DOMAIN_STORE, {})
285299
const darkMode = vueProps.$localStorage.get(DARK_MODE, false)
300+
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
286301
const hasAuth = Object.keys(cachedApis).length > 0
287302

288303
commit('SET_DOMAIN_STORE', domainStore)
289304
commit('SET_DARK_MODE', darkMode)
305+
commit('SET_LATEST_VERSION', latestVersion)
290306
if (hasAuth) {
291307
console.log('Login detected, using cached APIs')
292308
commit('SET_ZONES', cachedZones)
@@ -300,6 +316,7 @@ const user = {
300316
const result = response.listusersresponse.user[0]
301317
commit('SET_INFO', result)
302318
commit('SET_NAME', result.firstname + ' ' + result.lastname)
319+
store.dispatch('SetCsLatestVersion', result.rolename)
303320
resolve(cachedApis)
304321
}).catch(error => {
305322
reject(error)
@@ -338,12 +355,41 @@ const user = {
338355
}).catch(error => {
339356
reject(error)
340357
})
358+
359+
api('listNetworks', { restartrequired: true, forvpc: false }).then(response => {
360+
if (response.listnetworksresponse.count > 0) {
361+
store.dispatch('AddHeaderNotice', {
362+
key: 'NETWORK_RESTART_REQUIRED',
363+
title: i18n.global.t('label.network.restart.required'),
364+
description: i18n.global.t('message.network.restart.required'),
365+
path: '/guestnetwork/',
366+
query: { restartrequired: true, forvpc: false },
367+
status: 'done',
368+
timestamp: new Date()
369+
})
370+
}
371+
}).catch(ignored => {})
372+
373+
api('listVPCs', { restartrequired: true }).then(response => {
374+
if (response.listvpcsresponse.count > 0) {
375+
store.dispatch('AddHeaderNotice', {
376+
key: 'VPC_RESTART_REQUIRED',
377+
title: i18n.global.t('label.vpc.restart.required'),
378+
description: i18n.global.t('message.vpc.restart.required'),
379+
path: '/vpc/',
380+
query: { restartrequired: true },
381+
status: 'done',
382+
timestamp: new Date()
383+
})
384+
}
385+
}).catch(ignored => {})
341386
}
342387

343388
api('listUsers', { username: Cookies.get('username') }).then(response => {
344389
const result = response.listusersresponse.user[0]
345390
commit('SET_INFO', result)
346391
commit('SET_NAME', result.firstname + ' ' + result.lastname)
392+
store.dispatch('SetCsLatestVersion', result.rolename)
347393
}).catch(error => {
348394
reject(error)
349395
})
@@ -373,6 +419,8 @@ const user = {
373419
commit('SET_CLOUDIAN', cloudian)
374420
}).catch(ignored => {
375421
})
422+
}).catch(error => {
423+
console.error(error)
376424
})
377425
},
378426

@@ -495,6 +543,29 @@ const user = {
495543
SetDomainStore ({ commit }, domainStore) {
496544
commit('SET_DOMAIN_STORE', domainStore)
497545
},
546+
SetCsLatestVersion ({ commit }, rolename) {
547+
const lastFetchTs = store.getters.latestVersion?.fetchedTs ? store.getters.latestVersion.fetchedTs : 0
548+
if (rolename === 'Root Admin' && (+new Date() - lastFetchTs) > 24 * 60 * 60 * 1000) {
549+
axios.get(
550+
'https://api.github.com/repos/apache/cloudstack/releases'
551+
).then(response => {
552+
let latestReleaseVersion = getParsedVersion(response[0].tag_name)
553+
let latestTag = response[0].tag_name
554+
555+
for (const release of response) {
556+
if (release.tag_name.toLowerCase().includes('rc')) {
557+
continue
558+
}
559+
const parsedVersion = getParsedVersion(release.tag_name)
560+
if (semver.gte(parsedVersion, latestReleaseVersion)) {
561+
latestReleaseVersion = parsedVersion
562+
latestTag = release.tag_name
563+
commit('SET_LATEST_VERSION', { version: latestTag, fetchedTs: (+new Date()) })
564+
}
565+
}
566+
}).catch(ignored => {})
567+
}
568+
},
498569
SetDarkMode ({ commit }, darkMode) {
499570
commit('SET_DARK_MODE', darkMode)
500571
},

ui/src/store/mutation-types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const USE_BROWSER_TIMEZONE = 'USE_BROWSER_TIMEZONE'
3535
export const SERVER_MANAGER = 'SERVER_MANAGER'
3636
export const DOMAIN_STORE = 'DOMAIN_STORE'
3737
export const DARK_MODE = 'DARK_MODE'
38+
export const LATEST_CS_VERSION = 'LATEST_CS_VERSION'
3839
export const VUE_VERSION = 'VUE_VERSION'
3940
export const CUSTOM_COLUMNS = 'CUSTOM_COLUMNS'
4041
export const FAVICON_STATE_INTERVAL = 'FAVICON_STATE_INTERVAL'

0 commit comments

Comments
 (0)