Transfer.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <template>
  2. <base-dialog @cancel="cancelDialog">
  3. <div slot="header">{{$t('compute.text_1127')}}</div>
  4. <div slot="body">
  5. <dialog-selected-tips :name="$t('dictionary.server')" :count="params.data.length" :action="$t('compute.text_1127')" />
  6. <dialog-table :data="params.data" :columns="columns" />
  7. <a-form :form="form.fc" hideRequiredMark v-bind="formItemLayout">
  8. <!-- 自动启动 -->
  9. <a-form-item :label="$t('compute.text_494')" v-if="isSingle && isAllReady" :extra="$t('compute.text_1263')">
  10. <a-switch
  11. :checkedChildren="$t('compute.text_115')"
  12. :unCheckedChildren="$t('compute.text_116')"
  13. v-decorator="decorators.auto_start" />
  14. </a-form-item>
  15. <!-- 跳过CPU检查 -->
  16. <a-form-item :label="$t('compute.live_migrate.skip_cpu_check')" v-if="isKvm && isAllRunning" :extra="$t('compute.live_migrate.skip_cpu_check.explain')">
  17. <a-switch
  18. :checkedChildren="$t('compute.text_115')"
  19. :unCheckedChildren="$t('compute.text_116')"
  20. v-decorator="decorators.skip_cpu_check"
  21. @change="onSkipCpuCheck" />
  22. </a-form-item>
  23. <a-form-item
  24. :label="$t('compute.text_111')">
  25. <list-select
  26. v-decorator="decorators.host"
  27. :list-props="resourceProps"
  28. :formatter="v => v.name"
  29. :multiple="false"
  30. :placeholder="$t('compute.text_314')"
  31. :dialog-params="{ title: $t('compute.text_111'), width: 1060 }"
  32. :tab-props="tabProps"
  33. @change="hostChangeHandle" />
  34. <div :class="[hostValidateStatus]" style="line-height: 20px; color: rgba(0,0,0,.45);">{{ message || hostValidateMsg }}</div>
  35. </a-form-item>
  36. <template v-if="isKvm && isAllRunning">
  37. <a-form-item>
  38. <span slot="label">
  39. {{ $t('compute.vminstance.transfer.max_brand_width') }}
  40. <a-tooltip :title="$t('compute.transfer.max_brand_width.tooltip')"><a-icon type="question-circle-o" /></a-tooltip>
  41. </span>
  42. <migration-bandwidth :decorators="decorators" :form="form" />
  43. </a-form-item>
  44. </template>
  45. </a-form>
  46. </div>
  47. <div slot="footer">
  48. <a-button type="primary" @click="handleConfirm" :loading="loading" :disabled="handleConfirmDisabled">{{ $t('dialog.ok') }}</a-button>
  49. <a-button @click="cancelDialog">{{ $t('dialog.cancel') }}</a-button>
  50. </div>
  51. </base-dialog>
  52. </template>
  53. <script>
  54. import { mapGetters } from 'vuex'
  55. import DialogMixin from '@/mixins/dialog'
  56. import WindowsMixin from '@/mixins/windows'
  57. import { HYPERVISORS_MAP } from '@/constants'
  58. import ListSelect from '@/sections/ListSelect'
  59. import MigrationBandwidth from '@Compute/sections/MigrationBandwidth'
  60. import ResourceProps from '../mixins/resourceProps'
  61. export default {
  62. name: 'VmTransferDialog',
  63. components: {
  64. ListSelect,
  65. MigrationBandwidth,
  66. },
  67. mixins: [DialogMixin, WindowsMixin, ResourceProps],
  68. data () {
  69. const isVMware = this.params.data[0].hypervisor === HYPERVISORS_MAP.esxi.key
  70. return {
  71. loading: false,
  72. form: {
  73. fc: this.$form.createForm(this, {
  74. onValuesChange: (props, values) => {
  75. Object.keys(values).forEach((key) => {
  76. this.form.fd[key] = values[key]
  77. })
  78. },
  79. }),
  80. fd: {},
  81. },
  82. forcastData: null,
  83. hosts: [],
  84. message: '',
  85. decorators: {
  86. host: [
  87. 'host',
  88. {
  89. rules: [
  90. { required: isVMware, message: this.$t('compute.text_314'), trigger: 'change' },
  91. ],
  92. },
  93. ],
  94. auto_start: [
  95. 'auto_start',
  96. {
  97. initialValue: true,
  98. valuePropName: 'checked',
  99. },
  100. ],
  101. skip_cpu_check: [
  102. 'skip_cpu_check',
  103. {
  104. initialValue: false,
  105. valuePropName: 'checked',
  106. },
  107. ],
  108. brandWidth: [
  109. 'brandWidth',
  110. {
  111. initialValue: '-1',
  112. },
  113. ],
  114. customBrandWidth: [
  115. 'customBrandWidth',
  116. {
  117. validateFirst: true,
  118. rules: [
  119. { required: true, message: this.$t('compute.vminstance.transfer.max_brand_width.required'), trigger: 'change' },
  120. { pattern: /^[1-9][0-9]*$/, message: this.$t('compute.transfer.bandwidth.number.check') },
  121. ],
  122. },
  123. ],
  124. },
  125. formItemLayout: {
  126. wrapperCol: {
  127. span: 20,
  128. },
  129. labelCol: {
  130. span: 4,
  131. },
  132. },
  133. }
  134. },
  135. computed: {
  136. ...mapGetters(['scope', 'isAdminMode']),
  137. firstData () {
  138. return this.params.data[0]
  139. },
  140. isSingle () {
  141. return this.params.data.length === 1
  142. },
  143. isKvm () {
  144. return this.firstData.hypervisor === HYPERVISORS_MAP.kvm.key
  145. },
  146. isVMware () {
  147. return this.firstData.hypervisor === HYPERVISORS_MAP.esxi.key
  148. },
  149. filteredCandidates () {
  150. if (!this.forcastData) {
  151. return []
  152. }
  153. const candsMap = {}
  154. this.forcastData.data.map(results => {
  155. if (results.data.filtered_candidates) {
  156. results.data.filtered_candidates.map(candidate => {
  157. if (candsMap[candidate.id]) {
  158. candsMap[candidate.id].count++
  159. candidate.reasons.map(reason => {
  160. if (!candsMap[candidate.id].reasons.includes(reason)) {
  161. candsMap[candidate.id].reasons.push(reason)
  162. }
  163. })
  164. } else {
  165. candidate.count = 1
  166. candsMap[candidate.id] = candidate
  167. }
  168. })
  169. }
  170. })
  171. const ret = []
  172. for (var id in candsMap) {
  173. if (candsMap[id].count >= this.params.data.length) {
  174. ret.push(candsMap[id])
  175. }
  176. }
  177. return ret
  178. },
  179. canCreate () {
  180. if (!this.forcastData) {
  181. return true
  182. }
  183. let canCreate = false
  184. this.forcastData.data.map(results => {
  185. if (results.data.can_create) {
  186. canCreate = true
  187. }
  188. })
  189. return canCreate
  190. },
  191. hostsParams () {
  192. let hostType = 'hypervisor'
  193. const hostIds = this.filteredCandidates?.map(v => v.id) || []
  194. const managerIds = []
  195. this.params.data.map(item => {
  196. if (item.manager_id && !managerIds.includes(item.manager_id)) {
  197. managerIds.push(item.manager_id)
  198. }
  199. })
  200. if (!this.isKvm) {
  201. hostType = this.firstData.hypervisor
  202. }
  203. const ret = {
  204. scope: this.scope,
  205. host_type: hostType,
  206. limit: 10,
  207. enabled: 1,
  208. host_status: 'online',
  209. os_arch: this.firstData.os_arch,
  210. }
  211. if (this.isSingle) {
  212. ret.server_id_for_network = this.firstData.id
  213. if (!hostIds.includes(this.firstData.host_id)) {
  214. hostIds.push(this.firstData.host_id)
  215. }
  216. }
  217. if (this.isAdminMode && this.isSingle) {
  218. ret.project_domain = this.params.data[0].domain_id
  219. }
  220. if (hostIds && hostIds.length > 0) {
  221. ret.filter = [`id.notin(${hostIds.join(',')})`]
  222. }
  223. if (managerIds.length) {
  224. ret.filter = [...(ret.filter || []), `manager_id.in(${managerIds.join(',')})`]
  225. }
  226. return ret
  227. },
  228. hostsOptions () {
  229. const hostIds = this.filteredCandidates?.map(v => v.id) || []
  230. if (!this.canCreate) {
  231. return []
  232. }
  233. return this.hosts.filter(v => {
  234. return !hostIds.includes(v.id) && v.id !== this.firstData.host_id
  235. }).map(v => {
  236. return {
  237. key: v.id,
  238. label: v.name,
  239. }
  240. })
  241. },
  242. hostValidateStatus () {
  243. if (this.message) return 'warning-color'
  244. if (this.forcastData && this.hostsOptions?.length === 0) {
  245. return 'error-color'
  246. }
  247. return 'info-color'
  248. },
  249. hostValidateMsg () {
  250. if (this.isVMware) return this.$t('compute.vmware.transfer.message')
  251. if (this.forcastData && this.hostsOptions?.length === 0) {
  252. return this.$t('compute.transfer_host')
  253. }
  254. return this.$t('compute.text_1384')
  255. },
  256. handleConfirmDisabled () {
  257. return this.forcastData && this.hostsOptions?.length === 0
  258. },
  259. columns () {
  260. const fields = ['name', 'status', 'host']
  261. return this.params.columns.filter(item => {
  262. const { field } = item
  263. return fields.indexOf(field) > -1
  264. })
  265. },
  266. isAllRunning () {
  267. return this.params.data.every(item => item.status === 'running')
  268. },
  269. isAllReady () {
  270. return this.params.data.every(item => item.status === 'ready')
  271. },
  272. isESXiManager () {
  273. return this.params.data[0].manager_id
  274. },
  275. tabProps () {
  276. const filtered = this.filteredCandidates
  277. const filteredLabel = this.$t('compute.unavailable_host') + '(' + (filtered ? filtered.length : 0) + ')'
  278. const availableLabel = this.$t('compute.available_host')
  279. const tabs = [
  280. { label: availableLabel, value: 'available' },
  281. ]
  282. if (filtered && filtered.length > 0) {
  283. tabs.push({ label: filteredLabel, value: 'unavailable' })
  284. }
  285. return {
  286. curTab: 'available',
  287. tabs: tabs,
  288. listProps: {
  289. data: filtered || [],
  290. columns: [
  291. {
  292. field: 'name',
  293. title: this.$t('table.title.name'),
  294. slots: {
  295. default: ({ row }, h) => {
  296. return row.name
  297. },
  298. },
  299. },
  300. {
  301. field: 'reasons',
  302. title: this.$t('common.reason'),
  303. slots: {
  304. default: ({ row }, h) => {
  305. const ret = row.reasons.map(item => {
  306. return <li>{item}</li>
  307. })
  308. return [<ul style={{ marginLeft: '-26px' }}>{ ...ret }</ul>]
  309. },
  310. },
  311. },
  312. ],
  313. },
  314. }
  315. },
  316. },
  317. created () {
  318. !this.isESXiManager && this.queryForcastData()
  319. this.queryHosts()
  320. },
  321. methods: {
  322. doSingleTransfer (ids, values) {
  323. let action = 'migrate'
  324. const selectedItem = this.params.data[0]
  325. const data = {
  326. prefer_host: values.host,
  327. }
  328. if (this.isAllReady) {
  329. data.auto_start = values.auto_start
  330. }
  331. if (!this.isAllRunning) {
  332. action = 'migrate'
  333. } else {
  334. action = 'live-migrate'
  335. if (values.skip_cpu_check) {
  336. data.skip_cpu_check = true
  337. data.skip_kernel_check = true
  338. }
  339. if (values.brandWidth !== '-1') {
  340. data.max_bandwidth_mb = values.brandWidth === 'custom' ? (+values.customBrandWidth || 0) : +values.brandWidth
  341. }
  342. data.quickly_finish = true
  343. }
  344. if (selectedItem.host_enabled === false && selectedItem.host_status === 'offline') {
  345. action = 'migrate'
  346. data.rescue_mode = true
  347. }
  348. return this.params.onManager('performAction', {
  349. id: this.firstData.id,
  350. steadyStatus: ['running', 'ready'],
  351. managerArgs: {
  352. action,
  353. data,
  354. },
  355. })
  356. },
  357. doBatchTransfer (ids, values) {
  358. const data = {
  359. guest_ids: ids,
  360. prefer_host: values.host,
  361. }
  362. if (values.skip_cpu_check) {
  363. data.skip_cpu_check = true
  364. data.skip_kernel_check = true
  365. }
  366. if (values.brandWidth !== '-1') {
  367. data.max_bandwidth_mb = values.brandWidth === 'custom' ? (+values.customBrandWidth || 0) : +values.brandWidth
  368. }
  369. return this.params.onManager('performClassAction', {
  370. id: ids,
  371. steadyStatus: ['running', 'ready'],
  372. managerArgs: {
  373. action: 'batch-migrate',
  374. data,
  375. },
  376. })
  377. },
  378. async handleConfirm () {
  379. this.loading = true
  380. try {
  381. const values = await this.form.fc.validateFields()
  382. const ids = this.params.data.map(item => item.id)
  383. if (this.isSingle) {
  384. await this.doSingleTransfer(ids, values)
  385. } else {
  386. await this.doBatchTransfer(ids, values)
  387. }
  388. this.cancelDialog()
  389. } finally {
  390. this.loading = false
  391. }
  392. },
  393. doForecast (live_migrate = true, skip_cpu_check = false, prefer_host_id) {
  394. const manager = new this.$Manager('servers')
  395. const params = {
  396. live_migrate,
  397. skip_cpu_check,
  398. }
  399. if (prefer_host_id) {
  400. params.prefer_host_id = prefer_host_id
  401. }
  402. if (live_migrate && skip_cpu_check) {
  403. params.skip_kernel_check = true
  404. }
  405. const ids = []
  406. this.params.data.map(item => {
  407. ids.push(item.id)
  408. })
  409. return manager.batchPerformAction({
  410. ids: ids, // this.params.data[0].id,
  411. action: 'migrate-forecast',
  412. data: params,
  413. })
  414. },
  415. queryForcastData (skip_cpu_check, prefer_host_id) {
  416. const live_migrate = this.firstData.status === 'running'
  417. this.doForecast(live_migrate, skip_cpu_check, prefer_host_id).then((res) => {
  418. this.forcastData = res.data
  419. }).catch((err) => {
  420. throw err
  421. })
  422. },
  423. queryHosts () {
  424. const hostsManager = new this.$Manager('hosts')
  425. hostsManager.list({ params: this.hostsParams }).then((res) => {
  426. this.hosts = res.data.data || []
  427. }).catch((err) => {
  428. throw err
  429. })
  430. },
  431. hostChangeHandle (hostId) {
  432. const hostArr = this.params.data.filter(v => v.host_id === hostId)
  433. if (hostArr.length > 0) {
  434. this.message = this.$t('compute.transfer_mutiple_dialog_alert', [hostArr.length])
  435. } else {
  436. this.message = ''
  437. }
  438. },
  439. onSkipCpuCheck (e) {
  440. !this.isESXiManager && this.queryForcastData(e)
  441. },
  442. },
  443. }
  444. </script>