List.vue 78 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775
  1. <template>
  2. <page-list
  3. :fixed="true"
  4. show-tag-filter
  5. show-tag-columns
  6. show-tag-columns2
  7. show-tag-config
  8. :list="list"
  9. :columns="templateListColumns || columns"
  10. :show-single-actions="isTemplate ? false : showActions"
  11. :show-group-actions="showGroupActions && showActions"
  12. :group-actions="groupActions"
  13. :single-actions="singleActions"
  14. :export-data-options="exportDataOptions"
  15. :showSearchbox="showSearchbox"
  16. :defaultSearchKey="defaultSearchKey"
  17. :refresh-method="handleListRefresh"
  18. :tag-config-params="tagConfigParams"
  19. :tableOverviewIndexs="tableOverviewIndexs"
  20. :show-page="!isTemplate" />
  21. </template>
  22. <script>
  23. import * as R from 'ramda'
  24. import { mapGetters } from 'vuex'
  25. import _ from 'lodash'
  26. import { SERVER_TYPE } from '@Compute/constants'
  27. import ListMixin from '@/mixins/list'
  28. import {
  29. getNameFilter,
  30. getBrandFilter,
  31. getStatusFilter,
  32. getDomainFilter,
  33. getTenantFilter,
  34. getAccountFilter,
  35. getHostFilter,
  36. getVpcFilter,
  37. getOsArchFilter,
  38. getRegionFilter,
  39. getZoneFilter,
  40. getCloudProviderFilter,
  41. getDescriptionFilter,
  42. getCreatedAtFilter,
  43. } from '@/utils/common/tableFilter'
  44. import { getIpsTableColumn } from '@/utils/common/tableColumn'
  45. import { disableDeleteAction } from '@/utils/common/tableActions'
  46. import expectStatus from '@/constants/expectStatus'
  47. import WindowsMixin from '@/mixins/windows'
  48. import { typeClouds, findPlatform } from '@/utils/common/hypervisor'
  49. import GlobalSearchMixin from '@/mixins/globalSearch'
  50. import ResTemplateListMixin from '@/mixins/resTemplateList'
  51. import regexp from '@/utils/regexp'
  52. import { hasSetupKey, isLicense2 } from '@/utils/auth'
  53. import { sizeToDesignatedUnit } from '@/utils/utils'
  54. import { Manager } from '@/utils/manager'
  55. import { PROVIDER_MAP, BRAND_MAP } from '@/constants'
  56. import { KVM_SHARE_STORAGES } from '@/constants/storage'
  57. import SingleActionsMixin from '../mixins/singleActions'
  58. import ColumnsMixin from '../mixins/columns'
  59. import { cloudEnabled, cloudUnabledTip, commonEnabled, validateRescueMode } from '../utils'
  60. export default {
  61. name: 'VmInstanceList',
  62. mixins: [WindowsMixin, ListMixin, GlobalSearchMixin, ColumnsMixin, SingleActionsMixin, ResTemplateListMixin],
  63. props: {
  64. id: String,
  65. getParams: {
  66. type: Object,
  67. default: () => ({}),
  68. },
  69. cloudEnv: String,
  70. cloudEnvOptions: {
  71. type: Array,
  72. },
  73. hiddenFilterOptions: {
  74. type: Array,
  75. default: () => ([]),
  76. },
  77. hiddenActions: {
  78. type: Array,
  79. default: () => ([]),
  80. },
  81. },
  82. data () {
  83. const filter = {}
  84. if (!this.inBaseSidePage) {
  85. if (this.$route.query.id) {
  86. filter.id = [this.$route.query.id]
  87. }
  88. if (this.$route.query.status) {
  89. filter.status = this.$route.query.status
  90. }
  91. }
  92. const pci_model_types = this.$store.getters.capability?.pci_model_types || []
  93. let devTypes = pci_model_types.map(item => {
  94. return { key: item.dev_type, label: item.dev_type }
  95. })
  96. devTypes = _.unionWith(devTypes, _.isEqual)
  97. const filterOptions = {
  98. external_id: {
  99. label: this.$t('table.title.external_id'),
  100. },
  101. id: {
  102. label: this.$t('table.title.id'),
  103. },
  104. name: getNameFilter(),
  105. description: getDescriptionFilter(),
  106. brand: getBrandFilter('compute_engine_brands'),
  107. ip_addr: {
  108. label: 'IP',
  109. hiddenField: 'ips',
  110. },
  111. status: getStatusFilter('server'),
  112. power_states: getStatusFilter({ title: this.$t('compute.power_states'), statusModule: 'server', field: 'power_states' }),
  113. os_dist: {
  114. label: this.$t('table.title.os'),
  115. dropdown: true,
  116. multiple: true,
  117. distinctField: {
  118. type: 'extra_field',
  119. key: 'os_dist',
  120. },
  121. },
  122. projects: getTenantFilter(),
  123. project_domains: getDomainFilter(),
  124. billing_type: {
  125. label: this.$t('table.title.bill_type'),
  126. dropdown: true,
  127. items: [
  128. { label: this.$t('billingType.postpaid'), key: 'postpaid' },
  129. { label: this.$t('billingType.prepaid'), key: 'prepaid' },
  130. ],
  131. },
  132. cloudaccount: getAccountFilter(),
  133. manager: getCloudProviderFilter(),
  134. host: getHostFilter(),
  135. server_type: {
  136. label: this.$t('table.title.type'),
  137. dropdown: true,
  138. multiple: true,
  139. hiddenField: 'is_gpu',
  140. items: [
  141. { label: this.$t('compute.text_291', [this.$t('dictionary.server')]), key: 'normal' },
  142. { label: `USB${this.$t('dictionary.server')}`, key: 'usb' },
  143. { label: this.$t('compute.backup'), key: 'backup' },
  144. { label: this.$t('compute.trans_device_server'), key: 'gpu' },
  145. ...devTypes,
  146. ],
  147. },
  148. region: getRegionFilter(),
  149. zone_ids: getZoneFilter(),
  150. vpc: getVpcFilter(),
  151. os_arch: getOsArchFilter(),
  152. // vmem_size: {
  153. // label: this.$t('table.title.vmem_size'),
  154. // },
  155. vcpu_count: {
  156. label: 'CPU',
  157. },
  158. // disk: {
  159. // label: this.$t('table.title.disk'),
  160. // },
  161. created_at: getCreatedAtFilter(),
  162. }
  163. this.hiddenFilterOptions.forEach(key => {
  164. delete filterOptions[key]
  165. })
  166. let hasBastionService = false
  167. const { services = [] } = this.$store?.getters?.userInfo || {}
  168. const bastionService = services.find(val => val.type === 'bastionhost')
  169. if (bastionService && bastionService.status === true) {
  170. hasBastionService = true
  171. }
  172. const monitorManager = new this.$Manager('unifiedmonitors/resource-metrics', 'v1')
  173. return {
  174. monitorManager,
  175. list: this.$list.createList(this, {
  176. ctx: this,
  177. id: this.id,
  178. resource: 'servers',
  179. getParams: this.getParam,
  180. steadyStatus: {
  181. status: Object.values(expectStatus.server).flat(),
  182. checkBackup: (val) => {
  183. return val.metadata && (val.metadata.create_backup || val.metadata.switch_backup)
  184. },
  185. },
  186. filter,
  187. filterOptions,
  188. responseData: this.responseData,
  189. isTemplate: this.isTemplate,
  190. templateLimit: this.templateLimit,
  191. hiddenColumns: ['is_gpu', 'metadata', 'instance_type', 'os_type', 'vpc', 'host', 'account', 'created_at', 'macs', 'os_arch', 'vcpu_count', 'vmem_size', 'disk', 'power_states'],
  192. autoHiddenFilterKey: 'server_hidden_columns',
  193. fetchDataCb: async (response) => {
  194. if (response.data?.data?.length) {
  195. const list = response.data?.data || []
  196. const ids = list.map(item => item.id).filter(Boolean)
  197. if (ids.length) {
  198. const res = await monitorManager.create({
  199. data: {
  200. res_ids: ids,
  201. res_type: 'guest',
  202. },
  203. })
  204. const { resource_metrics = {} } = res.data || {}
  205. for (const key in resource_metrics) {
  206. const metrics = resource_metrics[key]
  207. list.find(item => item.id === key).alert_data = metrics
  208. }
  209. }
  210. response.data.data = list
  211. }
  212. return response
  213. },
  214. }),
  215. groupActions: [
  216. // 新建
  217. {
  218. label: this.$t('compute.perform_create'),
  219. permission: 'server_create',
  220. action: () => {
  221. this.$openNewWindowForMenuHook('vminstance_configured_callback_address.create_callback_address', () => {
  222. this.$router.push({
  223. path: '/vminstance/create',
  224. query: {
  225. type: this.cloudEnv === 'onpremise' ? 'idc' : this.cloudEnv || 'idc',
  226. },
  227. })
  228. })
  229. },
  230. meta: () => {
  231. return {
  232. buttonType: 'primary',
  233. validate: !this.cloudEnvEmpty,
  234. tooltip: this.cloudEnvEmpty ? this.$t('common.no_platform_available') : '',
  235. }
  236. },
  237. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_create') || this.hiddenActions.includes('create'),
  238. },
  239. // 开机
  240. {
  241. label: this.$t('compute.text_272'),
  242. permission: 'server_perform_start',
  243. action: () => {
  244. this.createDialog('VmStartDialog', {
  245. data: this.list.selectedItems,
  246. columns: this.columns,
  247. onManager: this.onManager,
  248. })
  249. },
  250. meta: () => {
  251. let ret = {
  252. validate: true,
  253. tooltip: null,
  254. }
  255. ret.validate = this.list.selectedItems.length > 0
  256. if (!ret.validate) return ret
  257. // 某些云不支持
  258. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems)
  259. if (!unenableCloudCheck.validate) {
  260. ret = unenableCloudCheck
  261. return ret
  262. }
  263. ret = this.$isValidateResourceLock(this.list.selectedItems, () => {
  264. ret.validate = this.list.selectedItems.every(item => item.status === 'ready')
  265. return ret
  266. })
  267. return ret
  268. },
  269. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_start'),
  270. },
  271. // 批量关机
  272. {
  273. label: this.$t('compute.text_273'),
  274. permission: 'server_perform_stop',
  275. action: () => {
  276. this.createDialog('VmShutDownDialog', {
  277. data: this.list.selectedItems,
  278. columns: this.columns,
  279. onManager: this.onManager,
  280. })
  281. },
  282. meta: () => {
  283. let ret = {
  284. validate: true,
  285. tooltip: null,
  286. }
  287. ret.validate = this.list.selectedItems.length > 0
  288. if (!ret.validate) return ret
  289. // 某些云不支持
  290. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems)
  291. if (!unenableCloudCheck.validate) {
  292. ret = unenableCloudCheck
  293. return ret
  294. }
  295. ret = this.$isValidateResourceLock(this.list.selectedItems, () => {
  296. ret.validate = this.list.selectedItems.every(item => item.status === 'running' || item.status === 'stop_fail')
  297. return ret
  298. })
  299. return ret
  300. },
  301. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_stop'),
  302. },
  303. // 重启
  304. {
  305. label: this.$t('compute.text_274'),
  306. permission: 'server_perform_restart',
  307. action: () => {
  308. this.createDialog('VmRestartDialog', {
  309. data: this.list.selectedItems,
  310. columns: this.columns,
  311. onManager: this.onManager,
  312. })
  313. },
  314. meta: () => {
  315. let ret = {
  316. validate: true,
  317. tooltip: null,
  318. }
  319. ret.validate = this.list.selectedItems.length > 0
  320. if (!ret.validate) return ret
  321. // 某些云不支持
  322. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems, [typeClouds.hypervisorMap.bingocloud.key, typeClouds.hypervisorMap.sangfor.key])
  323. if (!unenableCloudCheck.validate) {
  324. ret = unenableCloudCheck
  325. return ret
  326. }
  327. ret = this.$isValidateResourceLock(this.list.selectedItems, () => {
  328. ret.validate = this.list.selectedItems.every(item => ['running', 'stop_fail'].includes(item.status))
  329. return ret
  330. })
  331. return ret
  332. },
  333. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_restart'),
  334. },
  335. // 同步状态
  336. {
  337. label: this.$t('compute.perform_sync_status'),
  338. permission: 'server_perform_syncstatus',
  339. action: () => {
  340. this.onManager('batchPerformAction', {
  341. steadyStatus: ['running', 'ready'],
  342. managerArgs: {
  343. action: 'syncstatus',
  344. },
  345. })
  346. },
  347. meta: () => {
  348. return {
  349. validate: this.list.selectedItems.length > 0,
  350. }
  351. },
  352. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_syncstatus'),
  353. },
  354. /* 批量操作 */
  355. {
  356. label: this.$t('compute.text_275'),
  357. actions: () => {
  358. return [
  359. {
  360. // * 实例状态
  361. label: this.$t('compute.text_353'),
  362. submenus: [
  363. // 挂起
  364. {
  365. label: this.$t('compute.text_1128'),
  366. permission: 'server_perform_suspend',
  367. action: () => {
  368. this.createDialog('VmSuspendDialog', {
  369. data: this.list.selectedItems,
  370. columns: this.columns,
  371. onManager: this.onManager,
  372. })
  373. },
  374. meta: () => {
  375. const ret = {
  376. validate: true,
  377. tooltip: null,
  378. }
  379. const isAllSupportBrand = this.list.selectedItems.every(item => {
  380. return [
  381. BRAND_MAP.VMware.key,
  382. BRAND_MAP.OneCloud.key,
  383. ].includes(item.brand)
  384. })
  385. const isAllRunning = this.list.selectedItems.every(item => item.status === 'running')
  386. if (!isAllSupportBrand) {
  387. ret.validate = false
  388. ret.tooltip = this.$t('compute.text_1129')
  389. return ret
  390. }
  391. if (!isAllRunning) {
  392. ret.validate = false
  393. ret.tooltip = this.$t('compute.text_1130')
  394. return ret
  395. }
  396. ret.validate = true
  397. return ret
  398. },
  399. hidden: () => !hasSetupKey(['vmware']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_suspend'),
  400. },
  401. // 恢复
  402. {
  403. label: this.$t('compute.text_478'),
  404. permission: 'server_perform_resume',
  405. action: () => {
  406. this.createDialog('VmResumeDialog', {
  407. data: this.list.selectedItems,
  408. columns: this.columns,
  409. onManager: this.onManager,
  410. })
  411. },
  412. meta: () => {
  413. const ret = {
  414. validate: true,
  415. tooltip: null,
  416. }
  417. const isAllVMware = this.list.selectedItems.every(item => item.brand === BRAND_MAP.VMware.key || item.brand === BRAND_MAP.OneCloud.key)
  418. const isAllSuspend = this.list.selectedItems.every(item => item.status === 'suspend')
  419. if (!isAllVMware) {
  420. ret.validate = false
  421. ret.tooltip = this.$t('compute.text_1129')
  422. return ret
  423. }
  424. if (!isAllSuspend) {
  425. ret.validate = false
  426. ret.tooltip = this.$t('compute.text_1131')
  427. return ret
  428. }
  429. ret.validate = true
  430. return ret
  431. },
  432. hidden: () => !hasSetupKey(['vmware']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_resume'),
  433. },
  434. // 进入紧急模式
  435. {
  436. label: this.$t('compute.start_rescue'),
  437. permission: 'server_perform_start_rescue',
  438. action: () => {
  439. this.createDialog('VmStartRescueDialog', {
  440. data: this.list.selectedItems,
  441. columns: this.columns,
  442. onManager: this.onManager,
  443. refresh: this.refresh,
  444. })
  445. },
  446. meta: () => {
  447. const ret = { validate: true }
  448. let isAllKVM = true
  449. let isRescueMode = false
  450. this.list.selectedItems.forEach(item => {
  451. if (item.brand !== BRAND_MAP.OneCloud.key) {
  452. isAllKVM = false
  453. }
  454. if (item.rescue_mode === true) {
  455. isRescueMode = true
  456. }
  457. })
  458. if (!isAllKVM) {
  459. ret.validate = false
  460. ret.tooltip = this.$t('compute.text_1388')
  461. return ret
  462. }
  463. if (isRescueMode) {
  464. ret.validate = false
  465. ret.tooltip = this.$t('compute.start_rescue.validate_tooltip')
  466. return ret
  467. }
  468. return ret
  469. },
  470. hidden: () => !hasSetupKey(['onecloud']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_start_rescue'),
  471. },
  472. // 退出紧急模式
  473. {
  474. label: this.$t('compute.stop_rescue'),
  475. permission: 'server_perform_stop_rescue',
  476. action: () => {
  477. this.createDialog('VmStopRescueDialog', {
  478. data: this.list.selectedItems,
  479. columns: this.columns,
  480. onManager: this.onManager,
  481. refresh: this.refresh,
  482. })
  483. },
  484. meta: () => {
  485. const ret = { validate: true }
  486. let isAllKVM = true
  487. let isRescueMode = true
  488. this.list.selectedItems.forEach(item => {
  489. if (item.brand !== BRAND_MAP.OneCloud.key) {
  490. isAllKVM = false
  491. }
  492. if (item.rescue_mode !== true) {
  493. isRescueMode = false
  494. }
  495. })
  496. if (!isAllKVM) {
  497. ret.validate = false
  498. ret.tooltip = this.$t('compute.text_1388')
  499. return ret
  500. }
  501. if (!isRescueMode) {
  502. ret.validate = false
  503. ret.tooltip = this.$t('compute.stop_rescue.validate_tooltip')
  504. return ret
  505. }
  506. return ret
  507. },
  508. hidden: () => !hasSetupKey(['onecloud']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_stop_rescue'),
  509. },
  510. // 推送配置
  511. {
  512. label: this.$t('compute.sync_config'),
  513. permission: 'server_perform_sync_config',
  514. action: () => {
  515. this.createDialog('VmSyncConfigDialog', {
  516. data: this.list.selectedItems,
  517. columns: this.columns,
  518. onManager: this.onManager,
  519. })
  520. },
  521. meta: () => {
  522. const ret = {
  523. validate: true,
  524. tooltip: null,
  525. }
  526. const isAllKVM = this.list.selectedItems.every(item => item.brand === BRAND_MAP.OneCloud.key)
  527. const isAllRunningReady = this.list.selectedItems.every(item => (item.status === 'running' || item.status === 'ready'))
  528. if (!isAllKVM) {
  529. ret.validate = false
  530. ret.tooltip = this.$t('compute.text_1388')
  531. return ret
  532. }
  533. if (!isAllRunningReady) {
  534. ret.validate = false
  535. ret.tooltip = this.$t('compute.text_1126')
  536. return ret
  537. }
  538. ret.validate = true
  539. return ret
  540. },
  541. hidden: () => !hasSetupKey(['onecloud']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_sync_config'),
  542. },
  543. ],
  544. },
  545. {
  546. // * 属性设置
  547. label: this.$t('compute.text_356'),
  548. submenus: [
  549. // 修改属性
  550. {
  551. label: this.$t('compute.text_247'),
  552. permission: 'server_update',
  553. action: () => {
  554. this.createDialog('VmUpdateDialog', {
  555. data: this.list.selectedItems,
  556. columns: this.columns,
  557. onManager: this.onManager,
  558. })
  559. },
  560. meta: (row) => {
  561. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  562. if (!rescueModeValid.validate) return rescueModeValid
  563. const isOneCloud = this.list.selectedItems.every(item => item.brand === 'OneCloud')
  564. return {
  565. validate: isOneCloud,
  566. tooltip: !isOneCloud && this.$t('compute.text_355'),
  567. }
  568. },
  569. hidden: () => !hasSetupKey(['onecloud']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_update'),
  570. },
  571. // 更改项目
  572. {
  573. label: this.$t('compute.perform_change_owner', [this.$t('dictionary.project')]),
  574. permission: 'server_perform_change_owner',
  575. action: () => {
  576. this.createDialog('ChangeOwenrDialog', {
  577. data: this.list.selectedItems,
  578. columns: this.columns,
  579. onManager: this.onManager,
  580. name: this.$t('dictionary.server'),
  581. resource: 'servers',
  582. })
  583. },
  584. meta: () => {
  585. let ret = {
  586. validate: true,
  587. tooltip: null,
  588. }
  589. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  590. if (!rescueModeValid.validate) return rescueModeValid
  591. if (!this.isAdminMode && !this.isDomainMode) {
  592. ret.validate = false
  593. ret.tooltip = `仅系统或${this.$t('dictionary.domain')}管理员支持该操作`
  594. return ret
  595. }
  596. // 某些云不支持
  597. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems, [typeClouds.hypervisorMap.zettakit.key, typeClouds.hypervisorMap.uis.key])
  598. if (!unenableCloudCheck.validate) {
  599. ret = unenableCloudCheck
  600. return ret
  601. }
  602. const domains = this.list.selectedItems.map(item => item.domain_id)
  603. if (R.uniq(domains).length !== 1) {
  604. ret.validate = false
  605. ret.tooltip = this.$t('compute.text_280', [this.$t('dictionary.domain')])
  606. return ret
  607. }
  608. return ret
  609. },
  610. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_change_owner'),
  611. },
  612. // 到期释放
  613. {
  614. label: this.$t('compute.text_1132'),
  615. permission: 'server_perform_cancel_expire',
  616. action: () => {
  617. this.createDialog('SetDurationDialog', {
  618. data: this.list.selectedItems,
  619. columns: this.columns,
  620. onManager: this.onManager,
  621. refresh: this.refresh,
  622. alert: this.$t('compute.text_1391'),
  623. })
  624. },
  625. meta: () => {
  626. let ret = {
  627. validate: false,
  628. tooltip: null,
  629. }
  630. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  631. if (!rescueModeValid.validate) return rescueModeValid
  632. // 包年包月机器,不支持此操作
  633. const isSomePrepaid = this.list.selectedItems.some((item) => {
  634. return item.billing_type === 'prepaid'
  635. })
  636. if (isSomePrepaid) {
  637. ret.tooltip = this.$t('compute.text_285')
  638. return ret
  639. }
  640. // 某些云不支持
  641. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems, [typeClouds.hypervisorMap.bingocloud.key, typeClouds.hypervisorMap.sangfor.key, typeClouds.hypervisorMap.zettakit.key, typeClouds.hypervisorMap.uis.key])
  642. if (!unenableCloudCheck.validate) {
  643. ret = unenableCloudCheck
  644. return ret
  645. }
  646. // 暂只支持同时操作已设置到期或未设置到期释放的机器
  647. const isSomeExpired = this.list.selectedItems.some((item) => {
  648. return item.release_at
  649. })
  650. const isSomeNotExpired = this.list.selectedItems.some((item) => {
  651. return !item.release_at
  652. })
  653. if (isSomeExpired && isSomeNotExpired) {
  654. ret.tooltip = this.$t('compute.text_1133')
  655. return ret
  656. }
  657. ret.validate = true
  658. return ret
  659. },
  660. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_cancel_expire'),
  661. },
  662. // 主机克隆
  663. {
  664. label: this.$t('compute.text_1208'),
  665. permission: 'server_perform_snapshot_and_clone',
  666. action: () => {
  667. this.$openNewWindowForMenuHook('vminstance_configured_callback_address.host_clone_callback_address', () => {
  668. this.createDialog('VmCloneDeepDialog', {
  669. data: this.list.selectedItems,
  670. columns: this.columns,
  671. onManager: this.onManager,
  672. refresh: this.refresh,
  673. })
  674. })
  675. },
  676. meta: () => {
  677. const ret = {
  678. validate: true,
  679. tooltip: null,
  680. }
  681. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  682. if (!rescueModeValid.validate) return rescueModeValid
  683. for (const obj of this.list.selectedItems) {
  684. if (obj.brand !== BRAND_MAP.OneCloud.key) {
  685. ret.validate = false
  686. ret.tooltip = this.$t('compute.text_355')
  687. break
  688. }
  689. if (!['running', 'ready'].includes(obj.status)) {
  690. ret.validate = false
  691. ret.tooltip = this.$t('compute.text_1126')
  692. break
  693. }
  694. if (obj.backup_host_id) {
  695. ret.validate = false
  696. ret.tooltip = this.$t('compute.text_1283')
  697. break
  698. }
  699. }
  700. return ret
  701. },
  702. hidden: () => !hasSetupKey(['onecloud']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_clone'),
  703. },
  704. // 续费
  705. {
  706. label: this.$t('compute.text_1117'),
  707. permission: 'server_perform_renew',
  708. action: () => {
  709. this.createDialog('VmResourceFeeDialog', {
  710. data: this.list.selectedItems,
  711. columns: this.columns,
  712. onManager: this.onManager,
  713. })
  714. },
  715. meta: () => {
  716. const ret = {
  717. validate: true,
  718. tooltip: null,
  719. }
  720. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  721. if (!rescueModeValid.validate) return rescueModeValid
  722. const isAllPublic = this.list.selectedItems.every(item => findPlatform(item.hypervisor) === SERVER_TYPE.public)
  723. const isAllPrepaid = this.list.selectedItems.every(item => item.billing_type === 'prepaid')
  724. if (!isAllPublic) {
  725. ret.validate = false
  726. ret.tooltip = this.$t('compute.text_1118')
  727. }
  728. if (!isAllPrepaid) {
  729. ret.validate = false
  730. ret.tooltip = this.$t('compute.text_1119')
  731. }
  732. return ret
  733. },
  734. hidden: () => !hasSetupKey(['aliyun', 'qcloud', 'huawei', 'ucloud', 'ecloud', 'jdcloud', 'ctyun']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_Renew'),
  735. },
  736. // 自动续费设置
  737. {
  738. label: this.$t('compute.text_1120'),
  739. permission: 'server_perform_aet_auto_renew',
  740. action: () => {
  741. this.createDialog('VmResourceRenewFeeDialog', {
  742. data: this.list.selectedItems,
  743. columns: this.columns,
  744. onManager: this.onManager,
  745. refresh: this.refresh,
  746. })
  747. },
  748. meta: () => {
  749. const ret = {
  750. validate: true,
  751. tooltip: null,
  752. }
  753. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  754. if (!rescueModeValid.validate) return rescueModeValid
  755. const isAllPublic = this.list.selectedItems.every(item => findPlatform(item.hypervisor) === SERVER_TYPE.public)
  756. const isAllPrepaid = this.list.selectedItems.every(item => item.billing_type === 'prepaid')
  757. if (!isAllPublic) {
  758. ret.validate = false
  759. ret.tooltip = this.$t('compute.text_1118')
  760. }
  761. if (!isAllPrepaid) {
  762. ret.validate = false
  763. ret.tooltip = this.$t('compute.text_1119')
  764. }
  765. return ret
  766. },
  767. hidden: () => !(hasSetupKey(['aliyun', 'qcloud', 'huawei', 'ucloud', 'ecloud', 'jdcloud', 'ctyun'])) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_auto_renewal'),
  768. },
  769. // 更改计费模式
  770. {
  771. label: this.$t('compute.change_billing_type'),
  772. permission: 'server_perform_change_billing_type',
  773. action: () => {
  774. this.createDialog('VmChangeBillingTypeDialog', {
  775. steadyStatus: ['running', 'ready'],
  776. data: this.list.selectedItems,
  777. columns: this.columns,
  778. onManager: this.onManager,
  779. refresh: this.refresh,
  780. })
  781. },
  782. meta: () => {
  783. const ret = {
  784. validate: false,
  785. tooltip: null,
  786. }
  787. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  788. if (!rescueModeValid.validate) return rescueModeValid
  789. this.list.selectedItems.map(obj => {
  790. if (obj.brand !== BRAND_MAP.Aliyun.key && obj.brand !== BRAND_MAP.Qcloud.key) {
  791. ret.tooltip = this.$t('compute.text_473', [PROVIDER_MAP[obj.provider].label])
  792. }
  793. })
  794. if (!ret.tooltip) {
  795. ret.validate = true
  796. }
  797. return ret
  798. },
  799. hidden: () => !(hasSetupKey(['aliyun', 'qcloud'])) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_change_billing_type'),
  800. },
  801. // 编辑标签
  802. {
  803. label: this.$t('compute.text_283'),
  804. permission: 'server_perform_set_user_metadata',
  805. action: () => {
  806. this.createDialog('SetTagDialog', {
  807. data: this.list.selectedItems,
  808. columns: this.columns,
  809. onManager: this.onManager,
  810. params: {
  811. resources: 'server',
  812. },
  813. mode: 'add',
  814. })
  815. },
  816. meta: () => {
  817. let ret = { validate: true }
  818. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  819. if (!rescueModeValid.validate) return rescueModeValid
  820. // 某些云不支持
  821. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems)
  822. if (!unenableCloudCheck.validate) {
  823. ret = unenableCloudCheck
  824. return ret
  825. }
  826. return ret
  827. },
  828. },
  829. ],
  830. },
  831. {
  832. // * 配置修改
  833. label: this.$t('compute.group_action.update_config'),
  834. submenus: [
  835. // 重装系统
  836. {
  837. label: this.$t('compute.text_357'),
  838. permission: 'server_perform_rebuild_root',
  839. action: () => {
  840. this.createDialog('VmRebuildRootDialog', {
  841. data: this.list.selectedItems,
  842. columns: this.columns,
  843. onManager: this.onManager,
  844. })
  845. },
  846. meta: () => {
  847. const ret = {
  848. validate: true,
  849. tooltip: null,
  850. }
  851. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  852. if (!rescueModeValid.validate) return rescueModeValid
  853. const isSameStopCharging = this.list.selectedItems.some((item) => { return item.shutdown_mode === 'stop_charging' && item.status === 'ready' })
  854. if (isSameStopCharging) {
  855. ret.validate = false
  856. ret.tooltip = this.$t('compute.server.shutdown_mode.tooltip')
  857. return ret
  858. }
  859. // same cpu arch
  860. if (!this.isSameArch) {
  861. ret.validate = false
  862. ret.tooltip = this.$t('compute.vminstance.actions.adjust_config.cpu_arch.tips')
  863. return ret
  864. }
  865. if (this.isSameHyper) {
  866. ret.validate = cloudEnabled('rebuildRoot', this.list.selectedItems)
  867. ret.tooltip = cloudUnabledTip('rebuildRoot', this.list.selectedItems)
  868. return ret
  869. }
  870. return ret
  871. },
  872. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_rebuild_root'),
  873. },
  874. // 调整配置
  875. {
  876. label: this.$t('compute.text_1100'),
  877. permission: 'server_perform_change_config',
  878. action: () => {
  879. this.$openNewWindowForMenuHook('vminstance_configured_callback_address.adjust_configuration_callback_address', () => {
  880. this.$router.push({
  881. name: 'VMInstanceAdjustConfig',
  882. query: {
  883. id: this.list.selectedItems.map((item) => { return item.id }),
  884. },
  885. })
  886. })
  887. },
  888. meta: () => {
  889. const ret = {
  890. validate: true,
  891. tooltip: null,
  892. }
  893. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  894. if (!rescueModeValid.validate) return rescueModeValid
  895. if (this.isSameHyper) {
  896. const isSomeBackupHost = this.list.selectedItems.some((item) => { return item.backup_host_id })
  897. if (isSomeBackupHost) {
  898. ret.validate = false
  899. ret.tooltip = this.$t('compute.text_1111')
  900. return ret
  901. }
  902. ret.validate = cloudEnabled('adjustConfig', this.list.selectedItems)
  903. ret.tooltip = cloudUnabledTip('adjustConfig', this.list.selectedItems)
  904. // const googleItems = this.list.selectedItems.filter(val => val.brand === typeClouds.brandMap.Google.key)
  905. // if (googleItems && googleItems.length) { // 谷歌云 windows 仅支持关机下操作,linux 支持 开关机
  906. // ret.validate = googleItems.every(val => {
  907. // const os = val.os_type.toLowerCase()
  908. // if (os.includes('windows')) {
  909. // return val.status === 'ready'
  910. // }
  911. // if (os.includes('linux')) {
  912. // return val.status === 'ready' || val.status === 'running'
  913. // }
  914. // return true
  915. // })
  916. // if (!ret.validate) {
  917. // ret.tooltip = '谷歌云Windows虚拟机仅支持关机下操作,Linux虚拟机支持开机和关机下操作'
  918. // }
  919. // }
  920. // same cpu arch
  921. if (!this.isSameArch) {
  922. ret.validate = false
  923. ret.tooltip = this.$t('compute.vminstance.actions.adjust_config.cpu_arch.tips')
  924. return ret
  925. }
  926. const isSameStopCharging = this.list.selectedItems.some((item) => { return item.shutdown_mode === 'stop_charging' && item.status === 'ready' })
  927. if (isSameStopCharging) {
  928. ret.validate = false
  929. ret.tooltip = this.$t('compute.server.shutdown_mode.tooltip')
  930. return ret
  931. }
  932. return ret
  933. }
  934. ret.validate = false
  935. ret.tooltip = this.$t('compute.text_278')
  936. return ret
  937. },
  938. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_change_config'),
  939. },
  940. // 设置透传设备
  941. {
  942. label: this.$t('compute.text_1112'),
  943. permission: 'attach-isolated-device,server_perform_detach_isolated_device,server_perform_set_isolated_device',
  944. action: () => {
  945. this.createDialog('VmAttachGpuDialog', {
  946. data: this.list.selectedItems,
  947. columns: this.columns,
  948. onManager: this.onManager,
  949. })
  950. },
  951. meta: () => {
  952. let ret = {
  953. validate: true,
  954. tooltip: null,
  955. }
  956. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  957. if (!rescueModeValid.validate) return rescueModeValid
  958. const isAllIdc = this.list.selectedItems.every((item) => {
  959. return findPlatform(item.hypervisor, 'hypervisor') === SERVER_TYPE.idc
  960. })
  961. const isOk = this.list.selectedItems.every((item) => { return ['running', 'ready'].includes(item.status) })
  962. if (!isOk) {
  963. ret.validate = false
  964. ret.tooltip = this.$t('compute.text_1126')
  965. return ret
  966. }
  967. // 某些云不支持
  968. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems, [typeClouds.hypervisorMap.bingocloud.key, 'esxi', typeClouds.hypervisorMap.zettakit.key, typeClouds.hypervisorMap.uis.key])
  969. if (!isAllIdc) {
  970. ret.validate = false
  971. ret.tooltip = this.$t('compute.text_1115')
  972. return ret
  973. }
  974. if (!unenableCloudCheck.validate) {
  975. ret = unenableCloudCheck
  976. return ret
  977. }
  978. return ret
  979. },
  980. hidden: () => !hasSetupKey(['onecloud']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_set_gpu'),
  981. },
  982. ],
  983. },
  984. {
  985. // * 密码密钥
  986. label: this.$t('compute.text_360'),
  987. submenus: [
  988. // 重置密码
  989. {
  990. label: this.$t('compute.text_276'),
  991. permission: 'server_perform_set_password',
  992. action: () => {
  993. this.createDialog('VmResetPasswordDialog', {
  994. data: this.list.selectedItems,
  995. columns: this.columns,
  996. onManager: this.onManager,
  997. })
  998. },
  999. meta: () => {
  1000. const ret = {
  1001. validate: true,
  1002. tooltip: null,
  1003. }
  1004. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1005. if (!rescueModeValid.validate) return rescueModeValid
  1006. const isBindKeypair = this.list.selectedItems.some((item) => { return item.keypair_id && item.keypair_id.toLowerCase() !== 'none' })
  1007. if (isBindKeypair) {
  1008. ret.validate = false
  1009. ret.tooltip = this.$t('compute.text_277')
  1010. return ret
  1011. }
  1012. return {
  1013. validate: cloudEnabled('resetPassword', this.list.selectedItems),
  1014. tooltip: cloudUnabledTip('resetPassword', this.list.selectedItems),
  1015. }
  1016. },
  1017. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_deploy'),
  1018. },
  1019. // 设置免密登录
  1020. {
  1021. label: this.$t('compute.vminstance.actions.setup_ssh_authentication'),
  1022. permission: 'server_perform_setup_ssh_proxy',
  1023. action: () => {
  1024. this.createDialog('SetupSSHDialog', {
  1025. data: this.list.selectedItems,
  1026. columns: this.columns,
  1027. onManager: this.onManager,
  1028. })
  1029. },
  1030. meta: () => {
  1031. let ret = {
  1032. validate: true,
  1033. tooltip: null,
  1034. }
  1035. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1036. if (!rescueModeValid.validate) return rescueModeValid
  1037. // 某些云不支持
  1038. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems, [typeClouds.hypervisorMap.sangfor.key, typeClouds.hypervisorMap.zettakit.key, typeClouds.hypervisorMap.uis.key])
  1039. if (!unenableCloudCheck.validate) {
  1040. ret = unenableCloudCheck
  1041. return ret
  1042. }
  1043. if (this.list.selectedItems) {
  1044. const project = this.list.selectedItems[0].project || ''
  1045. const isSame = this.list.selectedItems.every((item) => {
  1046. return item.project === project
  1047. })
  1048. if (!isSame) {
  1049. ret.validate = false
  1050. ret.tooltip = this.$t('compute.vminstance.setup_ssh_authentication.group_action.project')
  1051. return ret
  1052. }
  1053. const isLinux = this.list.selectedItems.every((item) => {
  1054. return item.os_type && item.os_type.toLowerCase() === 'linux'
  1055. })
  1056. if (!isLinux) {
  1057. ret.validate = false
  1058. ret.tooltip = this.$t('compute.text_362')
  1059. return ret
  1060. }
  1061. }
  1062. for (const obj of this.list.selectedItems) {
  1063. if (!commonEnabled(obj, ['running'])) {
  1064. ret.validate = false
  1065. ret.tooltip = this.$t('db.text_156')
  1066. return ret
  1067. }
  1068. }
  1069. return ret
  1070. },
  1071. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_setup_ssh_proxy'),
  1072. },
  1073. // 探测免密登录
  1074. {
  1075. label: this.$t('compute.vminstance.actions.detect_ssh_authentication'),
  1076. permission: 'server_perform_make_sshable',
  1077. action: () => {
  1078. this.createDialog('DetectSSHDialog', {
  1079. data: this.list.selectedItems,
  1080. columns: this.columns,
  1081. onManager: this.onManager,
  1082. })
  1083. },
  1084. meta: () => {
  1085. let ret = {
  1086. validate: true,
  1087. tooltip: null,
  1088. }
  1089. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1090. if (!rescueModeValid.validate) return rescueModeValid
  1091. // 某些云不支持
  1092. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems, [typeClouds.hypervisorMap.sangfor.key, typeClouds.hypervisorMap.zettakit.key, typeClouds.hypervisorMap.uis.key])
  1093. if (!unenableCloudCheck.validate) {
  1094. ret = unenableCloudCheck
  1095. return ret
  1096. }
  1097. for (const obj of this.list.selectedItems) {
  1098. if (!commonEnabled(obj, ['running'])) {
  1099. ret.validate = false
  1100. ret.tooltip = this.$t('db.text_156')
  1101. return ret
  1102. }
  1103. }
  1104. return ret
  1105. },
  1106. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_detect_ssh_proxy'),
  1107. },
  1108. ],
  1109. },
  1110. {
  1111. // * 网络安全
  1112. label: this.$t('compute.text_1290'),
  1113. submenus: [
  1114. // 关联安全组
  1115. // {
  1116. // label: this.$t('compute.text_1116'),
  1117. // permission: 'server_perform_add_secgroup',
  1118. // action: () => {
  1119. // this.createDialog('VmSetSecgroupDialog', {
  1120. // vm: this,
  1121. // data: this.list.selectedItems,
  1122. // columns: this.columns,
  1123. // onManager: this.onManager,
  1124. // })
  1125. // },
  1126. // meta: () => {
  1127. // const ret = {
  1128. // validate: cloudEnabled('assignSecgroup', this.list.selectedItems),
  1129. // tooltip: cloudUnabledTip('assignSecgroup', this.list.selectedItems),
  1130. // }
  1131. // return ret
  1132. // },
  1133. // hidden: () => !(hasSetupKey(['onestack', 'onecloud', 'public', 'openstack', 'dstack', 'zstack', 'apsara', 'cloudpods', 'hcso', 'hcs'])) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_add_secgroup'),
  1134. // },
  1135. // 公网IP转EIP
  1136. {
  1137. label: this.$t('compute.text_1121'),
  1138. permission: 'server_perform_publicip_to_eip',
  1139. action: () => {
  1140. this.createDialog('VmPublicIpToEipDialog', {
  1141. data: this.list.selectedItems,
  1142. columns: this.columns,
  1143. onManager: this.onManager,
  1144. })
  1145. },
  1146. meta: () => {
  1147. const ret = {
  1148. validate: false,
  1149. tooltip: null,
  1150. }
  1151. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1152. if (!rescueModeValid.validate) return rescueModeValid
  1153. const isSomeBindEip = this.list.selectedItems.some((item) => { return item.eip && item.eip_mode === 'elastic_ip' })
  1154. const isAllBindPublicIp = this.list.selectedItems.every((item) => { return item.eip_mode === 'public_ip' })
  1155. if (isSomeBindEip) {
  1156. ret.validate = false
  1157. ret.tooltip = this.$t('compute.text_1122')
  1158. return ret
  1159. }
  1160. if (!isAllBindPublicIp) {
  1161. ret.validate = false
  1162. ret.tooltip = this.$t('compute.text_1123')
  1163. return ret
  1164. }
  1165. ret.validate = cloudEnabled('publicIpToEip', this.list.selectedItems)
  1166. ret.tooltip = cloudUnabledTip('publicIpToEip', this.list.selectedItems)
  1167. return ret
  1168. },
  1169. hidden: () => !(hasSetupKey(['aliyun', 'qcloud'])) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_public_ip_to_eip'),
  1170. },
  1171. // 设置源/目标检查
  1172. {
  1173. label: this.$t('compute.text_1124'),
  1174. permission: 'server_perform_modify_src_check',
  1175. action: () => {
  1176. this.createDialog('VmSourceTargetCheckDialog', {
  1177. data: this.list.selectedItems,
  1178. columns: this.columns,
  1179. onManager: this.onManager,
  1180. refresh: this.refresh,
  1181. })
  1182. },
  1183. meta: () => {
  1184. const ret = { validate: true, tooltip: null }
  1185. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1186. if (!rescueModeValid.validate) return rescueModeValid
  1187. const isAllOneCloud = this.list.selectedItems.every((item) => { return item.brand === BRAND_MAP.OneCloud.key })
  1188. const isOk = this.list.selectedItems.every((item) => { return ['running', 'ready'].includes(item.status) })
  1189. if (!isAllOneCloud) {
  1190. ret.validate = false
  1191. ret.tooltip = this.$t('compute.text_1125')
  1192. return ret
  1193. }
  1194. if (!isOk) {
  1195. ret.validate = false
  1196. ret.tooltip = this.$t('compute.text_1126')
  1197. return ret
  1198. }
  1199. return ret
  1200. },
  1201. hidden: () => !(hasSetupKey(['onecloud'])) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_set_source_check'),
  1202. },
  1203. // 添加到堡垒机
  1204. {
  1205. label: this.$t('compute.add_to_bastion'),
  1206. permission: 'bastion_servers_create',
  1207. action: () => {
  1208. this.createDialog('VmAddToBastionDialog', {
  1209. data: this.list.selectedItems,
  1210. columns: this.columns,
  1211. onManager: this.onManager,
  1212. refresh: this.refresh,
  1213. })
  1214. },
  1215. meta: () => {
  1216. const ret = { validate: true }
  1217. const isSomeBastionServer = this.list.selectedItems.some((item) => { return item.metadata?.bastion_server })
  1218. if (isSomeBastionServer) {
  1219. ret.validate = false
  1220. ret.tooltip = this.$t('compute.already_in_bastion')
  1221. return ret
  1222. }
  1223. const isEveryRunning = this.list.selectedItems.every((item) => { return item.status === 'running' })
  1224. if (!isEveryRunning) {
  1225. ret.validate = false
  1226. ret.tooltip = this.$t('compute.text_1282')
  1227. return ret
  1228. }
  1229. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1230. return rescueModeValid
  1231. },
  1232. hidden: () => {
  1233. if (!this.$appConfig.isPrivate || this.$store.getters.isSysCE || this.$isScopedPolicyMenuHidden('sub_hidden_menus.bastion_host') || (isLicense2() && !hasSetupKey('bastionhost')) || !hasBastionService) {
  1234. return true
  1235. }
  1236. return false
  1237. },
  1238. },
  1239. // 从堡垒机移除
  1240. {
  1241. label: this.$t('compute.remove_from_bastion'),
  1242. permission: 'bastion_servers_delete',
  1243. action: () => {
  1244. this.createDialog('VmRemoveFromBastionDialog', {
  1245. data: this.list.selectedItems,
  1246. columns: this.columns,
  1247. onManager: this.onManager,
  1248. refresh: this.refresh,
  1249. })
  1250. },
  1251. meta: () => {
  1252. const ret = { validate: true }
  1253. const isEveryBastionServer = this.list.selectedItems.every((item) => { return item.metadata?.bastion_server })
  1254. if (!isEveryBastionServer) {
  1255. ret.validate = false
  1256. return ret
  1257. }
  1258. const isEveryRunning = this.list.selectedItems.every((item) => { return item.status === 'running' || item.status === 'ready' })
  1259. if (!isEveryRunning) {
  1260. ret.validate = false
  1261. ret.tooltip = this.$t('compute.text_1126')
  1262. return ret
  1263. }
  1264. return ret
  1265. },
  1266. hidden: () => {
  1267. if (!this.$appConfig.isPrivate || this.$store.getters.isSysCE || this.$isScopedPolicyMenuHidden('sub_hidden_menus.bastion_host') || (isLicense2() && !hasSetupKey('bastionhost')) || !hasBastionService) {
  1268. return true
  1269. }
  1270. return false
  1271. },
  1272. },
  1273. ],
  1274. },
  1275. {
  1276. // * 高可用
  1277. label: this.$t('compute.text_1295'),
  1278. submenus: [
  1279. // 迁移
  1280. {
  1281. label: this.$t('compute.text_1127'),
  1282. permission: 'server_perform_migrate,server_perform_live_migrate',
  1283. action: () => {
  1284. this.createDialog('VmTransferDialog', {
  1285. data: this.list.selectedItems,
  1286. columns: this.columns,
  1287. onManager: this.onManager,
  1288. })
  1289. },
  1290. meta: () => {
  1291. const ret = { validate: true, tooltip: null }
  1292. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1293. if (!rescueModeValid.validate) return rescueModeValid
  1294. // 运行中、关机、状态未知
  1295. const statusSet = new Set()
  1296. const brandSet = new Set()
  1297. this.list.selectedItems.forEach(item => {
  1298. statusSet.add(item.status)
  1299. brandSet.add(item.brand)
  1300. })
  1301. if (!this.isAdminMode && !this.isDomainMode) {
  1302. ret.validate = false
  1303. ret.tooltip = this.$t('compute.tooltip.check_domain_permission')
  1304. return ret
  1305. }
  1306. if (statusSet.size > 1) {
  1307. ret.validate = false
  1308. ret.tooltip = this.$t('compute.tooltip.check_some_status_transfer')
  1309. return ret
  1310. }
  1311. const isStatusOk = [...statusSet].every((status) => {
  1312. return ['running', 'ready'].includes(status)
  1313. })
  1314. if (!isStatusOk) {
  1315. ret.tooltip = this.$t('compute.tooltip.check_status_transfer')
  1316. ret.validate = false
  1317. return ret
  1318. }
  1319. const isBackupHost = this.list.selectedItems.some((item) => item.backup_host_id)
  1320. if (isBackupHost) {
  1321. ret.tooltip = this.$t('compute.tooltip.check_backup_host_transfer')
  1322. ret.validate = false
  1323. return ret
  1324. }
  1325. const isGpu = this.list.selectedItems.some((item) => item.is_gpu)
  1326. if (isGpu) {
  1327. ret.tooltip = this.$t('compute.tooltip.check_gpu_transfer')
  1328. ret.validate = false
  1329. return ret
  1330. }
  1331. const isCdrom = this.list.selectedItems.some((item) => item.cdrom)
  1332. if (isCdrom) {
  1333. ret.tooltip = this.$t('compute.tooltip.check_cdrom_transfer')
  1334. ret.validate = false
  1335. return ret
  1336. }
  1337. if (brandSet.size > 1) {
  1338. ret.validate = false
  1339. ret.tooltip = this.$t('compute.v2vtransfer.same_brand')
  1340. return ret
  1341. }
  1342. ret.validate = cloudEnabled('transfer', this.list.selectedItems)
  1343. ret.tooltip = cloudUnabledTip('transfer', this.list.selectedItems)
  1344. return ret
  1345. },
  1346. hidden: () => !(hasSetupKey(['onecloud', 'openstack', 'vmware'])) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_transfer'),
  1347. },
  1348. // V2V迁移
  1349. {
  1350. label: this.$t('compute.v2vtransfer.label'),
  1351. permission: 'server_perform_migrate',
  1352. action: () => {
  1353. this.createDialog('VmV2vTransferDialog', {
  1354. data: this.list.selectedItems,
  1355. columns: this.columns,
  1356. onManager: this.onManager,
  1357. successCallback: this.refresh,
  1358. })
  1359. },
  1360. meta: () => {
  1361. const ret = { validate: true, tooltip: null }
  1362. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1363. if (!rescueModeValid.validate) return rescueModeValid
  1364. const statusSet = new Set()
  1365. const brandSet = new Set()
  1366. const backupHostSet = new Set()
  1367. let isIpEmpty = false
  1368. if (!this.isAdminMode && !this.isDomainMode) {
  1369. ret.validate = false
  1370. ret.tooltip = this.$t('compute.tooltip.check_domain_permission')
  1371. return ret
  1372. }
  1373. this.list.selectedItems.forEach((item) => {
  1374. statusSet.add(item.status)
  1375. brandSet.add(item.brand)
  1376. if (item.backup_host_id) {
  1377. backupHostSet.add(item.backup_host_id)
  1378. }
  1379. if (!item.ips) {
  1380. isIpEmpty = true
  1381. }
  1382. })
  1383. if (statusSet.size > 1 || !statusSet.has('ready')) {
  1384. ret.tooltip = this.$t('compute.tooltip.check_ready_status_transfer')
  1385. ret.validate = false
  1386. return ret
  1387. }
  1388. if (brandSet.size > 1) {
  1389. ret.tooltip = this.$t('compute.v2vtransfer.same_brand')
  1390. ret.validate = false
  1391. return ret
  1392. } else {
  1393. if (!brandSet.has(BRAND_MAP.Cloudpods.key) &&
  1394. !brandSet.has(BRAND_MAP.VMware.key)) {
  1395. ret.tooltip = this.$t('compute.brand_support', [`${BRAND_MAP.Cloudpods.label},${BRAND_MAP.VMware.label}`])
  1396. ret.validate = false
  1397. return ret
  1398. }
  1399. }
  1400. if (backupHostSet.size > 0) {
  1401. ret.tooltip = this.$t('compute.tooltip.check_backup_host_transfer')
  1402. ret.validate = false
  1403. return ret
  1404. }
  1405. if (isIpEmpty) {
  1406. ret.tooltip = this.$t('compute.fill_ips_tooltip')
  1407. ret.validate = false
  1408. return ret
  1409. }
  1410. ret.validate = cloudEnabled('v2vTransfer', this.list.selectedItems)
  1411. ret.tooltip = cloudUnabledTip('v2vTransfer', this.list.selectedItems)
  1412. return ret
  1413. },
  1414. hidden: () => !(hasSetupKey(['vmware', 'cloudpods'])) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_transfer'),
  1415. },
  1416. // 快速恢复
  1417. {
  1418. label: this.$t('compute.server.quick.recovery'),
  1419. action: () => {
  1420. this.createDialog('VmQuickRecoveryDialog', {
  1421. data: this.list.selectedItems,
  1422. columns: this.columns,
  1423. onManager: this.onManager,
  1424. })
  1425. },
  1426. meta: () => {
  1427. const ret = {
  1428. validate: true,
  1429. tooltip: '',
  1430. }
  1431. const rescueModeValid = validateRescueMode(this.list.selectedItems)
  1432. if (!rescueModeValid.validate) return rescueModeValid
  1433. const isAllOneCloud = this.list.selectedItems.every((item) => { return item.brand === BRAND_MAP.OneCloud.key })
  1434. if (!isAllOneCloud) {
  1435. ret.validate = false
  1436. ret.tooltip = this.$t('compute.text_1125')
  1437. return ret
  1438. }
  1439. const isAllHostOffline = this.list.selectedItems.every((item) => { return item.host_service_status === 'offline' })
  1440. if (!isAllHostOffline) {
  1441. ret.validate = false
  1442. ret.tooltip = this.$t('compute.quick.recovery.validate.host_status_tooltip')
  1443. return ret
  1444. }
  1445. const allStorages = R.flatten(this.list.selectedItems.map(item => item.disks_info || []))
  1446. const isAllKVMShareStorages = allStorages.every(item => KVM_SHARE_STORAGES.includes(item.storage_type))
  1447. if (!isAllKVMShareStorages) {
  1448. ret.validate = false
  1449. ret.tooltip = this.$t('compute.quick.recovery.validate.host_status_tooltip')
  1450. return ret
  1451. }
  1452. return ret
  1453. },
  1454. hidden: () => !hasSetupKey(['onecloud']) || this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_migrate'),
  1455. },
  1456. ],
  1457. },
  1458. {
  1459. // * 删除
  1460. label: this.$t('compute.perform_delete'),
  1461. submenus: [
  1462. // 设置删除保护
  1463. disableDeleteAction(Object.assign(this, {
  1464. permission: 'server_update',
  1465. }), {
  1466. name: this.$t('dictionary.server'),
  1467. meta: () => {
  1468. // 某些云不支持
  1469. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems)
  1470. return unenableCloudCheck
  1471. },
  1472. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_set_delete_protection'),
  1473. }),
  1474. // 删除
  1475. {
  1476. label: this.$t('compute.perform_delete'),
  1477. permission: 'server_delete',
  1478. action: () => {
  1479. this.$openNewWindowForMenuHook('vminstance_configured_callback_address.delete_callback_address', () => {
  1480. this.createDialog('DeleteVmDialog', {
  1481. vm: this,
  1482. data: this.list.selectedItems,
  1483. columns: this.columns,
  1484. onManager: this.onManager,
  1485. title: this.$t('compute.perform_delete'),
  1486. })
  1487. })
  1488. },
  1489. meta: () => {
  1490. const unenableCloudCheck = this.hasSomeCloud(this.list.selectedItems)
  1491. if (!unenableCloudCheck.validate) {
  1492. return unenableCloudCheck
  1493. }
  1494. const isHasRunning = this.list.selectedItems.some((item) => item.status === 'running')
  1495. const { server_delete_limit = false } = this.$store.getters.globalSetting.value || {}
  1496. if (server_delete_limit && isHasRunning) {
  1497. return {
  1498. validate: false,
  1499. tooltip: this.$t('compute.delete_limit'),
  1500. }
  1501. }
  1502. return this.$getDeleteResult(this.list.selectedItems)
  1503. },
  1504. hidden: () => this.$isScopedPolicyMenuHidden('vminstance_hidden_menus.server_perform_delete'),
  1505. },
  1506. ],
  1507. },
  1508. ]
  1509. },
  1510. meta: () => {
  1511. let ret = {
  1512. validate: true,
  1513. tooltip: null,
  1514. }
  1515. ret.validate = this.list.selectedItems.length > 0
  1516. if (!ret.validate) return ret
  1517. ret = this.$isValidateResourceLock(this.list.selectedItems)
  1518. return ret
  1519. },
  1520. },
  1521. ],
  1522. execLoading: false,
  1523. tagConfigParams: {
  1524. title: this.$t('common.text00124'),
  1525. resource: 'servers',
  1526. queryTreeId: 'project-tag-value-tree',
  1527. },
  1528. }
  1529. },
  1530. computed: {
  1531. ...mapGetters(['isAdminMode']),
  1532. isSameHyper () {
  1533. if (this.list.selectedItems.length > 0) {
  1534. const arr = this.list.selectedItems.map(v => v.hypervisor)
  1535. const noRepeatArr = Array.from(new Set(arr))
  1536. return noRepeatArr.length === 1
  1537. }
  1538. return true
  1539. },
  1540. isSameArch () {
  1541. if (this.list.selectedItems[0] && (this.list.selectedItems[0].hypervisor.toLowerCase() === 'hcso' || this.list.selectedItems[0].hypervisor.toLowerCase() === 'hcs')) {
  1542. const instancetype = this.list.selectedItems[0].instance_type || ''
  1543. const isArm = instancetype.startsWith('k')
  1544. return this.list.selectedItems.every(item => item.instance_type && item.instance_type.startsWith('k') === isArm)
  1545. }
  1546. return true
  1547. },
  1548. showActions () {
  1549. return !this.$isScopedPolicyMenuHidden('server_hidden_columns.perform_action')
  1550. },
  1551. exportDataOptions () {
  1552. const ret = {
  1553. isOpenExportUsernamePassword: false,
  1554. downloadType: 'local',
  1555. items: [
  1556. { label: 'ID', key: 'id' },
  1557. { label: this.$t('table.title.external_id'), key: 'external_id' },
  1558. ],
  1559. fixedItems: [
  1560. { key: 'metadata.os_distribution', label: this.$t('table.title.os') },
  1561. { key: 'disk', label: this.$t('table.title.disk') + '(M)' },
  1562. { key: 'vmem_size', label: this.$t('table.title.vmem_size') + '(M)' },
  1563. { key: 'eip', title: this.$t('common.eip') },
  1564. { key: 'ips', title: 'IP' },
  1565. { key: 'is_gpu', title: `${this.$t('table.title.type')}(${this.$t('compute.text_113')}${this.$t('dictionary.server')})` },
  1566. ],
  1567. hiddenFields: ['os_dist', 'elastic_ip', 'ip'],
  1568. async beforeExport () {
  1569. const checkOpenExportUsernamePassword = async () => {
  1570. try {
  1571. const configM = new Manager('services', 'v1')
  1572. const servicesRes = await configM.list({ params: { type: 'compute_v2' } })
  1573. const serviceId = servicesRes.data.data?.[0]?.id
  1574. const configData = await configM.getSpecific({ id: serviceId, spec: 'config' })
  1575. const { enable_export_username_password = false } = configData.data?.config?.default
  1576. ret.isOpenExportUsernamePassword = enable_export_username_password
  1577. return enable_export_username_password
  1578. } catch (error) {
  1579. console.log('fetch compute_v2 service config error!!!')
  1580. }
  1581. }
  1582. const isOpenExportUsernamePassword = await checkOpenExportUsernamePassword()
  1583. ret.isOpenExportUsernamePassword = isOpenExportUsernamePassword
  1584. ret.items.forEach(item => {
  1585. if (item.field === 'extra_user') {
  1586. item.hidden = !isOpenExportUsernamePassword
  1587. }
  1588. if (item.field === 'extra_password') {
  1589. item.hidden = !isOpenExportUsernamePassword
  1590. }
  1591. })
  1592. },
  1593. async callback (data, selectedExportKeys) {
  1594. if (!ret.isOpenExportUsernamePassword) {
  1595. return data
  1596. }
  1597. if (!selectedExportKeys.includes('extra_user') && !selectedExportKeys.includes('extra_password')) {
  1598. return data
  1599. }
  1600. const manager = new Manager('servers')
  1601. const allPromise = data.map(async item => {
  1602. let username = ''
  1603. let password = ''
  1604. try {
  1605. const { data } = await manager.objectRpc({
  1606. methodname: 'GetLoginInfo',
  1607. objId: item.id,
  1608. })
  1609. username = data.username
  1610. password = data.password
  1611. } catch (error) {
  1612. console.log(`server: ${item.id}, login info fetch error!!!`)
  1613. }
  1614. return Promise.resolve({ id: item.id, username, password })
  1615. })
  1616. const results = Promise.all(allPromise).then(values => {
  1617. const realData = data.map(item => {
  1618. const curObj = values.find(v => v.id === item.id)
  1619. return {
  1620. ...item,
  1621. extra_user: curObj.username,
  1622. extra_password: curObj.password,
  1623. }
  1624. })
  1625. return realData
  1626. })
  1627. return results
  1628. },
  1629. }
  1630. this.columns.map(col => {
  1631. if (col.field === 'ips') {
  1632. ret.items.push(getIpsTableColumn({ field: 'elastic_ip', title: this.$t('common.eip'), vm: this, onlyElastic: true }))
  1633. ret.items.push(getIpsTableColumn({ field: 'ip', title: 'IP', vm: this, noElastic: true }))
  1634. } else if (!(col.hidden && col.hidden()) && col.field !== 'password') {
  1635. const item = {
  1636. field: col.field,
  1637. title: col.title || col.label,
  1638. formatter: col.formatter,
  1639. }
  1640. if (col.field === 'vmem_size') {
  1641. item.title = `${item.title}(G)`
  1642. item.formatter = ({ row }) => {
  1643. if (row.vmem_size) {
  1644. const config = (row.vmem_size / 1024)
  1645. return config
  1646. }
  1647. return ''
  1648. }
  1649. }
  1650. if (col.field === 'disk') {
  1651. item.title = `${item.title}(G)`
  1652. item.formatter = ({ row }) => {
  1653. if (row.disk) {
  1654. const size = sizeToDesignatedUnit(row.disk, 'M', 'G', 1024, false, 2)
  1655. return size
  1656. }
  1657. return ''
  1658. }
  1659. }
  1660. ret.items.push(item)
  1661. } else if (col.field === 'password') {
  1662. ret.items.push({ field: 'extra_user', title: this.$t('compute.text_566') })
  1663. ret.items.push({ field: 'extra_password', title: this.$t('common_328') })
  1664. }
  1665. if (col.field === 'region') {
  1666. ret.items.push({ field: 'zone', title: this.$t('compute.text_270') })
  1667. }
  1668. })
  1669. ret.items.push({
  1670. field: 'expired_at',
  1671. title: this.$t('scope.text_791'),
  1672. formatter: ({ row }) => {
  1673. if (!row.expired_at) return '-'
  1674. return this.$moment(row.expired_at).format()
  1675. },
  1676. })
  1677. return ret
  1678. },
  1679. },
  1680. watch: {
  1681. cloudEnv (val) {
  1682. this.$nextTick(() => {
  1683. this.list.fetchData(0)
  1684. })
  1685. },
  1686. },
  1687. created () {
  1688. this.initSidePageTab('vm-instance-detail')
  1689. this.list.fetchData().then(() => {
  1690. this.$nextTick(() => {
  1691. if (this.$route.query.id && this.list.data[this.$route.query.id]) {
  1692. this.handleOpenSidepage(this.list.data[this.$route.query.id].data)
  1693. }
  1694. })
  1695. })
  1696. this.$bus.$on('VMInstanceListSingleUpdate', args => {
  1697. this.list.singleUpdate(...args)
  1698. }, this)
  1699. this.$bus.$on('VMInstanceListSingleRefresh', args => {
  1700. this.list.singleRefresh(...args)
  1701. }, this)
  1702. // this.$bus.$on('VMInstanceListRefresh', args => {
  1703. // this.list.resetRefresh()
  1704. // }, this)
  1705. },
  1706. methods: {
  1707. getParam () {
  1708. const ret = {
  1709. details: true,
  1710. with_meta: true,
  1711. filter: 'hypervisor.notin(baremetal,container,pod)',
  1712. ...this.getParams,
  1713. }
  1714. if (this.cloudEnv) ret.cloud_env = this.cloudEnv
  1715. return ret
  1716. },
  1717. handleOpenSidepage (row, tab) {
  1718. this.sidePageTriggerHandle(this, 'VmInstanceSidePage', {
  1719. id: row.id,
  1720. resource: 'servers',
  1721. getParams: this.getParam,
  1722. steadyStatus: Object.values(expectStatus.server).flat(),
  1723. }, {
  1724. list: this.list,
  1725. tab,
  1726. })
  1727. },
  1728. beforeShowMenu () {
  1729. return this.$store.dispatch('scopedPolicy/get', {
  1730. category: ['vminstance_hidden_menus', 'vminstance_configured_callback_address'],
  1731. })
  1732. },
  1733. defaultSearchKey (search) {
  1734. if (regexp.isIPv4(search)) {
  1735. return 'ip_addr'
  1736. }
  1737. },
  1738. handleListRefresh () {
  1739. this.list.refresh()
  1740. // 新建按钮无法点击时,刷新云资源情况
  1741. this.cloudEnvEmpty && this.$store.dispatch('auth/getCapabilities')
  1742. },
  1743. hasSomeCloud (selectItems, clouds = [typeClouds.hypervisorMap.bingocloud.key]) {
  1744. const ret = { validate: true, tooltip: '' }
  1745. const hasList = selectItems.filter(item => clouds.includes(item.hypervisor))
  1746. if (hasList && hasList[0]) {
  1747. ret.validate = false
  1748. ret.tooltip = this.$t('compute.text_473', [typeClouds.hypervisorMap[hasList[0].hypervisor].label])
  1749. }
  1750. return ret
  1751. },
  1752. },
  1753. }
  1754. </script>