mirror of https://github.com/apache/cloudstack.git
import network acl rules using csv (#12013)
This commit is contained in:
parent
bd459a4c4c
commit
a8f1e4a5ba
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue