Skip to content

Commit 01e2e5a

Browse files
committed
VNF: ui demo
1 parent 129f799 commit 01e2e5a

File tree

9 files changed

+493
-3
lines changed

9 files changed

+493
-3
lines changed

api/src/main/java/org/apache/cloudstack/vnf/VnfService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ enum VnfOperation {
133133
LOAD_BALANCER_POOL_CREATE(ServiceCategory.LOAD_BALANCING, "Create load balancer pool"),
134134
LOAD_BALANCER_VIRTUAL_SERVER_CREATE(ServiceCategory.LOAD_BALANCING, "Create load balancer virtual server"),
135135
LOAD_BALANCER_VIRTUAL_SERVER_DELETE(ServiceCategory.LOAD_BALANCING, "Delete load balancer virtual server"),
136+
LOAD_BALANCER_VIRTUAL_SERVER_LIST(ServiceCategory.LOAD_BALANCING, "List load balancer virtual servers"),
136137
LOAD_BALANCER_MEMBER_ADD(ServiceCategory.LOAD_BALANCING, "Add member to load balancer pool"),
137138
LOAD_BALANCER_MEMBER_REMOVE(ServiceCategory.LOAD_BALANCING, "Remove member from load balancer pool"),
138139

plugins/vnf-providers/dummy/src/main/java/org/apache/cloudstack/vnf/DummyVnfProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public Map<ServiceCategory, List<VnfOperation>> getSupportedOperations() {
3232
return Map.of(
3333
ServiceCategory.INTERFACE_MANAGEMENT, List.of(VnfOperation.INTERFACE_CONFIGURE),
3434
ServiceCategory.FIREWALL_RULES, List.of(VnfOperation.FIREWALL_RULE_CREATE, VnfOperation.FIREWALL_RULE_DELETE, VnfOperation.FIREWALL_RULE_UPDATE, VnfOperation.FIREWALL_RULE_LIST),
35-
ServiceCategory.LOAD_BALANCING, List.of(VnfOperation.NAT_SOURCE_CREATE, VnfOperation.NAT_DESTINATION_CREATE, VnfOperation.NAT_RULE_DELETE, VnfOperation.NAT_PORT_FORWARD_CREATE),
35+
ServiceCategory.LOAD_BALANCING, List.of(VnfOperation.LOAD_BALANCER_VIRTUAL_SERVER_CREATE, VnfOperation.LOAD_BALANCER_VIRTUAL_SERVER_DELETE, VnfOperation.LOAD_BALANCER_VIRTUAL_SERVER_LIST),
36+
ServiceCategory.NAT, List.of(VnfOperation.NAT_SOURCE_CREATE, VnfOperation.NAT_DESTINATION_CREATE, VnfOperation.NAT_RULE_DELETE, VnfOperation.NAT_PORT_FORWARD_CREATE),
3637
ServiceCategory.DHCP, List.of(VnfOperation.DHCP_SERVER_CONFIGURE, VnfOperation.DHCP_SERVER_RESTART, VnfOperation.DHCP_STATIC_LEASE_ADD, VnfOperation.DHCP_STATIC_LEASE_REMOVE),
3738
ServiceCategory.DNS, List.of(VnfOperation.DNS_HOST_OVERRIDE_ADD, VnfOperation.DNS_HOST_OVERRIDE_REMOVE)
3839
);

ui/public/locales/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2041,6 +2041,7 @@
20412041
"label.register.user.data": "Register User Data",
20422042
"label.register.cni.config": "Register CNI Configuration",
20432043
"label.register.user.data.details": "Enter the User Data in plain text or in Base64 encoding. Up to 32KB of Base64 encoded User Data can be sent by default. The setting vm.userdata.max.length can be used to increase the limit to upto 1MB.",
2044+
"label.register.vnf.provider": "Register VNF provider",
20442045
"label.reinstall.vm": "Reinstall Instance",
20452046
"label.reject": "Reject",
20462047
"label.related": "Related",
@@ -2235,6 +2236,8 @@
22352236
"label.select.source.vcenter.datacenter": "Select the source VMware vCenter Datacenter",
22362237
"label.select.tier": "Select Network Tier",
22372238
"label.select.vm": "Select Instance",
2239+
"label.select.vnf.service": "Select VNF service",
2240+
"label.select.vnf.operation": "Select VNF operation",
22382241
"label.select.volume": "Select Volume",
22392242
"label.select.zones": "Select zones",
22402243
"label.select.storagepools": "Select storage pools",
@@ -2755,6 +2758,7 @@
27552758
"label.vnf.app.action.reinstall": "Reinstall VNF Appliance",
27562759
"label.vnf.cidr.list": "CIDR from which access to the VNF appliance's Management interface should be allowed from",
27572760
"label.vnf.cidr.list.tooltip": "the CIDR list to forward traffic from to the VNF management interface. Multiple entries must be separated by a single comma character (,). The default value is 0.0.0.0/0.",
2761+
"label.vnf.configurations": "VNF Configurations",
27582762
"label.vnf.configure.management": "Configure network rules for VNF's management interfaces",
27592763
"label.vnf.configure.management.tooltip": "False by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. True otherwise. Learn what rules are configured at http://docs.cloudstack.apache.org/en/latest/adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances",
27602764
"label.vnf.detail.add": "Add VNF detail",
@@ -2772,6 +2776,11 @@
27722776
"label.vnf.nic.remove": "Remove VNF nic",
27732777
"label.vnf.nic.required": "True if VNF nic is required. Otherwise optional",
27742778
"label.vnf.nics": "VNF Nics",
2779+
"label.vnf.operation.parameters": "Please input the parameters of VNF operation",
2780+
"label.vnf.operation.response": "Response of VNF operation",
2781+
"label.vnf.provider": "VNF Provider",
2782+
"label.vnf.provider.definition": "Definition of VNF Provider",
2783+
"label.vnf.providers": "VNF Providers",
27752784
"label.vnf.settings": "VNF settings",
27762785
"label.vnf.templates": "VNF templates",
27772786
"label.vnf.template.register": "Register VNF template",

ui/src/components/view/DetailsTab.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
{{ service.name }} : {{ service.provider?.[0]?.name }}
5454
</div>
5555
</div>
56+
<div v-else-if="$route.meta.name === 'vnfprovider' && item === 'services'">
57+
<div v-for="(service, idx) in dataResource[item]" :key="idx">
58+
{{ service.name }}
59+
</div>
60+
</div>
5661
<div v-else-if="$route.meta.name === 'backup' && (item === 'size' || item === 'virtualsize')">
5762
{{ $bytesToHumanReadableSize(dataResource[item]) }}
5863
<a-tooltip placement="right">
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
<template>
19+
<div>
20+
{{ $t('label.select.vnf.service') + ':' }}
21+
<a-select
22+
v-focus="true"
23+
style="width: 40%; margin-left: 15px;margin-bottom: 15px"
24+
:loading="fetchLoading"
25+
defaultActiveFirstOption
26+
v-model:value="vnfServiceName"
27+
@change="handleVnfServiceSelect"
28+
showSearch
29+
optionFilterProp="label"
30+
:filterOption="(input, option) => {
31+
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
32+
}" >
33+
<a-select-option v-for="service in vnfServices" :key="service.name" :value="service.name" :label="service.name" style="width: 90%;">
34+
{{ service.name }}
35+
</a-select-option>
36+
</a-select>
37+
</div>
38+
<div>
39+
{{ $t('label.select.vnf.operation') + ':' }}
40+
<a-select
41+
v-focus="true"
42+
style="width: 40%; margin-left: 15px;margin-bottom: 15px"
43+
:loading="fetchLoading"
44+
defaultActiveFirstOption
45+
v-model:value="vnfOperationName"
46+
@change="handleVnfOperationSelect"
47+
showSearch
48+
optionFilterProp="label"
49+
:filterOption="(input, option) => {
50+
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
51+
}" >
52+
<a-select-option v-for="operation in vnfOperations" :key="operation.name" :value="operation.name" :label="operation.description" style="width: 90%;">
53+
{{ operation.description }}
54+
</a-select-option>
55+
</a-select>
56+
</div>
57+
<a-alert type="info" :showIcon="true" v-if="this.vnfOperation && this.vnfOperation.requiredParameters && !this.vnfOperationResponse">
58+
<template #description>
59+
<div>
60+
<strong>{{ $t('label.vnf.operation.parameters') + " : " + this.vnfOperation.name }}</strong>
61+
</div>
62+
<br>
63+
<a-form layout="vertical" style="margin-top: 10px">
64+
<a-row
65+
v-for="param in vnfOperation.requiredParameters"
66+
:key="param.name"
67+
gutter="16"
68+
style="margin-bottom: 1rem; align-items: center;"
69+
>
70+
<a-col :span="12">
71+
<strong>{{ param.description }}</strong>
72+
</a-col>
73+
74+
<a-col :span="6">
75+
<a-input
76+
v-if="param.type === 'string'"
77+
v-model:value="formData[param.name]"
78+
:placeholder="param.name"
79+
/>
80+
</a-col>
81+
</a-row>
82+
<a-button ref="submit" type="primary" @click="handleSubmit">{{ $t('label.submit') }}</a-button>
83+
</a-form>
84+
</template>
85+
</a-alert>
86+
<a-alert v-if="vnfOperationResponseNode" type="success" :showIcon="true">
87+
<template #description>
88+
<component :is="vnfOperationResponseNode" />
89+
</template>
90+
</a-alert>
91+
</template>
92+
93+
<script>
94+
import { getAPI, postAPI } from '@/api'
95+
import Status from '@/components/widgets/Status'
96+
import { h } from 'vue'
97+
98+
export default {
99+
name: 'VnfAppliancesTab',
100+
components: {
101+
Status
102+
},
103+
props: {
104+
resource: {
105+
type: Object,
106+
required: true
107+
},
108+
loading: {
109+
type: Boolean,
110+
default: false
111+
}
112+
},
113+
data () {
114+
return {
115+
fetchLoading: false,
116+
vnfService: null,
117+
vnfServiceName: '',
118+
vnfServices: [],
119+
vnfOperation: null,
120+
vnfOperationName: '',
121+
vnfOperations: [],
122+
formData: {},
123+
vnfOperationResponse: '',
124+
vnfOperationResponseNode: null
125+
}
126+
},
127+
created () {
128+
this.fetchData()
129+
},
130+
watch: {
131+
resource: {
132+
deep: true,
133+
handler (newItem) {
134+
if (!newItem || !newItem.id) {
135+
return
136+
}
137+
this.fetchData()
138+
}
139+
}
140+
},
141+
methods: {
142+
fetchData () {
143+
var params = {
144+
virtualmachineid: this.resource.id
145+
}
146+
this.fetchLoading = true
147+
getAPI('vnfListProviders', params).then(json => {
148+
this.vnfServices = json.vnflistprovidersresponse.vnfprovider?.[0]?.service || []
149+
this.vnfServiceName = this.vnfServices?.[0]?.name || ''
150+
if (this.vnfServiceName) {
151+
this.handleVnfServiceSelect(this.vnfServiceName)
152+
}
153+
}).catch(error => {
154+
this.$notifyError(error)
155+
}).finally(() => {
156+
this.fetchLoading = false
157+
})
158+
},
159+
resetVnfOperationRequestResponse () {
160+
this.formData = {}
161+
this.vnfOperationResponse = null
162+
this.vnfOperationResponseNode = null
163+
},
164+
handleVnfServiceSelect (service) {
165+
this.resetVnfOperationRequestResponse()
166+
this.vnfServiceName = service
167+
this.vnfService = this.vnfServices.find(vnfService => vnfService.name === service)
168+
this.vnfOperations = this.vnfService.operations
169+
this.vnfOperationName = this.vnfOperations?.[0]?.name || ''
170+
if (this.vnfOperationName) {
171+
this.handleVnfOperationSelect(this.vnfOperationName)
172+
}
173+
},
174+
handleVnfOperationSelect (operation) {
175+
this.resetVnfOperationRequestResponse()
176+
this.vnfOperationName = operation
177+
this.vnfOperation = this.vnfOperations.find(vnfOperation => vnfOperation.name === operation)
178+
if (['FIREWALL_RULE_LIST'].includes(this.vnfOperationName)) {
179+
this.vnfOperation.autoSubmit = true
180+
} else if (this.vnfOperationName === 'FIREWALL_RULE_CREATE') {
181+
this.vnfOperation.requiredParameters = JSON.parse('[{"name":"action","type":"string","description":"Action to perform: pass or block"},' +
182+
'{"name":"interface","type":"string","description":"Interface where the rule applies, e.g., lan, wan"},' +
183+
'{"name":"ipprotocol","type":"string","description":"IP protocol: inet (IPv4), inet6 (IPv6), or any"},' +
184+
'{"name":"protocol","type":"string","description":"Transport protocol: tcp, udp, icmp, or any"},' +
185+
'{"name":"source_net","type":"string","description":"Source network address or subnet"},' +
186+
'{"name":"source_port","type":"string","description":"Source port number or any"},' +
187+
'{"name":"destination_net","type":"string","description":"Destination network address or subnet"},' +
188+
'{"name":"destination_port","type":"string","description":"Destination port number or any"},' +
189+
'{"name":"descr","type":"string","description":"Description of the firewall rule"},' +
190+
'{"name":"vnf_rule_id","type":"string","description":"Unique identifier for the VNF firewall rule"}]')
191+
this.vnfOperation.optionalParameters = ''
192+
}
193+
if (this.vnfOperation.autoSubmit) {
194+
this.handleSubmit()
195+
}
196+
},
197+
handleSubmit () {
198+
if (this.formData) {
199+
console.log('data = ' + JSON.stringify(this.formData))
200+
}
201+
var params = {
202+
service: this.vnfServiceName,
203+
operation: this.vnfOperationName,
204+
data: JSON.stringify(this.formData)
205+
}
206+
postAPI('performVnfAction', params).then(json => {
207+
const response = json.performvnfactionresponse.response || null
208+
if (response) {
209+
console.log('response = ' + JSON.stringify(response))
210+
}
211+
}).finally(() => {
212+
this.fetchLoading = false
213+
this.vnfOperationResponse = '{}'
214+
if (this.vnfOperationName === 'FIREWALL_RULE_LIST') {
215+
this.vnfOperationResponse = '{"result":[' +
216+
'{"uuid":"123e4567-e89b-12d3-a456-426614174000","id":1,"action":"pass","enabled":true,"interface":"lan","direction":"in","ipprotocol":"inet","protocol":"tcp","source_net":"192.168.1.0/24","source_port":"any","destination_net":"any","destination_port":443,"description":"Allow LAN → any on HTTPS"},' +
217+
'{"uuid":"123e4567-e89b-12d3-a456-426614174001","id":2,"action":"block","enabled":true,"interface":"lan","direction":"in","ipprotocol":"inet","protocol":"any","source_net":"any","source_port":"any","destination_net":"192.168.1.0/24","destination_port":"any","description":"Block any inbound to LAN subnet"},' +
218+
'{"uuid":"123e4567-e89b-12d3-a456-426614174002","id":3,"action":"pass","enabled":true,"interface":"wan","direction":"in","ipprotocol":"inet","protocol":"tcp","source_net":"any","source_port":"any","destination_net":"203.0.113.10","destination_port":22,"description":"Allow SSH access to firewall host"}]}'
219+
}
220+
this.processVnfOperationResponse()
221+
})
222+
},
223+
processVnfOperationResponse () {
224+
const message = this.$t('label.vnf.operation.response') + ' : ' + this.vnfOperationName
225+
this.vnfOperationResponseNode = [h('p', `${message}`)]
226+
let parsedVnfOperationResponse = JSON.parse(this.vnfOperationResponse)?.result
227+
if (!parsedVnfOperationResponse) {
228+
this.vnfOperationResponseNode = h('div', this.vnfOperationResponseNode)
229+
return
230+
}
231+
if (parsedVnfOperationResponse && !Array.isArray(parsedVnfOperationResponse) && typeof parsedVnfOperationResponse === 'object' && Object.keys(parsedVnfOperationResponse).length > 0) {
232+
parsedVnfOperationResponse = [parsedVnfOperationResponse]
233+
}
234+
235+
if (Array.isArray(parsedVnfOperationResponse) && parsedVnfOperationResponse.length > 0) {
236+
this.vnfOperationResponseNode.push(
237+
h('div', {
238+
style: {
239+
marginTop: '1em',
240+
maxHeight: '50vh',
241+
maxWidth: '100%',
242+
overflow: 'auto',
243+
backgroundColor: '#f6f6f6',
244+
border: '1px solid #ddd',
245+
borderRadius: '4px',
246+
display: 'block'
247+
}
248+
}, [
249+
h('table', {
250+
style: {
251+
width: '100%',
252+
minWidth: 'max-content',
253+
borderCollapse: 'collapse',
254+
whiteSpace: 'pre-wrap'
255+
}
256+
}, [
257+
h('thead', [
258+
h('tr', Object.keys(parsedVnfOperationResponse[0]).map(key =>
259+
h('th', {
260+
style: {
261+
padding: '8px',
262+
border: '1px solid #ddd',
263+
textAlign: 'left',
264+
fontWeight: 'bold',
265+
backgroundColor: '#fafafa'
266+
}
267+
}, key)
268+
))
269+
]),
270+
h('tbody', parsedVnfOperationResponse.map(row =>
271+
h('tr', Object.values(row).map(value =>
272+
h('td', {
273+
style: {
274+
padding: '8px',
275+
border: '1px solid #ddd',
276+
fontFamily: 'monospace'
277+
}
278+
}, String(value))
279+
))
280+
))
281+
])
282+
])
283+
)
284+
}
285+
this.vnfOperationResponseNode = h('div', this.vnfOperationResponseNode)
286+
}
287+
}
288+
}
289+
</script>
290+
<style lang="scss" scoped>
291+
.status {
292+
margin-top: -5px;
293+
294+
&--end {
295+
margin-left: 5px;
296+
}
297+
}
298+
</style>

ui/src/config/section/network.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,27 @@ export default {
349349
}
350350
]
351351
},
352+
{
353+
name: 'vnfprovider',
354+
title: 'label.vnf.providers',
355+
icon: 'borderless-table-outlined',
356+
permission: ['vnfListProviders'],
357+
columns: () => {
358+
const fields = ['name', 'description', 'type']
359+
return fields
360+
},
361+
searchFilters: ['name'],
362+
details: ['name', 'description', 'type', 'services'],
363+
actions: [
364+
{
365+
api: 'registerVnfProvider',
366+
icon: 'plus-outlined',
367+
label: 'label.register.vnf.provider',
368+
listView: true,
369+
popup: true,
370+
component: shallowRef(defineAsyncComponent(() => import('@/views/network/RegisterVnfProvider.vue')))
371+
}]
372+
},
352373
{
353374
name: 'vnfapp',
354375
title: 'label.vnf.appliances',

0 commit comments

Comments
 (0)