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 <rfcvinc...@gmail.com> 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 <rohit.ya...@shapeblue.com> Co-authored-by: Rohit Yadav <ro...@apache.org> --- 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>