From 2b7dc78d0f91414f12ed44ce250c92d44d686465 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 7 Nov 2025 09:56:33 +0530 Subject: [PATCH 1/3] import network acl rules using csv --- .../main/java/com/cloud/event/EventTypes.java | 1 + .../cloud/network/vpc/NetworkACLService.java | 3 + .../user/network/CreateNetworkACLCmd.java | 24 ++++ .../user/network/ImportNetworkACLCmd.java | 134 ++++++++++++++++++ .../network/vpc/NetworkACLServiceImpl.java | 108 ++++++++++++++ 5 files changed, 270 insertions(+) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 38e601c790a7..6fa6a06ea82e 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -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"; diff --git a/api/src/main/java/com/cloud/network/vpc/NetworkACLService.java b/api/src/main/java/com/cloud/network/vpc/NetworkACLService.java index 40aee1f08f1d..84e48d5d5b8a 100644 --- a/api/src/main/java/com/cloud/network/vpc/NetworkACLService.java +++ b/api/src/main/java/com/cloud/network/vpc/NetworkACLService.java @@ -19,6 +19,7 @@ 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 importNetworkACLRules(ImportNetworkACLCmd cmd) throws ResourceUnavailableException; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java index 8d8e598bcab8..083948a9a9ee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java @@ -238,6 +238,30 @@ public String getReason() { return reason; } + public void setCidrlist(List 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); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java new file mode 100644 index 000000000000..f8b82a24f3e3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java @@ -0,0 +1,134 @@ +// 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; +import com.cloud.utils.Pair; + +@APICommand(name = "importNetworkACL", description = "Imports network ACL rules.", + responseObject = NetworkACLItemResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +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", + since = "4.22.0" + ) + private Long aclId; + + @Parameter(name = ApiConstants.RULES, type = CommandType.MAP, required = true, + description = "Rules param list, id and protocol are must. 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", + since = "4.22.0") + 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 importedRules = _networkACLService.importNetworkACLRules(this); + ListResponse response = new ListResponse<>(); + List 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_CREATE; + } + + @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 aclid"); + } + } +} diff --git a/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java b/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java index ecb164018ac0..ad86ff1c4a5d 100644 --- a/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java @@ -32,9 +32,12 @@ 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; @@ -79,6 +82,7 @@ import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; +import com.google.protobuf.Api; @Component public class NetworkACLServiceImpl extends ManagerBase implements NetworkACLService { @@ -1061,6 +1065,110 @@ public NetworkACLItem moveRuleToTheTopInACLList(NetworkACLItem ruleBeingMoved) { return moveRuleToTheTop(ruleBeingMoved, allRules); } + @Override + public List importNetworkACLRules(ImportNetworkACLCmd cmd) throws ResourceUnavailableException { + long aclId = cmd.getAclId(); + Map rules = cmd.getRules(); + List createdRules = new ArrayList<>(); + List errors = new ArrayList<>(); + for (Map.Entry entry : rules.entrySet()) { + try { + Map ruleMap = (Map) 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 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); + + // 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()); + + + // Optional parameters + if (ruleMap.containsKey(ApiConstants.CIDR_LIST)) { + Object cidrObj = ruleMap.get(ApiConstants.CIDR_LIST); + List 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) 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. *
    From 63cde640dfa62c59a6fad3b918fe5cd5080fbc67 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Mon, 10 Nov 2025 17:08:13 +0530 Subject: [PATCH 2/3] Parse csv in UI, prepare payload for import --- .../user/network/ImportNetworkACLCmd.java | 1 - .../network/vpc/NetworkACLServiceImpl.java | 14 +- ui/public/locales/en.json | 9 +- ui/src/views/network/AclRulesTab.vue | 32 +- ui/src/views/network/ImportNetworkACL.vue | 358 ++++++++++++++++++ 5 files changed, 402 insertions(+), 12 deletions(-) create mode 100644 ui/src/views/network/ImportNetworkACL.vue diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java index f8b82a24f3e3..0f7c2eed95ce 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ImportNetworkACLCmd.java @@ -36,7 +36,6 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.vpc.NetworkACLItem; import com.cloud.user.Account; -import com.cloud.utils.Pair; @APICommand(name = "importNetworkACL", description = "Imports network ACL rules.", responseObject = NetworkACLItemResponse.class, diff --git a/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java b/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java index ad86ff1c4a5d..9372eedf6a1e 100644 --- a/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java @@ -26,13 +26,6 @@ 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; @@ -50,15 +43,21 @@ 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; @@ -82,7 +81,6 @@ import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; -import com.google.protobuf.Api; @Component public class NetworkACLServiceImpl extends ManagerBase implements NetworkACLService { diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 4f450e940fc0..802f0f5ad0ee 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -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.", @@ -251,7 +253,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", @@ -699,6 +701,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", @@ -2895,6 +2898,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.
    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", diff --git a/ui/src/views/network/AclRulesTab.vue b/ui/src/views/network/AclRulesTab.vue index da6d834e4a35..e452e495a224 100644 --- a/ui/src/views/network/AclRulesTab.vue +++ b/ui/src/views/network/AclRulesTab.vue @@ -28,10 +28,16 @@ {{ $t('label.add.acl.rule') }} + + + {{ $t('label.acl.import') }} + + {{ $t('label.acl.export') }} +