infra: zone and physical network, ip ranges tabs for traffic types (#134)

Physical network and systemvms tabs for zone.
IP ranges tabs for traffic type management.

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
Co-authored-by: Rohit Yadav <rohit@apache.org>
This commit is contained in:
Ritchie Vincent 2020-02-08 11:45:19 +00:00 committed by Rohit Yadav
parent c599b2fc11
commit edeb25dfbc
10 changed files with 1678 additions and 22 deletions

View File

@ -23,10 +23,6 @@ export default {
columns: ['name', 'state', 'networktype', 'clusters', 'cpuused', 'cpumaxdeviation', 'cpuallocated', 'cputotal', 'memoryused', 'memorymaxdeviation', 'memoryallocated', 'memorytotal', 'order'],
details: ['name', 'id', 'allocationstate', 'networktype', 'guestcidraddress', 'localstorageenabled', 'securitygroupsenabled', 'dns1', 'dns2', 'internaldns1', 'internaldns2'],
related: [{
name: 'physicalnetwork',
title: 'Physical Networks',
param: 'zoneid'
}, {
name: 'pod',
title: 'Pods',
param: 'zoneid'
@ -38,10 +34,6 @@ export default {
name: 'host',
title: 'Hosts',
param: 'zoneid'
}, {
name: 'systemvm',
title: 'SystemVMs',
param: 'zoneid'
}, {
name: 'storagepool',
title: 'Primate Storage',
@ -54,9 +46,15 @@ export default {
tabs: [{
name: 'details',
component: () => import('@/components/view/DetailsTab.vue')
}, {
name: 'Physical Networks',
component: () => import('@/views/infra/zone/PhysicalNetworksTab.vue')
}, {
name: 'System VMs',
component: () => import('@/views/infra/zone/SystemVmsTab.vue')
}, {
name: 'resources',
component: () => import('@/views/infra/ZoneResources.vue')
component: () => import('@/views/infra/zone/ZoneResources.vue')
}, {
name: 'settings',
component: () => import('@/components/view/SettingsTab.vue')
@ -68,7 +66,7 @@ export default {
label: 'Add Zone',
listView: true,
popup: true,
component: () => import('@/views/infra/ZoneWizard.vue')
component: () => import('@/views/infra/zone/ZoneWizard.vue')
},
{
api: 'updateZone',

View File

@ -611,6 +611,7 @@
"label.recover.vm": "Recover VM",
"label.refresh.blades": "Refresh Blades",
"label.reinstall.vm": "Reinstall VM",
"label.release.account": "Release from Account",
"label.release.dedicated.cluster": "Release Dedicated Cluster",
"label.release.dedicated.host": "Release Dedicated Host",
"label.release.dedicated.pod": "Release Dedicated Pod",
@ -639,6 +640,7 @@
"label.secondary.storage.vm":"Secondary storage VM",
"label.service.offering":"Service Offering",
"label.set.default.NIC": "Set default NIC",
"label.set.reservation": "Set Reservation",
"label.shutdown.provider": "Shutdown provider",
"label.snapshot.schedule": "Set up Recurring Snapshot",
"label.standard.us.keyboard": "Standard (US) keyboard",
@ -1011,7 +1013,7 @@
"vmdisplayname": "VM display name",
"vmipaddress": "VM IP Address",
"vmname": "VM Name",
"vmstate": "VM state",
"vmstate": "VM State",
"vmtotal": "Total of VMs",
"vmwaredcId": "VMware Datacenter ID",
"vmwaredcName": "VMware Datacenter Name",

View File

@ -0,0 +1,413 @@
// 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>
<a-spin :spinning="componentLoading">
<a-button
type="dashed"
icon="plus"
style="margin-bottom: 20px; width: 100%"
@click="handleOpenAddIpRangeModal">
{{ $t('label.add.ip.range') }}
</a-button>
<a-table
style="overflow-y: auto"
size="small"
:columns="columns"
:dataSource="items"
:rowKey="record => record.id + record.startip"
:pagination="false"
>
<template slot="forsystemvms" slot-scope="text, record">
<a-checkbox :checked="record.forsystemvms" />
</template>
<template slot="actions" slot-scope="record">
<div class="actions">
<a-popover placement="bottom">
<template slot="content">{{ $t('label.remove.ip.range') }}</template>
<a-button
icon="delete"
shape="round"
type="danger"
size="small"
@click="handleDeleteIpRange(record)"></a-button>
</a-popover>
</div>
</template>
</a-table>
<a-pagination
class="row-element pagination"
size="small"
style="overflow-y: auto"
:current="page"
:pageSize="pageSize"
:total="items.length"
:showTotal="total => `Total ${total} items`"
:pageSizeOptions="['10', '20', '40', '80', '100']"
@change="changePage"
@showSizeChange="changePageSize"
showSizeChanger/>
<a-modal v-model="addIpRangeModal" :title="$t('label.add.ip.range')" @ok="handleAddIpRange">
<a-form
:form="form"
@submit="handleAddIpRange"
layout="vertical"
class="form"
>
<a-form-item :label="$t('podId')" class="form__item">
<a-select
v-decorator="['pod', {
rules: [{ required: true, message: 'Required' }]
}]"
>
<a-select-option v-for="item in items" :key="item.id" :value="item.id">{{ item.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('gateway')" class="form__item">
<a-input
v-decorator="['gateway', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('netmask')" class="form__item">
<a-input
v-decorator="['netmask', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('vlan')" class="form__item">
<a-input
v-decorator="['vlan']">
</a-input>
</a-form-item>
<a-form-item :label="$t('startip')" class="form__item">
<a-input
v-decorator="['startip', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('endip')" class="form__item">
<a-input
v-decorator="['endip', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('System VMs')" class="form__item">
<a-checkbox v-decorator="['vms']"></a-checkbox>
</a-form-item>
</a-form>
</a-modal>
</a-spin>
</template>
<script>
import { api } from '@/api'
export default {
name: 'IpRangesTabManagement',
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
componentLoading: false,
items: [],
domains: [],
domainsLoading: false,
addIpRangeModal: false,
defaultSelectedPod: null,
page: 1,
pageSize: 10,
columns: [
{
title: this.$t('podid'),
dataIndex: 'name'
},
{
title: this.$t('gateway'),
dataIndex: 'gateway'
},
{
title: this.$t('netmask'),
dataIndex: 'netmask'
},
{
title: this.$t('vlan'),
dataIndex: 'vlanid',
scopedSlots: { customRender: 'vlan' }
},
{
title: this.$t('startip'),
dataIndex: 'startip',
scopedSlots: { customRender: 'startip' }
},
{
title: this.$t('endip'),
dataIndex: 'endip',
scopedSlots: { customRender: 'endip' }
},
{
title: this.$t('System VMs'),
dataIndex: 'forsystemvms',
scopedSlots: { customRender: 'forsystemvms' }
},
{
title: this.$t('action'),
scopedSlots: { customRender: 'actions' }
}
]
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
mounted () {
this.fetchData()
},
watch: {
resource (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.fetchData()
}
},
methods: {
fetchData () {
this.componentLoading = true
api('listPods', {
zoneid: this.resource.zoneid,
page: this.page,
pagesize: this.pageSize
}).then(response => {
this.items = []
const pods = response.listpodsresponse.pod ? response.listpodsresponse.pod : []
for (const pod of pods) {
if (pod && pod.startip && pod.startip.length > 0) {
for (var idx = 0; idx < pod.startip.length; idx++) {
this.items.push({
id: pod.id,
name: pod.name,
gateway: pod.gateway,
netmask: pod.netmask,
vlanid: pod.vlanid[idx],
startip: pod.startip[idx],
endip: pod.endip[idx],
forsystemvms: pod.forsystemvms[idx] === '1'
})
}
}
}
}).catch(error => {
console.log(error)
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.listpodsresponse
? error.response.data.listpodsresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.componentLoading = false
})
},
handleOpenAddIpRangeModal () {
this.addIpRangeModal = true
setTimeout(() => {
if (this.items.length > 0) {
this.form.setFieldsValue({
pod: this.items[0].id
})
}
}, 200)
},
handleDeleteIpRange (record) {
this.componentLoading = true
api('deleteManagementNetworkIpRange', {
podid: record.id,
startip: record.startip,
endip: record.endip,
vlan: record.vlanid
}).then(response => {
this.$store.dispatch('AddAsyncJob', {
title: `Successfully removed IP Range`,
jobid: response.deletemanagementnetworkiprangeresponse.jobid,
status: 'progress'
})
this.$pollJob({
jobId: response.deletemanagementnetworkiprangeresponse.jobid,
successMethod: () => {
this.componentLoading = false
this.fetchData()
},
errorMessage: 'Removing failed',
errorMethod: () => {
this.componentLoading = false
this.fetchData()
},
loadingMessage: `Removing IP Range...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.componentLoading = false
this.fetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.deletemanagementnetworkiprangeresponse
? error.response.data.deletemanagementnetworkiprangeresponse.errortext : error.response.data.errorresponse.errortext
})
this.componentLoading = false
this.fetchData()
})
},
handleAddIpRange (e) {
this.form.validateFields((error, values) => {
if (error) return
this.componentLoading = true
this.addIpRangeModal = false
api('createManagementNetworkIpRange', {
podid: values.pod,
gateway: values.gateway,
netmask: values.netmask,
startip: values.startip,
endip: values.endip,
forsystemvms: values.vms,
vlan: values.vlan || null
}).then(response => {
this.$store.dispatch('AddAsyncJob', {
title: `Successfully added IP Range`,
jobid: response.createmanagementnetworkiprangeresponse.jobid,
status: 'progress'
})
this.$pollJob({
jobId: response.createmanagementnetworkiprangeresponse.jobid,
successMethod: () => {
this.componentLoading = false
this.fetchData()
},
errorMessage: 'Adding failed',
errorMethod: () => {
this.componentLoading = false
this.fetchData()
},
loadingMessage: `Adding IP Range...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.componentLoading = false
this.fetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.createmanagementnetworkiprangeresponse
? error.response.data.createmanagementnetworkiprangeresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.componentLoading = false
this.fetchData()
})
})
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
this.fetchData()
},
changePageSize (currentPage, pageSize) {
this.page = currentPage
this.pageSize = pageSize
this.fetchData()
}
}
}
</script>
<style lang="scss" scoped>
.list {
&__item {
display: flex;
}
&__data {
display: flex;
flex-wrap: wrap;
}
&__col {
flex-basis: calc((100% / 3) - 20px);
margin-right: 20px;
margin-bottom: 10px;
}
&__label {
}
}
.ant-list-item {
padding-top: 0;
padding-bottom: 0;
&:not(:first-child) {
padding-top: 20px;
}
&:not(:last-child) {
padding-bottom: 20px;
}
}
.actions {
button {
&:not(:last-child) {
margin-bottom: 10px;
}
}
}
.ant-select {
width: 100%;
}
.form {
.actions {
display: flex;
justify-content: flex-end;
button {
&:not(:last-child) {
margin-right: 10px;
}
}
}
&__item {
}
}
.pagination {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,489 @@
// 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>
<a-spin :spinning="componentLoading">
<a-button
type="dashed"
icon="plus"
style="margin-bottom: 20px; width: 100%"
@click="handleOpenAddIpRangeModal">
{{ $t('label.add.ip.range') }}
</a-button>
<a-table
size="small"
style="overflow-y: auto"
:columns="columns"
:dataSource="items"
:rowKey="record => record.id"
:pagination="false"
>
<template slot="account" slot-scope="record">
<a-button @click="() => handleOpenAccountModal(record)">{{ `[${record.domain}] ${record.account}` }}</a-button>
</template>
<template slot="actions" slot-scope="record">
<div class="actions">
<a-popover v-if="record.account === 'system'" placement="bottom">
<template slot="content">{{ $t('label.add.account') }}</template>
<a-button
icon="user-add"
shape="round"
type="primary"
@click="() => handleOpenAddAccountModal(record)"></a-button>
</a-popover>
<a-popover
v-else
placement="bottom">
<template slot="content">{{ $t('label.release.account') }}</template>
<a-button
icon="user-delete"
shape="round"
type="danger"
@click="() => handleRemoveAccount(record.id)"></a-button>
</a-popover>
<a-popover placement="bottom">
<template slot="content">{{ $t('label.remove.ip.range') }}</template>
<a-button icon="delete" shape="round" type="danger" @click="handleDeleteIpRange(record.id)"></a-button>
</a-popover>
</div>
</template>
</a-table>
<a-pagination
class="row-element pagination"
size="small"
style="overflow-y: auto"
:current="page"
:pageSize="pageSize"
:total="items.length"
:showTotal="total => `Total ${total} items`"
:pageSizeOptions="['10', '20', '40', '80', '100']"
@change="changePage"
@showSizeChange="changePageSize"
showSizeChanger/>
<a-modal v-model="accountModal" v-if="selectedItem" @ok="accountModal = false">
<div>
<div style="margin-bottom: 10px;">
<div class="list__label">{{ $t('account') }}</div>
<div>{{ selectedItem.account }}</div>
</div>
<div style="margin-bottom: 10px;">
<div class="list__label">{{ $t('domain') }}</div>
<div>{{ selectedItem.domain }}</div>
</div>
<div style="margin-bottom: 10px;">
<div class="list__label">{{ $t('System VMs') }}</div>
<div>{{ selectedItem.forsystemvms }}</div>
</div>
</div>
</a-modal>
<a-modal :zIndex="1001" v-model="addAccountModal" :title="$t('label.add.account')" @ok="handleAddAccount">
<a-spin :spinning="domainsLoading">
<div style="margin-bottom: 10px;">
<div class="list__label">{{ $t('account') }}:</div>
<a-input v-model="addAccount.account"></a-input>
</div>
<div>
<div class="list__label">{{ $t('domain') }}:</div>
<a-select v-model="addAccount.domain">
<a-select-option
v-for="domain in domains"
:key="domain.id"
:value="domain.id">{{ domain.name }}
</a-select-option>
</a-select>
</div>
</a-spin>
</a-modal>
<a-modal v-model="addIpRangeModal" :title="$t('label.add.ip.range')" @ok="handleAddIpRange">
<a-form
:form="form"
@submit="handleAddIpRange"
layout="vertical"
class="form"
>
<a-form-item :label="$t('gateway')" class="form__item">
<a-input
v-decorator="['gateway', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('netmask')" class="form__item">
<a-input
v-decorator="['netmask', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('vlan')" class="form__item">
<a-input
v-decorator="['vlan']">
</a-input>
</a-form-item>
<a-form-item :label="$t('startip')" class="form__item">
<a-input
v-decorator="['startip', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('endip')" class="form__item">
<a-input
v-decorator="['endip', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<div class="form__item">
<div style="color: black;">{{ $t('label.set.reservation') }}</div>
<a-switch @change="handleShowAccountFields"></a-switch>
</div>
<div v-if="showAccountFields" style="margin-top: 20px;">
<p>(optional) Please specify an account to be associated with this IP range.</p>
<p>System VMs: Enable dedication of public IP range for SSVM and CPVM, account field disabled. Reservation strictness defined on 'system.vm.public.ip.reservation.mode.strictness'.</p>
<a-form-item :label="$t('System VMs')" class="form__item">
<a-switch v-decorator="['forsystemvms']"></a-switch>
</a-form-item>
<a-spin :spinning="domainsLoading">
<a-form-item :label="$t('account')" class="form__item">
<a-input v-decorator="['account']"></a-input>
</a-form-item>
<a-form-item :label="$t('domain')" class="form__item">
<a-select v-decorator="['domain']">
<a-select-option
v-for="domain in domains"
:key="domain.id"
:value="domain.id">{{ domain.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-spin>
</div>
</a-form>
</a-modal>
</a-spin>
</template>
<script>
import { api } from '@/api'
export default {
name: 'IpRangesTabPublic',
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
},
network: {
type: Object,
required: true
}
},
data () {
return {
componentLoading: false,
items: [],
selectedItem: null,
accountModal: false,
addAccountModal: false,
addAccount: {
account: null,
domain: null
},
domains: [],
domainsLoading: false,
addIpRangeModal: false,
showAccountFields: false,
page: 1,
pageSize: 10,
columns: [
{
title: this.$t('gateway'),
dataIndex: 'gateway'
},
{
title: this.$t('netmask'),
dataIndex: 'netmask'
},
{
title: this.$t('vlan'),
dataIndex: 'vlan'
},
{
title: this.$t('startip'),
dataIndex: 'startip'
},
{
title: this.$t('endip'),
dataIndex: 'endip'
},
{
title: this.$t('account'),
scopedSlots: { customRender: 'account' }
},
{
title: this.$t('action'),
scopedSlots: { customRender: 'actions' }
}
]
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
mounted () {
this.fetchData()
},
watch: {
network (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.fetchData()
}
},
methods: {
fetchData () {
this.componentLoading = true
api('listVlanIpRanges', {
networkid: this.network.id,
zoneid: this.resource.zoneid,
page: this.page,
pagesize: this.pageSize
}).then(response => {
this.items = response.listvlaniprangesresponse.vlaniprange ? response.listvlaniprangesresponse.vlaniprange : []
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.listvlaniprangesresponse
? error.response.data.listvlaniprangesresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.componentLoading = false
})
},
fetchDomains () {
this.domainsLoading = true
api('listDomains', {
details: 'min',
listAll: true
}).then(response => {
this.domains = response.listdomainsresponse.domain ? response.listdomainsresponse.domain : []
if (this.domains.length > 0) {
this.addAccount.domain = this.domains[0].id
this.form.setFieldsValue({ domain: this.domains[0].id })
}
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.listdomains
? error.response.data.listdomains.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.domainsLoading = false
})
},
handleAddAccount () {
this.domainsLoading = true
if (this.addIpRangeModal === true) {
this.addAccountModal = false
return
}
api('dedicatePublicIpRange', {
id: this.selectedItem.id,
zoneid: this.selectedItem.zoneid,
domainid: this.addAccount.domain,
account: this.addAccount.account
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.dedicatepubliciprangeresponse
? error.response.data.dedicatepubliciprangeresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.addAccountModal = false
this.domainsLoading = false
this.fetchData()
})
},
handleRemoveAccount (id) {
this.componentLoading = true
api('releasePublicIpRange', { id }).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.releasepubliciprangeresponse
? error.response.data.releasepubliciprangeresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.fetchData()
})
},
handleOpenAccountModal (item) {
this.selectedItem = item
this.accountModal = true
},
handleOpenAddAccountModal (item) {
if (!this.addIpRangeModal) {
this.selectedItem = item
}
this.addAccountModal = true
this.fetchDomains()
},
handleShowAccountFields () {
if (this.showAccountFields === false) {
this.showAccountFields = true
this.fetchDomains()
return
}
this.showAccountFields = false
},
handleOpenAddIpRangeModal () {
this.addIpRangeModal = true
},
handleDeleteIpRange (id) {
this.componentLoading = true
api('deleteVlanIpRange', { id }).then(() => {
this.$notification.success({
message: 'Removed IP Range'
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.deletevlaniprangeresponse
? error.response.data.deletevlaniprangeresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.componentLoading = false
this.fetchData()
})
},
handleAddIpRange (e) {
this.form.validateFields((error, values) => {
if (error) return
this.componentLoading = true
this.addIpRangeModal = false
api('createVlanIpRange', {
zoneId: this.resource.zoneid,
vlan: values.vlan,
gateway: values.gateway,
netmask: values.netmask,
startip: values.startip,
endip: values.endip,
forsystemvms: values.forsystemvms,
account: values.forsystemvms ? null : values.account,
domainid: values.forsystemvms ? null : values.domain,
forvirtualnetwork: true
}).then(() => {
this.$notification.success({
message: 'Successfully added IP Range'
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.createvlaniprangeresponse
? error.response.data.createvlaniprangeresponse.errortext : error.response.data.errorresponse.errortext,
duration: 0
})
}).finally(() => {
this.componentLoading = false
this.fetchData()
})
})
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
this.fetchData()
},
changePageSize (currentPage, pageSize) {
this.page = currentPage
this.pageSize = pageSize
this.fetchData()
}
}
}
</script>
<style lang="scss" scoped>
.list {
&__item {
display: flex;
}
&__data {
display: flex;
flex-wrap: wrap;
}
&__col {
flex-basis: calc((100% / 3) - 20px);
margin-right: 20px;
margin-bottom: 10px;
}
&__label {
font-weight: bold;
}
}
.ant-list-item {
padding-top: 0;
padding-bottom: 0;
&:not(:first-child) {
padding-top: 20px;
}
&:not(:last-child) {
padding-bottom: 20px;
}
}
.actions {
button {
&:not(:last-child) {
margin-right: 10px;
}
}
}
.ant-select {
width: 100%;
}
.form {
.actions {
display: flex;
justify-content: flex-end;
button {
&:not(:last-child) {
margin-right: 10px;
}
}
}
}
.pagination {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,398 @@
// 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>
<a-spin :spinning="componentLoading">
<a-button
type="dashed"
icon="plus"
style="margin-bottom: 20px; width: 100%"
@click="handleOpenAddIpRangeModal">
{{ $t('label.add.ip.range') }}
</a-button>
<a-table
style="overflow-y: auto"
size="small"
:columns="columns"
:dataSource="items"
:rowKey="record => record.id"
:pagination="false"
>
<template slot="name" slot-scope="record">
<div>{{ returnPodName(record.podid) }}</div>
</template>
<template slot="actions" slot-scope="record">
<a-popover placement="bottom">
<template slot="content">{{ $t('label.remove.ip.range') }}</template>
<a-button
icon="delete"
shape="round"
type="danger"
@click="handleDeleteIpRange(record.id)"></a-button>
</a-popover>
</template>
</a-table>
<a-pagination
class="row-element pagination"
size="small"
style="overflow-y: auto"
:current="page"
:pageSize="pageSize"
:total="items.length"
:showTotal="total => `Total ${total} items`"
:pageSizeOptions="['10', '20', '40', '80', '100']"
@change="changePage"
@showSizeChange="changePageSize"
showSizeChanger/>
<a-modal v-model="addIpRangeModal" :title="$t('label.add.ip.range')" @ok="handleAddIpRange">
<a-form
:form="form"
@submit="handleAddIpRange"
layout="vertical"
class="form"
>
<a-form-item :label="$t('podId')" class="form__item">
<a-select
v-decorator="['pod', {
rules: [{ required: true, message: 'Required' }]
}]"
>
<a-select-option v-for="pod in pods" :key="pod.id" :value="pod.id">{{ pod.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('gateway')" class="form__item">
<a-input
v-decorator="['gateway', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('netmask')" class="form__item">
<a-input
v-decorator="['netmask', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('vlan')" class="form__item">
<a-input
v-decorator="['vlan']">
</a-input>
</a-form-item>
<a-form-item :label="$t('startip')" class="form__item">
<a-input
v-decorator="['startip', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
<a-form-item :label="$t('endip')" class="form__item">
<a-input
v-decorator="['endip', { rules: [{ required: true, message: 'Required' }] }]">
</a-input>
</a-form-item>
</a-form>
</a-modal>
</a-spin>
</template>
<script>
import { api } from '@/api'
export default {
name: 'IpRangesTabStorage',
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
componentLoading: false,
items: [],
pods: [],
domains: [],
domainsLoading: false,
addIpRangeModal: false,
defaultSelectedPod: null,
columns: [
{
title: this.$t('podId'),
scopedSlots: { customRender: 'name' }
},
{
title: this.$t('gateway'),
dataIndex: 'gateway'
},
{
title: this.$t('netmask'),
dataIndex: 'netmask'
},
{
title: this.$t('vlan'),
dataIndex: 'vlanid'
},
{
title: this.$t('startip'),
dataIndex: 'startip'
},
{
title: this.$t('endip'),
dataIndex: 'endip'
},
{
title: this.$t('action'),
scopedSlots: { customRender: 'actions' }
}
],
page: 1,
pageSize: 10
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
mounted () {
this.fetchData()
},
watch: {
resource (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.fetchData()
}
},
methods: {
fetchData () {
this.fetchPods()
this.componentLoading = true
api('listStorageNetworkIpRange', {
zoneid: this.resource.zoneid,
page: this.page,
pageSize: this.pageSize
}).then(response => {
this.items = response.liststoragenetworkiprangeresponse.storagenetworkiprange ? response.liststoragenetworkiprangeresponse.storagenetworkiprange : []
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.liststoragenetworkiprangeresponse
? error.response.data.liststoragenetworkiprangeresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.componentLoading = false
})
},
fetchPods () {
this.componentLoading = true
api('listPods', {
zoneid: this.resource.zoneid
}).then(response => {
this.pods = response.listpodsresponse.pod ? response.listpodsresponse.pod : []
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.listpodsresponse
? error.response.data.listpodsresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.componentLoading = false
})
},
returnPodName (id) {
const match = this.pods.find(i => i.id === id)
return match ? match.name : null
},
handleOpenAddIpRangeModal () {
this.addIpRangeModal = true
setTimeout(() => {
if (this.items.length > 0) {
this.form.setFieldsValue({
pod: this.pods[0].id
})
}
}, 200)
},
handleDeleteIpRange (id) {
this.componentLoading = true
api('deleteStorageNetworkIpRange', { id }).then(response => {
this.$store.dispatch('AddAsyncJob', {
title: `Successfully removed IP Range`,
jobid: response.deletestoragenetworkiprangeresponse.jobid,
status: 'progress'
})
this.$pollJob({
jobId: response.deletestoragenetworkiprangeresponse.jobid,
successMethod: () => {
this.componentLoading = false
this.fetchData()
},
errorMessage: 'Removing failed',
errorMethod: () => {
this.componentLoading = false
this.fetchData()
},
loadingMessage: `Removing IP Range...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.componentLoading = false
this.fetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.deletestoragenetworkiprangeresponse
? error.response.data.deletestoragenetworkiprangeresponse.errortext : error.response.data.errorresponse.errortext
})
this.componentLoading = false
this.fetchData()
})
},
handleAddIpRange (e) {
this.form.validateFields((error, values) => {
if (error) return
this.componentLoading = true
this.addIpRangeModal = false
api('createStorageNetworkIpRange', {
podid: values.pod,
zoneid: this.resource.zoneid,
gateway: values.gateway,
netmask: values.netmask,
startip: values.startip,
endip: values.endip,
vlan: values.vlan || null
}).then(response => {
this.$store.dispatch('AddAsyncJob', {
title: `Successfully added IP Range`,
jobid: response.createstoragenetworkiprangeresponse.jobid,
status: 'progress'
})
this.$pollJob({
jobId: response.createstoragenetworkiprangeresponse.jobid,
successMethod: () => {
this.componentLoading = false
this.fetchData()
},
errorMessage: 'Adding failed',
errorMethod: () => {
this.componentLoading = false
this.fetchData()
},
loadingMessage: `Adding IP Range...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.componentLoading = false
this.fetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.createstoragenetworkiprangeresponse
? error.response.data.createstoragenetworkiprangeresponse.errortext : error.response.data.errorresponse.errortext
})
}).finally(() => {
this.componentLoading = false
this.fetchData()
})
})
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
this.fetchData()
},
changePageSize (currentPage, pageSize) {
this.page = currentPage
this.pageSize = pageSize
this.fetchData()
}
}
}
</script>
<style lang="scss" scoped>
.list {
&__item {
display: flex;
}
&__data {
display: flex;
flex-wrap: wrap;
}
&__col {
flex-basis: calc((100% / 3) - 20px);
margin-right: 20px;
margin-bottom: 10px;
}
&__label {
}
}
.ant-list-item {
padding-top: 0;
padding-bottom: 0;
&:not(:first-child) {
padding-top: 20px;
}
&:not(:last-child) {
padding-bottom: 20px;
}
}
.actions {
button {
&:not(:last-child) {
margin-bottom: 10px;
}
}
}
.ant-select {
width: 100%;
}
.form {
.actions {
display: flex;
justify-content: flex-end;
button {
&:not(:last-child) {
margin-right: 10px;
}
}
}
&__item {
}
}
.pagination {
margin-top: 20px;
}
</style>

View File

@ -17,18 +17,34 @@
<template>
<a-spin :spinning="fetchLoading">
<a-tabs :animated="false" defaultActiveKey="0" tabPosition="left">
<a-tabs :tabPosition="device === 'mobile' ? 'top' : 'left'" :animated="false">
<a-tab-pane v-for="(item, index) in traffictypes" :tab="item.traffictype" :key="index">
<div>
<strong>{{ $t('id') }}</strong> {{ item.id }}
</div>
<div v-for="(type, idx) in ['kvmnetworklabel', 'vmwarenetworklabel', 'xennetworklabel', 'hypervnetworklabel', 'ovm3networklabel']" :key="idx">
<strong>{{ $t(type) }}</strong>
{{ item[type] || 'Use default gateway' }}
<div
v-for="(type, idx) in ['kvmnetworklabel', 'vmwarenetworklabel', 'xennetworklabel', 'hypervnetworklabel', 'ovm3networklabel']"
:key="idx"
style="margin-bottom: 10px;">
<div><strong>{{ $t(type) }}</strong></div>
<div>{{ item[type] || 'Use default gateway' }}</div>
</div>
<div v-if="item.traffictype === 'Public'">
Insert here form/component to manage public IP ranges
<IpRangesTab :resource="resource" />
<div style="margin-bottom: 10px;">
<div><strong>{{ $t('traffictype') }}</strong></div>
<div>{{ publicNetwork.traffictype }}</div>
</div>
<div style="margin-bottom: 10px;">
<div><strong>{{ $t('broadcastdomaintype') }}</strong></div>
<div>{{ publicNetwork.broadcastdomaintype }}</div>
</div>
<a-divider />
<IpRangesTabPublic :resource="resource" :loading="loading" :network="publicNetwork" />
</div>
<div v-if="item.traffictype === 'Management'">
<a-divider />
<IpRangesTabManagement :resource="resource" :loading="loading" />
</div>
<div v-if="item.traffictype === 'Storage'">
<a-divider />
<IpRangesTabStorage :resource="resource" />
</div>
</a-tab-pane>
<a-tab-pane tab="Service Providers" key="nsp">
@ -45,15 +61,21 @@
<script>
import { api } from '@/api'
import { mixinDevice } from '@/utils/mixin.js'
import Status from '@/components/widgets/Status'
import IpRangesTab from './IpRangesTab'
import IpRangesTabPublic from './IpRangesTabPublic'
import IpRangesTabManagement from './IpRangesTabManagement'
import IpRangesTabStorage from './IpRangesTabStorage'
export default {
name: 'NetworkTab',
components: {
IpRangesTab,
IpRangesTabPublic,
IpRangesTabManagement,
IpRangesTabStorage,
Status
},
mixins: [mixinDevice],
props: {
resource: {
type: Object,
@ -68,6 +90,7 @@ export default {
return {
traffictypes: [],
nsps: [],
publicNetwork: {},
fetchLoading: false
}
},
@ -95,6 +118,23 @@ export default {
this.fetchLoading = false
})
this.fetchLoading = true
api('listNetworks', {
listAll: true,
trafficType: 'Public',
isSystem: true,
zoneId: this.resource.zoneid
}).then(json => {
this.publicNetwork = json.listnetworksresponse.network[0] || {}
}).catch(error => {
this.$notification.error({
message: 'Request Failed',
description: error.response.headers['x-description']
})
}).finally(() => {
this.fetchLoading = false
})
this.fetchLoading = true
api('listNetworkServiceProviders', { physicalnetworkid: this.resource.id }).then(json => {
this.nsps = json.listnetworkserviceprovidersresponse.networkserviceprovider

View File

@ -0,0 +1,154 @@
// 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>
<a-spin :spinning="fetchLoading">
<a-list class="list">
<a-list-item v-for="network in networks" :key="network.id" class="list__item">
<div class="list__item-outer-container">
<div class="list__item-container">
<div class="list__col">
<div class="list__label">
{{ $t('name') }}
</div>
<div>
<router-link :to="{ path: '/physicalnetwork/' + network.id }">{{ network.name }}</router-link>
</div>
</div>
<div class="list__col">
<div class="list__label">{{ $t('state') }}</div>
<div><status :text="network.state" displayText></status></div>
</div>
<div class="list__col">
<div class="list__label">
{{ $t('isolationmethods') }}
</div>
<div>
{{ network.isolationmethods }}
</div>
</div>
<div class="list__col">
<div class="list__label">
{{ $t('vlan') }}
</div>
<div>{{ network.vlan }}</div>
</div>
<div class="list__col">
<div class="list__label">
{{ $t('broadcastdomainrange') }}
</div>
<div>{{ network.broadcastdomainrange }}</div>
</div>
</div>
</div>
</a-list-item>
</a-list>
</a-spin>
</template>
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
export default {
name: 'PhysicalNetworksTab',
components: {
Status
},
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
networks: [],
fetchLoading: false
}
},
mounted () {
this.fetchData()
},
watch: {
resource (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.fetchData()
}
},
methods: {
fetchData () {
this.fetchLoading = true
api('listPhysicalNetworks', { zoneid: this.resource.id }).then(json => {
this.networks = json.listphysicalnetworksresponse.physicalnetwork || []
}).catch(error => {
this.$notification.error({
message: 'Request Failed',
description: error.response.headers['x-description']
})
}).finally(() => {
this.fetchLoading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.list {
&__label {
font-weight: bold;
}
&__col {
flex: 1;
@media (min-width: 480px) {
&:not(:last-child) {
margin-right: 20px;
}
}
}
&__item {
margin-right: -8px;
align-items: flex-start;
&-outer-container {
width: 100%;
}
&-container {
display: flex;
flex-direction: column;
width: 100%;
@media (min-width: 480px) {
flex-direction: row;
margin-bottom: 10px;
}
}
}
}
</style>

View File

@ -0,0 +1,162 @@
// 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>
<a-spin :spinning="fetchLoading">
<a-list class="list">
<a-list-item v-for="vm in vms" :key="vm.id" class="list__item">
<div class="list__item-outer-container">
<div class="list__item-container">
<div class="list__col">
<div class="list__label">
{{ $t('name') }}
</div>
<div>
<router-link :to="{ path: '/systemvm/' + vm.id }">{{ vm.name }}</router-link>
</div>
</div>
<div class="list__col">
<div class="list__label">{{ $t('vmstate') }}</div>
<div><status :text="vm.state" displayText></status></div>
</div>
<div class="list__col">
<div class="list__label">{{ $t('agentstate') }}</div>
<div><status :text="vm.agentstate || 'Unknown'" displayText></status></div>
</div>
<div class="list__col">
<div class="list__label">
{{ $t('type') }}
</div>
<div>
{{ vm.systemvmtype == 'consoleproxy' ? 'Console Proxy VM' : 'Secondary Storage VM' }}
</div>
</div>
<div class="list__col">
<div class="list__label">
{{ $t('publicip') }}
</div>
<div>
{{ vm.publicip }}
</div>
</div>
<div class="list__col">
<div class="list__label">
{{ $t('hostname') }}
</div>
<div>
<router-link :to="{ path: '/host/' + vm.hostid }">{{ vm.hostname }}</router-link>
</div>
</div>
</div>
</div>
</a-list-item>
</a-list>
</a-spin>
</template>
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
export default {
name: 'SystemVmsTab',
components: {
Status
},
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
vms: [],
fetchLoading: false
}
},
mounted () {
this.fetchData()
},
watch: {
resource (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.fetchData()
}
},
methods: {
fetchData () {
this.fetchLoading = true
api('listSystemVms', { zoneid: this.resource.id }).then(json => {
this.vms = json.listsystemvmsresponse.systemvm || []
}).catch(error => {
this.$notification.error({
message: 'Request Failed',
description: error.response.headers['x-description']
})
}).finally(() => {
this.fetchLoading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.list {
&__label {
font-weight: bold;
}
&__col {
flex: 1;
@media (min-width: 480px) {
&:not(:last-child) {
margin-right: 20px;
}
}
}
&__item {
margin-right: -8px;
align-items: flex-start;
&-outer-container {
width: 100%;
}
&-container {
display: flex;
flex-direction: column;
width: 100%;
@media (min-width: 480px) {
flex-direction: row;
margin-bottom: 10px;
}
}
}
}
</style>