import network acl rules using csv (#12013)

This commit is contained in:
Manoj Kumar 2026-01-29 13:40:28 +05:30 committed by GitHub
parent bd459a4c4c
commit a8f1e4a5ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 699 additions and 12 deletions

View File

@ -582,6 +582,7 @@ public class EventTypes {
// Network ACL
public static final String EVENT_NETWORK_ACL_CREATE = "NETWORK.ACL.CREATE";
public static final String EVENT_NETWORK_ACL_IMPORT = "NETWORK.ACL.IMPORT";
public static final String EVENT_NETWORK_ACL_DELETE = "NETWORK.ACL.DELETE";
public static final String EVENT_NETWORK_ACL_REPLACE = "NETWORK.ACL.REPLACE";
public static final String EVENT_NETWORK_ACL_UPDATE = "NETWORK.ACL.UPDATE";

View File

@ -19,6 +19,7 @@ package com.cloud.network.vpc;
import java.util.List;
import org.apache.cloudstack.api.command.user.network.CreateNetworkACLCmd;
import org.apache.cloudstack.api.command.user.network.ImportNetworkACLCmd;
import org.apache.cloudstack.api.command.user.network.ListNetworkACLListsCmd;
import org.apache.cloudstack.api.command.user.network.ListNetworkACLsCmd;
import org.apache.cloudstack.api.command.user.network.MoveNetworkAclItemCmd;
@ -98,4 +99,6 @@ public interface NetworkACLService {
NetworkACLItem moveNetworkAclRuleToNewPosition(MoveNetworkAclItemCmd moveNetworkAclItemCmd);
NetworkACLItem moveRuleToTheTopInACLList(NetworkACLItem ruleBeingMoved);
List<NetworkACLItem> importNetworkACLRules(ImportNetworkACLCmd cmd) throws ResourceUnavailableException;
}

View File

@ -58,7 +58,7 @@ public class CreateNetworkACLCmd extends BaseAsyncCreateCmd {
private Integer publicEndPort;
@Parameter(name = ApiConstants.CIDR_LIST, type = CommandType.LIST, collectionType = CommandType.STRING, description = "The CIDR list to allow traffic from/to. Multiple entries must be separated by a single comma character (,).")
private List<String> cidrlist;
private List<String> cidrList;
@Parameter(name = ApiConstants.ICMP_TYPE, type = CommandType.INTEGER, description = "Type of the ICMP message being sent")
private Integer icmpType;
@ -118,8 +118,8 @@ public class CreateNetworkACLCmd extends BaseAsyncCreateCmd {
}
public List<String> getSourceCidrList() {
if (cidrlist != null) {
return cidrlist;
if (cidrList != null) {
return cidrList;
} else {
List<String> oneCidrList = new ArrayList<String>();
oneCidrList.add(NetUtils.ALL_IP4_CIDRS);
@ -238,6 +238,30 @@ public class CreateNetworkACLCmd extends BaseAsyncCreateCmd {
return reason;
}
public void setCidrList(List<String> cidrList) {
this.cidrList = cidrList;
}
public void setIcmpType(Integer icmpType) {
this.icmpType = icmpType;
}
public void setIcmpCode(Integer icmpCode) {
this.icmpCode = icmpCode;
}
public void setNumber(Integer number) {
this.number = number;
}
public void setDisplay(Boolean display) {
this.display = display;
}
public void setReason(String reason) {
this.reason = reason;
}
@Override
public void create() {
NetworkACLItem result = _networkACLService.createNetworkACLItem(this);

View File

@ -0,0 +1,132 @@
// 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.
package org.apache.cloudstack.api.command.user.network;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.NetworkACLItemResponse;
import org.apache.cloudstack.api.response.NetworkACLResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.collections.MapUtils;
import com.cloud.event.EventTypes;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.vpc.NetworkACLItem;
import com.cloud.user.Account;
@APICommand(name = "importNetworkACL", description = "Imports Network ACL rules.",
responseObject = NetworkACLItemResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
since = "4.22.1")
public class ImportNetworkACLCmd extends BaseAsyncCmd {
// ///////////////////////////////////////////////////
// ////////////// API parameters /////////////////////
// ///////////////////////////////////////////////////
@Parameter(
name = ApiConstants.ACL_ID,
type = CommandType.UUID,
entityType = NetworkACLResponse.class,
required = true,
description = "The ID of the Network ACL to which the rules will be imported"
)
private Long aclId;
@Parameter(name = ApiConstants.RULES, type = CommandType.MAP, required = true,
description = "Rules param list, id and protocol are must. Invalid rules will be discarded. Example: " +
"rules[0].id=101&rules[0].protocol=tcp&rules[0].traffictype=ingress&rules[0].state=active&rules[0].cidrlist=192.168.1.0/24" +
"&rules[0].tags=web&rules[0].aclid=acl-001&rules[0].aclname=web-acl&rules[0].number=1&rules[0].action=allow&rules[0].fordisplay=true" +
"&rules[0].description=allow%20web%20traffic&rules[1].id=102&rules[1].protocol=udp&rules[1].traffictype=egress&rules[1].state=enabled" +
"&rules[1].cidrlist=10.0.0.0/8&rules[1].tags=db&rules[1].aclid=acl-002&rules[1].aclname=db-acl&rules[1].number=2&rules[1].action=deny" +
"&rules[1].fordisplay=false&rules[1].description=deny%20database%20traffic")
private Map rules;
// ///////////////////////////////////////////////////
// ///////////////// Accessors ///////////////////////
// ///////////////////////////////////////////////////
// Returns map, corresponds to a rule with the details in the keys:
// id, protocol, startport, endport, traffictype, state, cidrlist, tags, aclid, aclname, number, action, fordisplay, description
public Map getRules() {
return rules;
}
public Long getAclId() {
return aclId;
}
// ///////////////////////////////////////////////////
// ///////////// API Implementation///////////////////
// ///////////////////////////////////////////////////
@Override
public void execute() throws ResourceUnavailableException {
validateParams();
List<NetworkACLItem> importedRules = _networkACLService.importNetworkACLRules(this);
ListResponse<NetworkACLItemResponse> response = new ListResponse<>();
List<NetworkACLItemResponse> aclResponse = new ArrayList<>();
for (NetworkACLItem acl : importedRules) {
NetworkACLItemResponse ruleData = _responseGenerator.createNetworkACLItemResponse(acl);
aclResponse.add(ruleData);
}
response.setResponses(aclResponse, importedRules.size());
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
Account account = CallContext.current().getCallingAccount();
if (account != null) {
return account.getId();
}
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public String getEventType() {
return EventTypes.EVENT_NETWORK_ACL_IMPORT;
}
@Override
public String getEventDescription() {
return "Importing ACL rules for ACL ID: " + getAclId();
}
private void validateParams() {
if(MapUtils.isEmpty(rules)) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Rules parameter is empty or null");
}
if (getAclId() == null || _networkACLService.getNetworkACL(getAclId()) == null) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find Network ACL with provided ACL ID");
}
}
}

View File

@ -26,15 +26,11 @@ import java.util.Objects;
import javax.inject.Inject;
import com.cloud.dc.DataCenter;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NsxProviderDao;
import com.cloud.network.element.NetrisProviderVO;
import com.cloud.network.element.NsxProviderVO;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.network.CreateNetworkACLCmd;
import org.apache.cloudstack.api.command.user.network.ImportNetworkACLCmd;
import org.apache.cloudstack.api.command.user.network.ListNetworkACLListsCmd;
import org.apache.cloudstack.api.command.user.network.ListNetworkACLsCmd;
import org.apache.cloudstack.api.command.user.network.MoveNetworkAclItemCmd;
@ -43,19 +39,26 @@ import org.apache.cloudstack.api.command.user.network.UpdateNetworkACLListCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.cloud.dc.DataCenter;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.NsxProviderDao;
import com.cloud.network.element.NetrisProviderVO;
import com.cloud.network.element.NsxProviderVO;
import com.cloud.network.vpc.NetworkACLItem.Action;
import com.cloud.network.vpc.NetworkACLItem.TrafficType;
import com.cloud.network.vpc.dao.NetworkACLDao;
@ -1070,6 +1073,111 @@ public class NetworkACLServiceImpl extends ManagerBase implements NetworkACLServ
return moveRuleToTheTop(ruleBeingMoved, allRules);
}
@Override
public List<NetworkACLItem> importNetworkACLRules(ImportNetworkACLCmd cmd) throws ResourceUnavailableException {
long aclId = cmd.getAclId();
Map<Object, Object> rules = cmd.getRules();
List<NetworkACLItem> createdRules = new ArrayList<>();
List<String> errors = new ArrayList<>();
for (Map.Entry<Object, Object> entry : rules.entrySet()) {
try {
Map<String, Object> ruleMap = (Map<String, Object>) entry.getValue();
NetworkACLItem item = createACLRuleFromMap(ruleMap, aclId);
createdRules.add(item);
} catch (Exception ex) {
String error = "Failed to import rule at index " + entry.getKey() + ": " + ex.getMessage();
errors.add(error);
logger.error(error, ex);
}
}
// no rules got imported
if (createdRules.isEmpty() && !errors.isEmpty()) {
logger.error("Failed to import any ACL rules. Errors: {}", String.join("; ", errors));
throw new CloudRuntimeException("Failed to import any ACL rules.");
}
// apply ACL to network
if (!createdRules.isEmpty()) {
applyNetworkACL(aclId);
}
return createdRules;
}
private NetworkACLItem createACLRuleFromMap(Map<String, Object> ruleMap, long aclId) {
String protocol = (String) ruleMap.get(ApiConstants.PROTOCOL);
if (protocol == null || protocol.trim().isEmpty()) {
throw new InvalidParameterValueException("Protocol is required");
}
String action = (String) ruleMap.getOrDefault(ApiConstants.ACTION, "deny");
String trafficType = (String) ruleMap.getOrDefault(ApiConstants.TRAFFIC_TYPE, NetworkACLItem.TrafficType.Ingress);
String forDisplay = (String) ruleMap.getOrDefault(ApiConstants.FOR_DISPLAY, "true");
// Create ACL rule using the service
CreateNetworkACLCmd cmd = new CreateNetworkACLCmd();
cmd.setAclId(aclId);
cmd.setProtocol(protocol.toLowerCase());
cmd.setAction(action.toLowerCase());
cmd.setTrafficType(trafficType.toLowerCase());
cmd.setDisplay(BooleanUtils.toBoolean(forDisplay));
// Optional parameters
if (ruleMap.containsKey(ApiConstants.CIDR_LIST)) {
Object cidrObj = ruleMap.get(ApiConstants.CIDR_LIST);
List<String> cidrList = new ArrayList<>();
if (cidrObj instanceof String) {
for (String cidr : ((String) cidrObj).split(",")) {
cidrList.add(cidr.trim());
}
} else if (cidrObj instanceof List) {
cidrList.addAll((List<String>) cidrObj);
}
cmd.setCidrList(cidrList);
}
if (ruleMap.containsKey(ApiConstants.START_PORT)) {
cmd.setPublicStartPort(parseInt(ruleMap.get(ApiConstants.START_PORT)));
}
if (ruleMap.containsKey(ApiConstants.END_PORT)) {
cmd.setPublicEndPort(parseInt(ruleMap.get(ApiConstants.END_PORT)));
}
if (ruleMap.containsKey(ApiConstants.NUMBER)) {
cmd.setNumber(parseInt(ruleMap.get(ApiConstants.NUMBER)));
}
if (ruleMap.containsKey(ApiConstants.ICMP_TYPE)) {
cmd.setIcmpType(parseInt(ruleMap.get(ApiConstants.ICMP_TYPE)));
}
if (ruleMap.containsKey(ApiConstants.ICMP_CODE)) {
cmd.setIcmpCode(parseInt(ruleMap.get(ApiConstants.ICMP_CODE)));
}
if (ruleMap.containsKey(ApiConstants.ACL_REASON)) {
cmd.setReason((String) ruleMap.get(ApiConstants.ACL_REASON));
}
return createNetworkACLItem(cmd);
}
private Integer parseInt(Object value) {
if (value == null) {
return null;
}
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof String) {
try {
return Integer.parseInt((String) value);
} catch (NumberFormatException e) {
throw new InvalidParameterValueException("Invalid integer value: " + value);
}
}
throw new InvalidParameterValueException("Cannot convert to integer: " + value);
}
/**
* Validates the consistency of the ACL; the validation process is the following.
* <ul>

View File

@ -454,6 +454,7 @@ import org.apache.cloudstack.api.command.user.network.CreateNetworkPermissionsCm
import org.apache.cloudstack.api.command.user.network.DeleteNetworkACLCmd;
import org.apache.cloudstack.api.command.user.network.DeleteNetworkACLListCmd;
import org.apache.cloudstack.api.command.user.network.DeleteNetworkCmd;
import org.apache.cloudstack.api.command.user.network.ImportNetworkACLCmd;
import org.apache.cloudstack.api.command.user.network.ListNetworkACLListsCmd;
import org.apache.cloudstack.api.command.user.network.ListNetworkACLsCmd;
import org.apache.cloudstack.api.command.user.network.ListNetworkOfferingsCmd;
@ -4039,6 +4040,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(EnableStaticNatCmd.class);
cmdList.add(ListIpForwardingRulesCmd.class);
cmdList.add(CreateNetworkACLCmd.class);
cmdList.add(ImportNetworkACLCmd.class);
cmdList.add(CreateNetworkCmd.class);
cmdList.add(DeleteNetworkACLCmd.class);
cmdList.add(DeleteNetworkCmd.class);

View File

@ -42,7 +42,9 @@
"label.accounts": "Accounts",
"label.accountstate": "Account state",
"label.accounttype": "Account type",
"label.acl.export": "Export ACL rules",
"label.import": "Import",
"label.acl.import": "Import rules",
"label.acl.export": "Export rules",
"label.acl.id": "ACL ID",
"label.acl.rules": "ACL rules",
"label.acl.reason.description": "Enter the reason behind an ACL rule.",
@ -252,7 +254,7 @@
"label.activeviewersessions": "Active sessions",
"label.add": "Add",
"label.add.account": "Add Account",
"label.add.acl.rule": "Add ACL rule",
"label.add.acl.rule": "Add rule",
"label.add.acl": "Add ACL",
"label.add.affinity.group": "Add new Affinity Group",
"label.add.backup.schedule": "Add Backup Schedule",
@ -704,6 +706,7 @@
"label.cron.mode": "Cron mode",
"label.crosszones": "Cross Zones",
"label.csienabled": "CSI Enabled",
"label.csv.preview": "Data preview",
"label.currency": "Currency",
"label.current": "Current",
"label.currentstep": "Current step",
@ -2902,6 +2905,8 @@
"message.action.create.snapshot.from.vmsnapshot": "Please confirm that you want to create Snapshot from Instance Snapshot",
"message.action.create.instance.from.backup": "Please confirm that you want to create a new Instance from the given Backup.<br>Click on configure to edit the parameters for the new Instance before creation.",
"message.create.instance.from.backup.different.zone": "Creating Instance from Backup on a different Zone. Please ensure that the backup repository is accessible in the selected Zone.",
"message.csv.empty": "Empty CSV File",
"message.csv.missing.headers": "Columns are missing from headers in CSV",
"message.template.ostype.different.from.backup": "Selected Template has a different OS type than the Backup. Please proceed with caution.",
"message.iso.ostype.different.from.backup": "Selected ISO has a different OS type than the Backup. Please proceed with caution.",
"message.action.delete.asnrange": "Please confirm the AS range that you want to delete",
@ -3643,6 +3648,7 @@
"message.move.acl.order.processing": "Moving ACL rule...",
"message.network.acl.default.allow": "Warning: With this policy all traffic will be allowed through the firewall to this VPC Network Tier. You should consider securing your Network.",
"message.network.acl.default.deny": "Warning: With this policy all traffic will be denied through the firewall to this VPC Network Tier. In order to allow traffic through you will need to change policies.",
"message.network.acl.import.note": "Note: Only valid rules from the CSV will be imported. Invalid entries will be discarded.",
"message.network.addvm.desc": "Please specify the Network that you would like to add this Instance to. A new NIC will be added for this Network.",
"message.network.description": "Setup Network and traffic.",
"message.network.error": "Network Error",

View File

@ -28,10 +28,16 @@
{{ $t('label.add.acl.rule') }}
</a-button>
<a-button type="dashed" @click="handleImportRules" style="width: 100%; margin-right: 10px">
<template #icon><upload-outlined /></template>
{{ $t('label.acl.import') }}
</a-button>
<a-button type="dashed" @click="exportAclList" style="width: 100%">
<template #icon><download-outlined /></template>
{{ $t('label.acl.export') }}
</a-button>
<div class="search-bar">
<a-input-search
style="width: 25vw;float: right;margin-left: 10px; z-index: 8"
@ -304,6 +310,21 @@
</div>
</a-form>
</a-modal>
<a-modal
v-if="showImportModal"
:visible="showImportModal"
:title="$t('label.acl.import')"
:closable="true"
:maskClosable="false"
:footer="null"
:width="800"
@cancel="closeImportModal">
<import-network-a-c-l
:resource="resource"
@refresh-data="fetchData"
@close-action="closeImportModal" />
</a-modal>
</a-spin>
</template>
@ -313,13 +334,15 @@ import { getAPI, postAPI } from '@/api'
import draggable from 'vuedraggable'
import { mixinForm } from '@/utils/mixin'
import TooltipButton from '@/components/widgets/TooltipButton'
import ImportNetworkACL from './ImportNetworkACL'
export default {
name: 'AclListRulesTab',
mixins: [mixinForm],
components: {
draggable,
TooltipButton
TooltipButton,
ImportNetworkACL
},
props: {
resource: {
@ -344,6 +367,7 @@ export default {
tagsModalVisible: false,
tagsLoading: false,
ruleModalVisible: false,
showImportModal: false,
ruleModalTitle: this.$t('label.edit.rule'),
ruleFormMode: 'edit'
}
@ -788,6 +812,12 @@ export default {
},
capitalise (val) {
return val.toUpperCase()
},
handleImportRules () {
this.showImportModal = true
},
closeImportModal () {
this.showImportModal = false
}
}
}

View File

@ -0,0 +1,381 @@
// 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 v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
:ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
@finish="handleSubmit">
<div class="info-section" style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 4px;">
<a-descriptions :column="1" size="small">
<a-descriptions-item :label="$t('label.acl.id')">
<span style="font-family: monospace;">{{ resource.id }}</span>
</a-descriptions-item>
<a-descriptions-item :label="$t('label.add.acl.name')">
<strong>{{ resource.name }}</strong>
</a-descriptions-item>
</a-descriptions>
</div>
<a-form-item name="file" ref="file">
<template #label>
<tooltip-label :title="$t('label.rules.file')" :tooltip="$t('label.rules.file.to.import')"/>
</template>
<a-upload-dragger
:multiple=false
:fileList="fileList"
@remove="handleRemove"
:beforeUpload="beforeUpload"
@change="handleChange"
v-model:value="form.file">
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-text" v-if="fileList.length === 0">
{{ $t('label.rules.file.import.description') }}
</p>
</a-upload-dragger>
</a-form-item>
<a-form-item v-if="csvData.length > 0" :label="$t('label.csv.preview')">
<div class="csv-preview">
<a-table
:columns="columns"
:dataSource="csvData"
:pagination="{ pageSize: 5 }"
:scroll="{ x: true }"
size="small">
<template #action="{ record }">
<a-tag :color="record.action && record.action.toLowerCase() === 'allow' ? 'green' : 'red'">
{{ record.action ? record.action.toUpperCase() : 'N/A' }}
</a-tag>
</template>
<template #traffictype="{ record }">
<a-tag :color="record.traffictype && record.traffictype.toLowerCase() === 'ingress' ? 'blue' : 'orange'">
{{ record.traffictype ? record.traffictype.toUpperCase() : 'N/A' }}
</a-tag>
</template>
</a-table>
</div>
</a-form-item>
<span>{{ $t('message.network.acl.import.note') }}</span><br/>
<div :span="24" class="action-button">
<a-button class="button-cancel" @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button
class="button-submit"
ref="submit"
type="primary"
:loading="loading"
:disabled="csvData.length === 0"
@click="handleSubmit">{{ $t('label.import') }}</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import { postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
name: 'ImportNetworkACL',
components: {
TooltipLabel
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
loading: false,
fileList: [],
csvData: '',
csvFileType: ['.csv', 'text/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'],
columns: [
{
title: this.$t('label.protocol'),
dataIndex: 'protocol',
key: 'protocol',
width: 100
},
{
title: this.$t('label.action'),
dataIndex: 'action',
key: 'action',
width: 100,
slots: { customRender: 'action' }
},
{
title: this.$t('label.cidr'),
dataIndex: 'cidrlist',
width: 150,
ellipsis: true
},
{
title: this.$t('label.startport'),
dataIndex: 'startport',
width: 100
},
{
title: this.$t('label.endport'),
dataIndex: 'endport',
width: 100
},
{
title: this.$t('label.traffictype'),
dataIndex: 'traffictype',
key: 'traffictype',
width: 120,
slots: { customRender: 'traffictype' }
},
{
title: this.$t('label.number'),
dataIndex: 'number',
width: 80
},
{
title: this.$t('label.reason'),
dataIndex: 'reason',
ellipsis: true
}
]
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('importNetworkACL')
},
created () {
this.initForm()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({})
this.rules = reactive({
file: [
{ required: true, message: this.$t('message.error.required.input') },
{
validator: this.checkCsvRulesFile,
message: this.$t('label.error.rules.file.import')
}
]
})
},
beforeUpload (file) {
if (!this.csvFileType.includes(file.type)) {
return false
}
this.fileList = [file]
this.form.file = file
return false // Stop from uploading automatically
},
handleSubmit (e) {
e.preventDefault()
if (this.loading) return
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
const params = {}
for (const key in values) {
const input = values[key]
if (input === undefined) {
continue
}
if (key === 'file') {
continue
}
params[key] = input
}
if (this.csvData.length === 0) {
this.$message.error(this.$t('message.csv.no.data'))
return
}
this.importNetworkACL()
}).catch(error => {
this.formRef.value.scrollToField(error.errorFields[0].name)
})
},
handleRemove (file) {
const index = this.fileList.indexOf(file)
const newFileList = this.fileList.slice()
newFileList.splice(index, 1)
this.fileList = newFileList
this.form.file = undefined
},
handleChange (info) {
if (info.file.status === 'error') {
this.$notification.error({
message: this.$t('label.error.file.upload'),
description: this.$t('label.error.file.upload')
})
}
},
async checkCsvRulesFile (rule, value) {
if (!value || value === '') {
return Promise.resolve()
} else {
if (!this.csvFileType.includes(value.type)) {
return Promise.reject(rule.message)
}
try {
const validFile = await this.readCsvFile(value)
if (!validFile) {
return Promise.reject(rule.message)
} else {
return Promise.resolve()
}
} catch (reason) {
return Promise.reject(rule.message)
}
}
},
readCsvFile (file) {
return new Promise((resolve, reject) => {
if (window.FileReader) {
const reader = new FileReader()
reader.onload = (event) => {
const text = event.target.result
const lines = text.split('\n').filter(line => line.trim() !== '')
if (lines.length < 2) {
this.$message.error(this.$t('message.csv.empty'))
resolve(false)
}
const headers = this.parseCSVLine(lines[0])
const requiredHeaders = ['protocol', 'cidrlist', 'traffictype']
const missingHeaders = requiredHeaders.filter(h => !headers.includes(h.toLowerCase()))
if (missingHeaders.length > 0) {
this.$message.error(this.$t('message.csv.missing.headers') + ': ' + missingHeaders.join(', '))
resolve(false)
}
// Parse data rows
const data = []
for (let i = 1; i < lines.length; i++) {
const values = this.parseCSVLine(lines[i])
if (values.length === headers.length) {
const row = {}
headers.forEach((header, index) => {
const value = values[index].trim()
if (value !== '' && value !== 'null') {
row[header.toLowerCase()] = value
}
})
data.push(row)
}
}
this.csvData = data
resolve(true)
}
reader.onerror = (event) => {
if (event.target.error.name === 'NotReadableError') {
reject(event.target.error)
}
}
reader.readAsText(file)
} else {
reject(this.$t('label.error.file.read'))
}
})
},
parseCSVLine (line) {
const result = []
let current = ''
let inQuotes = false
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '"') {
inQuotes = !inQuotes
} else if (char === ',' && !inQuotes) {
result.push(current)
current = ''
} else {
current += char
}
}
result.push(current)
return result.map(v => v.trim())
},
closeAction () {
this.$emit('close-action')
},
importNetworkACL () {
this.loading = true
const params = {
aclid: this.resource.id
}
this.csvData.forEach(function (values, index) {
for (const key in values) {
params['rules[' + index + '].' + key] = values[key]
}
})
postAPI('importNetworkACL', params).then(response => {
this.$pollJob({
jobId: response.importnetworkaclresponse.jobid,
title: this.$t('message.success.add.network.acl'),
successMethod: () => {
this.loading = false
},
errorMessage: this.$t('message.add.network.acl.failed'),
errorMethod: () => {
this.loading = false
},
loadingMessage: this.$t('message.add.network.acl.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loading = false
}
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped lang="less">
.csv-preview {
max-height: 400px;
overflow: auto;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 10px;
}
</style>