compute: NICs and IPs management (#71)

Adds VM nic/ip management, implement some placement fixes.

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Ritchie Vincent 2019-12-19 02:55:14 +00:00 committed by Rohit Yadav
parent 200f89bc08
commit b140b738fb
10 changed files with 665 additions and 154 deletions

58
ui/package-lock.json generated
View File

@ -2341,9 +2341,9 @@
}
},
"@fortawesome/vue-fontawesome": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.8.tgz",
"integrity": "sha512-SdFiUD+vFDA/xKuEbnQTVrK8FDxoV0eyQaiHxmCcjAc0+vQe0Kf6oGm28opNPIt8MTgKWR3+Yg3xXP455Ae4tQ=="
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.9.tgz",
"integrity": "sha512-h/emhmZz+DfB2zOGLWawNwXq82UYhn9waTfUjLLmeaIqtnIyNt6kYlpQT/vzJjLZRDRvY2IEJAh1di5qKpKVpA=="
},
"@hapi/address": {
"version": "2.1.4",
@ -8526,9 +8526,9 @@
}
},
"core-js": {
"version": "3.4.8",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.4.8.tgz",
"integrity": "sha512-b+BBmCZmVgho8KnBUOXpvlqEMguko+0P+kXCwD4vIprsXC6ht1qgPxtb1OK6XgSlrySF71wkwBQ0Hv695bk9gQ=="
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.5.0.tgz",
"integrity": "sha512-Ifh3kj78gzQ7NAoJXeTu+XwzDld0QRIwjBLRqAMhuLhP3d2Av5wmgE9ycfnvK6NAEjTkQ1sDPeoEZAWO3Hx1Uw=="
},
"core-js-compat": {
"version": "3.4.7",
@ -10009,9 +10009,9 @@
"dev": true
},
"elliptic": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz",
"integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
@ -20621,9 +20621,9 @@
}
},
"serialize-javascript": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz",
"integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
"dev": true
},
"serve-index": {
@ -22016,16 +22016,16 @@
}
},
"terser-webpack-plugin": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz",
"integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
"dev": true,
"requires": {
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
"serialize-javascript": "^1.7.0",
"serialize-javascript": "^2.1.2",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
@ -23005,9 +23005,9 @@
"dev": true
},
"vue": {
"version": "2.6.10",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
},
"vue-cli-plugin-apollo": {
"version": "0.21.3",
@ -23259,9 +23259,9 @@
"dev": true
},
"vue-i18n": {
"version": "8.15.1",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.1.tgz",
"integrity": "sha512-GBbz8qYCu0U2LNu4IcuFLZiuyninG4k26knvhL7GZG5Ncp4RR2VKDEH6g8gQ6I+UUBCvH2MBQVPSdxWe4DBkPw=="
"version": "8.15.3",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.3.tgz",
"integrity": "sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew=="
},
"vue-i18n-extract": {
"version": "0.4.14",
@ -23506,9 +23506,9 @@
}
},
"vue-template-compiler": {
"version": "2.6.10",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz",
"integrity": "sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==",
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz",
"integrity": "sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==",
"dev": true,
"requires": {
"de-indent": "^1.0.2",
@ -23607,9 +23607,9 @@
"dev": true
},
"webpack": {
"version": "4.41.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.2.tgz",
"integrity": "sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==",
"version": "4.41.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.3.tgz",
"integrity": "sha512-EcNzP9jGoxpQAXq1VOoTet0ik7/VVU1MovIfcUSAjLowc7GhcQku/sOXALvq5nPpSei2HF6VRhibeJSC3i/Law==",
"dev": true,
"requires": {
"@webassemblyjs/ast": "1.8.5",
@ -23632,7 +23632,7 @@
"node-libs-browser": "^2.2.1",
"schema-utils": "^1.0.0",
"tapable": "^1.1.3",
"terser-webpack-plugin": "^1.4.1",
"terser-webpack-plugin": "^1.4.3",
"watchpack": "^1.6.0",
"webpack-sources": "^1.4.1"
}

View File

@ -37,10 +37,10 @@
"@fortawesome/free-brands-svg-icons": "^5.12.0",
"@fortawesome/free-regular-svg-icons": "^5.12.0",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/vue-fontawesome": "^0.1.8",
"@fortawesome/vue-fontawesome": "^0.1.9",
"ant-design-vue": "~1.4.10",
"axios": "^0.19.0",
"core-js": "^3.4.8",
"core-js": "^3.5.0",
"enquire.js": "^2.1.6",
"js-cookie": "^2.2.1",
"lodash.get": "^4.4.2",
@ -51,10 +51,10 @@
"npm-check-updates": "^4.0.1",
"nprogress": "^0.2.0",
"viser-vue": "^2.4.7",
"vue": "^2.6.10",
"vue": "^2.6.11",
"vue-clipboard2": "^0.3.1",
"vue-cropper": "0.4.9",
"vue-i18n": "^8.15.1",
"vue-i18n": "^8.15.3",
"vue-ls": "^3.2.1",
"vue-router": "^3.1.3",
"vue-svg-component-runtime": "^1.0.1",
@ -87,8 +87,8 @@
"sass-loader": "^8.0.0",
"vue-cli-plugin-i18n": "^0.6.0",
"vue-svg-icon-loader": "^2.1.1",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.41.2"
"vue-template-compiler": "^2.6.11",
"webpack": "^4.41.3"
},
"eslintConfig": {
"root": true,

View File

@ -35,13 +35,36 @@
:dataSource="detailOptions[newKey]"
placeholder="Value"
@change="e => onAddInputChange(e, 'newValue')" />
<a-button type="dashed" style="width: 50%" icon="close" @click="showAddDetail = false">Cancel</a-button>
<a-button type="primary" style="width: 50%" icon="plus" @click="addDetail">Add Setting</a-button>
<a-button type="primary" style="width: 25%" icon="plus" @click="addDetail">Add Setting</a-button>
<a-button type="dashed" style="width: 25%" icon="close" @click="showAddDetail = false">Cancel</a-button>
</div>
<a-list size="large">
<a-list-item :key="index" v-for="(item, index) in details">
<a-list-item-meta>
<span slot="title"><strong>{{ item.name }}</strong></span>
<span slot="title">
{{ item.name }}
<a-button shape="circle" size="small" @click="updateDetail(index)" v-if="item.edit">
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
</a-button>
<a-button shape="circle" size="small" @click="hideEditDetail(index)" v-if="item.edit" style="margin-left: 5px">
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
<a-button shape="circle" size="small" @click="showEditDetail(index)" v-if="!item.edit">
<a-icon type="edit" />
</a-button>
<a-divider type="vertical" />
<a-popconfirm
title="Delete setting?"
@confirm="deleteDetail(index)"
okText="Yes"
cancelText="No"
placement="right"
>
<a-button shape="circle" size="small">
<a-icon type="delete" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
</a-popconfirm>
</span>
<span slot="description" style="word-break: break-all">
<span v-if="item.edit" style="display: flex">
<a-auto-complete
@ -54,30 +77,6 @@
<span v-else @click="showEditDetail(index)">{{ item.value }}</span>
</span>
</a-list-item-meta>
<div slot="actions">
<a-button shape="circle" size="default" @click="updateDetail(index)" v-if="item.edit">
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
</a-button>
<a-button shape="circle" size="default" @click="hideEditDetail(index)" v-if="item.edit">
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
<a-button shape="circle" @click="showEditDetail(index)" v-if="!item.edit">
<a-icon type="edit" />
</a-button>
</div>
<div slot="actions">
<a-popconfirm
title="Delete setting?"
@confirm="deleteDetail(index)"
okText="Yes"
cancelText="No"
placement="left"
>
<a-button shape="circle">
<a-icon type="delete" theme="twoTone" twoToneColor="#f5222d" />
</a-button>
</a-popconfirm>
</div>
</a-list-item>
</a-list>
</a-spin>

View File

@ -17,13 +17,13 @@
<template>
<a-table
size="small"
size="middle"
:loading="loading"
:columns="columns"
:dataSource="items"
:rowKey="record => record.id || record.name"
:pagination="false"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:rowSelection="['vm', 'event', 'alert'].includes($route.name) ? {selectedRowKeys: selectedRowKeys, onChange: onSelectChange} : null"
:rowClassName="getRowClassName"
>
<template slot="footer">
@ -33,7 +33,7 @@
</template>
<a slot="name" slot-scope="text, record" href="javascript:;">
<div>
<div style="min-width: 150px; padding-left: 5px">
<span v-if="$route.path.startsWith('/project')" style="margin-right: 5px">
<a-button type="dashed" size="small" shape="circle" icon="login" @click="changeProject(record)" />
</span>
@ -41,7 +41,7 @@
<router-link :to="{ path: $route.path + '/' + record.id }" v-if="record.id">{{ text }}</router-link>
<router-link :to="{ path: $route.path + '/' + record.name }" v-else>{{ text }}</router-link>
</div>
<div v-if="$route.meta.related" style="padding-top: 5px">
<div v-if="$route.meta.related" style="padding-top: 10px; padding-left: 5px; display: inline-flex">
<span v-for="item in $route.meta.related" :key="item.path">
<router-link
v-if="$router.resolve('/' + item.name).route.name !== '404'"

View File

@ -2,7 +2,7 @@
<a-list size="large" class="list" :loading="loading || tabLoading">
<a-list-item :key="index" v-for="(item, index) in items" class="item">
<a-list-item-meta>
<span slot="title" style="word-break: break-all"><strong>{{ item.name }}</strong></span>
<span slot="title" style="word-break: break-all">{{ item.name }}</span>
<span slot="description" style="word-break: break-all">{{ item.description }}</span>
</a-list-item-meta>

View File

@ -173,8 +173,16 @@ export default {
icon: 'safety-certificate',
label: 'Add certificate',
dataView: true,
args: ['name', 'certificate', 'privatekey', 'certchain', 'password'],
show: (record) => { return record.state === 'enabled' }
args: ['name', 'certificate', 'privatekey', 'certchain', 'password', 'account', 'domainid'],
show: (record) => { return record.state === 'enabled' },
mapping: {
account: {
value: (record) => { return record.name }
},
domainid: {
value: (record) => { return record.domainid }
}
}
},
{
api: 'deleteAccount',

View File

@ -134,6 +134,7 @@
"current": "isCurrent",
"date": "Date",
"dedicated": "Dedicated",
"default": "Default",
"deleteconfirm": "Please confirm that you would like to delete this {name}",
"deleteprofile": "Delete Profile",
"deploymentPlanner": "Deployment planner",
@ -632,6 +633,10 @@
"memused": "Memory Usage",
"message.edit.account": "Edit (\"-1\" indicates no limit to the amount of resources create)",
"message.assign.instance.another": "Please specify the account type, domain, account name and network (optional) of the new account. <br> If the default nic of the vm is on a shared network, CloudStack will check if the network can be used by the new account if you do not specify one network. <br> If the default nic of the vm is on a isolated network, and the new account has more one isolated networks, you should specify one.",
"message.network.addVM.desc":"Please specify the network that you would like to add this VM to. A new NIC will be added for this network.",
"message.network.removeNIC": "Please confirm that want to remove this NIC, which will also remove the associated network from the VM.",
"message.network.secondaryIP" : "Please confirm that you would like to acquire a new secondary IP for this NIC. \n NOTE: You need to manually configure the newly-acquired secondary IP inside the virtual machine.",
"message.network.updateIp": "Please confirm that you would like to change the IP address for this NIC on VM.",
"minCPUNumber": "Min CPU Cores",
"minInstance": "Min Instances",
"minIops": "Min IOPS",
@ -729,6 +734,7 @@
"protocolnumber": "#Protocol",
"provider": "HA Provider",
"providername": "Provider",
"provisioning": "Provisioning",
"provisioningType": "Provisioning Type",
"provisioningtype": "Provisioning Type",
"publicinterface": "Public Interface",
@ -754,6 +760,7 @@
"redundantstate": "Redundant state",
"redundantvpcrouter": "Redundant VPC",
"reenterpassword": "Re-enter Password",
"refresh": "Refresh",
"relationaloperator": "Operator",
"required": "Required",
"requireshvm": "HVM",
@ -946,6 +953,7 @@
"zoneId": "Zone",
"zoneid": "Zone",
"zonename": "Zone",
"zonenamelabel": "Zone Name",
"instance": "Instance",
"yourInstance": "Your instance",
"newInstance": "New instance",

View File

@ -19,23 +19,25 @@
<div>
<a-card class="breadcrumb-card">
<a-row>
<a-col :span="12">
<breadcrumb style="padding-top: 6px" />
<a-col :span="24" style="display: flex">
<breadcrumb />
<a-tooltip placement="bottom">
<template slot="title">
{{ "Refresh" }}
</template>
<a-button
style="margin-left: 8px"
:loading="loading"
shape="round"
size="small"
icon="sync"
@click="fetchData()">
{{ $t('refresh') }}
</a-button>
</a-tooltip>
</a-col>
<a-col :span="12">
<span style="float: right">
<a-tooltip placement="bottom">
<template slot="title">
{{ "Refresh" }}
</template>
<a-button
:loading="loading"
shape="circle"
type="dashed"
icon="reload"
style="margin-right: 5px"
@click="fetchData()" />
</a-tooltip>
<a-col :span="24" style="padding-top: 12px; margin-bottom: -6px">
<span style="padding-left: 5px">
<a-tooltip
v-for="(action, actionIndex) in actions"
:key="actionIndex"
@ -56,7 +58,7 @@
</a-button>
</a-tooltip>
<a-input-search
style="width: unset"
style="width: 50%; padding-left: 6px"
placeholder="Search"
v-model="searchQuery"
v-if="!dataView && !treeView"

View File

@ -17,18 +17,22 @@
<template>
<div>
<a-collapse v-model="activeKey">
<a-collapse v-model="activeKey" :bordered="false">
<a-collapse-panel :header="'ISO: ' + vm.isoname" v-if="vm.isoid" key="1">
<a-list
itemLayout="horizontal">
<a-list-item>
<a-list-item-meta :description="vm.isoid">
<a slot="title" href="">
<router-link :to="{ path: '/iso/' + vm.isoid }">{{ vm.isoname }}</router-link>
</a> ({{ vm.isoname }})
<a-avatar slot="avatar">
<a-icon type="usb" />
</a-avatar>
<a-list-item-meta>
<div slot="avatar">
<a-avatar>
<a-icon type="usb" />
</a-avatar>
</div>
<div slot="title">
<router-link :to="{ path: '/iso/' + vm.isoid }">{{ vm.isoname }}</router-link> <br/>
<a-icon type="barcode"/> {{ vm.isoid }}
</div>
</a-list-item-meta>
</a-list-item>
</a-list>
@ -42,59 +46,220 @@
>
<a-list-item slot="renderItem" slot-scope="item">
<a-list-item-meta>
<div slot="avatar">
<a-avatar>
<a-icon type="hdd" />
</a-avatar>
</div>
<div slot="title">
<router-link :to="{ path: '/volume/' + item.id }">{{ item.name }}</router-link> ({{ item.type }}) <br/>
<status :text="item.state" displayText /><br/>
<router-link :to="{ path: '/volume/' + item.id }">{{ item.name }} </router-link>
<a-tag v-if="item.type">
{{ item.type }}
</a-tag>
<a-tag v-if="item.state">
{{ item.state }}
</a-tag>
<a-tag v-if="item.provisioningtype">
{{ item.provisioningtype }}
</a-tag>
<br/>
{{ $t('size') }}: {{ (item.size / (1024 * 1024 * 1024.0)).toFixed(2) }} GB<br/>
{{ $t('physicalsize') }}: {{ (item.physicalsize / (1024 * 1024 * 1024.0)).toFixed(4) }} GB<br/>
{{ $t('storagePool') }}: {{ item.storage }} ({{ item.storagetype }})<br/>
<a-icon type="barcode"/> {{ item.id }} <br/>
</div>
<div slot="description">
<a-icon type="barcode"/> {{ item.id }}
</div>
<a-avatar slot="avatar">
<a-icon type="hdd" />
</a-avatar>
</a-list-item-meta>
<p>
Size: {{ (item.size / (1024 * 1024 * 1024.0)).toFixed(4) }} GB<br/>
Physical Size: {{ (item.physicalsize / (1024 * 1024 * 1024.0)).toFixed(4) }} GB<br/>
Provisioning: {{ item.provisioningtype }}<br/>
Storage Pool: {{ item.storage }} ({{ item.storagetype }})<br/>
</p>
<div slot="actions" class="actions">
</div>
</a-list-item>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="'Network Adapter(s): ' + (vm && vm.nic ? vm.nic.length : 0)" key="3" >
<a-button type="primary" @click="showAddModal" :loading="loadingNic">
<a-icon type="plus"></a-icon> {{ $t('label.network.addVM') }}
</a-button>
<a-list
size="small"
itemLayout="horizontal"
:dataSource="vm.nic"
class="list"
:loading="loadingNic"
>
<a-list-item slot="renderItem" slot-scope="item">
<a-list-item slot="renderItem" slot-scope="item" class="list__item">
<a-list-item-meta>
<div slot="title">
<span v-show="item.isdefault">(Default) </span>
<router-link :to="{ path: '/guestnetwork/' + item.networkid }">{{ item.networkname }} </router-link><br/>
Mac Address: {{ item.macaddress }}<br/>
<span v-if="item.ipaddress">Address: {{ item.ipaddress }} <br/></span>
Netmask: {{ item.netmask }}<br/>
Gateway: {{ item.gateway }}<br/>
<div slot="avatar">
<a-avatar slot="avatar">
<a-icon type="wifi" />
</a-avatar>
<br/>
<a-popconfirm
title="Please confirm that you would like to make this NIC the default for this VM."
@confirm="setAsDefault(item)"
okText="Yes"
cancelText="No"
>
<a-button
style="margin-top: 10px"
icon="arrow-right"
size="small"
shape="round" />
</a-popconfirm>
<br/>
<a-tooltip placement="right" v-if="item.type !== 'L2'">
<template slot="title">
{{ "Change IP Address" }}
</template>
<a-button
style="margin-top: 10px"
icon="swap"
size="small"
shape="round"
@click="editIpAddressNic = item.id; showUpdateIpModal = true" />
</a-tooltip>
<br/>
<a-tooltip placement="right" v-if="item.type !== 'L2'">
<template slot="title">
{{ "Manage Secondary IP Addresses" }}
</template>
<a-button
style="margin-top: 10px"
icon="environment"
size="small"
shape="round"
@click="fetchSecondaryIPs(item.id)" />
</a-tooltip>
<br/>
<a-popconfirm
:title="$t('message.network.removeNIC')"
@confirm="removeNIC(item)"
okText="Yes"
cancelText="No"
v-if="!item.isdefault"
>
<a-button
style="margin-top: 10px"
type="danger"
icon="delete"
size="small"
shape="round" />
</a-popconfirm>
</div>
<div slot="description">
<div slot="title">
<router-link :to="{ path: '/guestnetwork/' + item.networkid }">{{ item.networkname }} </router-link>
<a-tag v-if="item.isdefault">
{{ $t('default') }}
</a-tag>
<a-tag v-if="item.type">
{{ item.type }}
</a-tag>
<a-tag v-if="item.broadcasturi">
{{ item.broadcasturi }}
</a-tag>
<a-tag v-if="item.isolationuri">
{{ item.isolationuri }}
</a-tag>
<br />
{{ $t('macaddress') }}: {{ item.macaddress }}<br/>
<span v-if="item.ipaddress">
{{ $t('IP Address') }}: {{ item.ipaddress }}
<br/>
</span>
<span v-if="item.secondaryip && item.type !== 'L2'">
{{ $t('Secondary IPs') }}: {{ item.secondaryip.map(x => x.ipaddress).join(', ') }}
<br/>
</span>
<span v-if="item.netmask">
{{ $t('netmask') }}: {{ item.netmask }}
<br/>
</span>
<span v-if="item.gateway">
{{ $t('gateway') }}: {{ item.gateway }}
<br/>
</span>
<a-icon type="barcode"/> {{ item.id }}
</div>
<a-avatar slot="avatar">
<a-icon type="wifi" />
</a-avatar>
</a-list-item-meta>
<p>
Type: {{ item.type }}<br/>
Broadcast URI: {{ item.broadcasturi }}<br/>
Isolation URI: {{ item.isolationuri }}<br/>
</p>
</a-list-item>
</a-list>
</a-collapse-panel>
</a-collapse>
<a-modal
:visible="showAddNetworkModal"
:title="$t('label.network.addVM')"
@cancel="closeModals"
@ok="submitAddNetwork">
{{ $t('message.network.addVM.desc') }}
<div class="modal-form">
<p class="modal-form__label">{{ $t('Network') }}:</p>
<a-select :defaultValue="addNetworkData.network" @change="e => addNetworkData.network === e">
<a-select-option
v-for="network in addNetworkData.allNetworks"
:key="network.id"
:value="network.id">
{{ network.name }}
</a-select-option>
</a-select>
<p class="modal-form__label">{{ $t('publicip') }}:</p>
<a-input v-model="addNetworkData.ip"></a-input>
</div>
</a-modal>
<a-modal
:visible="showUpdateIpModal"
:title="$t('label.change.ipaddress')"
@cancel="closeModals"
@ok="submitUpdateIP"
>
{{ $t('message.network.updateIp') }}
<div class="modal-form">
<p class="modal-form__label">{{ $t('publicip') }}:</p>
<a-input v-model="editIpAddressValue"></a-input>
</div>
</a-modal>
<a-modal
:visible="showSecondaryIpModal"
:title="$t('label.acquire.new.secondary.ip')"
:footer="null"
:closable="false"
class="wide-modal"
>
<p>
{{ $t('message.network.secondaryIP') }}
</p>
<a-divider />
<a-input placeholder="Enter new secondary IP address" v-model="newSecondaryIp"></a-input>
<div style="margin-top: 10px; display: flex; justify-content:flex-end;">
<a-button @click="submitSecondaryIP" type="primary" style="margin-right: 10px;">Add Secondary IP</a-button>
<a-button @click="closeModals">Close</a-button>
</div>
<a-divider />
<a-list itemLayout="vertical">
<a-list-item v-for="(ip, index) in secondaryIPs" :key="index">
<a-popconfirm
title="Release IP?"
@confirm="removeSecondaryIP(ip.id)"
okText="Yes"
cancelText="No"
>
<a-button
type="danger"
shape="circle"
size="small"
icon="delete" />
{{ ip.ipaddress }}
</a-popconfirm>
</a-list-item>
</a-list>
</a-modal>
</div>
</template>
@ -120,12 +285,27 @@ export default {
default: false
}
},
inject: ['parentFetchData'],
data () {
return {
vm: {},
volumes: [],
totalStorage: 0,
activeKey: ['1', '2', '3']
activeKey: ['1', '2', '3'],
showAddNetworkModal: false,
showUpdateIpModal: false,
showSecondaryIpModal: false,
addNetworkData: {
allNetworks: [],
network: '',
ip: ''
},
loadingNic: false,
editIpAddressNic: '',
editIpAddressValue: '',
secondaryIPs: [],
selectedNicId: '',
newSecondaryIp: ''
}
},
created () {
@ -154,26 +334,339 @@ export default {
}
this.$set(this.resource, 'volumes', this.volumes)
})
},
listNetworks () {
api('listNetworks', {
listAll: 'true',
zoneid: this.vm.zoneid
}).then(response => {
this.addNetworkData.allNetworks = response.listnetworksresponse.network.filter(network => !this.vm.nic.map(nic => nic.networkid).includes(network.id))
this.addNetworkData.network = this.addNetworkData.allNetworks[0].id
})
},
fetchSecondaryIPs (nicId) {
this.showSecondaryIpModal = true
this.selectedNicId = nicId
api('listNics', {
nicId: nicId,
keyword: '',
virtualmachineid: this.vm.id
}).then(response => {
this.secondaryIPs = response.listnicsresponse.nic[0].secondaryip
})
},
showAddModal () {
this.showAddNetworkModal = true
this.listNetworks()
},
closeModals () {
this.showAddNetworkModal = false
this.showUpdateIpModal = false
this.showSecondaryIpModal = false
this.addNetworkData.network = ''
this.addNetworkData.ip = ''
this.editIpAddressValue = ''
this.newSecondaryIp = ''
},
submitAddNetwork () {
const params = {}
params.virtualmachineid = this.vm.id
params.networkid = this.addNetworkData.network
if (this.addNetworkData.ip) {
params.ipaddress = this.addNetworkData.ip
}
this.showAddNetworkModal = false
this.loadingNic = true
api('addNicToVirtualMachine', params).then(response => {
this.$pollJob({
jobId: response.addnictovirtualmachineresponse.jobid,
successMessage: `Successfully added network`,
successMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
},
errorMessage: 'Adding network failed',
errorMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
},
loadingMessage: `Adding network...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.loadingNic = false
})
},
setAsDefault (item) {
this.loadingNic = true
api('updateDefaultNicForVirtualMachine', {
virtualmachineid: this.vm.id,
nicid: item.id
}).then(response => {
this.$pollJob({
jobId: response.updatedefaultnicforvirtualmachineresponse.jobid,
successMessage: `Successfully set ${item.networkname} to default. Please manually update the default NIC on the VM now.`,
successMethod: () => {
this.loadingNic = false
this.parentFetchData()
},
errorMessage: `Error setting ${item.networkname} to default`,
errorMethod: () => {
this.loadingNic = false
this.parentFetchData()
},
loadingMessage: `Setting ${item.networkname} to default...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.loadingNic = false
this.parentFetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.loadingNic = false
})
},
submitUpdateIP () {
this.loadingNic = true
this.showUpdateIpModal = false
api('updateVmNicIp', {
nicId: this.editIpAddressNic,
ipaddress: this.editIpAddressValue
}).then(response => {
this.$pollJob({
jobId: response.updatevmnicipresponse.jobid,
successMessage: `Successfully updated IP Address`,
successMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
},
errorMessage: `Error`,
errorMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
},
loadingMessage: `Updating IP Address...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
}
})
})
.catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.loadingNic = false
})
},
removeNIC (item) {
this.loadingNic = true
api('removeNicFromVirtualMachine', {
nicid: item.id,
virtualmachineid: this.vm.id
}).then(response => {
this.$pollJob({
jobId: response.removenicfromvirtualmachineresponse.jobid,
successMessage: `Successfully removed`,
successMethod: () => {
this.loadingNic = false
this.parentFetchData()
},
errorMessage: `There was an error`,
errorMethod: () => {
this.loadingNic = false
this.parentFetchData()
},
loadingMessage: `Removing NIC...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.loadingNic = false
this.parentFetchData()
}
})
})
.catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.loadingNic = false
})
},
submitSecondaryIP () {
this.loadingNic = true
const params = {}
params.nicid = this.selectedNicId
if (this.newSecondaryIp) {
params.ipaddress = this.newSecondaryIp
}
api('addIpToNic', params).then(response => {
this.$pollJob({
jobId: response.addiptovmnicresponse.jobid,
successMessage: `Successfully added secondary IP Address`,
successMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
},
errorMessage: `There was an error adding the secondary IP Address`,
errorMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
},
loadingMessage: `Add Secondary IP address...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.addiptovmnicresponse.errortext
})
this.loadingNic = false
})
},
removeSecondaryIP (id) {
this.loadingNic = true
api('removeIpFromNic', { id }).then(response => {
this.$pollJob({
jobId: response.removeipfromnicresponse.jobid,
successMessage: `Successfully removed secondary IP Address`,
successMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
},
errorMessage: `There was an error removing the secondary IP Address`,
errorMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
},
loadingMessage: `Removing Secondary IP address...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
})
}
}
}
</script>
<style lang="less" scoped>
.page-header-wrapper-grid-content-main {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
.vm-detail {
.svg-inline--fa {
margin-left: -1px;
margin-right: 8px;
<style lang="scss" scoped>
.page-header-wrapper-grid-content-main {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
.vm-detail {
.svg-inline--fa {
margin-left: -1px;
margin-right: 8px;
}
span {
margin-left: 10px;
}
margin-bottom: 8px;
}
}
.list {
margin-top: 20px;
&__item {
display: flex;
flex-direction: column;
align-items: flex-start;
@media (min-width: 760px) {
flex-direction: row;
align-items: center;
}
}
}
.modal-form {
display: flex;
flex-direction: column;
&__label {
margin-top: 20px;
margin-bottom: 5px;
font-weight: bold;
&--no-margin {
margin-top: 0;
}
}
}
.actions {
display: flex;
margin-left: -24px;
button {
&:not(:last-child) {
margin-right: 10px;
}
}
@media (min-width: 760px) {
flex-direction: column;
margin-left: 24px;
button {
&:not(:last-child) {
margin-bottom: 10px;
margin-right: 0;
}
}
}
span {
margin-left: 10px;
}
margin-bottom: 8px;
}
}
</style>
<style lang="scss">
.wide-modal {
min-width: 50vw;
}
</style>

View File

@ -19,23 +19,24 @@
<a-row :gutter="24">
<a-col :md="24">
<a-card class="breadcrumb-card">
<a-col :md="14">
<a-col :md="24" style="display: flex">
<breadcrumb style="padding-top: 6px" />
</a-col>
<a-col :md="10">
<a-button
style="margin-left: 10px; float: right"
@click="fetchData()"
icon="reload"
style="margin-left: 12px; margin-top: 4px"
:loading="loading"
type="primary">
{{ $t('Refresh') }}
icon="reload"
size="small"
shape="round"
@click="fetchData()" >
{{ $t('refresh') }}
</a-button>
<a-button
style="margin-left: 10px; float: right"
@click="sslFormVisible = true"
icon="safety-certificate">
{{ $t('SSL Certificate') }}
style="margin-left: 12px; margin-top: 4px"
icon="safety-certificate"
size="small"
shape="round"
@click="sslFormVisible = true">
{{ $t('Setup SSL Certificate') }}
</a-button>
<a-modal
:title="$t('SSL Certificate')"