This is an automated email from the ASF dual-hosted git repository.
vishesh92 pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new ff9ee24d125 Fix local upload from browser failing due to ssvm cert not
trusted (#13204)
ff9ee24d125 is described below
commit ff9ee24d1250c9d394ae60af8c70120416045691
Author: Abhisar Sinha <[email protected]>
AuthorDate: Tue May 26 13:27:33 2026 +0530
Fix local upload from browser failing due to ssvm cert not trusted (#13204)
---
ui/public/locales/en.json | 5 +
ui/src/style/vars.less | 2 +-
ui/src/utils/ssvmProbe.js | 30 ++++++
ui/src/views/image/RegisterOrUploadIso.vue | 50 +++++++--
ui/src/views/image/RegisterOrUploadTemplate.vue | 45 +++++++-
ui/src/views/storage/UploadLocalVolume.vue | 133 ++++++++++++++++--------
6 files changed, 208 insertions(+), 57 deletions(-)
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 3160e00ba30..008bf59b3ca 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -1617,6 +1617,8 @@
"label.offeringid": "Offering ID",
"label.offeringtype": "Compute Offering type",
"label.ok": "OK",
+"label.ssvm.open.cert.page": "Open Certificate Page",
+"label.retry.upload": "Retry Upload",
"label.only.end.date.and.time": "Only end date and time",
"label.only.start.date.and.time": "Only start date and time",
"label.open.documentation": "Open documentation",
@@ -3667,6 +3669,9 @@
"message.upload.iso.failed.description": "Failed to upload ISO.",
"message.upload.template.failed.description": "Failed to upload Template",
"message.upload.volume.failed": "Volume upload failed",
+"message.ssvm.cert.untrusted": "Unable to reach the upload server.",
+"message.ssvm.cert.trust.instructions": "The upload server may be using a
self-signed or untrusted certificate. Click 'Open Certificate Page' to open the
server in a new browser tab, accept the certificate warning, then return here
and click 'Retry Upload'. If the server remains unreachable, contact your
administrator.",
+"message.ssvm.unreachable.retry": "The upload server is still unreachable. If
it uses a self-signed certificate, please accept it in the opened tab and try
again.",
"message.user.not.permitted.api": "User is not permitted to use the API",
"message.validate.equalto": "Please enter the same value again.",
"message.validate.max": "Please enter a value less than or equal to {0}.",
diff --git a/ui/src/style/vars.less b/ui/src/style/vars.less
index de2d494c878..133244473e2 100644
--- a/ui/src/style/vars.less
+++ b/ui/src/style/vars.less
@@ -355,7 +355,7 @@ a {
text-align: right;
padding-top: 15px;
- button {
+ button, a.ant-btn {
margin-right: 5px;
}
}
diff --git a/ui/src/utils/ssvmProbe.js b/ui/src/utils/ssvmProbe.js
new file mode 100644
index 00000000000..55690aea898
--- /dev/null
+++ b/ui/src/utils/ssvmProbe.js
@@ -0,0 +1,30 @@
+// 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.
+
+const SSVM_PROBE_TIMEOUT_MS = 5000
+export async function probeSsvmCert (origin) {
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), SSVM_PROBE_TIMEOUT_MS)
+ try {
+ await fetch(origin, { method: 'HEAD', mode: 'no-cors', signal:
controller.signal })
+ return true
+ } catch (e) {
+ return false
+ } finally {
+ clearTimeout(timeoutId)
+ }
+}
diff --git a/ui/src/views/image/RegisterOrUploadIso.vue
b/ui/src/views/image/RegisterOrUploadIso.vue
index 37ae369727f..1984a6a6144 100644
--- a/ui/src/views/image/RegisterOrUploadIso.vue
+++ b/ui/src/views/image/RegisterOrUploadIso.vue
@@ -19,11 +19,27 @@
<div
class="form-layout"
@keyup.ctrl.enter="handleSubmit">
- <span v-if="uploadPercentage > 0">
+ <span v-if="uploading">
<loading-outlined />
{{ $t('message.upload.file.processing') }}
<a-progress :percent="uploadPercentage" />
</span>
+ <div v-else-if="ssvmCertUntrusted" class="ssvm-cert-warning">
+ <a-alert
+ type="warning"
+ show-icon
+ :message="$t('message.ssvm.cert.untrusted')"
+ :description="$t('message.ssvm.cert.trust.instructions')" />
+ <div :span="24" class="action-button">
+ <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+ <a-button :href="ssvmOrigin" target="_blank" rel="noopener noreferrer">
+ {{ $t('label.ssvm.open.cert.page') }}
+ </a-button>
+ <a-button type="primary" :loading="loading" @click="retryUpload">
+ {{ $t('label.retry.upload') }}
+ </a-button>
+ </div>
+ </div>
<a-spin :spinning="loading" v-else>
<a-form
:ref="formRef"
@@ -311,6 +327,7 @@ import { api } from '@/api'
import store from '@/store'
import { axios } from '../../utils/request'
import { mixinForm } from '@/utils/mixin'
+import { probeSsvmCert } from '@/utils/ssvmProbe'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
@@ -343,9 +360,12 @@ export default {
userdatapolicy: null,
userdatapolicylist: {},
loading: false,
+ uploading: false,
allowed: false,
uploadParams: null,
uploadPercentage: 0,
+ ssvmCertUntrusted: false,
+ ssvmOrigin: '',
currentForm: ['plus-outlined',
'PlusOutlined'].includes(this.action.currentAction.icon) ? 'Create' : 'Upload',
domains: [],
accounts: [],
@@ -489,6 +509,17 @@ export default {
this.form.file = file
return false
},
+ async retryUpload () {
+ this.loading = true
+ const reachable = await probeSsvmCert(this.ssvmOrigin)
+ this.loading = false
+ if (!reachable) {
+ this.$message.warning(this.$t('message.ssvm.unreachable.retry'))
+ return
+ }
+ this.ssvmCertUntrusted = false
+ this.handleUpload()
+ },
handleUpload () {
const { fileList } = this
if (this.fileList.length > 1) {
@@ -502,6 +533,7 @@ export default {
fileList.forEach(file => {
formData.append('files[]', file)
})
+ this.uploading = true
this.uploadPercentage = 0
axios.post(this.uploadParams.postURL,
formData,
@@ -529,6 +561,8 @@ export default {
description: `${this.$t('message.upload.iso.failed.description')} -
${e}`,
duration: 0
})
+ }).finally(() => {
+ this.uploading = false
})
},
handleSubmit (e) {
@@ -583,18 +617,18 @@ export default {
}
params.format = 'ISO'
this.loading = true
- api('getUploadParamsForIso', params).then(json => {
+ api('getUploadParamsForIso', params).then(async json => {
this.uploadParams = (json.postuploadisoresponse &&
json.postuploadisoresponse.getuploadparams) ?
json.postuploadisoresponse.getuploadparams : ''
- const response = this.handleUpload()
if (this.userdataid !== null) {
this.linkUserdataToTemplate(this.userdataid,
json.postuploadisoresponse.iso[0].id)
}
- if (response === 'upload successful') {
- this.$notification.success({
- message: this.$t('message.success.upload'),
- description: this.$t('message.success.upload.iso.description')
- })
+ this.ssvmOrigin = new URL(this.uploadParams.postURL).origin
+ const trusted = await probeSsvmCert(this.ssvmOrigin)
+ if (!trusted) {
+ this.ssvmCertUntrusted = true
+ return
}
+ this.handleUpload()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue
b/ui/src/views/image/RegisterOrUploadTemplate.vue
index 3ada9f6fd53..1267e5d45c1 100644
--- a/ui/src/views/image/RegisterOrUploadTemplate.vue
+++ b/ui/src/views/image/RegisterOrUploadTemplate.vue
@@ -19,11 +19,27 @@
<div
:class="'form-layout'"
@keyup.ctrl.enter="handleSubmit">
- <span v-if="uploadPercentage > 0">
+ <span v-if="uploading">
<loading-outlined />
{{ $t('message.upload.file.processing') }}
<a-progress :percent="uploadPercentage" />
</span>
+ <div v-else-if="ssvmCertUntrusted" class="ssvm-cert-warning">
+ <a-alert
+ type="warning"
+ show-icon
+ :message="$t('message.ssvm.cert.untrusted')"
+ :description="$t('message.ssvm.cert.trust.instructions')" />
+ <div :span="24" class="action-button">
+ <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+ <a-button :href="ssvmOrigin" target="_blank" rel="noopener noreferrer">
+ {{ $t('label.ssvm.open.cert.page') }}
+ </a-button>
+ <a-button type="primary" :loading="loading" @click="retryUpload">
+ {{ $t('label.retry.upload') }}
+ </a-button>
+ </div>
+ </div>
<a-spin :spinning="loading" v-else>
<a-form
:ref="formRef"
@@ -472,6 +488,7 @@ import { api } from '@/api'
import store from '@/store'
import { axios } from '../../utils/request'
import { mixinForm } from '@/utils/mixin'
+import { probeSsvmCert } from '@/utils/ssvmProbe'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
@@ -497,6 +514,8 @@ export default {
uploadPercentage: 0,
uploading: false,
fileList: [],
+ ssvmCertUntrusted: false,
+ ssvmOrigin: '',
zones: {},
defaultZone: '',
hyperVisor: {},
@@ -610,12 +629,24 @@ export default {
this.form.file = file
return false
},
+ async retryUpload () {
+ this.loading = true
+ const reachable = await probeSsvmCert(this.ssvmOrigin)
+ this.loading = false
+ if (!reachable) {
+ this.$message.warning(this.$t('message.ssvm.unreachable.retry'))
+ return
+ }
+ this.ssvmCertUntrusted = false
+ this.handleUpload()
+ },
handleUpload () {
const { fileList } = this
const formData = new FormData()
fileList.forEach(file => {
formData.append('files[]', file)
})
+ this.uploading = true
this.uploadPercentage = 0
axios.post(this.uploadParams.postURL,
formData,
@@ -639,6 +670,8 @@ export default {
this.closeAction()
}).catch(e => {
this.$notifyError(e)
+ }).finally(() => {
+ this.uploading = false
})
},
fetchCustomHypervisorName () {
@@ -1124,12 +1157,18 @@ export default {
duration: 0
})
}
- api('getUploadParamsForTemplate', params).then(json => {
+ api('getUploadParamsForTemplate', params).then(async json => {
this.uploadParams = (json.postuploadtemplateresponse &&
json.postuploadtemplateresponse.getuploadparams) ?
json.postuploadtemplateresponse.getuploadparams : ''
- this.handleUpload()
if (this.userdataid !== null) {
this.linkUserdataToTemplate(this.userdataid,
json.postuploadtemplateresponse.template[0].id)
}
+ this.ssvmOrigin = new URL(this.uploadParams.postURL).origin
+ const trusted = await probeSsvmCert(this.ssvmOrigin)
+ if (!trusted) {
+ this.ssvmCertUntrusted = true
+ return
+ }
+ this.handleUpload()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
diff --git a/ui/src/views/storage/UploadLocalVolume.vue
b/ui/src/views/storage/UploadLocalVolume.vue
index 3a0bf4e129f..b7303117e5a 100644
--- a/ui/src/views/storage/UploadLocalVolume.vue
+++ b/ui/src/views/storage/UploadLocalVolume.vue
@@ -16,13 +16,29 @@
// under the License.
<template>
- <div class="form-layout" v-ctrl-enter="handleSubmit">
- <span v-if="uploadPercentage > 0">
+ <div class="form-layout">
+ <span v-if="uploading">
<loading-outlined />
{{ $t('message.upload.file.processing') }}
<a-progress :percent="uploadPercentage" />
</span>
- <a-spin :spinning="loading" v-else>
+ <div v-else-if="ssvmCertUntrusted" class="ssvm-cert-warning">
+ <a-alert
+ type="warning"
+ show-icon
+ :message="$t('message.ssvm.cert.untrusted')"
+ :description="$t('message.ssvm.cert.trust.instructions')" />
+ <div :span="24" class="action-button">
+ <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+ <a-button :href="ssvmOrigin" target="_blank" rel="noopener noreferrer">
+ {{ $t('label.ssvm.open.cert.page') }}
+ </a-button>
+ <a-button type="primary" :loading="loading" @click="retryUpload">
+ {{ $t('label.retry.upload') }}
+ </a-button>
+ </div>
+ </div>
+ <a-spin :spinning="loading" v-else v-ctrl-enter="handleSubmit">
<a-form
:ref="formRef"
:model="form"
@@ -156,6 +172,7 @@ import { ref, reactive, toRaw } from 'vue'
import { api } from '@/api'
import { axios } from '../../utils/request'
import { mixinForm } from '@/utils/mixin'
+import { probeSsvmCert } from '@/utils/ssvmProbe'
import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import InfiniteScrollSelect from
'@/components/widgets/InfiniteScrollSelect.vue'
@@ -178,7 +195,10 @@ export default {
customDiskOffering: false,
isCustomizedDiskIOps: false,
loading: false,
- uploadPercentage: 0
+ uploading: false,
+ uploadPercentage: 0,
+ ssvmCertUntrusted: false,
+ ssvmOrigin: ''
}
},
beforeCreate () {
@@ -267,6 +287,63 @@ export default {
this.form.account = accountName
this.account = accountName
},
+ async retryUpload () {
+ this.loading = true
+ const reachable = await probeSsvmCert(this.ssvmOrigin)
+ this.loading = false
+ if (!reachable) {
+ this.$message.warning(this.$t('message.ssvm.unreachable.retry'))
+ return
+ }
+ this.ssvmCertUntrusted = false
+ this.handleUpload()
+ },
+ handleUpload () {
+ if (this.fileList.length > 1) {
+ this.$notification.error({
+ message: this.$t('message.upload.volume.failed'),
+ description: this.$t('message.upload.file.limit'),
+ duration: 0
+ })
+ return
+ }
+ const { fileList } = this
+ const formData = new FormData()
+ fileList.forEach(file => {
+ formData.append('files[]', file)
+ })
+ this.uploading = true
+ this.uploadPercentage = 0
+ axios.post(this.uploadParams.postURL,
+ formData,
+ {
+ headers: {
+ 'content-type': 'multipart/form-data',
+ 'x-signature': this.uploadParams.signature,
+ 'x-expires': this.uploadParams.expires,
+ 'x-metadata': this.uploadParams.metadata
+ },
+ onUploadProgress: (progressEvent) => {
+ this.uploadPercentage = Number(parseFloat(100 *
progressEvent.loaded / progressEvent.total).toFixed(1))
+ },
+ timeout: 86400000
+ }).then((json) => {
+ this.$notification.success({
+ message: this.$t('message.success.upload'),
+ description: this.$t('message.success.upload.volume.description')
+ })
+ this.closeAction()
+ }).catch(e => {
+ this.$notification.error({
+ message: this.$t('message.upload.failed'),
+ description: `${this.$t('message.upload.volume.failed')} - ${e}`,
+ duration: 0
+ })
+ }).finally(() => {
+ this.uploading = false
+ this.loading = false
+ })
+ },
handleSubmit (e) {
e.preventDefault()
if (this.loading) return
@@ -286,49 +363,15 @@ export default {
}
params.domainId = this.domainId
this.loading = true
- api('getUploadParamsForVolume', params).then(json => {
+ api('getUploadParamsForVolume', params).then(async json => {
this.uploadParams = json.postuploadvolumeresponse?.getuploadparams
|| ''
- const { fileList } = this
- if (this.fileList.length > 1) {
- this.$notification.error({
- message: this.$t('message.upload.volume.failed'),
- description: this.$t('message.upload.file.limit'),
- duration: 0
- })
+ this.ssvmOrigin = new URL(this.uploadParams.postURL).origin
+ const trusted = await probeSsvmCert(this.ssvmOrigin)
+ if (!trusted) {
+ this.ssvmCertUntrusted = true
+ return
}
- const formData = new FormData()
- fileList.forEach(file => {
- formData.append('files[]', file)
- })
- this.uploadPercentage = 0
- axios.post(this.uploadParams.postURL,
- formData,
- {
- headers: {
- 'content-type': 'multipart/form-data',
- 'x-signature': this.uploadParams.signature,
- 'x-expires': this.uploadParams.expires,
- 'x-metadata': this.uploadParams.metadata
- },
- onUploadProgress: (progressEvent) => {
- this.uploadPercentage = Number(parseFloat(100 *
progressEvent.loaded / progressEvent.total).toFixed(1))
- },
- timeout: 86400000
- }).then((json) => {
- this.$notification.success({
- message: this.$t('message.success.upload'),
- description: this.$t('message.success.upload.volume.description')
- })
- this.closeAction()
- }).catch(e => {
- this.$notification.error({
- message: this.$t('message.upload.failed'),
- description: `${this.$t('message.upload.volume.failed')} -
${e}`,
- duration: 0
- })
- }).finally(() => {
- this.loading = false
- })
+ this.handleUpload()
}).catch(e => {
this.$notification.error({
message: this.$t('message.upload.failed'),