mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
This PR adds the capability in CloudStack to convert VMware Instances disk(s) to KVM using virt-v2v and import them as CloudStack instances. It enables CloudStack operators to import VMware instances from vSphere into a KVM cluster managed by CloudStack. vSphere/VMware setup might be managed by CloudStack or be a standalone setup.
CloudStack will let the administrator select a VM from an existing VMware vCenter in the CloudStack environment or external vCenter requesting vCenter IP, Datacenter name and credentials.
The migrated VM will be imported as a KVM instance
The migration is done through virt-v2v: https://access.redhat.com/articles/1351473, https://www.ovirt.org/develop/release-management/features/virt/virt-v2v-integration.html
The migration process timeout can be set by the setting convert.instance.process.timeout
Before attempting the virt-v2v migration, CloudStack will create a clone of the source VM on VMware. The clone VM will be removed after the registration process finishes.
CloudStack will delegate the migration action to a KVM host and the host will attempt to migrate the VM invoking virt-v2v. In case the guest OS is not supported then CloudStack will handle the error operation as a failure
The migration process using virt-v2v may not be a fast process
CloudStack will not perform any check about the guest OS compatibility for the virt-v2v library as indicated on: https://access.redhat.com/articles/1351473.
323 lines
10 KiB
Vue
323 lines
10 KiB
Vue
// 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.
|
|
|
|
<template>
|
|
<div style="margin-top: 10px;" v-if="this.vnf">
|
|
<label>{{ $t('message.configure.network.ip.and.mac') }}</label>
|
|
</div>
|
|
<div style="margin-top: 10px;" v-else>
|
|
<label>{{ $t('message.configure.network.select.default.network') }}</label>
|
|
</div>
|
|
<a-form
|
|
:ref="formRef"
|
|
:model="form"
|
|
:rules="rules">
|
|
<a-table
|
|
:columns="columns"
|
|
:dataSource="dataItems"
|
|
:pagination="false"
|
|
:rowSelection="rowSelection"
|
|
:customRow="onClickRow"
|
|
:rowKey="record => record.id"
|
|
size="middle"
|
|
:scroll="{ y: 225 }">
|
|
<template #bodyCell="{ column, text, record }">
|
|
<template v-if="column.key === 'name'">
|
|
<div>{{ text }}</div>
|
|
<small v-if="record.type!=='L2'">{{ $t('label.cidr') + ': ' + record.cidr }}</small>
|
|
</template>
|
|
<template v-if="!this.autoscale">
|
|
<template v-if="column.key === 'ipAddress'">
|
|
<a-form-item
|
|
style="display: block"
|
|
v-if="record.type !== 'L2'"
|
|
:name="'ipAddress' + record.id">
|
|
<a-input
|
|
style="width: 150px;"
|
|
v-model:value="form['ipAddress' + record.id]"
|
|
:placeholder="record.cidr"
|
|
@change="($event) => updateNetworkData('ipAddress', record.id, $event.target.value)">
|
|
<template #suffix>
|
|
<a-tooltip :title="getIpRangeDescription(record)">
|
|
<info-circle-outlined style="color: rgba(0,0,0,.45)" />
|
|
</a-tooltip>
|
|
</template>
|
|
</a-input>
|
|
</a-form-item>
|
|
</template>
|
|
<template v-if="column.key === 'macAddress'">
|
|
<a-form-item style="display: block" :name="'macAddress' + record.id">
|
|
<a-input
|
|
style="width: 150px;"
|
|
:placeholder="$t('label.macaddress')"
|
|
v-model:value="form[`macAddress` + record.id]"
|
|
@change="($event) => updateNetworkData('macAddress', record.id, $event.target.value)">
|
|
<template #suffix>
|
|
<a-tooltip :title="$t('label.macaddress.example')">
|
|
<info-circle-outlined style="color: rgba(0,0,0,.45)" />
|
|
</a-tooltip>
|
|
</template>
|
|
</a-input>
|
|
</a-form-item>
|
|
</template>
|
|
</template>
|
|
</template>
|
|
</a-table>
|
|
</a-form>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, reactive } from 'vue'
|
|
export default {
|
|
name: 'NetworkConfiguration',
|
|
props: {
|
|
items: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
value: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
autoscale: {
|
|
type: Boolean,
|
|
default: () => false
|
|
},
|
|
vnf: {
|
|
type: Boolean,
|
|
default: () => false
|
|
},
|
|
preFillContent: {
|
|
type: Object,
|
|
default: () => {}
|
|
}
|
|
},
|
|
data () {
|
|
return {
|
|
networks: [],
|
|
columns: [
|
|
{
|
|
key: 'name',
|
|
dataIndex: 'name',
|
|
title: this.$t('label.network'),
|
|
width: '30%'
|
|
},
|
|
{
|
|
key: 'ipAddress',
|
|
dataIndex: 'ip',
|
|
title: this.$t('label.ip'),
|
|
width: '30%'
|
|
},
|
|
{
|
|
key: 'macAddress',
|
|
dataIndex: 'mac',
|
|
title: this.$t('label.macaddress'),
|
|
width: '30%'
|
|
}
|
|
],
|
|
selectedRowKeys: [],
|
|
dataItems: [],
|
|
macRegex: /^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$/i,
|
|
ipV4Regex: /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i
|
|
}
|
|
},
|
|
beforeCreate () {
|
|
this.dataItems = []
|
|
},
|
|
created () {
|
|
this.dataItems = this.items
|
|
this.initForm()
|
|
if (this.dataItems.length > 0) {
|
|
this.selectedRowKeys = [this.dataItems[0].id]
|
|
this.$emit('select-default-network-item', this.dataItems[0].id)
|
|
}
|
|
},
|
|
computed: {
|
|
rowSelection () {
|
|
if (this.vnf) {
|
|
return null
|
|
}
|
|
return {
|
|
type: 'radio',
|
|
selectedRowKeys: this.selectedRowKeys,
|
|
onChange: this.onSelectRow
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
value (newValue, oldValue) {
|
|
if (newValue && newValue !== oldValue) {
|
|
this.selectedRowKeys = [newValue]
|
|
}
|
|
},
|
|
items: {
|
|
deep: true,
|
|
handler (newData) {
|
|
if (newData && newData.length > 0) {
|
|
this.dataItems = newData
|
|
this.initForm()
|
|
const keyEx = this.dataItems.filter((item) => this.selectedRowKeys.includes(item.id))
|
|
if (!keyEx || keyEx.length === 0) {
|
|
this.selectedRowKeys = [this.dataItems[0].id]
|
|
this.$emit('select-default-network-item', this.dataItems[0].id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
initForm () {
|
|
this.formRef = ref()
|
|
this.form = reactive({})
|
|
this.rules = reactive({})
|
|
|
|
const form = {}
|
|
const rules = {}
|
|
|
|
let presetMacAddressIndex = 0
|
|
|
|
this.dataItems.forEach(record => {
|
|
const ipAddressKey = 'ipAddress' + record.id
|
|
const macAddressKey = 'macAddress' + record.id
|
|
rules[ipAddressKey] = [{
|
|
validator: this.validatorIpAddress,
|
|
cidr: record.cidr,
|
|
networkType: record.type
|
|
}]
|
|
if (record.ipAddress) {
|
|
form[ipAddressKey] = record.ipAddress
|
|
}
|
|
rules[macAddressKey] = [{ validator: this.validatorMacAddress }]
|
|
if (record.macAddress) {
|
|
form[macAddressKey] = record.macAddress
|
|
} else if (this.preFillContent.macAddressArray && this.preFillContent.macAddressArray[presetMacAddressIndex]) {
|
|
form[macAddressKey] = this.preFillContent.macAddressArray[presetMacAddressIndex]
|
|
presetMacAddressIndex++
|
|
}
|
|
})
|
|
this.form = reactive(form)
|
|
this.rules = reactive(rules)
|
|
},
|
|
onSelectRow (value) {
|
|
this.selectedRowKeys = value
|
|
this.$emit('select-default-network-item', value[0])
|
|
},
|
|
updateNetworkData (name, key, value) {
|
|
this.formRef.value.validate().then(() => {
|
|
this.$emit('handler-error', false)
|
|
const index = this.networks.findIndex(item => item.key === key)
|
|
if (index === -1) {
|
|
const networkItem = {}
|
|
networkItem.key = key
|
|
networkItem[name] = value
|
|
this.networks.push(networkItem)
|
|
this.$emit('update-network-config', this.networks)
|
|
return
|
|
}
|
|
|
|
this.networks.filter((item, index) => {
|
|
if (item.key === key) {
|
|
this.networks[index][name] = value
|
|
}
|
|
})
|
|
this.$emit('update-network-config', this.networks)
|
|
}).catch((error) => {
|
|
this.formRef.value.scrollToField(error.errorFields[0].name)
|
|
this.$emit('handler-error', true)
|
|
})
|
|
},
|
|
removeItem (id) {
|
|
this.dataItems = this.dataItems.filter(item => item.id !== id)
|
|
if (this.selectedRowKeys.includes(id)) {
|
|
if (this.dataItems && this.dataItems.length > 0) {
|
|
this.selectedRowKeys = [this.dataItems[0].id]
|
|
this.$emit('select-default-network-item', this.dataItems[0].id)
|
|
}
|
|
}
|
|
},
|
|
async validatorMacAddress (rule, value) {
|
|
if (!value || value === '') {
|
|
return Promise.resolve()
|
|
} else if (!this.macRegex.test(value)) {
|
|
return Promise.reject(this.$t('message.error.macaddress'))
|
|
} else {
|
|
return Promise.resolve()
|
|
}
|
|
},
|
|
async validatorIpAddress (rule, value) {
|
|
if (!value || value === '') {
|
|
return Promise.resolve()
|
|
} else if (!this.ipV4Regex.test(value)) {
|
|
return Promise.reject(this.$t('message.error.ipv4.address'))
|
|
} else if (rule.networkType !== 'L2' && !this.isIp4InCidr(value, rule.cidr)) {
|
|
const rangeIps = this.calculateCidrRange(rule.cidr)
|
|
const message = `${this.$t('message.error.ip.range')} ${this.$t('label.from')} ${rangeIps[0]} ${this.$t('label.to')} ${rangeIps[1]}`
|
|
return Promise.reject(message)
|
|
} else {
|
|
return Promise.resolve()
|
|
}
|
|
},
|
|
getIpRangeDescription (network) {
|
|
const rangeIps = this.calculateCidrRange(network.cidr)
|
|
const rangeIpDescription = [`${this.$t('label.ip.range')}:`, rangeIps[0], '-', rangeIps[1]].join(' ')
|
|
return rangeIpDescription
|
|
},
|
|
isIp4InCidr (ip, cidr) {
|
|
const [range, bits = 32] = cidr.split('/')
|
|
const mask = ~(2 ** (32 - bits) - 1)
|
|
return (this.ip4ToInt(ip) & mask) === (this.ip4ToInt(range) & mask)
|
|
},
|
|
calculateCidrRange (cidr) {
|
|
const [range, bits = 32] = cidr.split('/')
|
|
const mask = ~(2 ** (32 - bits) - 1)
|
|
return [this.intToIp4(this.ip4ToInt(range) & mask), this.intToIp4(this.ip4ToInt(range) | ~mask)]
|
|
},
|
|
ip4ToInt (ip) {
|
|
return ip.split('.').reduce((int, oct) => (int << 8) + parseInt(oct, 10), 0) >>> 0
|
|
},
|
|
intToIp4 (int) {
|
|
return [(int >>> 24) & 0xFF, (int >>> 16) & 0xFF, (int >>> 8) & 0xFF, int & 0xFF].join('.')
|
|
},
|
|
onClickRow (record, index) {
|
|
return {
|
|
onClick: (event) => {
|
|
if (event.target.tagName.toLowerCase() !== 'input') {
|
|
this.selectedRowKeys = [record.id]
|
|
this.$emit('select-default-network-item', record.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.ant-table-wrapper {
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
:deep(.ant-table-tbody) > tr > td {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.ant-form .ant-form-item {
|
|
margin-bottom: 0;
|
|
padding-bottom: 0;
|
|
}
|
|
</style>
|