This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new 3e2c584 network: Egress, PF, FW, VPN, LB tabs (#84)
3e2c584 is described below
commit 3e2c584ee0650d8d096084a533b56b943202b469
Author: Ritchie Vincent <[email protected]>
AuthorDate: Tue Jan 21 07:56:30 2020 +0000
network: Egress, PF, FW, VPN, LB tabs (#84)
Implements the egress, pf, fw, vpn and lb tabs for a guest network (ip).
Signed-off-by: Rohit Yadav <[email protected]>
Co-authored-by: Rohit Yadav <[email protected]>
---
docs/api/apis.remaining | 43 -
legacy/generateOldLayout.js | 14 +-
src/components/view/InfoCard.vue | 2 +-
src/components/view/ResourceView.vue | 38 +-
src/config/section/network.js | 61 +-
src/locales/en.json | 14 +
src/utils/device.js | 4 +-
src/views/network/EgressConfigure.vue | 271 ++++-
src/views/network/FirewallRules.vue | 476 ++++++++
src/views/network/IngressEgressRuleConfigure.vue | 2 +-
src/views/network/IpConfigure.vue | 40 -
src/views/network/LoadBalancing.vue | 1373 ++++++++++++++++++++++
src/views/network/PortForwarding.vue | 675 +++++++++++
src/views/network/VpnDetails.vue | 175 ++-
14 files changed, 3041 insertions(+), 147 deletions(-)
diff --git a/docs/api/apis.remaining b/docs/api/apis.remaining
index c776794..a9775d3 100644
--- a/docs/api/apis.remaining
+++ b/docs/api/apis.remaining
@@ -2,24 +2,16 @@ addNetworkServiceProvider
addResourceDetail
addTrafficType
assignCertToLoadBalancer
-assignToLoadBalancerRule
authorizeSamlSso
-authorizeSecurityGroupEgress
-authorizeSecurityGroupIngress
configureInternalLoadBalancerElement
configureVirtualRouterElement
-createEgressFirewallRule
-createFirewallRule
createLBHealthCheckPolicy
-createLBStickinessPolicy
createLoadBalancer
-createLoadBalancerRule
createManagementNetworkIpRange
createNetworkACL
createNetworkACLList
createPhysicalNetwork
createPortableIpRange
-createPortForwardingRule
createPrivateGateway
createSecondaryStagingStore
createSnapshotFromVMSnapshot
@@ -30,25 +22,17 @@ createVpnConnection
createVpnGateway
dedicateGuestVlanRange
dedicatePublicIpRange
-deleteAccountFromProject
-deleteEgressFirewallRule
-deleteFirewallRule
deleteLBHealthCheckPolicy
-deleteLBStickinessPolicy
-deleteLdapConfiguration
deleteLoadBalancer
-deleteLoadBalancerRule
deleteManagementNetworkIpRange
deleteNetworkACL
deleteNetworkACLList
deleteNetworkServiceProvider
deletePhysicalNetwork
deletePortableIpRange
-deletePortForwardingRule
deletePrivateGateway
deleteProjectInvitation
deleteSecondaryStagingStore
-deleteSnapshotPolicies
deleteStaticRoute
deleteStorageNetworkIpRange
deleteVlanIpRange
@@ -57,74 +41,47 @@ deleteVpnGateway
findStoragePoolsForMigration
importLdapUsers
ldapCreateAccount
-linkDomainToLdap
listAffinityGroupTypes
listAndSwitchSamlAccount
-listCapabilities
listDedicatedClusters
listDedicatedGuestVlanRanges
listDedicatedHosts
listDedicatedPods
listDedicatedZones
listDeploymentPlanners
-listEgressFirewallRules
-listFirewallRules
listHostHAProviders
listHostTags
-listHypervisors
listIdps
listInternalLoadBalancerElements
listInternalLoadBalancerVMs
listLBHealthCheckPolicies
-listLBStickinessPolicies
listLdapUsers
-listLoadBalancerRuleInstances
-listLoadBalancerRules
listLoadBalancers
-listNetworkACLLists
listNetworkACLs
listNetworkServiceProviders
listOsCategories
listPortableIpRanges
-listPortForwardingRules
-listPrivateGateways
-listProjectAccounts
-listProjectInvitations
listRegisteredServicePackages
-listRemoteAccessVpns
-listResourceLimits
listSamlAuthorization
listSecondaryStagingStores
-listSnapshotPolicies
listStaticRoutes
listStorageNetworkIpRange
listStorageProviders
listStorageTags
listSupportedNetworkServices
listTemplateOvfProperties
-listTemplatePermissions
listTrafficTypes
listVirtualRouterElements
listVlanIpRanges
listVmwareDcs
-listVpnConnections
-listVpnGateways
moveNetworkAclItem
releaseDedicatedGuestVlanRange
releasePublicIpRange
-removeFromLoadBalancerRule
-replaceNetworkACLList
resetVpnConnection
-revokeSecurityGroupEgress
-revokeSecurityGroupIngress
startInternalLoadBalancerVM
stopInternalLoadBalancerVM
-updateLoadBalancerRule
updateNetworkACLItem
updateNetworkACLList
updateNetworkServiceProvider
updatePhysicalNetwork
-updateProjectInvitation
-updateResourceLimit
updateTrafficType
-updateVpnCustomerGateway
diff --git a/legacy/generateOldLayout.js b/legacy/generateOldLayout.js
index df7424c..9850162 100644
--- a/legacy/generateOldLayout.js
+++ b/legacy/generateOldLayout.js
@@ -13,8 +13,8 @@ var loadLabel = function (allFields, fieldDict, prefix) {
allFields[fieldId].components.push(prefix)
} else {
allFields[fieldId] = {
- 'labels': [fieldDict[fieldId].label],
- 'components': [prefix]
+ labels: [fieldDict[fieldId].label],
+ components: [prefix]
}
}
cols = cols + "'" + fieldId + "', "
@@ -28,8 +28,8 @@ var loadLabel = function (allFields, fieldDict, prefix) {
allFields[colId].components.push(prefix)
} else {
allFields[colId] = {
- 'labels': [columns[colId].label],
- 'components': [prefix]
+ labels: [columns[colId].label],
+ components: [prefix]
}
}
})
@@ -63,9 +63,9 @@ var loadFields = function (data, prefix) {
var curActions = []
$.each(Object.keys(acVal), function (idx, acKey) {
if (acVal[acKey].createForm) {
- curActions.push({ 'action': acKey, 'label': acVal[acKey].label,
'keys': acVal[acKey].createForm.fields })
+ curActions.push({ action: acKey, label: acVal[acKey].label, keys:
acVal[acKey].createForm.fields })
} else {
- curActions.push({ 'action': acKey, 'label': acVal[acKey].label })
+ curActions.push({ action: acKey, label: acVal[acKey].label })
}
})
countActions = countActions + curActions.length
@@ -77,5 +77,5 @@ var loadFields = function (data, prefix) {
$.extend(actions, recRes.actions)
}
})
- return { 'allFields': allFields, 'columnsOrder': columnsOrder, 'actions':
actions }
+ return { allFields: allFields, columnsOrder: columnsOrder, actions: actions }
}
diff --git a/src/components/view/InfoCard.vue b/src/components/view/InfoCard.vue
index 7ac739b..e63a31a 100644
--- a/src/components/view/InfoCard.vue
+++ b/src/components/view/InfoCard.vue
@@ -29,7 +29,7 @@
</div>
<slot name="name">
<h4 class="name">
- {{ resource.displayname || resource.name }}
+ {{ resource.displayname || resource.name ||
resource.displaytext || resource.hostname || resource.username ||
resource.ipaddress }}
</h4>
<console style="margin-left: 10px" :resource="resource"
size="default" v-if="resource.id" />
</slot>
diff --git a/src/components/view/ResourceView.vue
b/src/components/view/ResourceView.vue
index f197628..a1c4bf7 100644
--- a/src/components/view/ResourceView.vue
+++ b/src/components/view/ResourceView.vue
@@ -36,7 +36,7 @@
v-for="tab in tabs"
:tab="$t(tab.name)"
:key="tab.name"
- v-if="'show' in tab ? tab.show(resource, $route,
$store.getters.userInfo) : true">
+ v-if="showHideTab(tab)">
<component :is="tab.component" :resource="resource"
:loading="loading" :tab="activeTab" />
</a-tab-pane>
</a-tabs>
@@ -49,6 +49,7 @@
import DetailsTab from '@/components/view/DetailsTab'
import InfoCard from '@/components/view/InfoCard'
import ResourceLayout from '@/layouts/ResourceLayout'
+import { api } from '@/api'
export default {
name: 'ResourceView',
@@ -77,12 +78,45 @@ export default {
},
data () {
return {
- activeTab: ''
+ activeTab: '',
+ networkService: null,
+ vpnEnabled: false
+ }
+ },
+ watch: {
+ resource: function (newItem, oldItem) {
+ this.resource = newItem
+ if (newItem.id === oldItem.id) return
+
+ if (this.resource.associatednetworkid) {
+ api('listNetworks', { id: this.resource.associatednetworkid
}).then(response => {
+ this.networkService = response.listnetworksresponse.network[0]
+ })
+ }
+
+ if (this.resource.id && this.resource.ipaddress) {
+ api('listRemoteAccessVpns', {
+ publicipid: this.resource.id,
+ listAll: true
+ }).then(response => {
+ this.vpnEnabled =
response.listremoteaccessvpnsresponse.remoteaccessvpn &&
response.listremoteaccessvpnsresponse.remoteaccessvpn.length > 0
+ })
+ }
}
},
methods: {
onTabChange (key) {
this.activeTab = key
+ },
+ showHideTab (tab) {
+ if ('networkServiceFilter' in tab) {
+ return this.networkService && this.networkService.service &&
+ tab.networkServiceFilter(this.networkService.service)
+ } else if ('show' in tab) {
+ return tab.show(this.resource, this.$route,
this.$store.getters.userInfo)
+ } else {
+ return true
+ }
}
}
}
diff --git a/src/config/section/network.js b/src/config/section/network.js
index 6fbb781..a2a429a 100644
--- a/src/config/section/network.js
+++ b/src/config/section/network.js
@@ -45,8 +45,9 @@ export default {
name: 'details',
component: () => import('@/components/view/DetailsTab.vue')
}, {
- name: 'egress-rules',
- component: () => import('@/views/network/EgressConfigure.vue')
+ name: 'Egress Rules',
+ component: () => import('@/views/network/EgressConfigure.vue'),
+ show: () => true
}],
actions: [
{
@@ -227,14 +228,23 @@ export default {
columns: ['ipaddress', 'state', 'associatednetworkname',
'virtualmachinename', 'allocated', 'account', 'zonename'],
details: ['ipaddress', 'id', 'associatednetworkname',
'virtualmachinename', 'networkid', 'issourcenat', 'isstaticnat',
'virtualmachinename', 'vmipaddress', 'vlan', 'allocated', 'account',
'zonename'],
tabs: [{
- name: 'configure',
- component: () => import('@/views/network/IpConfigure.vue')
- }, {
- name: 'vpn',
- component: () => import('@/views/network/VpnDetails.vue')
- }, {
name: 'details',
component: () => import('@/components/view/DetailsTab.vue')
+ }, {
+ name: 'Firewall',
+ component: () => import('@/views/network/FirewallRules.vue'),
+ networkServiceFilter: networkService => networkService.filter(x =>
x.name === 'Firewall').length > 0
+ }, {
+ name: 'Port Forwarding',
+ component: () => import('@/views/network/PortForwarding.vue'),
+ networkServiceFilter: networkService => networkService.filter(x =>
x.name === 'PortForwarding').length > 0
+ }, {
+ name: 'Load Balancing',
+ component: () => import('@/views/network/LoadBalancing.vue'),
+ networkServiceFilter: networkService => networkService.filter(x =>
x.name === 'Lb').length > 0
+ }, {
+ name: 'VPN',
+ component: () => import('@/views/network/VpnDetails.vue')
}],
actions: [
{
@@ -245,39 +255,6 @@ export default {
args: ['networkid']
},
{
- api: 'createRemoteAccessVpn',
- icon: 'link',
- label: 'Enable Remote Access VPN',
- dataView: true,
- args: ['publicipid', 'domainid', 'account'],
- mapping: {
- publicipid: {
- value: (record) => { return record.id }
- },
- domainid: {
- value: (record) => { return record.domainid }
- },
- account: {
- value: (record) => { return record.account }
- }
- }
- },
- {
- api: 'deleteRemoteAccessVpn',
- icon: 'disconnect',
- label: 'Disable Remove Access VPN',
- dataView: true,
- args: ['publicipid', 'domainid'],
- mapping: {
- publicipid: {
- value: (record) => { return record.id }
- },
- domainid: {
- value: (record) => { return record.domainid }
- }
- }
- },
- {
api: 'enableStaticNat',
icon: 'plus-circle',
label: 'Enable Static NAT',
@@ -306,7 +283,7 @@ export default {
{
api: 'disassociateIpAddress',
icon: 'delete',
- label: 'Delete IP',
+ label: 'Release IP',
dataView: true,
show: (record) => { return !record.issourcenat }
}
diff --git a/src/locales/en.json b/src/locales/en.json
index 86d1684..f4abfaf 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -2,6 +2,7 @@
"Accounts": "Accounts",
"Affinity Groups": "Affinity Groups",
"Alerts": "Alerts",
+"cancel": "Cancel",
"CPU Sockets": "CPU Sockets",
"Cloudian Storage": "Cloudian Storage",
"Clusters": "Clusters",
@@ -13,6 +14,7 @@
"Dashboard": "Dashboard",
"Disk Offerings": "Disk Offerings",
"Domains": "Domains",
+"done": "Done",
"Events": "Events",
"Global Settings": "Global Settings",
"Hosts": "Hosts",
@@ -77,6 +79,7 @@
"agentUsername": "Agent Username",
"agentstate": "Agent State",
"algorithm": "Algorithm",
+"all": "All",
"allocatediops": "IOPS Allocated",
"allocationstate": "Allocation State",
"annotation": "Annotation",
@@ -256,6 +259,7 @@
"hypervisortype": "Hypervisor",
"hypervisorversion": "Hypervisor version",
"hypervnetworklabel": "HyperV Traffic Label",
+"icmp": "ICMP",
"icmpcode": "ICMP Code",
"icmptype": "ICMP Type",
"id": "ID",
@@ -340,6 +344,7 @@
"label.action.cancel.maintenance.mode": "Cancel Maintenance Mode",
"label.action.change.password": "Change Password",
"label.action.configure.samlauthorization": "Configure SAML SSO Authorization",
+"label.action.configure.stickiness": "Stickiness",
"label.action.copy.ISO": "Copy ISO",
"label.action.copy.template": "Copy Template",
"label.action.create.volume": "Create Volume",
@@ -430,6 +435,8 @@
"label.add.OpenDaylight.device": "Add OpenDaylight Controller",
"label.add.PA.device": "Add Palo Alto device",
"label.add.SRX.device": "Add SRX device",
+"label.add.VM": "Add VM",
+"label.add.VMs": "Add VMs",
"label.add.VM.to.tier": "Add VM to tier",
"label.add.account": "Add Account",
"label.add.acl.list": "Add ACL List",
@@ -460,8 +467,10 @@
"label.add.primary.storage": "Add Primary Storage",
"label.add.region": "Add Region",
"label.add.role": "Add Role",
+"label.add.rule": "Add Rule",
"label.add.secondary.storage": "Add Secondary Storage",
"label.add.security.group": "Add Security Group",
+"label.add.setting": "Add Setting",
"label.add.system.service.offering": "Add System Service Offering",
"label.add.ucs.manager": "Add UCS Manager",
"label.add.user": "Add User",
@@ -840,6 +849,7 @@
"secretkey": "Secret Key",
"securityGroups": "Security Groups",
"securitygroup": "Security Group",
+"select": "Select",
"sent": "Date",
"sentbytes": "Bytes Sent",
"server": "Server",
@@ -872,6 +882,7 @@
"snmpCommunity": "SNMP Community",
"snmpPort": "SNMP Port",
"sockettimeout": "Socket Timeout",
+"sourcecidr": "Source CIDR",
"sourceNat": "Source NAT",
"sourceipaddress": "Source IP Address",
"sourceport": "Source Port",
@@ -905,6 +916,7 @@
"systemvmtype": "System VM Type",
"tags": "Tags",
"tariffValue": "Tariff Value",
+"tcp": "TCP",
"template": "Select a template",
"templateFileUpload": "Local file",
"templateLimit": "Template Limits",
@@ -926,6 +938,7 @@
"traffictype": "Traffic Type",
"transportzoneuuid": "Transport Zone Uuid",
"type": "Type",
+"udp": "UDP",
"unit": "Usage Unit",
"url": "URL",
"usageName": "Usage Type",
@@ -964,6 +977,7 @@
"vlanRange": "VLAN/VNI Range",
"vlanname": "VLAN",
"vlanrange": "VLAN/VNI Range",
+"vm": "VM",
"vmLimit": "Instance Limits",
"vmTotal": "Instances",
"vmdisplayname": "VM display name",
diff --git a/src/utils/device.js b/src/utils/device.js
index 8c31370..731270d 100644
--- a/src/utils/device.js
+++ b/src/utils/device.js
@@ -45,6 +45,6 @@ export const deviceEnquire = function (callback) {
// screen and (max-width: 1087.99px)
enquireJs
.register('screen and (max-width: 576px)', matchMobile)
- .register('screen and (min-width: 576px) and (max-width: 1366px)',
matchTablet)
- .register('screen and (min-width: 1367px)', matchDesktop)
+ .register('screen and (min-width: 576px) and (max-width: 1280px)',
matchTablet)
+ .register('screen and (min-width: 1281px)', matchDesktop)
}
diff --git a/src/views/network/EgressConfigure.vue
b/src/views/network/EgressConfigure.vue
index 1bd3670..22d9db6 100644
--- a/src/views/network/EgressConfigure.vue
+++ b/src/views/network/EgressConfigure.vue
@@ -17,24 +17,287 @@
<template>
<div>
- TODO: Egress view for isolated network
+ <div>
+ <div class="form">
+ <div class="form__item">
+ <div class="form__label">Source CIDR</div>
+ <a-input v-model="newRule.cidrlist"></a-input>
+ </div>
+ <div class="form__item">
+ <div class="form__label">Destination CIDR</div>
+ <a-input v-model="newRule.destcidrlist"></a-input>
+ </div>
+ <div class="form__item">
+ <div class="form__label">Protocol</div>
+ <a-select v-model="newRule.protocol" style="width: 100%;"
@change="resetRulePorts">
+ <a-select-option value="tcp">TCP</a-select-option>
+ <a-select-option value="udp">UDP</a-select-option>
+ <a-select-option value="icmp">ICMP</a-select-option>
+ <a-select-option value="all">All</a-select-option>
+ </a-select>
+ </div>
+ <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'"
class="form__item">
+ <div class="form__label">Start Port</div>
+ <a-input v-model="newRule.startport"></a-input>
+ </div>
+ <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'"
class="form__item">
+ <div class="form__label">End Port</div>
+ <a-input v-model="newRule.endport"></a-input>
+ </div>
+ <div v-show="newRule.protocol === 'icmp'" class="form__item">
+ <div class="form__label">ICMP Type</div>
+ <a-input v-model="newRule.icmptype"></a-input>
+ </div>
+ <div v-show="newRule.protocol === 'icmp'" class="form__item">
+ <div class="form__label">ICMP Code</div>
+ <a-input v-model="newRule.icmpcode"></a-input>
+ </div>
+ <div class="form__item">
+ <a-button type="primary" icon="plus" @click="addRule">{{ $t('add')
}}</a-button>
+ </div>
+ </div>
+ </div>
+
+ <a-divider/>
+
+ <a-list :loading="loading" style="min-height: 25px;">
+ <a-list-item v-for="rule in egressRules" :key="rule.id" class="rule">
+ <div class="rule-container">
+ <div class="rule__item">
+ <div class="rule__title">Source CIDR</div>
+ <div>{{ rule.cidrlist }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">Destination CIDR</div>
+ <div>{{ rule.destcidrlist }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">Protocol</div>
+ <div>{{ rule.protocol | capitalise }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ rule.protocol === 'icmp' ? 'ICMP Type'
: 'Start Port' }}</div>
+ <div>{{ rule.icmptype || rule.startport >= 0 ? rule.icmptype ||
rule.startport : 'All' }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ rule.protocol === 'icmp' ? 'ICMP Code'
: 'End Port' }}</div>
+ <div>{{ rule.icmpcode || rule.endport >= 0 ? rule.icmpcode ||
rule.endport : 'All' }}</div>
+ </div>
+ <div slot="actions">
+ <a-button shape="round" type="danger" icon="delete"
@click="deleteRule(rule)" />
+ </div>
+ </div>
+ </a-list-item>
+ </a-list>
</div>
</template>
<script>
+import { api } from '@/api'
export default {
- name: '',
- components: {
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
},
data () {
return {
+ loading: true,
+ egressRules: [],
+ newRule: {
+ protocol: 'tcp',
+ cidrlist: null,
+ destcidrlist: null,
+ networkid: this.resource.id,
+ icmptype: null,
+ icmpcode: null,
+ startport: null,
+ endport: null
+ }
+ }
+ },
+ mounted () {
+ this.fetchData()
+ },
+ filters: {
+ capitalise: val => {
+ if (val === 'all') return 'All'
+ return val.toUpperCase()
+ }
+ },
+ watch: {
+ resource: function (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.resource = newItem
+ this.fetchData()
}
},
methods: {
+ fetchData () {
+ this.loading = true
+ api('listEgressFirewallRules', {
+ listAll: true,
+ networkid: this.resource.id
+ }).then(response => {
+ this.egressRules =
response.listegressfirewallrulesresponse.firewallrule
+ this.loading = false
+ })
+ },
+ deleteRule (rule) {
+ this.loading = true
+ api('deleteEgressFirewallRule', { id: rule.id }).then(response => {
+ this.$pollJob({
+ jobId: response.deleteegressfirewallruleresponse.jobid,
+ successMessage: `Successfully removed Egress rule`,
+ successMethod: () => this.fetchData(),
+ errorMessage: 'Removing Egress rule failed',
+ errorMethod: () => this.fetchData(),
+ loadingMessage: `Deleting Egress rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => this.fetchData()
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.fetchData()
+ })
+ },
+ addRule () {
+ this.loading = true
+ api('createEgressFirewallRule', { ...this.newRule }).then(response => {
+ this.$pollJob({
+ jobId: response.createegressfirewallruleresponse.jobid,
+ successMessage: `Successfully added new Egress rule`,
+ successMethod: () => {
+ this.resetAllRules()
+ this.fetchData()
+ },
+ errorMessage: 'Adding new Egress rule failed',
+ errorMethod: () => {
+ this.resetAllRules()
+ this.fetchData()
+ },
+ loadingMessage: `Adding new Egress rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.resetAllRules()
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description:
error.response.data.createegressfirewallruleresponse.errortext
+ })
+ this.resetAllRules()
+ this.fetchData()
+ })
+ },
+ resetAllRules () {
+ this.newRule.protocol = 'tcp'
+ this.newRule.cidrlist = null
+ this.newRule.destcidrlist = null
+ this.newRule.networkid = this.resource.id
+ this.resetRulePorts()
+ },
+ resetRulePorts () {
+ this.newRule.icmptype = null
+ this.newRule.icmpcode = null
+ this.newRule.startport = null
+ this.newRule.endport = null
+ }
}
}
</script>
-<style scoped>
+<style scoped lang="scss">
+ .rule {
+
+ &-container {
+ display: flex;
+ width: 100%;
+ flex-wrap: wrap;
+ margin-right: -20px;
+ margin-bottom: -10px;
+ }
+
+ &__item {
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ flex: 1;
+ }
+
+ }
+
+ &__title {
+ font-weight: bold;
+ }
+
+ }
+
+ .add-btn {
+ width: 100%;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ height: auto;
+ }
+
+ .add-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-right: -20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ margin-top: 20px;
+ }
+
+ button {
+ margin-right: 20px;
+ }
+
+ }
+
+ .form {
+ display: flex;
+ margin-right: -20px;
+ margin-bottom: 20px;
+ flex-direction: column;
+ align-items: flex-end;
+
+ @media (min-width: 760px) {
+ flex-direction: row;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ margin-bottom: 0;
+ }
+
+ input,
+ .ant-select {
+ margin-top: auto;
+ }
+
+ }
+
+ &__label {
+ font-weight: bold;
+ }
+
+ }
</style>
diff --git a/src/views/network/FirewallRules.vue
b/src/views/network/FirewallRules.vue
new file mode 100644
index 0000000..a0a2479
--- /dev/null
+++ b/src/views/network/FirewallRules.vue
@@ -0,0 +1,476 @@
+// 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>
+ <div>
+ <div class="form">
+ <div class="form__item">
+ <div class="form__label">{{ $t('sourcecidr') }}</div>
+ <a-input v-model="newRule.cidrlist"></a-input>
+ </div>
+ <div class="form__item">
+ <div class="form__label">{{ $t('protocol') }}</div>
+ <a-select v-model="newRule.protocol" style="width: 100%;"
@change="resetRulePorts">
+ <a-select-option value="tcp">{{ $t('tcp') }}</a-select-option>
+ <a-select-option value="udp">{{ $t('udp') }}</a-select-option>
+ <a-select-option value="icmp">{{ $t('icmp') }}</a-select-option>
+ </a-select>
+ </div>
+ <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'"
class="form__item">
+ <div class="form__label">{{ $t('startport') }}</div>
+ <a-input v-model="newRule.startport"></a-input>
+ </div>
+ <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'"
class="form__item">
+ <div class="form__label">{{ $t('endport') }}</div>
+ <a-input v-model="newRule.endport"></a-input>
+ </div>
+ <div v-show="newRule.protocol === 'icmp'" class="form__item">
+ <div class="form__label">{{ $t('icmptype') }}</div>
+ <a-input v-model="newRule.icmptype"></a-input>
+ </div>
+ <div v-show="newRule.protocol === 'icmp'" class="form__item">
+ <div class="form__label">{{ $t('icmpcode') }}</div>
+ <a-input v-model="newRule.icmpcode"></a-input>
+ </div>
+ <div class="form__item" style="margin-left: auto;">
+ <a-button type="primary" @click="addRule">{{ $t('add') }}</a-button>
+ </div>
+ </div>
+ </div>
+
+ <a-divider/>
+
+ <a-list :loading="loading" style="min-height: 25px;">
+ <a-list-item v-for="rule in firewallRules" :key="rule.id" class="rule">
+ <div class="rule-container">
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('sourcecidr') }}</div>
+ <div>{{ rule.cidrlist }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('protocol') }}</div>
+ <div>{{ rule.protocol | capitalise }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ rule.protocol === 'icmp' ?
$t('icmptype') : $t('startport') }}</div>
+ <div>{{ rule.icmptype || rule.startport >= 0 ? rule.icmptype ||
rule.startport : $t('all') }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ rule.protocol === 'icmp' ? 'ICMP Code'
: 'End Port' }}</div>
+ <div>{{ rule.icmpcode || rule.endport >= 0 ? rule.icmpcode ||
rule.endport : $t('all') }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('state') }}</div>
+ <div>{{ rule.state }}</div>
+ </div>
+ <div slot="actions">
+ <a-button shape="round" icon="tag" class="rule-action" @click="()
=> openTagsModal(rule.id)" />
+ <a-button shape="round" type="danger" icon="delete"
class="rule-action" @click="deleteRule(rule)" />
+ </div>
+ </div>
+ </a-list-item>
+ </a-list>
+
+ <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null"
:afterClose="closeModal">
+ <div class="add-tags">
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('key') }}</p>
+ <a-input v-model="newTag.key"></a-input>
+ </div>
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('value') }}</p>
+ <a-input v-model="newTag.value"></a-input>
+ </div>
+ <a-button type="primary" @click="() => handleAddTag()">{{ $t('add')
}}</a-button>
+ </div>
+
+ <a-divider></a-divider>
+
+ <div class="tags-container">
+ <div class="tags" v-for="(tag, index) in tags" :key="index">
+ <a-tag :key="index" :closable="true" :afterClose="() =>
handleDeleteTag(tag)">
+ {{ tag.key }} = {{ tag.value }}
+ </a-tag>
+ </div>
+ </div>
+
+ <a-button class="add-tags-done" @click="tagsModalVisible = false"
type="primary">{{ $t('done') }}</a-button>
+ </a-modal>
+
+ </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
+ },
+ inject: ['parentFetchData', 'parentToggleLoading'],
+ data () {
+ return {
+ loading: true,
+ firewallRules: [],
+ newRule: {
+ protocol: 'tcp',
+ cidrlist: null,
+ ipaddressid: this.resource.id,
+ icmptype: null,
+ icmpcode: null,
+ startport: null,
+ endport: null
+ },
+ tagsModalVisible: false,
+ selectedRule: null,
+ tags: [],
+ newTag: {
+ key: null,
+ value: null
+ }
+ }
+ },
+ mounted () {
+ this.fetchData()
+ },
+ filters: {
+ capitalise: val => {
+ if (val === 'all') return 'All'
+ return val.toUpperCase()
+ }
+ },
+ watch: {
+ resource: function (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.resource = newItem
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.loading = true
+ api('listFirewallRules', {
+ listAll: true,
+ ipaddressid: this.resource.id
+ }).then(response => {
+ this.firewallRules = response.listfirewallrulesresponse.firewallrule
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.loading = false
+ })
+ },
+ deleteRule (rule) {
+ this.loading = true
+ api('deleteFirewallRule', { id: rule.id }).then(response => {
+ this.$pollJob({
+ jobId: response.deletefirewallruleresponse.jobid,
+ successMessage: `Successfully removed Firewall rule`,
+ successMethod: () => this.fetchData(),
+ errorMessage: 'Removing Firewall rule failed',
+ errorMethod: () => this.fetchData(),
+ loadingMessage: `Deleting Firewall rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => this.fetchData()
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.fetchData()
+ })
+ },
+ addRule () {
+ this.loading = true
+ api('createFirewallRule', { ...this.newRule }).then(response => {
+ this.$pollJob({
+ jobId: response.createfirewallruleresponse.jobid,
+ successMessage: `Successfully added new Firewall rule`,
+ successMethod: () => {
+ this.resetAllRules()
+ this.fetchData()
+ },
+ errorMessage: 'Adding new Firewall rule failed',
+ errorMethod: () => {
+ this.resetAllRules()
+ this.fetchData()
+ },
+ loadingMessage: `Adding new Firewall rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.resetAllRules()
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createfirewallruleresponse.errortext
+ })
+ this.resetAllRules()
+ this.fetchData()
+ })
+ },
+ resetAllRules () {
+ this.newRule.protocol = 'tcp'
+ this.newRule.cidrlist = null
+ this.newRule.networkid = this.resource.id
+ this.resetRulePorts()
+ },
+ resetRulePorts () {
+ this.newRule.icmptype = null
+ this.newRule.icmpcode = null
+ this.newRule.startport = null
+ this.newRule.endport = null
+ },
+ closeModal () {
+ this.selectedRule = null
+ this.tagsModalVisible = false
+ this.newTag.key = null
+ this.newTag.value = null
+ },
+ openTagsModal (id) {
+ this.selectedRule = id
+ this.tagsModalVisible = true
+ api('listTags', {
+ resourceId: id,
+ resourceType: 'FirewallRule',
+ listAll: true
+ }).then(response => {
+ this.tags = response.listtagsresponse.tag
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleAddTag () {
+ api('createTags', {
+ 'tags[0].key': this.newTag.key,
+ 'tags[0].value': this.newTag.value,
+ resourceIds: this.selectedRule,
+ resourceType: 'FirewallRule'
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.createtagsresponse.jobid,
+ successMessage: `Successfully added tag`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.openTagsModal(this.selectedRule)
+ },
+ errorMessage: 'Failed to add new tag',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ },
+ loadingMessage: `Adding tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createtagsresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleDeleteTag (tag) {
+ api('deleteTags', {
+ 'tags[0].key': tag.key,
+ 'tags[0].value': tag.value,
+ resourceIds: this.selectedRule,
+ resourceType: 'FirewallRule'
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.deletetagsresponse.jobid,
+ successMessage: `Successfully removed tag`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.openTagsModal(this.selectedRule)
+ },
+ errorMessage: 'Failed to remove tag',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ },
+ loadingMessage: `Removing tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deletetagsresponse.errortext
+ })
+ this.closeModal()
+ })
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ .rule {
+
+ &-container {
+ display: flex;
+ width: 100%;
+ flex-wrap: wrap;
+ margin-right: -20px;
+ margin-bottom: -10px;
+ }
+
+ &__item {
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ flex: 1;
+ }
+
+ }
+
+ &__title {
+ font-weight: bold;
+ }
+
+ }
+
+ .add-btn {
+ width: 100%;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ height: auto;
+ }
+
+ .add-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-right: -20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ margin-top: 20px;
+ }
+
+ button {
+ margin-right: 20px;
+ }
+
+ }
+
+ .form {
+ display: flex;
+ align-items: flex-end;
+ margin-right: -20px;
+ flex-direction: column;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ flex-direction: row;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ /*flex: 1;*/
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ margin-bottom: 0;
+ }
+
+ input,
+ .ant-select {
+ margin-top: auto;
+ }
+
+ }
+
+ &__label {
+ font-weight: bold;
+ }
+
+ }
+
+ .rule-action {
+ margin-bottom: 20px;
+
+ &:not(:last-of-type) {
+ margin-right: 10px;
+ }
+
+ }
+
+ .tags {
+ margin-bottom: 10px;
+ }
+
+ .add-tags {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+
+ &__input {
+ margin-right: 10px;
+ }
+
+ &__label {
+ margin-bottom: 5px;
+ font-weight: bold;
+ }
+
+ }
+
+ .tags-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+ }
+
+ .add-tags-done {
+ display: block;
+ margin-left: auto;
+ }
+
+</style>
diff --git a/src/views/network/IngressEgressRuleConfigure.vue
b/src/views/network/IngressEgressRuleConfigure.vue
index 8d17bba..24b6cfc 100644
--- a/src/views/network/IngressEgressRuleConfigure.vue
+++ b/src/views/network/IngressEgressRuleConfigure.vue
@@ -107,7 +107,7 @@
okText="Yes"
cancelText="No"
>
- <a-button shape="round" type="danger" icon="close-circle"
class="rule-action" />
+ <a-button shape="round" type="danger" icon="delete"
class="rule-action" />
</a-popconfirm>
</div>
</a-list-item>
diff --git a/src/views/network/IpConfigure.vue
b/src/views/network/IpConfigure.vue
deleted file mode 100644
index d0c6213..0000000
--- a/src/views/network/IpConfigure.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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>
- TODO: IP configure view: firewall, pf, lb
- </div>
-</template>
-
-<script>
-
-export default {
- name: '',
- components: {
- },
- data () {
- return {
- }
- },
- methods: {
- }
-}
-</script>
-
-<style scoped>
-</style>
diff --git a/src/views/network/LoadBalancing.vue
b/src/views/network/LoadBalancing.vue
new file mode 100644
index 0000000..5f175b6
--- /dev/null
+++ b/src/views/network/LoadBalancing.vue
@@ -0,0 +1,1373 @@
+// 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>
+ <div>
+ <div class="form">
+ <div class="form__item" ref="newRuleName">
+ <div class="form__label"><span class="form__required">*</span>{{
$t('name') }}</div>
+ <a-input v-model="newRule.name"></a-input>
+ <span class="error-text">Required</span>
+ </div>
+ <div class="form__item" ref="newRulePublicPort">
+ <div class="form__label"><span class="form__required">*</span>{{
$t('publicport') }}</div>
+ <a-input v-model="newRule.publicport"></a-input>
+ <span class="error-text">Required</span>
+ </div>
+ <div class="form__item" ref="newRulePrivatePort">
+ <div class="form__label"><span class="form__required">*</span>{{
$t('privateport') }}</div>
+ <a-input v-model="newRule.privateport"></a-input>
+ <span class="error-text">Required</span>
+ </div>
+ <div class="form__item">
+ <div class="form__label">{{ $t('algorithm') }}</div>
+ <a-select v-model="newRule.algorithm">
+ <a-select-option value="roundrobin">Round-robin</a-select-option>
+ <a-select-option value="leastconn">Least
connections</a-select-option>
+ <a-select-option value="source">Source</a-select-option>
+ </a-select>
+ </div>
+ <div class="form__item">
+ <div class="form__label">{{ $t('protocol') }}</div>
+ <a-select v-model="newRule.protocol" style="min-width: 100px">
+ <a-select-option value="tcp-proxy">TCP Proxy</a-select-option>
+ <a-select-option value="tcp">TCP</a-select-option>
+ <a-select-option value="udp">UDP</a-select-option>
+ </a-select>
+ </div>
+ <div class="form__item">
+ <div class="form__label" style="white-space: nowrap;">{{
$t('label.add.VMs') }}</div>
+ <a-button type="primary" @click="handleOpenAddVMModal">Add</a-button>
+ </div>
+ </div>
+ </div>
+
+ <a-divider />
+
+ <a-list :loading="loading" style="min-height: 25px;">
+ <a-list-item v-for="rule in lbRules" :key="rule.id" class="rule
custom-ant-list">
+ <div class="rule-container">
+ <div class="rule__row">
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('name') }}</div>
+ <div>{{ rule.name }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('publicport') }}</div>
+ <div>{{ rule.publicport }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('privateport') }}</div>
+ <div>{{ rule.privateport }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('algorithm') }}</div>
+ <div>{{ returnAlgorithmName(rule.algorithm) }}</div>
+ </div>
+ </div>
+ <div class="rule__row">
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('protocol') }}</div>
+ <div>{{ rule.protocol | capitalise }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('state') }}</div>
+ <div>{{ rule.state }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{
$t('label.action.configure.stickiness') }}</div>
+ <a-button @click="() => openStickinessModal(rule.id)">
+ {{ returnStickinessLabel(rule.id) }}
+ </a-button>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('label.add.VMs') }}</div>
+ <a-button type="primary" icon="plus" @click="() => {
selectedRule = rule; handleOpenAddVMModal() }">
+ {{ $t('add') }}
+ </a-button>
+ </div>
+ </div>
+ <div class="rule__row" v-if="rule.ruleInstances">
+ <a-collapse :bordered="false" class="rule-instance-collapse">
+ <template v-slot:expandIcon="props">
+ <a-icon type="caret-right" :rotate="props.isActive ? 90 : 0" />
+ </template>
+ <a-collapse-panel header="View Instances">
+ <div class="rule-instance-list">
+ <div v-for="instance in rule.ruleInstances"
:key="instance.loadbalancerruleinstance.id">
+ <div v-for="ip in instance.lbvmipaddresses" :key="ip"
class="rule-instance-list__item">
+ <div>
+ <a-icon type="desktop" />
+ <router-link :to="{ path: '/vm/' +
rule.virtualmachineid }"> {{ instance.loadbalancerruleinstance.displayname
}}</router-link>
+ </div>
+ <div>{{ ip }}</div>
+ <div><status
:text="instance.loadbalancerruleinstance.state" displayText /></div>
+ <a-button
+ shape="round"
+ type="danger"
+ icon="delete"
+ @click="() => handleDeleteInstanceFromRule(instance,
rule, ip)" />
+ </div>
+ </div>
+ </div>
+ </a-collapse-panel>
+ </a-collapse>
+ </div>
+ </div>
+ <div class="rule__item">
+ <a-button shape="circle" icon="edit" class="rule-action" @click="()
=> openEditRuleModal(rule)"></a-button>
+ <a-button shape="circle" icon="tag" class="rule-action" @click="()
=> openTagsModal(rule.id)" />
+ <a-popconfirm
+ :title="$t('label.delete') + '?'"
+ @confirm="handleDeleteRule(rule)"
+ okText="Yes"
+ cancelText="No"
+ >
+ <a-button shape="circle" type="danger" icon="delete"
class="rule-action" />
+ </a-popconfirm>
+ </div>
+ </a-list-item>
+ </a-list>
+
+ <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null"
:afterClose="closeModal" class="tags-modal">
+ <span v-show="tagsModalLoading" class="modal-loading">
+ <a-icon type="loading"></a-icon>
+ </span>
+
+ <a-form :form="newTagsForm" class="add-tags" @submit="handleAddTag">
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('key') }}</p>
+ <a-form-item>
+ <a-input v-decorator="['key', { rules: [{ required: true, message:
'Please specify a tag key'}] }]" />
+ </a-form-item>
+ </div>
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('value') }}</p>
+ <a-form-item>
+ <a-input v-decorator="['value', { rules: [{ required: true,
message: 'Please specify a tag value'}] }]" />
+ </a-form-item>
+ </div>
+ <a-button type="primary" html-type="submit">{{ $t('label.add')
}}</a-button>
+ </a-form>
+
+ <a-divider></a-divider>
+
+ <div v-show="!tagsModalLoading" class="tags-container">
+ <div class="tags" v-for="(tag, index) in tags" :key="index">
+ <a-tag :key="index" :closable="true" :afterClose="() =>
handleDeleteTag(tag)">
+ {{ tag.key }} = {{ tag.value }}
+ </a-tag>
+ </div>
+ </div>
+
+ <a-button class="add-tags-done" @click="tagsModalVisible = false"
type="primary">{{ $t('done') }}</a-button>
+ </a-modal>
+
+ <a-modal
+ title="Configure Sticky Policy"
+ v-model="stickinessModalVisible"
+ :footer="null"
+ :afterClose="closeModal"
+ :okButtonProps="{ props: {htmlType: 'submit'}}">
+
+ <span v-show="stickinessModalLoading" class="modal-loading">
+ <a-icon type="loading"></a-icon>
+ </span>
+
+ <a-form :form="stickinessPolicyForm"
@submit="handleSubmitStickinessForm" class="custom-ant-form">
+ <a-form-item label="Stickiness method">
+ <a-select v-decorator="['methodname']"
@change="handleStickinessMethodSelectChange">
+ <a-select-option value="LbCookie">LbCookie</a-select-option>
+ <a-select-option value="AppCookie">AppCookie</a-select-option>
+ <a-select-option value="SourceBased">SourceBased</a-select-option>
+ <a-select-option value="none">None</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item
+ label="Sticky Name"
+ v-show="stickinessPolicyMethod === 'LbCookie' ||
stickinessPolicyMethod ===
+ 'AppCookie' || stickinessPolicyMethod === 'SourceBased'">
+ <a-input v-decorator="['name', { rules: [{ required: true, message:
'Please specify a sticky name'}] }]" />
+ </a-form-item>
+ <a-form-item
+ label="Cookie name"
+ v-show="stickinessPolicyMethod === 'LbCookie' ||
stickinessPolicyMethod ===
+ 'AppCookie'">
+ <a-input v-decorator="['cookieName']" />
+ </a-form-item>
+ <a-form-item
+ label="Mode"
+ v-show="stickinessPolicyMethod === 'LbCookie' ||
stickinessPolicyMethod ===
+ 'AppCookie'">
+ <a-input v-decorator="['mode']" />
+ </a-form-item>
+ <a-form-item label="No cache" v-show="stickinessPolicyMethod ===
'LbCookie'">
+ <a-checkbox v-decorator="['nocache']"
v-model="stickinessNoCache"></a-checkbox>
+ </a-form-item>
+ <a-form-item label="Indirect" v-show="stickinessPolicyMethod ===
'LbCookie'">
+ <a-checkbox v-decorator="['indirect']"
v-model="stickinessIndirect"></a-checkbox>
+ </a-form-item>
+ <a-form-item label="Post only" v-show="stickinessPolicyMethod ===
'LbCookie'">
+ <a-checkbox v-decorator="['postonly']"
v-model="stickinessPostOnly"></a-checkbox>
+ </a-form-item>
+ <a-form-item label="Domain" v-show="stickinessPolicyMethod ===
'LbCookie'">
+ <a-input v-decorator="['domain']" />
+ </a-form-item>
+ <a-form-item label="Length" v-show="stickinessPolicyMethod ===
'AppCookie'">
+ <a-input v-decorator="['length']" type="number" />
+ </a-form-item>
+ <a-form-item label="Hold time" v-show="stickinessPolicyMethod ===
'AppCookie'">
+ <a-input v-decorator="['holdtime']" type="number" />
+ </a-form-item>
+ <a-form-item label="Request learn" v-show="stickinessPolicyMethod ===
'AppCookie'">
+ <a-checkbox v-decorator="['requestLearn']"
v-model="stickinessRequestLearn"></a-checkbox>
+ </a-form-item>
+ <a-form-item label="Prefix" v-show="stickinessPolicyMethod ===
'AppCookie'">
+ <a-checkbox v-decorator="['prefix']"
v-model="stickinessPrefix"></a-checkbox>
+ </a-form-item>
+ <a-form-item label="Table size" v-show="stickinessPolicyMethod ===
'SourceBased'">
+ <a-input v-decorator="['tablesize']" />
+ </a-form-item>
+ <a-form-item label="Expires" v-show="stickinessPolicyMethod ===
'SourceBased'">
+ <a-input v-decorator="['expire']" />
+ </a-form-item>
+ <a-button type="primary" html-type="submit">OK</a-button>
+ </a-form>
+ </a-modal>
+
+ <a-modal title="Edit rule" v-model="editRuleModalVisible"
:afterClose="closeModal" @ok="handleSubmitEditForm">
+ <span v-show="editRuleModalLoading" class="modal-loading">
+ <a-icon type="loading"></a-icon>
+ </span>
+
+ <div class="edit-rule" v-if="selectedRule">
+ <div class="edit-rule__item">
+ <p class="edit-rule__label">{{ $t('name') }}</p>
+ <a-input v-model="editRuleDetails.name" />
+ </div>
+ <div class="edit-rule__item">
+ <p class="edit-rule__label">{{ $t('algorithm') }}</p>
+ <a-select v-model="editRuleDetails.algorithm">
+ <a-select-option value="roundrobin">Round-robin</a-select-option>
+ <a-select-option value="leastconn">Least
connections</a-select-option>
+ <a-select-option value="source">Source</a-select-option>
+ </a-select>
+ </div>
+ <div class="edit-rule__item">
+ <p class="edit-rule__label">{{ $t('protocol') }}</p>
+ <a-select v-model="editRuleDetails.protocol">
+ <a-select-option value="tcp-proxy">TCP proxy</a-select-option>
+ <a-select-option value="tcp">TCP</a-select-option>
+ <a-select-option value="udp">UDP</a-select-option>
+ </a-select>
+ </div>
+ </div>
+ </a-modal>
+
+ <a-modal
+ title="Add VMs"
+ v-model="addVmModalVisible"
+ class="vm-modal"
+ width="60vw"
+ @ok="handleAddNewRule"
+ :okButtonProps="{ props:
+ {disabled: newRule.virtualmachineid === [] } }"
+ @cancel="closeModal"
+ >
+
+ <a-icon v-if="addVmModalLoading" type="loading"></a-icon>
+
+ <div v-else>
+ <div class="vm-modal__header">
+ <span style="min-width: 200px;">{{ $t('name') }}</span>
+ <span>{{ $t('instancename') }}</span>
+ <span>{{ $t('displayname') }}</span>
+ <span>{{ $t('ip') }}</span>
+ <span>{{ $t('account') }}</span>
+ <span>{{ $t('zonenamelabel') }}</span>
+ <span>{{ $t('state') }}</span>
+ <span>{{ $t('select') }}</span>
+ </div>
+
+ <a-checkbox-group style="width: 100%;">
+ <div v-for="(vm, index) in vms" :key="index" class="vm-modal__item">
+ <span style="min-width: 200px;">
+ <span>
+ {{ vm.name }}
+ </span>
+ <a-icon v-if="addVmModalNicLoading" type="loading"></a-icon>
+ <a-select
+ v-else-if="!addVmModalNicLoading &&
newRule.virtualmachineid[index] === vm.id"
+ mode="multiple"
+ v-model="newRule.vmguestip[index]"
+ >
+ <a-select-option v-for="(nic, nicIndex) in nics[index]"
:key="nic" :value="nic">
+ {{ nic }}{{ nicIndex === 0 ? ' (Primary)' : null }}
+ </a-select-option>
+ </a-select>
+ </span>
+ <span>{{ vm.instancename }}</span>
+ <span>{{ vm.displayname }}</span>
+ <span></span>
+ <span>{{ vm.account }}</span>
+ <span>{{ vm.zonename }}</span>
+ <span>{{ vm.state }}</span>
+ <a-checkbox :value="vm.id" @change="e => fetchNics(e, index)" />
+ </div>
+ </a-checkbox-group>
+ </div>
+
+ </a-modal>
+
+ </div>
+</template>
+
+<script>
+import { api } from '@/api'
+import Status from '@/components/widgets/Status'
+
+export default {
+ name: 'LoadBalancing',
+ components: {
+ Status
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
+ },
+ inject: ['parentFetchData', 'parentToggleLoading'],
+ data () {
+ return {
+ loading: true,
+ lbRules: [],
+ newTagsForm: this.$form.createForm(this),
+ tagsModalVisible: false,
+ tagsModalLoading: false,
+ tags: [],
+ selectedRule: null,
+ stickinessModalVisible: false,
+ stickinessPolicies: [],
+ stickinessPolicyForm: this.$form.createForm(this),
+ stickinessModalLoading: false,
+ selectedStickinessPolicy: null,
+ stickinessPolicyMethod: 'LbCookie',
+ stickinessNoCache: null,
+ stickinessIndirect: null,
+ stickinessPostOnly: null,
+ stickinessRequestLearn: null,
+ stickinessPrefix: null,
+ editRuleModalVisible: false,
+ editRuleModalLoading: false,
+ editRuleDetails: {
+ name: '',
+ algorithm: '',
+ protocol: ''
+ },
+ newRule: {
+ algorithm: 'roundrobin',
+ name: '',
+ privateport: '',
+ publicport: '',
+ protocol: 'tcp',
+ virtualmachineid: [],
+ vmguestip: []
+ },
+ addVmModalVisible: false,
+ addVmModalLoading: false,
+ addVmModalNicLoading: false,
+ vms: [],
+ nics: []
+ }
+ },
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ resource: function (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.resource = newItem
+ this.fetchData()
+ }
+ },
+ filters: {
+ capitalise: val => {
+ if (val === 'all') return 'All'
+ return val.toUpperCase()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.loading = true
+ this.lbRules = []
+ this.stickinessPolicies = []
+ api('listLoadBalancerRules', {
+ listAll: true,
+ publicipid: this.resource.id
+ }).then(response => {
+ this.lbRules = response.listloadbalancerrulesresponse.loadbalancerrule
+ ? response.listloadbalancerrulesresponse.loadbalancerrule
+ : []
+ }).then(() => {
+ if (this.lbRules.length > 0) {
+ setTimeout(() => {
+ this.fetchLBRuleInstances()
+ }, 100)
+ this.fetchLBStickinessPolicies()
+ return
+ }
+ this.loading = false
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.loading = false
+ })
+ },
+ fetchLBRuleInstances () {
+ for (const rule of this.lbRules) {
+ this.loading = true
+ api('listLoadBalancerRuleInstances', {
+ listAll: true,
+ lbvmips: true,
+ id: rule.id
+ }).then(response => {
+ this.$set(rule, 'ruleInstances',
response.listloadbalancerruleinstancesresponse.lbrulevmidip)
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.loading = false
+ })
+ }
+ },
+ fetchLBStickinessPolicies () {
+ this.loading = true
+ this.lbRules.forEach(rule => {
+ api('listLBStickinessPolicies', {
+ listAll: true,
+ lbruleid: rule.id
+ }).then(response => {
+
this.stickinessPolicies.push(...response.listlbstickinesspoliciesresponse.stickinesspolicies)
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.loading = false
+ })
+ })
+ },
+ returnAlgorithmName (name) {
+ switch (name) {
+ case 'leastconn':
+ return 'Least connections'
+ case 'roundrobin' :
+ return 'Round-robin'
+ case 'source':
+ return 'Source'
+ default :
+ return ''
+ }
+ },
+ returnStickinessLabel (id) {
+ const match = this.stickinessPolicies.filter(policy => policy.lbruleid
=== id)
+ if (match.length > 0 && match[0].stickinesspolicy.length > 0) {
+ return match[0].stickinesspolicy[0].methodname
+ }
+ return 'Configure'
+ },
+ openTagsModal (id) {
+ this.tagsModalLoading = true
+ this.tagsModalVisible = true
+ this.tags = []
+ this.selectedRule = id
+ this.newTagsForm.resetFields()
+ api('listTags', {
+ resourceId: id,
+ resourceType: 'LoadBalancer',
+ listAll: true
+ }).then(response => {
+ this.tags = response.listtagsresponse.tag
+ this.tagsModalLoading = false
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleAddTag (e) {
+ this.tagsModalLoading = true
+
+ e.preventDefault()
+ this.newTagsForm.validateFields((err, values) => {
+ if (err) {
+ this.tagsModalLoading = false
+ return
+ }
+
+ api('createTags', {
+ 'tags[0].key': values.key,
+ 'tags[0].value': values.value,
+ resourceIds: this.selectedRule,
+ resourceType: 'LoadBalancer'
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.createtagsresponse.jobid,
+ successMessage: `Successfully added tag`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.openTagsModal(this.selectedRule)
+ },
+ errorMessage: 'Failed to add new tag',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ },
+ loadingMessage: `Adding tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createtagsresponse.errortext
+ })
+ this.closeModal()
+ })
+ })
+ },
+ handleDeleteTag (tag) {
+ this.tagsModalLoading = true
+ api('deleteTags', {
+ 'tags[0].key': tag.key,
+ 'tags[0].value': tag.value,
+ resourceIds: tag.resourceid,
+ resourceType: 'LoadBalancer'
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.deletetagsresponse.jobid,
+ successMessage: `Successfully removed tag`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.openTagsModal(this.selectedRule)
+ },
+ errorMessage: 'Failed to remove tag',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ },
+ loadingMessage: `Removing tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deletetagsresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ openStickinessModal (id) {
+ this.stickinessModalVisible = true
+ this.selectedRule = id
+ const match = this.stickinessPolicies.find(policy => policy.lbruleid ===
id)
+
+ if (match && match.stickinesspolicy.length > 0) {
+ this.selectedStickinessPolicy = match.stickinesspolicy[0]
+ this.stickinessPolicyMethod = this.selectedStickinessPolicy.methodname
+ this.$nextTick(() => {
+ this.stickinessPolicyForm.setFieldsValue({ methodname:
this.selectedStickinessPolicy.methodname })
+ this.stickinessPolicyForm.setFieldsValue({ name:
this.selectedStickinessPolicy.name })
+ this.stickinessPolicyForm.setFieldsValue({ cookieName:
this.selectedStickinessPolicy.params['cookie-name'] })
+ this.stickinessPolicyForm.setFieldsValue({ mode:
this.selectedStickinessPolicy.params.mode })
+ this.stickinessPolicyForm.setFieldsValue({ domain:
this.selectedStickinessPolicy.params.domain })
+ this.stickinessPolicyForm.setFieldsValue({ length:
this.selectedStickinessPolicy.params.length })
+ this.stickinessPolicyForm.setFieldsValue({ holdtime:
this.selectedStickinessPolicy.params.holdtime })
+ this.stickinessNoCache =
!!this.selectedStickinessPolicy.params.nocache
+ this.stickinessIndirect =
!!this.selectedStickinessPolicy.params.indirect
+ this.stickinessPostOnly =
!!this.selectedStickinessPolicy.params.postonly
+ this.stickinessRequestLearn =
!!this.selectedStickinessPolicy.params['request-learn']
+ this.stickinessPrefix = !!this.selectedStickinessPolicy.params.prefix
+ })
+ }
+ },
+ handleAddStickinessPolicy (data, values) {
+ api('createLBStickinessPolicy', {
+ ...data,
+ lbruleid: this.selectedRule,
+ name: values.name,
+ methodname: values.methodname
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.createLBStickinessPolicy.jobid,
+ successMessage: `Successfully configured sticky policy`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ errorMessage: 'Failed to configure sticky policy',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ loadingMessage: `Updating sticky policy...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createLBStickinessPolicy.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleDeleteStickinessPolicy () {
+ this.stickinessModalLoading = true
+ api('deleteLBStickinessPolicy', { id: this.selectedStickinessPolicy.id
}).then(response => {
+ this.$pollJob({
+ jobId: response.deleteLBstickinessrruleresponse.jobid,
+ successMessage: `Successfully removed sticky policy`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ errorMessage: 'Failed to remove sticky policy',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ loadingMessage: `Removing sticky policy...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleSubmitStickinessForm (e) {
+ this.stickinessModalLoading = true
+ e.preventDefault()
+ this.stickinessPolicyForm.validateFields((err, values) => {
+ if (err) {
+ this.stickinessModalLoading = false
+ return
+ }
+ if (values.methodname === 'none') {
+ this.handleDeleteStickinessPolicy()
+ return
+ }
+
+ values.nocache = this.stickinessNoCache
+ values.indirect = this.stickinessIndirect
+ values.postonly = this.stickinessPostOnly
+ values.requestLearn = this.stickinessRequestLearn
+ values.prefix = this.stickinessPrefix
+
+ let data = {}
+ let count = 0
+ Object.entries(values).forEach(([key, val]) => {
+ if (val && key !== 'name' && key !== 'methodname') {
+ if (key === 'cookieName') {
+ data = { ...data, ...{ [`param[${count}].name`]: 'cookie-name' }
}
+ } else if (key === 'requestLearn') {
+ data = { ...data, ...{ [`param[${count}].name`]: 'request-learn'
} }
+ } else {
+ data = { ...data, ...{ [`param[${count}].name`]: key } }
+ }
+ data = { ...data, ...{ [`param[${count}].value`]: val } }
+ count++
+ }
+ })
+
+ this.handleAddStickinessPolicy(data, values)
+ })
+ },
+ handleStickinessMethodSelectChange (e) {
+ this.stickinessPolicyMethod = e
+ },
+ handleDeleteInstanceFromRule (instance, rule, ip) {
+ this.loading = true
+ api('removeFromLoadBalancerRule', {
+ id: rule.id,
+ 'vmidipmap[0].vmid': instance.loadbalancerruleinstance.id,
+ 'vmidipmap[0].vmip': ip
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.removefromloadbalancerruleresponse.jobid,
+ successMessage: `Successfully removed instance from rule`,
+ successMethod: () => {
+ this.fetchData()
+ },
+ errorMessage: 'Failed to remove instance',
+ errorMethod: () => {
+ this.fetchData()
+ },
+ loadingMessage: `Removing...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.fetchData()
+ })
+ },
+ openEditRuleModal (rule) {
+ this.selectedRule = rule
+ this.editRuleModalVisible = true
+ this.editRuleDetails.name = this.selectedRule.name
+ this.editRuleDetails.algorithm = this.selectedRule.algorithm
+ this.editRuleDetails.protocol = this.selectedRule.protocol
+ },
+ handleSubmitEditForm () {
+ this.loading = true
+ this.editRuleModalLoading = true
+ api('updateLoadBalancerRule', {
+ ...this.editRuleDetails,
+ id: this.selectedRule.id
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.updateloadbalancerruleresponse.jobid,
+ successMessage: `Successfully edited rule`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ errorMessage: 'Failed to edit rule',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ loadingMessage: `Updating rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.loading = false
+ this.closeModal()
+ })
+ },
+ handleDeleteRule (rule) {
+ this.loading = true
+ api('deleteLoadBalancerRule', {
+ id: rule.id
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.deleteloadbalancerruleresponse.jobid,
+ successMessage: `Successfully deleted rule`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ errorMessage: 'Failed to delete rule',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ loadingMessage: `Deleting rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.loading = false
+ this.closeModal()
+ })
+ },
+ handleOpenAddVMModal () {
+ if (!this.selectedRule) {
+ if (!this.newRule.name) {
+ this.$refs.newRuleName.classList.add('error')
+ } else {
+ this.$refs.newRuleName.classList.remove('error')
+ }
+ if (!this.newRule.publicport) {
+ this.$refs.newRulePublicPort.classList.add('error')
+ } else {
+ this.$refs.newRulePublicPort.classList.remove('error')
+ }
+ if (!this.newRule.privateport) {
+ this.$refs.newRulePrivatePort.classList.add('error')
+ } else {
+ this.$refs.newRulePrivatePort.classList.remove('error')
+ }
+ if (!this.newRule.name || !this.newRule.publicport ||
!this.newRule.privateport) return
+ }
+ this.addVmModalVisible = true
+ this.addVmModalLoading = true
+ api('listVirtualMachines', {
+ listAll: true,
+ page: 1,
+ pagesize: 500,
+ networkid: this.resource.associatednetworkid,
+ account: this.resource.account,
+ domainid: this.resource.domainid
+ }).then(response => {
+ this.vms = response.listvirtualmachinesresponse.virtualmachine
+ this.vms.forEach((vm, index) => {
+ this.newRule.virtualmachineid[index] = null
+ this.nics[index] = null
+ this.newRule.vmguestip[index] = null
+ })
+ this.addVmModalLoading = false
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ fetchNics (e, index) {
+ if (!e.target.checked) {
+ this.newRule.virtualmachineid[index] = null
+ this.nics[index] = null
+ this.newRule.vmguestip[index] = null
+ return
+ }
+ this.newRule.virtualmachineid[index] = e.target.value
+ this.addVmModalNicLoading = true
+
+ api('listNics', {
+ virtualmachineid: e.target.value,
+ networkid: this.resource.associatednetworkid
+ }).then(response => {
+ if (!response.listnicsresponse.nic[0]) return
+ const newItem = []
+ newItem.push(response.listnicsresponse.nic[0].ipaddress)
+ if (response.listnicsresponse.nic[0].secondaryip) {
+ newItem.push(...response.listnicsresponse.nic[0].secondaryip.map(ip
=> ip.ipaddress))
+ }
+ this.nics[index] = newItem
+ this.newRule.vmguestip[index] = this.nics[index][0]
+ this.addVmModalNicLoading = false
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleAssignToLBRule (data) {
+ const vmIDIpMap = {}
+
+ let count = 0
+ let innerCount = 0
+ this.newRule.vmguestip.forEach(ip => {
+ if (Array.isArray(ip)) {
+ ip.forEach(i => {
+ vmIDIpMap[`vmidipmap[${innerCount}].vmid`] =
this.newRule.virtualmachineid[count]
+ vmIDIpMap[`vmidipmap[${innerCount}].vmip`] = i
+ innerCount++
+ })
+ } else {
+ vmIDIpMap[`vmidipmap[${innerCount}].vmid`] =
this.newRule.virtualmachineid[count]
+ vmIDIpMap[`vmidipmap[${innerCount}].vmip`] = ip
+ innerCount++
+ }
+ count++
+ })
+
+ this.loading = true
+ api('assignToLoadBalancerRule', {
+ id: data,
+ ...vmIDIpMap
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.assigntoloadbalancerruleresponse.jobid,
+ successMessage: `Successfully assigned VM`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ errorMessage: 'Failed to assign VM',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ },
+ loadingMessage: `Assigning VM...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.fetchData()
+ this.closeModal()
+ }
+ })
+ })
+ },
+ handleAddNewRule () {
+ this.loading = true
+
+ if (this.selectedRule) {
+ this.handleAssignToLBRule(this.selectedRule.id)
+ return
+ }
+
+ api('createLoadBalancerRule', {
+ openfirewall: false,
+ networkid: this.resource.associatednetworkid,
+ publicipid: this.resource.id,
+ algorithm: this.newRule.algorithm,
+ name: this.newRule.name,
+ privateport: this.newRule.privateport,
+ protocol: this.newRule.protocol,
+ publicport: this.newRule.publicport
+ }).then(response => {
+ this.addVmModalVisible = false
+ this.handleAssignToLBRule(response.createloadbalancerruleresponse.id)
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description:
error.response.data.createloadbalancerruleresponse.errortext
+ })
+ this.loading = false
+ })
+
+ // assigntoloadbalancerruleresponse.jobid
+ },
+ closeModal () {
+ this.selectedRule = null
+ this.tagsModalVisible = false
+ this.stickinessModalVisible = false
+ this.stickinessModalLoading = false
+ this.selectedStickinessPolicy = null
+ this.stickinessPolicyMethod = 'LbCookie'
+ this.stickinessNoCache = null
+ this.stickinessIndirect = null
+ this.stickinessPostOnly = null
+ this.editRuleModalVisible = false
+ this.editRuleModalLoading = false
+ this.addVmModalLoading = false
+ this.addVmModalNicLoading = false
+ this.vms = []
+ this.nics = []
+ this.addVmModalVisible = false
+ this.newRule.virtualmachineid = []
+ this.newTagsForm.resetFields()
+ this.stickinessPolicyForm.resetFields()
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ .rule {
+
+ &-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ @media (min-width: 760px) {
+ margin-right: -20px;
+ margin-bottom: -10px;
+ }
+
+ }
+
+ &__row {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__item {
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ flex: 1;
+ }
+
+ }
+
+ &__title {
+ font-weight: bold;
+ }
+
+ }
+
+ .add-btn {
+ width: 100%;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ height: auto;
+ }
+
+ .add-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-right: -20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ margin-top: 20px;
+ }
+
+ button {
+ margin-right: 20px;
+ }
+
+ }
+
+ .form {
+ display: flex;
+ margin-right: -20px;
+ flex-direction: column;
+ align-items: flex-start;
+
+ @media (min-width: 760px) {
+ flex-direction: row;
+ }
+
+ &__required {
+ margin-right: 5px;
+ color: red;
+ }
+
+ .error-text {
+ display: none;
+ color: red;
+ font-size: 0.8rem;
+ }
+
+ .error {
+
+ input {
+ border-color: red;
+ }
+
+ .error-text {
+ display: block;
+ }
+
+ }
+
+ &--column {
+ flex-direction: column;
+ margin-right: 0;
+ align-items: flex-end;
+
+ .form__item {
+ width: 100%;
+ padding-right: 0;
+ }
+
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 1200px) {
+ margin-bottom: 0;
+ flex: 1;
+ }
+
+ input,
+ .ant-select {
+ margin-top: auto;
+ }
+
+ &__input-container {
+ display: flex;
+
+ input {
+
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+
+ }
+
+ }
+
+ }
+
+ &__label {
+ font-weight: bold;
+ }
+
+ }
+
+ .rule-action {
+ margin-bottom: 10px;
+ }
+
+ .tags-modal {
+
+ .ant-divider {
+ margin-top: 0;
+ }
+
+ }
+
+ .tags {
+ margin-bottom: 10px;
+ }
+
+ .add-tags {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ &__input {
+ margin-right: 10px;
+ }
+
+ &__label {
+ margin-bottom: 5px;
+ font-weight: bold;
+ }
+
+ }
+
+ .tags-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+ }
+
+ .add-tags-done {
+ display: block;
+ margin-left: auto;
+ }
+
+ .modal-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(0,0,0,0.5);
+ z-index: 1;
+ color: #1890ff;
+ font-size: 2rem;
+ }
+
+ .ant-list-item {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ @media (min-width: 760px) {
+ flex-direction: row;
+ align-items: center;
+ }
+
+ }
+
+ .rule-instance-collapse {
+ width: 100%;
+ margin-left: -15px;
+
+ .ant-collapse-item {
+ border: 0;
+ }
+
+ }
+
+ .rule-instance-list {
+ display: flex;
+ flex-direction: column;
+
+ &__item {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+
+ div {
+ margin-left: 25px;
+ margin-bottom: 10px;
+ }
+ }
+ }
+
+ .edit-rule {
+
+ .ant-select {
+ width: 100%;
+ }
+
+ &__item {
+ margin-bottom: 10px;
+ }
+
+ &__label {
+ margin-bottom: 5px;
+ font-weight: bold;
+ }
+
+ }
+
+ .vm-modal {
+
+ &__header {
+ display: flex;
+
+ span {
+ flex: 1;
+ font-weight: bold;
+ margin-right: 10px;
+ }
+
+ }
+
+ &__item {
+ display: flex;
+ margin-top: 10px;
+
+ span,
+ label {
+ display: block;
+ flex: 1;
+ margin-right: 10px;
+ }
+
+ }
+
+ }
+
+ .custom-ant-form {
+ .ant-form-item-label {
+ font-weight: bold;
+ line-height: 1;
+ }
+ .ant-form-item {
+ margin-bottom: 10px;
+ }
+ }
+
+ .custom-ant-list {
+ .ant-list-item-action {
+ margin-top: 10px;
+ margin-left: 0;
+
+ @media (min-width: 760px) {
+ margin-top: 0;
+ margin-left: 24px;
+ }
+
+ }
+ }
+
+ .rule-instance-collapse {
+ .ant-collapse-header,
+ .ant-collapse-content {
+ margin-left: -12px;
+ }
+ }
+
+ .rule {
+ .ant-list-item-content-single {
+ width: 100%;
+
+ @media (min-width: 760px) {
+ width: auto;
+ }
+
+ }
+ }
+</style>
diff --git a/src/views/network/PortForwarding.vue
b/src/views/network/PortForwarding.vue
new file mode 100644
index 0000000..eb196a7
--- /dev/null
+++ b/src/views/network/PortForwarding.vue
@@ -0,0 +1,675 @@
+// 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>
+ <div>
+ <div class="form">
+ <div class="form__item">
+ <div class="form__label">{{ $t('privateport') }}</div>
+ <a-input-group class="form__item__input-container" compact>
+ <a-input
+ v-model="newRule.privateport"
+ placeholder="Start"
+ style="border-right: 0; width: 60px; margin-right: 0;"></a-input>
+ <a-input
+ placeholder="-"
+ disabled
+ style="width: 30px; border-left: 0; border-right: 0;
pointer-events: none; backgroundColor: #fff; text-align:
+ center; margin-right: 0;"></a-input>
+ <a-input
+ v-model="newRule.privateendport"
+ placeholder="End"
+ style="border-left: 0; width: 60px; text-align: right;
margin-right: 0;"></a-input>
+ </a-input-group>
+ </div>
+ <div class="form__item">
+ <div class="form__label">{{ $t('publicport') }}</div>
+ <a-input-group class="form__item__input-container" compact>
+ <a-input
+ v-model="newRule.publicport"
+ placeholder="Start"
+ style="border-right: 0; width: 60px; margin-right: 0;"></a-input>
+ <a-input
+ placeholder="-"
+ disabled
+ style="width: 30px; border-left: 0; border-right: 0;
pointer-events: none; backgroundColor: #fff;
+ text-align: center; margin-right: 0;"></a-input>
+ <a-input
+ v-model="newRule.publicendport"
+ placeholder="End"
+ style="border-left: 0; width: 60px; text-align: right;
margin-right: 0;"></a-input>
+ </a-input-group>
+ </div>
+ <div class="form__item">
+ <div class="form__label">{{ $t('protocol') }}</div>
+ <a-select v-model="newRule.protocol" style="width: 100%;">
+ <a-select-option value="tcp">{{ $t('tcp') }}</a-select-option>
+ <a-select-option value="udp">{{ $t('udp') }}</a-select-option>
+ </a-select>
+ </div>
+ <div class="form__item" style="margin-left: auto;">
+ <div class="form__label">{{ $t('label.add.VM') }}</div>
+ <a-button type="primary" @click="openAddVMModal">{{ $t('add')
}}</a-button>
+ </div>
+ </div>
+ </div>
+
+ <a-divider/>
+
+ <a-list :loading="loading" style="min-height: 25px;">
+ <a-list-item v-for="rule in portForwardRules" :key="rule.id"
class="rule">
+ <div class="rule-container">
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('privateport') }}</div>
+ <div>{{ rule.privateport }} - {{ rule.privateendport }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('publicport') }}</div>
+ <div>{{ rule.publicport }} - {{ rule.publicendport }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('protocol') }}</div>
+ <div>{{ rule.protocol | capitalise }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('state') }}</div>
+ <div>{{ rule.state }}</div>
+ </div>
+ <div class="rule__item">
+ <div class="rule__title">{{ $t('vm') }}</div>
+ <div class="rule__title"></div>
+ <div><a-icon type="desktop"/> <router-link :to="{ path: '/vm/' +
rule.virtualmachineid }">{{ rule.virtualmachinename }}</router-link> ({{
rule.vmguestip }})</div>
+ </div>
+ <div slot="actions">
+ <a-button shape="round" icon="tag" class="rule-action" @click="()
=> openTagsModal(rule.id)" />
+ <a-button shape="round" type="danger" icon="delete"
class="rule-action" @click="deleteRule(rule)" />
+ </div>
+ </div>
+ </a-list-item>
+ </a-list>
+
+ <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null"
:afterClose="closeModal">
+ <span v-show="tagsModalLoading" class="tags-modal-loading">
+ <a-icon type="loading"></a-icon>
+ </span>
+
+ <div class="add-tags">
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('key') }}</p>
+ <a-input v-model="newTag.key"></a-input>
+ </div>
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('value') }}</p>
+ <a-input v-model="newTag.value"></a-input>
+ </div>
+ <a-button type="primary" @click="() => handleAddTag()">{{
$t('label.add') }}</a-button>
+ </div>
+
+ <a-divider></a-divider>
+
+ <div v-show="!tagsModalLoading" class="tags-container">
+ <div class="tags" v-for="(tag, index) in tags" :key="index">
+ <a-tag :key="index" :closable="true" :afterClose="() =>
handleDeleteTag(tag)">
+ {{ tag.key }} = {{ tag.value }}
+ </a-tag>
+ </div>
+ </div>
+
+ <a-button class="add-tags-done" @click="tagsModalVisible = false"
type="primary">{{ $t('done') }}</a-button>
+ </a-modal>
+
+ <a-modal
+ title="Add VM"
+ v-model="addVmModalVisible"
+ class="vm-modal"
+ width="60vw"
+ @ok="addRule"
+ :okButtonProps="{ props:
+ {disabled: newRule.virtualmachineid === null } }"
+ @cancel="closeModal"
+ >
+
+ <a-icon v-if="addVmModalLoading" type="loading"></a-icon>
+
+ <div v-else>
+ <div class="vm-modal__header">
+ <span style="min-width: 200px;">{{ $t('name') }}</span>
+ <span>{{ $t('instancename') }}</span>
+ <span>{{ $t('displayname') }}</span>
+ <span>{{ $t('ip') }}</span>
+ <span>{{ $t('account') }}</span>
+ <span>{{ $t('zone') }}</span>
+ <span>{{ $t('state') }}</span>
+ <span>{{ $t('select') }}</span>
+ </div>
+
+ <a-radio-group v-model="newRule.virtualmachineid" style="width: 100%;"
@change="fetchNics">
+ <div v-for="(vm, index) in vms" :key="index" class="vm-modal__item">
+
+ <span style="min-width: 200px;">
+ <span>
+ {{ vm.name }}
+ </span>
+ <a-icon v-if="addVmModalNicLoading" type="loading"></a-icon>
+ <a-select
+ v-else-if="!addVmModalNicLoading && newRule.virtualmachineid
=== vm.id"
+ v-model="newRule.vmguestip">
+ <a-select-option v-for="(nic, nicIndex) in nics" :key="nic"
:value="nic">
+ {{ nic }}{{ nicIndex === 0 ? ' (Primary)' : null }}
+ </a-select-option>
+ </a-select>
+ </span>
+ <span>{{ vm.instancename }}</span>
+ <span>{{ vm.displayname }}</span>
+ <span></span>
+ <span>{{ vm.account }}</span>
+ <span>{{ vm.zonename }}</span>
+ <span>{{ vm.state }}</span>
+ <a-radio :value="vm.id" />
+ </div>
+ </a-radio-group>
+ </div>
+
+ </a-modal>
+
+ </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
+ },
+ inject: ['parentFetchData', 'parentToggleLoading'],
+ data () {
+ return {
+ loading: true,
+ portForwardRules: [],
+ newRule: {
+ protocol: 'tcp',
+ privateport: null,
+ privateendport: null,
+ publicport: null,
+ publicendport: null,
+ openfirewall: false,
+ vmguestip: null,
+ virtualmachineid: null
+ },
+ tagsModalVisible: false,
+ selectedRule: null,
+ tags: [],
+ newTag: {
+ key: null,
+ value: null
+ },
+ tagsModalLoading: false,
+ addVmModalVisible: false,
+ addVmModalLoading: false,
+ addVmModalNicLoading: false,
+ vms: [],
+ nics: []
+ }
+ },
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ resource: function (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.resource = newItem
+ this.fetchData()
+ }
+ },
+ filters: {
+ capitalise: val => {
+ if (val === 'all') return 'All'
+ return val.toUpperCase()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.loading = true
+ api('listPortForwardingRules', {
+ listAll: true,
+ ipaddressid: this.resource.id
+ }).then(response => {
+ this.portForwardRules =
response.listportforwardingrulesresponse.portforwardingrule
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.loading = false
+ })
+ },
+ deleteRule (rule) {
+ this.loading = true
+ api('deletePortForwardingRule', { id: rule.id }).then(response => {
+ this.$pollJob({
+ jobId: response.deleteportforwardingruleresponse.jobid,
+ successMessage: `Successfully removed Port Forwarding rule`,
+ successMethod: () => this.fetchData(),
+ errorMessage: 'Removing Port Forwarding rule failed',
+ errorMethod: () => this.fetchData(),
+ loadingMessage: `Deleting Port Forwarding rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => this.fetchData()
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.fetchData()
+ })
+ },
+ addRule () {
+ this.loading = true
+ this.addVmModalVisible = false
+ api('createPortForwardingRule', {
+ ...this.newRule,
+ ipaddressid: this.resource.id,
+ networkid: this.resource.associatednetworkid
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.createportforwardingruleresponse.jobid,
+ successMessage: `Successfully added new Port Forwarding rule`,
+ successMethod: () => {
+ this.closeModal()
+ this.fetchData()
+ },
+ errorMessage: 'Adding new Port Forwarding rule failed',
+ errorMethod: () => {
+ this.closeModal()
+ this.fetchData()
+ },
+ loadingMessage: `Adding new Port Forwarding rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.closeModal()
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description:
error.response.data.createportforwardingruleresponse.errortext
+ })
+ this.closeModal()
+ this.fetchData()
+ })
+ },
+ resetAllRules () {
+ this.newRule.protocol = 'tcp'
+ this.newRule.privateport = null
+ this.newRule.privateendport = null
+ this.newRule.publicport = null
+ this.newRule.publicendport = null
+ this.newRule.openfirewall = false
+ this.newRule.vmguestip = null
+ this.newRule.virtualmachineid = null
+ },
+ resetTagInputs () {
+ this.newTag.key = null
+ this.newTag.value = null
+ },
+ closeModal () {
+ this.selectedRule = null
+ this.tagsModalVisible = false
+ this.addVmModalVisible = false
+ this.newRule.virtualmachineid = null
+ this.addVmModalLoading = false
+ this.addVmModalNicLoading = false
+ this.nics = []
+ this.resetTagInputs()
+ this.resetAllRules()
+ },
+ openTagsModal (id) {
+ this.tagsModalLoading = true
+ this.selectedRule = id
+ this.tagsModalVisible = true
+ this.tags = []
+ this.resetTagInputs()
+ api('listTags', {
+ resourceId: id,
+ resourceType: 'PortForwardingRule',
+ listAll: true
+ }).then(response => {
+ this.tags = response.listtagsresponse.tag
+ this.tagsModalLoading = false
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleAddTag () {
+ this.tagsModalLoading = true
+ api('createTags', {
+ 'tags[0].key': this.newTag.key,
+ 'tags[0].value': this.newTag.value,
+ resourceIds: this.selectedRule,
+ resourceType: 'PortForwardingRule'
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.createtagsresponse.jobid,
+ successMessage: `Successfully added tag`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.openTagsModal(this.selectedRule)
+ },
+ errorMessage: 'Failed to add new tag',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ },
+ loadingMessage: `Adding tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createtagsresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ handleDeleteTag (tag) {
+ this.tagsModalLoading = true
+ api('deleteTags', {
+ 'tags[0].key': tag.key,
+ 'tags[0].value': tag.value,
+ resourceIds: this.selectedRule,
+ resourceType: 'PortForwardingRule'
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.deletetagsresponse.jobid,
+ successMessage: `Successfully removed tag`,
+ successMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.openTagsModal(this.selectedRule)
+ },
+ errorMessage: 'Failed to remove tag',
+ errorMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ },
+ loadingMessage: `Removing tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.parentFetchData()
+ this.parentToggleLoading()
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deletetagsresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ openAddVMModal () {
+ this.addVmModalVisible = true
+ this.addVmModalLoading = true
+ api('listVirtualMachines', {
+ listAll: true,
+ page: 1,
+ pagesize: 500,
+ networkid: this.resource.associatednetworkid,
+ account: this.resource.account,
+ domainid: this.resource.domainid
+ }).then(response => {
+ this.vms = response.listvirtualmachinesresponse.virtualmachine
+ this.addVmModalLoading = false
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ },
+ fetchNics (e) {
+ this.addVmModalNicLoading = true
+ api('listNics', {
+ virtualmachineid: e.target.value,
+ networkid: this.resource.associatednetworkid
+ }).then(response => {
+ if (!response.listnicsresponse.nic ||
response.listnicsresponse.nic.length < 1) return
+ const nic = response.listnicsresponse.nic[0]
+ this.nics.push(nic.ipaddress)
+ if (nic.secondaryip && nic.secondaryip.length > 0) {
+ this.nics.push(...nic.secondaryip.map(ip => ip.ipaddress))
+ }
+ this.newRule.vmguestip = this.nics[0]
+ this.addVmModalNicLoading = false
+ }).catch(error => {
+ console.log(error)
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ this.closeModal()
+ })
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ .rule {
+
+ &-container {
+ display: flex;
+ width: 100%;
+ flex-wrap: wrap;
+ margin-right: -20px;
+ margin-bottom: -10px;
+ }
+
+ &__item {
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ flex: 1;
+ }
+
+ }
+
+ &__title {
+ font-weight: bold;
+ }
+
+ }
+
+ .add-btn {
+ width: 100%;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ height: auto;
+ }
+
+ .add-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-right: -20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ margin-top: 20px;
+ }
+
+ button {
+ margin-right: 20px;
+ }
+
+ }
+
+ .form {
+ display: flex;
+ margin-right: -20px;
+ margin-bottom: 20px;
+ flex-direction: column;
+
+ @media (min-width: 760px) {
+ flex-direction: row;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ /*flex: 1;*/
+ padding-right: 20px;
+ margin-bottom: 20px;
+
+ @media (min-width: 760px) {
+ margin-bottom: 0;
+ }
+
+ input,
+ .ant-select {
+ margin-top: auto;
+ }
+
+ &__input-container {
+ display: flex;
+
+ input {
+
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+
+ }
+
+ }
+
+ }
+
+ &__label {
+ font-weight: bold;
+ }
+
+ }
+
+ .rule-action {
+ margin-bottom: 20px;
+
+ &:not(:last-of-type) {
+ margin-right: 10px;
+ }
+
+ }
+
+ .tags {
+ margin-bottom: 10px;
+ }
+
+ .add-tags {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+
+ &__input {
+ margin-right: 10px;
+ }
+
+ &__label {
+ margin-bottom: 5px;
+ font-weight: bold;
+ }
+
+ }
+
+ .tags-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+ }
+
+ .add-tags-done {
+ display: block;
+ margin-left: auto;
+ }
+
+ .tags-modal-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(0,0,0,0.5);
+ z-index: 1;
+ color: #1890ff;
+ font-size: 2rem;
+ }
+
+ .vm-modal {
+
+ &__header {
+ display: flex;
+
+ span {
+ flex: 1;
+ font-weight: bold;
+ margin-right: 10px;
+ }
+
+ }
+
+ &__item {
+ display: flex;
+ margin-top: 10px;
+
+ span,
+ label {
+ display: block;
+ flex: 1;
+ margin-right: 10px;
+ }
+
+ }
+
+ }
+
+</style>
diff --git a/src/views/network/VpnDetails.vue b/src/views/network/VpnDetails.vue
index a0b23ad..f630d18 100644
--- a/src/views/network/VpnDetails.vue
+++ b/src/views/network/VpnDetails.vue
@@ -16,25 +16,190 @@
// under the License.
<template>
- <div>
- TODO: VPN configure/detail tab view
+ <div v-if="remoteAccessVpn">
+ <div>
+ <p>Your Remote Access VPN is currently enabled and can be accessed via
the IP <strong>{{ remoteAccessVpn.publicip }}</strong></p>
+ <p>Your IPSec pre-shared key is <strong>{{ remoteAccessVpn.presharedkey
}}</strong></p>
+ <a-divider/>
+ <a-button><router-link :to="{ path: '/vpnuser'}">Manage VPN
Users</router-link></a-button>
+ <a-button style="margin-left: 10px" type="danger" @click="disableVpn =
true">Disable VPN</a-button>
+ </div>
+
+ <a-modal v-model="disableVpn" :footer="null" oncancel="disableVpn = false"
title="Disable Remove Access VPN">
+ <p>Are you sure you want to disable VPN?</p>
+
+ <a-divider></a-divider>
+
+ <div class="actions">
+ <a-button @click="() => disableVpn = false">Cancel</a-button>
+ <a-button type="primary" @click="handleDisableVpn">Yes</a-button>
+ </div>
+ </a-modal>
+
+ </div>
+ <div v-else>
+ <a-button type="primary" @click="enableVpn = true">Enable VPN</a-button>
+
+ <a-modal v-model="enableVpn" :footer="null" onCancel="enableVpn = false"
title="Enable Remote Access VPN">
+ <p>Please confirm that you want Remote Access VPN enabled for this IP
address.</p>
+
+ <a-divider></a-divider>
+
+ <div class="actions">
+ <a-button @click="() => enableVpn = false">Cancel</a-button>
+ <a-button type="primary" @click="handleCreateVpn">Yes</a-button>
+ </div>
+ </a-modal>
+
</div>
</template>
<script>
+import { api } from '@/api'
export default {
- name: '',
- components: {
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
},
data () {
return {
+ remoteAccessVpn: null,
+ enableVpn: false,
+ disableVpn: false
+ }
+ },
+ inject: ['parentFetchData', 'parentToggleLoading'],
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ resource: function (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.resource = newItem
+ this.fetchData()
}
},
methods: {
+ fetchData () {
+ api('listRemoteAccessVpns', {
+ publicipid: this.resource.id,
+ listAll: true
+ }).then(response => {
+ this.remoteAccessVpn =
response.listremoteaccessvpnsresponse.remoteaccessvpn
+ ? response.listremoteaccessvpnsresponse.remoteaccessvpn[0] : null
+ }).catch(error => {
+ console.log(error)
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ })
+ },
+ handleCreateVpn () {
+ this.parentToggleLoading()
+ this.enableVpn = false
+ api('createRemoteAccessVpn', {
+ publicipid: this.resource.id,
+ domainid: this.resource.domainid,
+ account: this.resource.account
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.createremoteaccessvpnresponse.jobid,
+ successMethod: result => {
+ const res = result.jobresult.remoteaccessvpn
+ this.$notification.success({
+ message: 'Status',
+ description:
+ `Your Remote Access VPN is currently enabled and can be
accessed via the IP ${res.publicip}. Your IPSec pre-shared key is
${res.presharedkey}`,
+ duration: 0
+ })
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ },
+ errorMessage: 'Failed to enable VPN',
+ errorMethod: () => {
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ },
+ loadingMessage: `Enabling VPN...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createremoteaccessvpnresponse
+ ? error.response.data.createremoteaccessvpnresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ })
+ },
+ handleDisableVpn () {
+ this.parentToggleLoading()
+ this.disableVpn = false
+ api('deleteRemoteAccessVpn', {
+ publicipid: this.resource.id,
+ domainid: this.resource.domainid
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.deleteremoteaccessvpnresponse.jobid,
+ successMessage: 'Successfully disabled VPN',
+ successMethod: () => {
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ },
+ errorMessage: 'Failed to disable VPN',
+ errorMethod: () => {
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ },
+ loadingMessage: `Disabling VPN...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deleteremoteaccessvpnresponse
+ ? error.response.data.deleteremoteaccessvpnresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ this.fetchData()
+ this.parentFetchData()
+ this.parentToggleLoading()
+ })
+ }
}
}
</script>
-<style scoped>
+<style scoped lang="scss">
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+
+ button {
+ &:not(:last-child) {
+ margin-right: 20px;
+ }
+ }
+ }
</style>