From 1f29f6f04096d04a1284a4a8061262b3c3033de3 Mon Sep 17 00:00:00 2001 From: Bryan Lima <42067040+BryanMLima@users.noreply.github.com> Date: Wed, 15 Nov 2023 06:29:22 -0300 Subject: [PATCH] Public IP quarantine feature (#7378) --- .../com/cloud/network/NetworkService.java | 6 + .../com/cloud/network/PublicIpQuarantine.java | 36 ++ .../apache/cloudstack/api/ApiConstants.java | 4 + .../cloudstack/api/ResponseGenerator.java | 4 + .../user/address/ListQuarantinedIpsCmd.java | 51 +++ .../user/address/RemoveQuarantinedIpCmd.java | 72 ++++ .../user/address/UpdateQuarantinedIpCmd.java | 75 ++++ .../api/response/IpQuarantineResponse.java | 130 +++++++ .../apache/cloudstack/query/QueryService.java | 4 + .../com/cloud/network/IpAddressManager.java | 48 +++ .../com/cloud/network/dao/IPAddressDao.java | 3 + .../cloud/network/dao/IPAddressDaoImpl.java | 20 ++ .../com/cloud/network/dao/IPAddressVO.java | 9 - .../network/dao/PublicIpQuarantineDao.java | 27 ++ .../dao/PublicIpQuarantineDaoImpl.java | 71 ++++ .../network/vo/PublicIpQuarantineVO.java | 131 +++++++ ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-41810to41900.sql | 15 + .../main/java/com/cloud/utils/db/Filter.java | 4 + .../java/com/cloud/utils/db/GenericDao.java | 2 + .../com/cloud/utils/db/GenericDaoBase.java | 23 +- .../java/com/cloud/api/ApiResponseHelper.java | 21 ++ .../com/cloud/api/query/QueryManagerImpl.java | 51 +++ .../cloud/network/IpAddressManagerImpl.java | 129 ++++++- .../com/cloud/network/NetworkServiceImpl.java | 76 ++++ .../com/cloud/network/vpc/VpcManagerImpl.java | 8 +- .../cloud/server/ManagementServerImpl.java | 12 + .../cloud/network/IpAddressManagerTest.java | 213 ++++++++++-- .../cloud/network/NetworkServiceImplTest.java | 248 ++++++++++--- .../com/cloud/user/MockUsageEventDao.java | 5 + .../com/cloud/vpc/MockNetworkManagerImpl.java | 67 ++-- .../CreateNetworkOfferingTest.java | 50 +-- .../test/resources/createNetworkOffering.xml | 1 + .../integration/smoke/test_quarantined_ips.py | 329 ++++++++++++++++++ tools/apidoc/gen_toc.py | 3 + tools/marvin/marvin/lib/base.py | 5 +- ui/src/views/network/IpAddressesTab.vue | 4 +- 37 files changed, 1815 insertions(+), 143 deletions(-) create mode 100644 api/src/main/java/com/cloud/network/PublicIpQuarantine.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/address/ListQuarantinedIpsCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/address/RemoveQuarantinedIpCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/address/UpdateQuarantinedIpCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/IpQuarantineResponse.java create mode 100644 engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java create mode 100644 engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java create mode 100644 engine/schema/src/main/java/com/cloud/network/vo/PublicIpQuarantineVO.java create mode 100644 test/integration/smoke/test_quarantined_ips.py diff --git a/api/src/main/java/com/cloud/network/NetworkService.java b/api/src/main/java/com/cloud/network/NetworkService.java index 099d73d5fcb..d959e3abce0 100644 --- a/api/src/main/java/com/cloud/network/NetworkService.java +++ b/api/src/main/java/com/cloud/network/NetworkService.java @@ -24,6 +24,8 @@ import org.apache.cloudstack.api.command.admin.network.DedicateGuestVlanRangeCmd import org.apache.cloudstack.api.command.admin.network.ListDedicatedGuestVlanRangesCmd; import org.apache.cloudstack.api.command.admin.network.ListGuestVlansCmd; import org.apache.cloudstack.api.command.admin.usage.ListTrafficTypeImplementorsCmd; +import org.apache.cloudstack.api.command.user.address.RemoveQuarantinedIpCmd; +import org.apache.cloudstack.api.command.user.address.UpdateQuarantinedIpCmd; import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; import org.apache.cloudstack.api.command.user.network.CreateNetworkPermissionsCmd; import org.apache.cloudstack.api.command.user.network.ListNetworkPermissionsCmd; @@ -246,4 +248,8 @@ public interface NetworkService { boolean resetNetworkPermissions(ResetNetworkPermissionsCmd resetNetworkPermissionsCmd); void validateIfServiceOfferingIsActiveAndSystemVmTypeIsDomainRouter(final Long serviceOfferingId) throws InvalidParameterValueException; + + PublicIpQuarantine updatePublicIpAddressInQuarantine(UpdateQuarantinedIpCmd cmd); + + void removePublicIpAddressFromQuarantine(RemoveQuarantinedIpCmd cmd); } diff --git a/api/src/main/java/com/cloud/network/PublicIpQuarantine.java b/api/src/main/java/com/cloud/network/PublicIpQuarantine.java new file mode 100644 index 00000000000..d1ec98afe46 --- /dev/null +++ b/api/src/main/java/com/cloud/network/PublicIpQuarantine.java @@ -0,0 +1,36 @@ +// 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 com.cloud.network; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface PublicIpQuarantine extends InternalIdentity, Identity { + Long getPublicIpAddressId(); + + Long getPreviousOwnerId(); + + Date getEndDate(); + + String getRemovalReason(); + + Date getRemoved(); + + Date getCreated(); +} diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index a6f27d1469a..5b6647991ef 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -224,6 +224,8 @@ public class ApiConstants { public static final String INSTANCES_STATS_USER_ONLY = "instancesstatsuseronly"; public static final String PREFIX = "prefix"; public static final String PREVIOUS_ACL_RULE_ID = "previousaclruleid"; + public static final String PREVIOUS_OWNER_ID = "previousownerid"; + public static final String PREVIOUS_OWNER_NAME = "previousownername"; public static final String NEXT_ACL_RULE_ID = "nextaclruleid"; public static final String MOVE_ACL_CONSISTENCY_HASH = "aclconsistencyhash"; public static final String IMAGE_PATH = "imagepath"; @@ -402,6 +404,7 @@ public class ApiConstants { public static final String SHOW_CAPACITIES = "showcapacities"; public static final String SHOW_REMOVED = "showremoved"; public static final String SHOW_RESOURCE_ICON = "showicon"; + public static final String SHOW_INACTIVE = "showinactive"; public static final String SHOW_UNIQUE = "showunique"; public static final String SIGNATURE = "signature"; public static final String SIGNATURE_VERSION = "signatureversion"; @@ -794,6 +797,7 @@ public class ApiConstants { public static final String IPSEC_PSK = "ipsecpsk"; public static final String GUEST_IP = "guestip"; public static final String REMOVED = "removed"; + public static final String REMOVAL_REASON = "removalreason"; public static final String COMPLETED = "completed"; public static final String IKE_VERSION = "ikeversion"; public static final String IKE_POLICY = "ikepolicy"; diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index 1a0df486298..030f70805d2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -64,6 +64,7 @@ import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.HypervisorCapabilitiesResponse; import org.apache.cloudstack.api.response.HypervisorGuestOsNamesResponse; import org.apache.cloudstack.api.response.IPAddressResponse; +import org.apache.cloudstack.api.response.IpQuarantineResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; import org.apache.cloudstack.api.response.InstanceGroupResponse; import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse; @@ -169,6 +170,7 @@ import com.cloud.network.OvsProvider; import com.cloud.network.PhysicalNetwork; import com.cloud.network.PhysicalNetworkServiceProvider; import com.cloud.network.PhysicalNetworkTrafficType; +import com.cloud.network.PublicIpQuarantine; import com.cloud.network.RemoteAccessVpn; import com.cloud.network.RouterHealthCheckResult; import com.cloud.network.Site2SiteCustomerGateway; @@ -529,4 +531,6 @@ public interface ResponseGenerator { DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvisionResponse(Long certificateId, Long hostId, Pair result); FirewallResponse createIpv6FirewallRuleResponse(FirewallRule acl); + + IpQuarantineResponse createQuarantinedIpsResponse(PublicIpQuarantine publicIp); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListQuarantinedIpsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListQuarantinedIpsCmd.java new file mode 100644 index 00000000000..cc014702c81 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListQuarantinedIpsCmd.java @@ -0,0 +1,51 @@ +// 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.address; + +import com.cloud.network.PublicIpQuarantine; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.IpQuarantineResponse; +import org.apache.cloudstack.api.response.ListResponse; + +@APICommand(name = "listQuarantinedIps", responseObject = IpQuarantineResponse.class, description = "List public IP addresses in quarantine.", since = "4.19", + entityType = {PublicIpQuarantine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin, RoleType.DomainAdmin}) +public class ListQuarantinedIpsCmd extends BaseListCmd { + + @Parameter(name = ApiConstants.SHOW_REMOVED, type = CommandType.BOOLEAN, description = "Show IPs removed from quarantine.") + private boolean showRemoved = false; + + @Parameter(name = ApiConstants.SHOW_INACTIVE, type = CommandType.BOOLEAN, description = "Show IPs that are no longer in quarantine.") + private boolean showInactive = false; + + public boolean isShowRemoved() { + return showRemoved; + } + public boolean isShowInactive() { + return showInactive; + } + + @Override + public void execute() { + ListResponse response = _queryService.listQuarantinedIps(this); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/RemoveQuarantinedIpCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/RemoveQuarantinedIpCmd.java new file mode 100644 index 00000000000..82e8373d93a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/RemoveQuarantinedIpCmd.java @@ -0,0 +1,72 @@ +// 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.address; + +import com.cloud.network.PublicIpQuarantine; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.IpQuarantineResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +@APICommand(name = "removeQuarantinedIp", responseObject = IpQuarantineResponse.class, description = "Removes a public IP address from quarantine. Only IPs in active " + + "quarantine can be removed.", + since = "4.19", entityType = {PublicIpQuarantine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.DomainAdmin}) +public class RemoveQuarantinedIpCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = IpQuarantineResponse.class, description = "The ID of the public IP address in active quarantine. " + + "Either the IP address is informed, or the ID of the IP address in quarantine.") + private Long id; + + @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, description = "The public IP address in active quarantine. Either the IP address is informed, or the ID" + + " of the IP address in quarantine.") + private String ipAddress; + + @Parameter(name = ApiConstants.REMOVAL_REASON, type = CommandType.STRING, required = true, description = "The reason for removing the public IP address from quarantine " + + "prematurely.") + private String removalReason; + + public Long getId() { + return id; + } + + public String getIpAddress() { + return ipAddress; + } + + public String getRemovalReason() { + return removalReason; + } + + @Override + public void execute() { + _networkService.removePublicIpAddressFromQuarantine(this); + final SuccessResponse response = new SuccessResponse(); + response.setResponseName(getCommandName()); + response.setSuccess(true); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/UpdateQuarantinedIpCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/UpdateQuarantinedIpCmd.java new file mode 100644 index 00000000000..b3b71c33781 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/UpdateQuarantinedIpCmd.java @@ -0,0 +1,75 @@ +// 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.address; + +import com.cloud.network.PublicIpQuarantine; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.IpQuarantineResponse; + +import java.util.Date; + +@APICommand(name = "updateQuarantinedIp", responseObject = IpQuarantineResponse.class, description = "Updates the quarantine end date for the given public IP address.", + since = "4.19", entityType = {PublicIpQuarantine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.DomainAdmin}) +public class UpdateQuarantinedIpCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = IpQuarantineResponse.class, description = "The ID of the public IP address in " + + "active quarantine.") + private Long id; + + @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, description = "The public IP address in active quarantine. Either the IP address is informed, or the ID" + + " of the IP address in quarantine.") + private String ipAddress; + + @Parameter(name = ApiConstants.END_DATE, type = BaseCmd.CommandType.DATE, required = true, description = "The date when the quarantine will no longer be active.") + private Date endDate; + + public Long getId() { + return id; + } + + public String getIpAddress() { + return ipAddress; + } + + public Date getEndDate() { + return endDate; + } + + @Override + public void execute() { + PublicIpQuarantine publicIpQuarantine = _networkService.updatePublicIpAddressInQuarantine(this); + if (publicIpQuarantine == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update public IP quarantine."); + } + IpQuarantineResponse response = _responseGenerator.createQuarantinedIpsResponse(publicIpQuarantine); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/IpQuarantineResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/IpQuarantineResponse.java new file mode 100644 index 00000000000..55720296315 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/IpQuarantineResponse.java @@ -0,0 +1,130 @@ +//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.response; + +import com.cloud.network.PublicIpQuarantine; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import java.util.Date; + +@EntityReference(value = {PublicIpQuarantine.class}) +public class IpQuarantineResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the quarantine process.") + private String id; + + @SerializedName(ApiConstants.IP_ADDRESS) + @Param(description = "The public IP address in quarantine.") + private String publicIpAddress; + + @SerializedName(ApiConstants.PREVIOUS_OWNER_ID) + @Param(description = "Account ID of the previous public IP address owner.") + private String previousOwnerId; + + @SerializedName(ApiConstants.PREVIOUS_OWNER_NAME) + @Param(description = "Account name of the previous public IP address owner.") + private String previousOwnerName; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "When the quarantine was created.") + private Date created; + + @SerializedName(ApiConstants.REMOVED) + @Param(description = "When the quarantine was removed.") + private Date removed; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "End date for the quarantine.") + private Date endDate; + + @SerializedName(ApiConstants.REMOVAL_REASON) + @Param(description = "The reason for removing the IP from quarantine prematurely.") + private String removalReason; + + public IpQuarantineResponse() { + super("quarantinedips"); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getPublicIpAddress() { + return publicIpAddress; + } + + public void setPublicIpAddress(String publicIpAddress) { + this.publicIpAddress = publicIpAddress; + } + + public String getPreviousOwnerId() { + return previousOwnerId; + } + + public void setPreviousOwnerId(String previousOwnerId) { + this.previousOwnerId = previousOwnerId; + } + + public String getPreviousOwnerName() { + return previousOwnerName; + } + + public void setPreviousOwnerName(String previousOwnerName) { + this.previousOwnerName = previousOwnerName; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public String getRemovalReason() { + return removalReason; + } + + public void setRemovalReason(String removalReason) { + this.removalReason = removalReason; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 097a3c3f262..6f404452598 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -34,6 +34,7 @@ import org.apache.cloudstack.api.command.admin.storage.ListStorageTagsCmd; import org.apache.cloudstack.api.command.admin.user.ListUsersCmd; import org.apache.cloudstack.api.command.user.account.ListAccountsCmd; import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; +import org.apache.cloudstack.api.command.user.address.ListQuarantinedIpsCmd; import org.apache.cloudstack.api.command.user.affinitygroup.ListAffinityGroupsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.iso.ListIsosCmd; @@ -64,6 +65,7 @@ import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.HostTagResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; import org.apache.cloudstack.api.response.InstanceGroupResponse; +import org.apache.cloudstack.api.response.IpQuarantineResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.ProjectAccountResponse; @@ -183,6 +185,8 @@ public interface QueryService { List listRouterHealthChecks(GetRouterHealthCheckResultsCmd cmd); + ListResponse listQuarantinedIps(ListQuarantinedIpsCmd cmd); + ListResponse listSnapshots(ListSnapshotsCmd cmd); SnapshotResponse listSnapshot(CopySnapshotCmd cmd); diff --git a/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java b/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java index ab179d302d0..36937460b20 100644 --- a/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.network; +import java.util.Date; import java.util.List; import org.apache.cloudstack.api.response.AcquirePodIpCmdResponse; @@ -238,5 +239,52 @@ public interface IpAddressManager { public static final String MESSAGE_ASSIGN_IPADDR_EVENT = "Message.AssignIpAddr.Event"; public static final String MESSAGE_RELEASE_IPADDR_EVENT = "Message.ReleaseIpAddr.Event"; + + /** + * Checks if the given public IP address is not in active quarantine. + * It returns `true` if: + *
    + *
  • The IP was never in quarantine;
  • + *
  • The IP was in quarantine, but the quarantine expired;
  • + *
  • The IP is still in quarantine; however, the new owner is the same as the previous owner, therefore, the IP can be allocated.
  • + *
+ * + * It returns `false` if: + *
    + *
  • The IP is in active quarantine and the new owner is different from the previous owner.
  • + *
+ * + * @param ip used to check if it is in active quarantine. + * @param account used to identify the new owner of the public IP. + * @return true if the IP can be allocated, and false otherwise. + */ + boolean canPublicIpAddressBeAllocated(IpAddress ip, Account account); + + /** + * Adds the given public IP address to quarantine for the duration of the global configuration `public.ip.address.quarantine.duration` value. + * + * @param publicIpAddress to be quarantined. + * @param domainId used to retrieve the quarantine duration. + * @return the {@link PublicIpQuarantine} persisted in the database. + */ + PublicIpQuarantine addPublicIpAddressToQuarantine(IpAddress publicIpAddress, Long domainId); + + /** + * Prematurely removes a public IP address from quarantine. It is required to provide a reason for removing it. + * + * @param quarantineProcessId the ID of the active quarantine process. + * @param removalReason for prematurely removing the public IP address from quarantine. + */ + void removePublicIpAddressFromQuarantine(Long quarantineProcessId, String removalReason); + + /** + * Updates the end date of a public IP address in active quarantine. It can increase and decrease the duration of the quarantine. + * + * @param quarantineProcessId the ID of the quarantine process. + * @param endDate the new end date for the quarantine. + * @return the updated quarantine object. + */ + PublicIpQuarantine updatePublicIpAddressInQuarantine(Long quarantineProcessId, Date endDate); + void updateSourceNatIpAddress(IPAddressVO requestedIp, List userIps) throws Exception; } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java index bb4822ebb38..f75dc8a6661 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java @@ -21,6 +21,7 @@ import java.util.List; import com.cloud.dc.Vlan.VlanType; import com.cloud.network.IpAddress.State; import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.net.Ip; public interface IPAddressDao extends GenericDao { @@ -100,4 +101,6 @@ public interface IPAddressDao extends GenericDao { List listByDcIdAndAssociatedNetwork(long dcId); List listByNetworkId(long networkId); + + void buildQuarantineSearchCriteria(SearchCriteria sc); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java index d82ffbe2af0..9ffc4c9159c 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java @@ -24,6 +24,7 @@ import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.resourcedetail.dao.UserIpAddressDetailsDao; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -32,6 +33,7 @@ import com.cloud.dc.Vlan.VlanType; import com.cloud.dc.VlanVO; import com.cloud.dc.dao.VlanDao; import com.cloud.network.IpAddress.State; +import com.cloud.network.vo.PublicIpQuarantineVO; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.tags.dao.ResourceTagDao; import com.cloud.utils.db.DB; @@ -69,6 +71,9 @@ public class IPAddressDaoImpl extends GenericDaoBase implemen @Inject UserIpAddressDetailsDao _detailsDao; + @Inject + PublicIpQuarantineDao publicIpQuarantineDao; + // make it public for JUnit test public IPAddressDaoImpl() { } @@ -534,4 +539,19 @@ public class IPAddressDaoImpl extends GenericDaoBase implemen sc.setParameters("state", State.Allocated); return listBy(sc); } + + @Override + public void buildQuarantineSearchCriteria(SearchCriteria sc) { + long accountId = CallContext.current().getCallingAccount().getAccountId(); + SearchBuilder listAllIpsInQuarantine = publicIpQuarantineDao.createSearchBuilder(); + listAllIpsInQuarantine.and("quarantineEndDate", listAllIpsInQuarantine.entity().getEndDate(), SearchCriteria.Op.GT); + listAllIpsInQuarantine.and("previousOwnerId", listAllIpsInQuarantine.entity().getPreviousOwnerId(), Op.NEQ); + + SearchCriteria searchCriteria = listAllIpsInQuarantine.create(); + searchCriteria.setParameters("quarantineEndDate", new Date()); + searchCriteria.setParameters("previousOwnerId", accountId); + Object[] quarantinedIpsIdsAllowedToUser = publicIpQuarantineDao.search(searchCriteria, null).stream().map(PublicIpQuarantineVO::getPublicIpAddressId).toArray(); + + sc.setParametersIfNotNull("quarantinedPublicIpsIdsNIN", quarantinedIpsIdsAllowedToUser); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java index 7c4d56bd1ee..4c7569a55b9 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java @@ -29,7 +29,6 @@ import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; -import javax.persistence.Transient; import com.cloud.network.IpAddress; import com.cloud.utils.db.GenericDao; @@ -97,14 +96,6 @@ public class IPAddressVO implements IpAddress { @Column(name = "is_system") private boolean system; - @Column(name = "account_id") - @Transient - private Long accountId = null; - - @Transient - @Column(name = "domain_id") - private Long domainId = null; - @Column(name = "vpc_id") private Long vpcId; diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java new file mode 100644 index 00000000000..ccba6bb1889 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java @@ -0,0 +1,27 @@ +// 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 com.cloud.network.dao; + +import com.cloud.network.vo.PublicIpQuarantineVO; +import com.cloud.utils.db.GenericDao; + +public interface PublicIpQuarantineDao extends GenericDao { + + PublicIpQuarantineVO findByPublicIpAddressId(long publicIpAddressId); + + PublicIpQuarantineVO findByIpAddress(String publicIpAddress); +} diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java new file mode 100644 index 00000000000..a1b789b8a46 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java @@ -0,0 +1,71 @@ +// 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 com.cloud.network.dao; + +import com.cloud.network.vo.PublicIpQuarantineVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.JoinBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +@Component +public class PublicIpQuarantineDaoImpl extends GenericDaoBase implements PublicIpQuarantineDao { + private SearchBuilder publicIpAddressByIdSearch; + + private SearchBuilder ipAddressSearchBuilder; + + @Inject + IPAddressDao ipAddressDao; + + @PostConstruct + public void init() { + publicIpAddressByIdSearch = createSearchBuilder(); + publicIpAddressByIdSearch.and("publicIpAddressId", publicIpAddressByIdSearch.entity().getPublicIpAddressId(), SearchCriteria.Op.EQ); + + ipAddressSearchBuilder = ipAddressDao.createSearchBuilder(); + ipAddressSearchBuilder.and("ipAddress", ipAddressSearchBuilder.entity().getAddress(), SearchCriteria.Op.EQ); + ipAddressSearchBuilder.and("removed", ipAddressSearchBuilder.entity().getRemoved(), SearchCriteria.Op.NULL); + publicIpAddressByIdSearch.join("quarantineJoin", ipAddressSearchBuilder, ipAddressSearchBuilder.entity().getId(), + publicIpAddressByIdSearch.entity().getPublicIpAddressId(), JoinBuilder.JoinType.INNER); + + ipAddressSearchBuilder.done(); + publicIpAddressByIdSearch.done(); + } + + @Override + public PublicIpQuarantineVO findByPublicIpAddressId(long publicIpAddressId) { + SearchCriteria sc = publicIpAddressByIdSearch.create(); + sc.setParameters("publicIpAddressId", publicIpAddressId); + final Filter filter = new Filter(PublicIpQuarantineVO.class, "created", false); + + return findOneBy(sc, filter); + } + + @Override + public PublicIpQuarantineVO findByIpAddress(String publicIpAddress) { + SearchCriteria sc = publicIpAddressByIdSearch.create(); + sc.setJoinParameters("quarantineJoin", "ipAddress", publicIpAddress); + final Filter filter = new Filter(PublicIpQuarantineVO.class, "created", false); + + return findOneBy(sc, filter); + } +} diff --git a/engine/schema/src/main/java/com/cloud/network/vo/PublicIpQuarantineVO.java b/engine/schema/src/main/java/com/cloud/network/vo/PublicIpQuarantineVO.java new file mode 100644 index 00000000000..56d167a0060 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/vo/PublicIpQuarantineVO.java @@ -0,0 +1,131 @@ +// 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 com.cloud.network.vo; + +import com.cloud.network.PublicIpQuarantine; +import com.cloud.utils.db.GenericDao; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "quarantined_ips") +public class PublicIpQuarantineVO implements PublicIpQuarantine { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "uuid", nullable = false) + private String uuid = UUID.randomUUID().toString(); + + @Column(name = "public_ip_address_id", nullable = false) + private Long publicIpAddressId; + + @Column(name = "previous_owner_id", nullable = false) + private Long previousOwnerId; + + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + + @Column(name = "end_date", nullable = false) + @Temporal(value = TemporalType.TIMESTAMP) + private Date endDate; + + @Column(name = "removal_reason") + private String removalReason = null; + + public PublicIpQuarantineVO() { + } + + public PublicIpQuarantineVO(Long publicIpAddressId, Long previousOwnerId, Date created, Date endDate) { + this.publicIpAddressId = publicIpAddressId; + this.previousOwnerId = previousOwnerId; + this.created = created; + this.endDate = endDate; + } + + @Override + public long getId() { + return id; + } + + @Override + public Long getPublicIpAddressId() { + return publicIpAddressId; + } + + @Override + public Long getPreviousOwnerId() { + return previousOwnerId; + } + + @Override + public Date getEndDate() { + return endDate; + } + + @Override + public String getRemovalReason() { + return removalReason; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setRemovalReason(String removalReason) { + this.removalReason = removalReason; + } + + @Override + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 2a0597ce9b5..f0c5e7630e1 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -276,6 +276,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql index dd730058b7b..f2585deea1a 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql @@ -136,6 +136,21 @@ UPDATE `cloud`.`console_session` SET removed=now(); -- Modify acquired column in console_session to datetime type ALTER TABLE `cloud`.`console_session` DROP `acquired`, ADD `acquired` datetime COMMENT 'When the session was acquired' AFTER `host_id`; +-- IP quarantine PR#7378 +CREATE TABLE IF NOT EXISTS `cloud`.`quarantined_ips` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `uuid` varchar(255) UNIQUE, + `public_ip_address_id` bigint(20) unsigned NOT NULL COMMENT 'ID of the quarantined public IP address, foreign key to `user_ip_address` table', + `previous_owner_id` bigint(20) unsigned NOT NULL COMMENT 'ID of the previous owner of the public IP address, foreign key to `account` table', + `created` datetime NOT NULL, + `removed` datetime DEFAULT NULL, + `end_date` datetime NOT NULL, + `removal_reason` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_quarantined_ips__public_ip_address_id` FOREIGN KEY(`public_ip_address_id`) REFERENCES `cloud`.`user_ip_address`(`id`), + CONSTRAINT `fk_quarantined_ips__previous_owner_id` FOREIGN KEY(`previous_owner_id`) REFERENCES `cloud`.`account`(`id`) +); + -- create_public_parameter_on_roles. #6960 ALTER TABLE `cloud`.`roles` ADD COLUMN `public_role` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Indicates whether the role will be visible to all users (public) or only to root admins (private). If this parameter is not specified during the creation of the role its value will be defaulted to true (public).'; diff --git a/framework/db/src/main/java/com/cloud/utils/db/Filter.java b/framework/db/src/main/java/com/cloud/utils/db/Filter.java index 59dc8c1477e..15161ab058f 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/Filter.java +++ b/framework/db/src/main/java/com/cloud/utils/db/Filter.java @@ -51,6 +51,10 @@ public class Filter { addOrderBy(clazz, field, ascending); } + public Filter(Class clazz, String field, boolean ascending) { + this(clazz, field, ascending, null, null); + } + public Filter(long limit) { _orderBy = " ORDER BY RAND() LIMIT " + limit; } diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java index 1eae0edd9c3..b2a9e6f733a 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java @@ -258,6 +258,8 @@ public interface GenericDao { public T findOneBy(final SearchCriteria sc); + T findOneBy(SearchCriteria sc, Filter filter); + /** * @return */ diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java index 5fd9580342c..6bf36df0941 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java @@ -422,7 +422,7 @@ public abstract class GenericDaoBase extends Compone return result; } catch (final SQLException e) { throw new CloudRuntimeException("DB Exception on: " + pstmt, e); - } catch (final Throwable e) { + } catch (final Exception e) { throw new CloudRuntimeException("Caught: " + pstmt, e); } } @@ -499,7 +499,7 @@ public abstract class GenericDaoBase extends Compone return results; } catch (final SQLException e) { throw new CloudRuntimeException("DB Exception on: " + pstmt, e); - } catch (final Throwable e) { + } catch (final Exception e) { throw new CloudRuntimeException("Caught: " + pstmt, e); } } @@ -907,6 +907,15 @@ public abstract class GenericDaoBase extends Compone return findOneIncludingRemovedBy(sc); } + @Override + @DB() + public T findOneBy(SearchCriteria sc, final Filter filter) { + sc = checkAndSetRemovedIsNull(sc); + filter.setLimit(1L); + List results = searchIncludingRemoved(sc, filter, null, false); + return results.isEmpty() ? null : results.get(0); + } + @DB() protected List listBy(SearchCriteria sc, final Filter filter) { sc = checkAndSetRemovedIsNull(sc); @@ -1145,7 +1154,7 @@ public abstract class GenericDaoBase extends Compone return result; } catch (final SQLException e) { throw new CloudRuntimeException("DB Exception on: " + pstmt, e); - } catch (final Throwable e) { + } catch (final Exception e) { throw new CloudRuntimeException("Caught: " + pstmt, e); } } @@ -1227,7 +1236,7 @@ public abstract class GenericDaoBase extends Compone return pstmt.executeUpdate(); } catch (final SQLException e) { throw new CloudRuntimeException("DB Exception on: " + pstmt, e); - } catch (final Throwable e) { + } catch (final Exception e) { throw new CloudRuntimeException("Caught: " + pstmt, e); } } @@ -2050,7 +2059,7 @@ public abstract class GenericDaoBase extends Compone return 0; } catch (final SQLException e) { throw new CloudRuntimeException("DB Exception on: " + pstmt, e); - } catch (final Throwable e) { + } catch (final Exception e) { throw new CloudRuntimeException("Caught: " + pstmt, e); } } @@ -2101,7 +2110,7 @@ public abstract class GenericDaoBase extends Compone return 0; } catch (final SQLException e) { throw new CloudRuntimeException("DB Exception in executing: " + sql, e); - } catch (final Throwable e) { + } catch (final Exception e) { throw new CloudRuntimeException("Caught exception in : " + sql, e); } } @@ -2158,7 +2167,7 @@ public abstract class GenericDaoBase extends Compone return 0; } catch (final SQLException e) { throw new CloudRuntimeException("DB Exception on: " + pstmt, e); - } catch (final Throwable e) { + } catch (final Exception e) { throw new CloudRuntimeException("Caught: " + pstmt, e); } } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 0d00fece4c3..3649197a218 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -100,6 +100,7 @@ import org.apache.cloudstack.api.response.ImageStoreResponse; import org.apache.cloudstack.api.response.InstanceGroupResponse; import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse; import org.apache.cloudstack.api.response.IpForwardingRuleResponse; +import org.apache.cloudstack.api.response.IpQuarantineResponse; import org.apache.cloudstack.api.response.IpRangeResponse; import org.apache.cloudstack.api.response.Ipv6RouteResponse; import org.apache.cloudstack.api.response.IsolationMethodResponse; @@ -282,6 +283,7 @@ import com.cloud.network.OvsProvider; import com.cloud.network.PhysicalNetwork; import com.cloud.network.PhysicalNetworkServiceProvider; import com.cloud.network.PhysicalNetworkTrafficType; +import com.cloud.network.PublicIpQuarantine; import com.cloud.network.RemoteAccessVpn; import com.cloud.network.RouterHealthCheckResult; import com.cloud.network.Site2SiteCustomerGateway; @@ -5088,4 +5090,23 @@ public class ApiResponseHelper implements ResponseGenerator { response.setObjectName("firewallrule"); return response; } + + @Override + public IpQuarantineResponse createQuarantinedIpsResponse(PublicIpQuarantine quarantinedIp) { + IpQuarantineResponse quarantinedIpsResponse = new IpQuarantineResponse(); + String ipAddress = userIpAddressDao.findById(quarantinedIp.getPublicIpAddressId()).getAddress().toString(); + Account previousOwner = _accountMgr.getAccount(quarantinedIp.getPreviousOwnerId()); + + quarantinedIpsResponse.setId(quarantinedIp.getUuid()); + quarantinedIpsResponse.setPublicIpAddress(ipAddress); + quarantinedIpsResponse.setPreviousOwnerId(previousOwner.getUuid()); + quarantinedIpsResponse.setPreviousOwnerName(previousOwner.getName()); + quarantinedIpsResponse.setCreated(quarantinedIp.getCreated()); + quarantinedIpsResponse.setRemoved(quarantinedIp.getRemoved()); + quarantinedIpsResponse.setEndDate(quarantinedIp.getEndDate()); + quarantinedIpsResponse.setRemovalReason(quarantinedIp.getRemovalReason()); + quarantinedIpsResponse.setResponseName("quarantinedip"); + + return quarantinedIpsResponse; + } } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 91b8d7eb988..0056be5fce9 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -43,6 +43,9 @@ import com.cloud.network.as.dao.AutoScaleVmGroupDao; import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.PublicIpQuarantineDao; +import com.cloud.network.PublicIpQuarantine; +import com.cloud.network.vo.PublicIpQuarantineVO; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.SSHKeyPairVO; import com.cloud.user.dao.SSHKeyPairDao; @@ -88,6 +91,7 @@ import org.apache.cloudstack.api.command.admin.user.ListUsersCmd; import org.apache.cloudstack.api.command.admin.zone.ListZonesCmdByAdmin; import org.apache.cloudstack.api.command.user.account.ListAccountsCmd; import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; +import org.apache.cloudstack.api.command.user.address.ListQuarantinedIpsCmd; import org.apache.cloudstack.api.command.user.affinitygroup.ListAffinityGroupsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.iso.ListIsosCmd; @@ -119,6 +123,7 @@ import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.HostTagResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; import org.apache.cloudstack.api.response.InstanceGroupResponse; +import org.apache.cloudstack.api.response.IpQuarantineResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.ProjectAccountResponse; @@ -541,6 +546,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject EntityManager entityManager; + @Inject + private PublicIpQuarantineDao publicIpQuarantineDao; + private SearchCriteria getMinimumCpuServiceOfferingJoinSearchCriteria(int cpu) { SearchCriteria sc = _srvOfferingJoinDao.createSearchCriteria(); SearchCriteria sc1 = _srvOfferingJoinDao.createSearchCriteria(); @@ -4754,6 +4762,49 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } @Override + public ListResponse listQuarantinedIps(ListQuarantinedIpsCmd cmd) { + ListResponse response = new ListResponse<>(); + Pair, Integer> result = listQuarantinedIpsInternal(cmd.isShowRemoved(), cmd.isShowInactive()); + List ipsQuarantinedResponses = new ArrayList<>(); + + for (PublicIpQuarantine quarantinedIp : result.first()) { + IpQuarantineResponse ipsInQuarantineResponse = responseGenerator.createQuarantinedIpsResponse(quarantinedIp); + ipsQuarantinedResponses.add(ipsInQuarantineResponse); + } + + response.setResponses(ipsQuarantinedResponses); + return response; + } + + /** + * It lists the quarantine IPs that the caller account is allowed to see by filtering the domain path of the caller account. + * Furthermore, it lists inactive and removed quarantined IPs according to the command parameters. + */ + private Pair, Integer> listQuarantinedIpsInternal(boolean showRemoved, boolean showInactive) { + String callingAccountDomainPath = _domainDao.findById(CallContext.current().getCallingAccount().getDomainId()).getPath(); + + SearchBuilder filterAllowedOnly = _accountJoinDao.createSearchBuilder(); + filterAllowedOnly.and("path", filterAllowedOnly.entity().getDomainPath(), SearchCriteria.Op.LIKE); + + SearchBuilder listAllPublicIpsInQuarantineAllowedToTheCaller = publicIpQuarantineDao.createSearchBuilder(); + listAllPublicIpsInQuarantineAllowedToTheCaller.join("listQuarantinedJoin", filterAllowedOnly, + listAllPublicIpsInQuarantineAllowedToTheCaller.entity().getPreviousOwnerId(), + filterAllowedOnly.entity().getId(), JoinBuilder.JoinType.INNER); + + if (!showInactive) { + listAllPublicIpsInQuarantineAllowedToTheCaller.and("endDate", listAllPublicIpsInQuarantineAllowedToTheCaller.entity().getEndDate(), SearchCriteria.Op.GT); + } + + filterAllowedOnly.done(); + listAllPublicIpsInQuarantineAllowedToTheCaller.done(); + + SearchCriteria searchCriteria = listAllPublicIpsInQuarantineAllowedToTheCaller.create(); + searchCriteria.setJoinParameters("listQuarantinedJoin", "path", callingAccountDomainPath + "%"); + searchCriteria.setParametersIfNotNull("endDate", new Date()); + + return publicIpQuarantineDao.searchAndCount(searchCriteria, null, showRemoved); + } + public ListResponse listSnapshots(ListSnapshotsCmd cmd) { Account caller = CallContext.current().getCallingAccount(); Pair, Integer> result = searchForSnapshotsWithParams(cmd.getId(), cmd.getIds(), diff --git a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java index c85328807df..75ea572491e 100644 --- a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java +++ b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java @@ -18,11 +18,13 @@ package com.cloud.network; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -31,6 +33,8 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.network.dao.PublicIpQuarantineDao; +import com.cloud.network.vo.PublicIpQuarantineVO; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.annotation.AnnotationService; @@ -308,6 +312,9 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage @Inject MessageBus messageBus; + @Inject + PublicIpQuarantineDao publicIpQuarantineDao; + SearchBuilder AssignIpAddressSearch; SearchBuilder AssignIpAddressFromPodVlanSearch; private static final Object allocatedLock = new Object(); @@ -318,6 +325,9 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage Boolean.class, "system.vm.public.ip.reservation.mode.strictness", "false", "If enabled, the use of System VMs public IP reservation is strict, preferred if not.", true, ConfigKey.Scope.Global); + public static final ConfigKey PUBLIC_IP_ADDRESS_QUARANTINE_DURATION = new ConfigKey<>("Network", Integer.class, "public.ip.address.quarantine.duration", + "0", "The duration (in minutes) for the public IP address to be quarantined when it is disassociated.", true, ConfigKey.Scope.Domain); + private Random rand = new Random(System.currentTimeMillis()); private List getIpv6SupportingVlanRangeIds(long dcId) throws InsufficientAddressCapacityException { @@ -523,8 +533,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage return true; } - private IpAddress allocateIP(Account ipOwner, boolean isSystem, long zoneId) throws ResourceAllocationException, InsufficientAddressCapacityException, - ConcurrentOperationException { + private IpAddress allocateIP(Account ipOwner, boolean isSystem, long zoneId) throws InsufficientAddressCapacityException, ConcurrentOperationException { Account caller = CallContext.current().getCallingAccount(); long callerUserId = CallContext.current().getCallingUserId(); // check permissions @@ -698,6 +707,9 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage public boolean disassociatePublicIpAddress(long addrId, long userId, Account caller) { boolean success = true; + IPAddressVO ipToBeDisassociated = _ipAddressDao.findById(addrId); + + PublicIpQuarantine publicIpQuarantine = null; // Cleanup all ip address resources - PF/LB/Static nat rules if (!cleanupIpResources(addrId, userId, caller)) { success = false; @@ -723,10 +735,9 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage } catch (ResourceUnavailableException e) { throw new CloudRuntimeException("We should never get to here because we used true when applyIpAssociations", e); } - } else { - if (ip.getState() == IpAddress.State.Releasing) { - _ipAddressDao.unassignIpAddress(ip.getId()); - } + } else if (ip.getState() == State.Releasing) { + publicIpQuarantine = addPublicIpAddressToQuarantine(ipToBeDisassociated, caller.getDomainId()); + _ipAddressDao.unassignIpAddress(ip.getId()); } annotationDao.removeByEntityType(AnnotationService.EntityType.PUBLIC_IP_ADDRESS.name(), ip.getUuid()); @@ -736,6 +747,8 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage releasePortableIpAddress(addrId); } s_logger.debug("Released a public ip id=" + addrId); + } else if (publicIpQuarantine != null) { + removePublicIpAddressFromQuarantine(publicIpQuarantine.getId(), "Public IP address removed from quarantine as there was an error while disassociating it."); } return success; @@ -972,6 +985,13 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage if (lockOneRow) { assert (addrs.size() == 1) : "Return size is incorrect: " + addrs.size(); + IpAddress ipAddress = addrs.get(0); + boolean ipCanBeAllocated = canPublicIpAddressBeAllocated(ipAddress, owner); + + if (!ipCanBeAllocated) { + throw new InsufficientAddressCapacityException(String.format("Failed to allocate public IP address [%s] as it is in quarantine.", ipAddress.getAddress()), + DataCenter.class, dcId); + } } if (assign && !fetchFromDedicatedRange && VlanType.VirtualNetwork.equals(vlanUse)) { @@ -1126,6 +1146,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage } else if (addr.getState() == IpAddress.State.Releasing) { // Cleanup all the resources for ip address if there are any, and only then un-assign ip in the system if (cleanupIpResources(addr.getId(), Account.ACCOUNT_ID_SYSTEM, _accountMgr.getSystemAccount())) { + addPublicIpAddressToQuarantine(addr, network.getDomainId()); _ipAddressDao.unassignIpAddress(addr.getId()); messageBus.publish(_name, MESSAGE_RELEASE_IPADDR_EVENT, PublishScope.LOCAL, addr); } else { @@ -1258,8 +1279,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage @DB @Override public IpAddress allocateIp(final Account ipOwner, final boolean isSystem, Account caller, long callerUserId, final DataCenter zone, final Boolean displayIp, final String ipaddress) - throws ConcurrentOperationException, - ResourceAllocationException, InsufficientAddressCapacityException { + throws ConcurrentOperationException, InsufficientAddressCapacityException, CloudRuntimeException { final VlanType vlanType = VlanType.VirtualNetwork; final boolean assign = false; @@ -2347,7 +2367,8 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] {UseSystemPublicIps, RulesContinueOnError, SystemVmPublicIpReservationModeStrictness, VrouterRedundantTiersPlacement, AllowUserListAvailableIpsOnSharedNetwork}; + return new ConfigKey[] {UseSystemPublicIps, RulesContinueOnError, SystemVmPublicIpReservationModeStrictness, VrouterRedundantTiersPlacement, AllowUserListAvailableIpsOnSharedNetwork, + PUBLIC_IP_ADDRESS_QUARANTINE_DURATION}; } /** @@ -2381,6 +2402,96 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage return SystemVmPublicIpReservationModeStrictness; } + @Override + public boolean canPublicIpAddressBeAllocated(IpAddress ip, Account newOwner) { + PublicIpQuarantineVO publicIpQuarantineVO = publicIpQuarantineDao.findByPublicIpAddressId(ip.getId()); + + if (publicIpQuarantineVO == null) { + s_logger.debug(String.format("Public IP address [%s] is not in quarantine; therefore, it is allowed to be allocated.", ip)); + return true; + } + + if (!isPublicIpAddressStillInQuarantine(publicIpQuarantineVO, new Date())) { + s_logger.debug(String.format("Public IP address [%s] is no longer in quarantine; therefore, it is allowed to be allocated.", ip)); + return true; + } + + Account previousOwner = _accountMgr.getAccount(publicIpQuarantineVO.getPreviousOwnerId()); + + if (Objects.equals(previousOwner.getUuid(), newOwner.getUuid())) { + s_logger.debug(String.format("Public IP address [%s] is in quarantine; however, the Public IP previous owner [%s] is the same as the new owner [%s]; therefore the IP" + + " can be allocated. The public IP address will be removed from quarantine.", ip, previousOwner, newOwner)); + removePublicIpAddressFromQuarantine(publicIpQuarantineVO.getId(), "IP was removed from quarantine because it has been allocated by the previous owner"); + return true; + } + + s_logger.error(String.format("Public IP address [%s] is in quarantine and the previous owner [%s] is different than the new owner [%s]; therefore, the IP cannot be " + + "allocated.", ip, previousOwner, newOwner)); + return false; + } + + public boolean isPublicIpAddressStillInQuarantine(PublicIpQuarantineVO publicIpQuarantineVO, Date currentDate) { + Date quarantineEndDate = publicIpQuarantineVO.getEndDate(); + Date removedDate = publicIpQuarantineVO.getRemoved(); + boolean hasQuarantineEndedEarly = removedDate != null; + + return hasQuarantineEndedEarly && currentDate.before(removedDate) || + !hasQuarantineEndedEarly && currentDate.before(quarantineEndDate); + } + + @Override + public PublicIpQuarantine addPublicIpAddressToQuarantine(IpAddress publicIpAddress, Long domainId) { + Integer quarantineDuration = PUBLIC_IP_ADDRESS_QUARANTINE_DURATION.valueInDomain(domainId); + if (quarantineDuration <= 0) { + s_logger.debug(String.format("Not adding IP [%s] to quarantine because configuration [%s] has value equal or less to 0.", publicIpAddress.getAddress(), + PUBLIC_IP_ADDRESS_QUARANTINE_DURATION.key())); + return null; + } + + long ipId = publicIpAddress.getId(); + long accountId = publicIpAddress.getAccountId(); + + if (accountId == Account.ACCOUNT_ID_SYSTEM) { + s_logger.debug(String.format("Not adding IP [%s] to quarantine because it belongs to the system account.", publicIpAddress.getAddress())); + return null; + } + + Date currentDate = new Date(); + Calendar quarantineEndDate = Calendar.getInstance(); + quarantineEndDate.setTime(currentDate); + quarantineEndDate.add(Calendar.MINUTE, quarantineDuration); + + PublicIpQuarantineVO publicIpQuarantine = new PublicIpQuarantineVO(ipId, accountId, currentDate, quarantineEndDate.getTime()); + s_logger.debug(String.format("Adding public IP Address [%s] to quarantine for the duration of [%s] minute(s).", publicIpAddress.getAddress(), quarantineDuration)); + return publicIpQuarantineDao.persist(publicIpQuarantine); + } + + @Override + public void removePublicIpAddressFromQuarantine(Long quarantineProcessId, String removalReason) { + PublicIpQuarantineVO publicIpQuarantineVO = publicIpQuarantineDao.findById(quarantineProcessId); + Ip ipAddress = _ipAddressDao.findById(publicIpQuarantineVO.getPublicIpAddressId()).getAddress(); + Date removedDate = new Date(); + + publicIpQuarantineVO.setRemoved(removedDate); + publicIpQuarantineVO.setRemovalReason(removalReason); + + s_logger.debug(String.format("Removing public IP Address [%s] from quarantine by updating the removed date to [%s].", ipAddress, removedDate)); + publicIpQuarantineDao.persist(publicIpQuarantineVO); + } + + @Override + public PublicIpQuarantine updatePublicIpAddressInQuarantine(Long quarantineProcessId, Date newEndDate) { + PublicIpQuarantineVO publicIpQuarantineVO = publicIpQuarantineDao.findById(quarantineProcessId); + Ip ipAddress = _ipAddressDao.findById(publicIpQuarantineVO.getPublicIpAddressId()).getAddress(); + Date currentEndDate = publicIpQuarantineVO.getEndDate(); + + publicIpQuarantineVO.setEndDate(newEndDate); + + s_logger.debug(String.format("Updating the end date for the quarantine of the public IP Address [%s] from [%s] to [%s].", ipAddress, currentEndDate, newEndDate)); + publicIpQuarantineDao.persist(publicIpQuarantineVO); + return publicIpQuarantineVO; + } + @Override public void updateSourceNatIpAddress(IPAddressVO requestedIp, List userIps) throws Exception{ Transaction.execute((TransactionCallbackWithException) status -> { diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index 55373f0dd0f..0c17efaab36 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -41,6 +41,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.network.dao.PublicIpQuarantineDao; import com.cloud.offering.ServiceOffering; import com.cloud.service.dao.ServiceOfferingDao; import org.apache.cloudstack.acl.ControlledEntity.ACLType; @@ -55,6 +56,8 @@ import org.apache.cloudstack.api.command.admin.network.ListGuestVlansCmd; import org.apache.cloudstack.api.command.admin.network.ListNetworksCmdByAdmin; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkCmdByAdmin; import org.apache.cloudstack.api.command.admin.usage.ListTrafficTypeImplementorsCmd; +import org.apache.cloudstack.api.command.user.address.RemoveQuarantinedIpCmd; +import org.apache.cloudstack.api.command.user.address.UpdateQuarantinedIpCmd; import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; import org.apache.cloudstack.api.command.user.network.CreateNetworkPermissionsCmd; import org.apache.cloudstack.api.command.user.network.ListNetworkPermissionsCmd; @@ -401,6 +404,8 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C CommandSetupHelper commandSetupHelper; @Inject ServiceOfferingDao serviceOfferingDao; + @Inject + PublicIpQuarantineDao publicIpQuarantineDao; @Autowired @Qualifier("networkHelper") @@ -5939,4 +5944,75 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C public ConfigKey[] getConfigKeys() { return new ConfigKey[] {AllowDuplicateNetworkName, AllowEmptyStartEndIpAddress, VRPrivateInterfaceMtu, VRPublicInterfaceMtu, AllowUsersToSpecifyVRMtu}; } + + @Override + public PublicIpQuarantine updatePublicIpAddressInQuarantine(UpdateQuarantinedIpCmd cmd) throws CloudRuntimeException { + Long ipId = cmd.getId(); + String ipAddress = cmd.getIpAddress(); + Date newEndDate = cmd.getEndDate(); + + if (new Date().after(newEndDate)) { + throw new InvalidParameterValueException(String.format("The given end date [%s] is invalid as it is before the current date.", newEndDate)); + } + + PublicIpQuarantine publicIpQuarantine = retrievePublicIpQuarantine(ipId, ipAddress); + checkCallerForPublicIpQuarantineAccess(publicIpQuarantine); + + String publicIpQuarantineAddress = _ipAddressDao.findById(publicIpQuarantine.getPublicIpAddressId()).getAddress().toString(); + Date currentEndDate = publicIpQuarantine.getEndDate(); + + if (new Date().after(currentEndDate)) { + throw new CloudRuntimeException(String.format("The quarantine for the public IP address [%s] is no longer active; thus, it cannot be updated.", publicIpQuarantineAddress)); + } + + return _ipAddrMgr.updatePublicIpAddressInQuarantine(publicIpQuarantine.getId(), newEndDate); + } + + @Override + public void removePublicIpAddressFromQuarantine(RemoveQuarantinedIpCmd cmd) throws CloudRuntimeException { + Long ipId = cmd.getId(); + String ipAddress = cmd.getIpAddress(); + PublicIpQuarantine publicIpQuarantine = retrievePublicIpQuarantine(ipId, ipAddress); + + String removalReason = cmd.getRemovalReason(); + if (StringUtils.isBlank(removalReason)) { + s_logger.error("The removalReason parameter cannot be blank."); + ipAddress = ObjectUtils.defaultIfNull(ipAddress, _ipAddressDao.findById(publicIpQuarantine.getPublicIpAddressId()).getAddress().toString()); + throw new CloudRuntimeException(String.format("The given reason for removing the public IP address [%s] from quarantine is blank.", ipAddress)); + } + + checkCallerForPublicIpQuarantineAccess(publicIpQuarantine); + + _ipAddrMgr.removePublicIpAddressFromQuarantine(publicIpQuarantine.getId(), removalReason); + } + + /** + * Retrieves the active quarantine for the given public IP address. It can find by the ID of the quarantine or the address of the public IP. + * @throws CloudRuntimeException if it does not find an active quarantine for the given public IP. + */ + protected PublicIpQuarantine retrievePublicIpQuarantine(Long ipId, String ipAddress) throws CloudRuntimeException { + PublicIpQuarantine publicIpQuarantine; + if (ipId != null) { + s_logger.debug("The ID of the IP in quarantine was informed; therefore, the `ipAddress` parameter will be ignored."); + publicIpQuarantine = publicIpQuarantineDao.findById(ipId); + } else if (ipAddress != null) { + s_logger.debug("The address of the IP in quarantine was informed, it will be used to fetch its metadata."); + publicIpQuarantine = publicIpQuarantineDao.findByIpAddress(ipAddress); + } else { + throw new CloudRuntimeException("Either the ID or the address of the IP in quarantine must be informed."); + } + + if (publicIpQuarantine == null) { + throw new CloudRuntimeException("There is no active quarantine for the specified IP address."); + } + + return publicIpQuarantine; + } + + protected void checkCallerForPublicIpQuarantineAccess(PublicIpQuarantine publicIpQuarantine) { + Account callingAccount = CallContext.current().getCallingAccount(); + DomainVO domainOfThePreviousOwner = _domainDao.findById(_accountDao.findById(publicIpQuarantine.getPreviousOwnerId()).getDomainId()); + + _accountMgr.checkAccess(callingAccount, domainOfThePreviousOwner); + } } diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index 4456f10b546..b4cb73f035d 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -1105,8 +1105,12 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis Vpc vpc = createVpc(cmd.getZoneId(), cmd.getVpcOffering(), cmd.getEntityOwnerId(), cmd.getVpcName(), cmd.getDisplayText(), cmd.getCidr(), cmd.getNetworkDomain(), cmd.getIp4Dns1(), cmd.getIp4Dns2(), cmd.getIp6Dns1(), cmd.getIp6Dns2(), cmd.isDisplay(), cmd.getPublicMtu()); - // associate cmd.getSourceNatIP() with this vpc - allocateSourceNatIp(vpc, cmd.getSourceNatIP()); + + String sourceNatIP = cmd.getSourceNatIP(); + if (sourceNatIP != null) { + s_logger.info(String.format("Trying to allocate the specified IP [%s] as the source NAT of VPC [%s].", sourceNatIP, vpc)); + allocateSourceNatIp(vpc, sourceNatIP); + } return vpc; } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 5be85d051f4..7548b768327 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -43,6 +43,7 @@ import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.network.dao.PublicIpQuarantineDao; import com.cloud.hypervisor.HypervisorGuru; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; @@ -329,9 +330,12 @@ import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; import org.apache.cloudstack.api.command.user.address.AssociateIPAddrCmd; import org.apache.cloudstack.api.command.user.address.DisassociateIPAddrCmd; import org.apache.cloudstack.api.command.user.address.ListPublicIpAddressesCmd; +import org.apache.cloudstack.api.command.user.address.ListQuarantinedIpsCmd; import org.apache.cloudstack.api.command.user.address.ReleaseIPAddrCmd; +import org.apache.cloudstack.api.command.user.address.RemoveQuarantinedIpCmd; import org.apache.cloudstack.api.command.user.address.ReserveIPAddrCmd; import org.apache.cloudstack.api.command.user.address.UpdateIPAddrCmd; +import org.apache.cloudstack.api.command.user.address.UpdateQuarantinedIpCmd; import org.apache.cloudstack.api.command.user.affinitygroup.CreateAffinityGroupCmd; import org.apache.cloudstack.api.command.user.affinitygroup.DeleteAffinityGroupCmd; import org.apache.cloudstack.api.command.user.affinitygroup.ListAffinityGroupTypesCmd; @@ -979,6 +983,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject UserDataManager userDataManager; + @Inject + private PublicIpQuarantineDao publicIpQuarantineDao; + private LockControllerListener _lockControllerListener; private final ScheduledExecutorService _eventExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("EventChecker")); private final ScheduledExecutorService _alertExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("AlertChecker")); @@ -2508,10 +2515,12 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe final SearchBuilder sb2 = _publicIpAddressDao.createSearchBuilder(); buildParameters(sb2, cmd, false); sb2.and("ids", sb2.entity().getId(), SearchCriteria.Op.IN); + sb2.and("quarantinedPublicIpsIdsNIN", sb2.entity().getId(), SearchCriteria.Op.NIN); SearchCriteria sc2 = sb2.create(); setParameters(sc2, cmd, vlanType, isAllocated); sc2.setParameters("ids", freeAddrIds.toArray()); + _publicIpAddressDao.buildQuarantineSearchCriteria(sc2); addrs.addAll(_publicIpAddressDao.search(sc2, searchFilter)); // Allocated + Free } Collections.sort(addrs, Comparator.comparing(IPAddressVO::getAddress)); @@ -3798,6 +3807,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(UpdateRemoteAccessVpnCmd.class); cmdList.add(UpdateVpnConnectionCmd.class); cmdList.add(UpdateVpnGatewayCmd.class); + cmdList.add(ListQuarantinedIpsCmd.class); + cmdList.add(UpdateQuarantinedIpCmd.class); + cmdList.add(RemoveQuarantinedIpCmd.class); // separated admin commands cmdList.add(ListAccountsCmdByAdmin.class); cmdList.add(ListZonesCmdByAdmin.class); diff --git a/server/src/test/java/com/cloud/network/IpAddressManagerTest.java b/server/src/test/java/com/cloud/network/IpAddressManagerTest.java index d7863e438e6..935fb4e8c3b 100644 --- a/server/src/test/java/com/cloud/network/IpAddressManagerTest.java +++ b/server/src/test/java/com/cloud/network/IpAddressManagerTest.java @@ -17,19 +17,25 @@ package com.cloud.network; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.network.Network.Service; -import com.cloud.network.dao.IPAddressDao; -import com.cloud.network.dao.IPAddressVO; -import com.cloud.network.dao.NetworkDao; -import com.cloud.network.dao.NetworkVO; -import com.cloud.network.rules.StaticNat; -import com.cloud.network.rules.StaticNatImpl; -import com.cloud.offerings.NetworkOfferingVO; -import com.cloud.offerings.dao.NetworkOfferingDao; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Vector; + +import com.cloud.network.dao.PublicIpQuarantineDao; +import com.cloud.network.vo.PublicIpQuarantineVO; import com.cloud.user.Account; -import com.cloud.user.AccountVO; -import com.cloud.utils.net.Ip; +import com.cloud.user.AccountManager; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -40,19 +46,18 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.runners.MockitoJUnitRunner; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Vector; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.Network.Service; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.rules.StaticNat; +import com.cloud.network.rules.StaticNatImpl; +import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.user.AccountVO; +import com.cloud.utils.net.Ip; @RunWith(MockitoJUnitRunner.class) public class IpAddressManagerTest { @@ -80,6 +85,34 @@ public class IpAddressManagerTest { AccountVO account; + @Mock + PublicIpQuarantineVO publicIpQuarantineVOMock; + + @Mock + PublicIpQuarantineDao publicIpQuarantineDaoMock; + + @Mock + IpAddress ipAddressMock; + + @Mock + AccountVO newOwnerMock; + + @Mock + AccountVO previousOwnerMock; + + @Mock + AccountManager accountManagerMock; + + final long dummyID = 1L; + + final String UUID = "uuid"; + + private static final Date currentDate = new Date(100L); + + private static final Date beforeCurrentDate = new Date(99L); + + private static final Date afterCurrentDate = new Date(101L); + @Before public void setup() throws ResourceUnavailableException { @@ -234,6 +267,136 @@ public class IpAddressManagerTest { return network; } + @Test + public void isPublicIpAddressStillInQuarantineTestRemovedDateIsNullAndCurrentDateIsEqualToEndDateShouldReturnFalse() { + Date endDate = currentDate; + + Mockito.when(publicIpQuarantineVOMock.getRemoved()).thenReturn(null); + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(endDate); + + boolean result = ipAddressManager.isPublicIpAddressStillInQuarantine(publicIpQuarantineVOMock, currentDate); + + Assert.assertFalse(result); + } + + @Test + public void isPublicIpAddressStillInQuarantineTestRemovedDateIsNullAndEndDateIsBeforeCurrentDateShouldReturnFalse() { + Date endDate = beforeCurrentDate; + + Mockito.when(publicIpQuarantineVOMock.getRemoved()).thenReturn(null); + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(endDate); + + boolean result = ipAddressManager.isPublicIpAddressStillInQuarantine(publicIpQuarantineVOMock, currentDate); + + Assert.assertFalse(result); + } + + @Test + public void isPublicIpAddressStillInQuarantineTestRemovedDateIsNullAndEndDateIsAfterCurrentDateShouldReturnTrue() { + Date endDate = afterCurrentDate; + + Mockito.when(publicIpQuarantineVOMock.getRemoved()).thenReturn(null); + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(endDate); + + boolean result = ipAddressManager.isPublicIpAddressStillInQuarantine(publicIpQuarantineVOMock, currentDate); + + Assert.assertTrue(result); + } + + @Test + public void isPublicIpAddressStillInQuarantineTestRemovedDateIsEqualCurrentDateShouldReturnFalse() { + Date removedDate = currentDate; + + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(currentDate); + Mockito.when(publicIpQuarantineVOMock.getRemoved()).thenReturn(removedDate); + + boolean result = ipAddressManager.isPublicIpAddressStillInQuarantine(publicIpQuarantineVOMock, currentDate); + + Assert.assertFalse(result); + } + + @Test + public void isPublicIpAddressStillInQuarantineTestRemovedDateIsBeforeCurrentDateShouldReturnFalse() { + Date removedDate = beforeCurrentDate; + + Mockito.when(publicIpQuarantineVOMock.getRemoved()).thenReturn(removedDate); + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(null); + + boolean result = ipAddressManager.isPublicIpAddressStillInQuarantine(publicIpQuarantineVOMock, currentDate); + + Assert.assertFalse(result); + } + + @Test + public void isPublicIpAddressStillInQuarantineTestRemovedDateIsAfterCurrentDateShouldReturnTrue() { + Date removedDate = afterCurrentDate; + + Mockito.when(publicIpQuarantineVOMock.getRemoved()).thenReturn(removedDate); + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(null); + + boolean result = ipAddressManager.isPublicIpAddressStillInQuarantine(publicIpQuarantineVOMock, currentDate); + + Assert.assertTrue(result); + } + + @Test + public void checkIfPublicIpAddressIsNotInQuarantineAndCanBeAllocatedTestIpIsNotInQuarantineShouldReturnTrue() { + Mockito.when(ipAddressMock.getId()).thenReturn(dummyID); + Mockito.when(publicIpQuarantineDaoMock.findByPublicIpAddressId(Mockito.anyLong())).thenReturn(null); + + boolean result = ipAddressManager.canPublicIpAddressBeAllocated(ipAddressMock, account); + + Assert.assertTrue(result); + } + + @Test + public void checkIfPublicIpAddressIsNotInQuarantineAndCanBeAllocatedTestIpIsNoLongerInQuarantineShouldReturnTrue() { + Mockito.when(ipAddressMock.getId()).thenReturn(dummyID); + Mockito.when(publicIpQuarantineDaoMock.findByPublicIpAddressId(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); + Mockito.doReturn(false).when(ipAddressManager).isPublicIpAddressStillInQuarantine(Mockito.any(PublicIpQuarantineVO.class), Mockito.any(Date.class)); + + boolean result = ipAddressManager.canPublicIpAddressBeAllocated(ipAddressMock, newOwnerMock); + + Assert.assertTrue(result); + } + + @Test + public void checkIfPublicIpAddressIsNotInQuarantineAndCanBeAllocatedTestIpIsInQuarantineAndThePreviousOwnerIsTheSameAsTheNewOwnerShouldReturnTrue() { + Mockito.when(ipAddressMock.getId()).thenReturn(dummyID); + Mockito.when(publicIpQuarantineDaoMock.findByPublicIpAddressId(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); + + Mockito.doReturn(true).when(ipAddressManager).isPublicIpAddressStillInQuarantine(Mockito.any(PublicIpQuarantineVO.class), Mockito.any(Date.class)); + Mockito.doNothing().when(ipAddressManager).removePublicIpAddressFromQuarantine(Mockito.anyLong(), Mockito.anyString()); + + Mockito.when(publicIpQuarantineVOMock.getPreviousOwnerId()).thenReturn(dummyID); + Mockito.when(accountManagerMock.getAccount(Mockito.anyLong())).thenReturn(previousOwnerMock); + Mockito.when(previousOwnerMock.getUuid()).thenReturn(UUID); + Mockito.when(newOwnerMock.getUuid()).thenReturn(UUID); + + boolean result = ipAddressManager.canPublicIpAddressBeAllocated(ipAddressMock, newOwnerMock); + + Assert.assertTrue(result); + } + + @Test + public void checkIfPublicIpAddressIsNotInQuarantineAndCanBeAllocatedTestIpIsInQuarantineAndThePreviousOwnerIsDifferentFromTheNewOwnerShouldReturnFalse() { + final String UUID_2 = "uuid_2"; + + Mockito.when(ipAddressMock.getId()).thenReturn(dummyID); + Mockito.when(publicIpQuarantineDaoMock.findByPublicIpAddressId(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); + + Mockito.doReturn(true).when(ipAddressManager).isPublicIpAddressStillInQuarantine(Mockito.any(PublicIpQuarantineVO.class), Mockito.any(Date.class)); + + Mockito.when(publicIpQuarantineVOMock.getPreviousOwnerId()).thenReturn(dummyID); + Mockito.when(accountManagerMock.getAccount(Mockito.anyLong())).thenReturn(previousOwnerMock); + Mockito.when(previousOwnerMock.getUuid()).thenReturn(UUID); + Mockito.when(newOwnerMock.getUuid()).thenReturn(UUID_2); + + boolean result = ipAddressManager.canPublicIpAddressBeAllocated(ipAddressMock, newOwnerMock); + + Assert.assertFalse(result); + } + @Test public void updateSourceNatIpAddress() throws Exception { IPAddressVO requestedIp = Mockito.mock(IPAddressVO.class); diff --git a/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java b/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java index d6f5dbd9a7b..c993f7b7095 100644 --- a/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java @@ -16,13 +16,60 @@ // under the License. package com.cloud.network; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doReturn; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.network.dao.PublicIpQuarantineDao; +import com.cloud.network.vo.PublicIpQuarantineVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.net.Ip; +import com.cloud.exception.InsufficientAddressCapacityException; +import org.apache.cloudstack.alert.AlertService; +import org.apache.cloudstack.api.command.user.address.UpdateQuarantinedIpCmd; +import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; +import org.apache.cloudstack.api.command.user.network.UpdateNetworkCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.test.util.ReflectionTestUtils; + import com.cloud.agent.api.to.IpAddressTO; import com.cloud.alert.AlertManager; import com.cloud.configuration.ConfigurationManager; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; -import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.ResourceAllocationException; @@ -60,44 +107,9 @@ import com.cloud.vm.NicVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; -import org.apache.cloudstack.alert.AlertService; -import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; -import org.apache.cloudstack.api.command.user.network.UpdateNetworkCmd; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.framework.config.ConfigKey; import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; -import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class NetworkServiceImplTest { @@ -157,7 +169,7 @@ public class NetworkServiceImplTest { ServiceOfferingVO serviceOfferingVoMock; @Mock - IpAddressManager ipAddressManager; + ConfigKey privateMtuKey; @Mock private CallContext callContextMock; @InjectMocks @@ -169,9 +181,46 @@ public class NetworkServiceImplTest { CommandSetupHelper commandSetupHelper; @Mock private Account accountMock; + + @Mock + private AccountVO accountVOMock; + @Mock + private DomainVO domainVOMock; @InjectMocks NetworkServiceImpl service = new NetworkServiceImpl(); + @Mock + DomainDao domainDaoMock; + + @Mock + AccountDao accountDaoMock; + + @Mock + UpdateQuarantinedIpCmd updateQuarantinedIpCmdMock; + + @Mock + PublicIpQuarantineDao publicIpQuarantineDaoMock; + + @Mock + private PublicIpQuarantineVO publicIpQuarantineVOMock; + + @Mock + private IPAddressVO ipAddressVOMock; + + @Mock + private IpAddressManager ipAddressManagerMock; + + @Mock + private Ip ipMock; + + private static Date beforeDate; + + private static Date afterDate; + + private final Long publicIpId = 1L; + + private final String dummyIpAddress = "192.168.0.1"; + private static final String VLAN_ID_900 = "900"; private static final String VLAN_ID_901 = "901"; private static final String VLAN_ID_902 = "902"; @@ -196,6 +245,20 @@ public class NetworkServiceImplTest { private AutoCloseable closeable; + @BeforeClass + public static void setUpBeforeClass() { + Date date = new Date(); + Calendar calendar = Calendar.getInstance(); + + calendar.setTime(date); + calendar.add(Calendar.DATE, -1); + beforeDate = calendar.getTime(); + + calendar.setTime(date); + calendar.add(Calendar.DATE, 1); + afterDate = calendar.getTime(); + } + private void registerCallContext() { account = new AccountVO("testaccount", 1L, "networkdomain", Account.Type.NORMAL, "uuid"); account.setId(ACCOUNT_ID); @@ -231,7 +294,7 @@ public class NetworkServiceImplTest { service.routerDao = routerDao; service.commandSetupHelper = commandSetupHelper; service.networkHelper = networkHelper; - service._ipAddrMgr = ipAddressManager; + service._ipAddrMgr = ipAddressManagerMock; callContextMocked = Mockito.mockStatic(CallContext.class); CallContext callContextMock = Mockito.mock(CallContext.class); callContextMocked.when(CallContext::current).thenReturn(callContextMock); @@ -744,6 +807,113 @@ public class NetworkServiceImplTest { networkServiceImplMock.validateIfServiceOfferingIsActiveAndSystemVmTypeIsDomainRouter(1l); } + @Test + public void updatePublicIpAddressInQuarantineTestQuarantineIsAlreadyExpiredShouldThrowCloudRuntimeException() { + Mockito.when(updateQuarantinedIpCmdMock.getId()).thenReturn(publicIpId); + Mockito.when(updateQuarantinedIpCmdMock.getEndDate()).thenReturn(afterDate); + Mockito.when(publicIpQuarantineDaoMock.findById(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); + Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(accountVOMock); + Mockito.when(domainDaoMock.findById(Mockito.anyLong())).thenReturn(domainVOMock); + Mockito.doNothing().when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + Mockito.when(ipAddressDao.findById(Mockito.anyLong())).thenReturn(ipAddressVOMock); + Mockito.when(ipAddressVOMock.getAddress()).thenReturn(ipMock); + Mockito.when(ipMock.toString()).thenReturn(dummyIpAddress); + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(beforeDate); + String expectedMessage = String.format("The quarantine for the public IP address [%s] is no longer active; thus, it cannot be updated.", dummyIpAddress); + CloudRuntimeException assertThrows = Assert.assertThrows(CloudRuntimeException.class, + () -> service.updatePublicIpAddressInQuarantine(updateQuarantinedIpCmdMock)); + + Assert.assertEquals(expectedMessage, assertThrows.getMessage()); + } + + @Test + public void updatePublicIpAddressInQuarantineTestGivenEndDateIsBeforeCurrentDateShouldThrowInvalidParameterValueException() { + Mockito.when(updateQuarantinedIpCmdMock.getId()).thenReturn(publicIpId); + Mockito.when(updateQuarantinedIpCmdMock.getEndDate()).thenReturn(beforeDate); + + String expectedMessage = String.format("The given end date [%s] is invalid as it is before the current date.", beforeDate); + InvalidParameterValueException assertThrows = Assert.assertThrows(InvalidParameterValueException.class, + () -> service.updatePublicIpAddressInQuarantine(updateQuarantinedIpCmdMock)); + + Assert.assertEquals(expectedMessage, assertThrows.getMessage()); + } + + @Test + public void updatePublicIpAddressInQuarantineTestQuarantineIsStillValidAndGivenEndDateIsAfterCurrentDateShouldWork() { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(afterDate); + calendar.add(Calendar.DATE, 5); + Date expectedNewEndDate = calendar.getTime(); + + Mockito.when(updateQuarantinedIpCmdMock.getId()).thenReturn(publicIpId); + Mockito.when(updateQuarantinedIpCmdMock.getEndDate()).thenReturn(expectedNewEndDate); + Mockito.when(publicIpQuarantineDaoMock.findById(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); + Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(accountVOMock); + Mockito.when(domainDaoMock.findById(Mockito.anyLong())).thenReturn(domainVOMock); + Mockito.doNothing().when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + Mockito.when(ipAddressDao.findById(Mockito.anyLong())).thenReturn(ipAddressVOMock); + Mockito.when(ipAddressDao.findById(Mockito.anyLong())).thenReturn(ipAddressVOMock); + Mockito.when(ipAddressVOMock.getAddress()).thenReturn(ipMock); + Mockito.when(ipMock.toString()).thenReturn(dummyIpAddress); + Mockito.when(publicIpQuarantineVOMock.getEndDate()).thenReturn(afterDate); + Mockito.when(ipAddressManagerMock.updatePublicIpAddressInQuarantine(anyLong(), Mockito.any(Date.class))).thenReturn(publicIpQuarantineVOMock); + + PublicIpQuarantine actualPublicIpQuarantine = service.updatePublicIpAddressInQuarantine(updateQuarantinedIpCmdMock); + Mockito.when(actualPublicIpQuarantine.getEndDate()).thenReturn(expectedNewEndDate); + + Assert.assertEquals(expectedNewEndDate , actualPublicIpQuarantine.getEndDate()); + } + + @Test(expected = CloudRuntimeException.class) + public void retrievePublicIpQuarantineTestIpIdNullAndIpAddressNullShouldThrowException() { + service.retrievePublicIpQuarantine(null, null); + } + + @Test + public void retrievePublicIpQuarantineTestValidIpIdShouldReturnPublicQuarantine() { + Mockito.when(publicIpQuarantineDaoMock.findById(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); + + service.retrievePublicIpQuarantine(1L, null); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(1)).findById(Mockito.anyLong()); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(0)).findByIpAddress(Mockito.anyString()); + } + + @Test(expected = CloudRuntimeException.class) + public void retrievePublicIpQuarantineTestInvalidIpIdShouldThrowException() { + Mockito.when(publicIpQuarantineDaoMock.findById(Mockito.anyLong())).thenReturn(null); + + service.retrievePublicIpQuarantine(1L, null); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(1)).findById(Mockito.anyLong()); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(0)).findByIpAddress(Mockito.anyString()); + } + + @Test + public void retrievePublicIpQuarantineTestValidIpAddressShouldReturnPublicQuarantine() { + Mockito.when(publicIpQuarantineDaoMock.findByIpAddress(Mockito.anyString())).thenReturn(publicIpQuarantineVOMock); + + service.retrievePublicIpQuarantine(null, "10.1.1.1"); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(0)).findById(Mockito.anyLong()); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(1)).findByIpAddress(Mockito.anyString()); + } + + @Test(expected = CloudRuntimeException.class) + public void retrievePublicIpQuarantineTestInvalidIpAddressShouldThrowException() { + Mockito.when(publicIpQuarantineDaoMock.findByIpAddress(Mockito.anyString())).thenReturn(null); + + service.retrievePublicIpQuarantine(null, "10.1.1.1"); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(0)).findById(Mockito.anyLong()); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(1)).findByIpAddress(Mockito.anyString()); + } + + @Test + public void retrievePublicIpQuarantineTestIpIdAndAddressInformedShouldUseId() { + Mockito.when(publicIpQuarantineDaoMock.findById(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); + + service.retrievePublicIpQuarantine(1L, "10.1.1.1"); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(1)).findById(Mockito.anyLong()); + Mockito.verify(publicIpQuarantineDaoMock, Mockito.times(0)).findByIpAddress(Mockito.anyString()); + } + @Test public void validateNotSharedNetworkRouterIPv4() { NetworkOffering ntwkOff = Mockito.mock(NetworkOffering.class); @@ -876,7 +1046,7 @@ public class NetworkServiceImplTest { when(networkVO.getId()).thenReturn(networkId); when(networkVO.getGuestType()).thenReturn(Network.GuestType.Isolated); try { - when(ipAddressManager.allocateIp(any(), anyBoolean(), any(), anyLong(), any(), any(), eq(srcNatIp))).thenReturn(ipAddress); + when(ipAddressManagerMock.allocateIp(any(), anyBoolean(), any(), anyLong(), any(), any(), eq(srcNatIp))).thenReturn(ipAddress); service.checkAndSetRouterSourceNatIp(account, createNetworkCmd, networkVO); } catch (InsufficientAddressCapacityException | ResourceAllocationException e) { Assert.fail(e.getMessage()); diff --git a/server/src/test/java/com/cloud/user/MockUsageEventDao.java b/server/src/test/java/com/cloud/user/MockUsageEventDao.java index 97792871b4d..4639c509249 100644 --- a/server/src/test/java/com/cloud/user/MockUsageEventDao.java +++ b/server/src/test/java/com/cloud/user/MockUsageEventDao.java @@ -261,6 +261,11 @@ public class MockUsageEventDao implements UsageEventDao{ return null; } + @Override + public UsageEventVO findOneBy(SearchCriteria sc, Filter filter) { + return null; + } + @Override public Class getEntityBeanType() { return null; diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index da251cb502b..945448f7173 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -16,6 +16,37 @@ // under the License. package com.cloud.vpc; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import com.cloud.network.PublicIpQuarantine; +import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.api.command.admin.address.ReleasePodIpCmdByAdmin; +import org.apache.cloudstack.api.command.admin.network.DedicateGuestVlanRangeCmd; +import org.apache.cloudstack.api.command.admin.network.ListDedicatedGuestVlanRangesCmd; +import org.apache.cloudstack.api.command.admin.network.ListGuestVlansCmd; +import org.apache.cloudstack.api.command.admin.usage.ListTrafficTypeImplementorsCmd; +import org.apache.cloudstack.api.command.user.address.RemoveQuarantinedIpCmd; +import org.apache.cloudstack.api.command.user.address.UpdateQuarantinedIpCmd; +import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; +import org.apache.cloudstack.api.command.user.network.CreateNetworkPermissionsCmd; +import org.apache.cloudstack.api.command.user.network.ListNetworkPermissionsCmd; +import org.apache.cloudstack.api.command.user.network.ListNetworksCmd; +import org.apache.cloudstack.api.command.user.network.RemoveNetworkPermissionsCmd; +import org.apache.cloudstack.api.command.user.network.ResetNetworkPermissionsCmd; +import org.apache.cloudstack.api.command.user.network.RestartNetworkCmd; +import org.apache.cloudstack.api.command.user.network.UpdateNetworkCmd; +import org.apache.cloudstack.api.command.user.vm.ListNicsCmd; +import org.apache.cloudstack.api.response.AcquirePodIpCmdResponse; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + import com.cloud.deploy.DataCenterDeployment; import com.cloud.deploy.DeployDestination; import com.cloud.deploy.DeploymentPlan; @@ -66,32 +97,6 @@ import com.cloud.vm.ReservationContext; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; import com.cloud.vm.VirtualMachineProfile; -import org.apache.cloudstack.acl.ControlledEntity.ACLType; -import org.apache.cloudstack.api.command.admin.address.ReleasePodIpCmdByAdmin; -import org.apache.cloudstack.api.command.admin.network.DedicateGuestVlanRangeCmd; -import org.apache.cloudstack.api.command.admin.network.ListDedicatedGuestVlanRangesCmd; -import org.apache.cloudstack.api.command.admin.network.ListGuestVlansCmd; -import org.apache.cloudstack.api.command.admin.usage.ListTrafficTypeImplementorsCmd; -import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; -import org.apache.cloudstack.api.command.user.network.CreateNetworkPermissionsCmd; -import org.apache.cloudstack.api.command.user.network.ListNetworkPermissionsCmd; -import org.apache.cloudstack.api.command.user.network.ListNetworksCmd; -import org.apache.cloudstack.api.command.user.network.RemoveNetworkPermissionsCmd; -import org.apache.cloudstack.api.command.user.network.ResetNetworkPermissionsCmd; -import org.apache.cloudstack.api.command.user.network.RestartNetworkCmd; -import org.apache.cloudstack.api.command.user.network.UpdateNetworkCmd; -import org.apache.cloudstack.api.command.user.vm.ListNicsCmd; -import org.apache.cloudstack.api.response.AcquirePodIpCmdResponse; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; @Component public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrchestrationService, NetworkService { @@ -1052,4 +1057,14 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches @Override public void validateIfServiceOfferingIsActiveAndSystemVmTypeIsDomainRouter(final Long serviceOfferingId) { } + + @Override + public PublicIpQuarantine updatePublicIpAddressInQuarantine(UpdateQuarantinedIpCmd cmd) { + return null; + } + + @Override + public void removePublicIpAddressFromQuarantine(RemoveQuarantinedIpCmd cmd) { + + } } diff --git a/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java b/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java index adb974a2a06..d0b7ace4711 100644 --- a/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java +++ b/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java @@ -17,6 +17,31 @@ package org.apache.cloudstack.networkoffering; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import com.cloud.network.dao.PublicIpQuarantineDao; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.cloudstack.resourcedetail.dao.UserIpAddressDetailsDao; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + import com.cloud.configuration.ConfigurationManager; import com.cloud.event.dao.UsageEventDao; import com.cloud.event.dao.UsageEventDetailsDao; @@ -38,28 +63,6 @@ import com.cloud.user.UserVO; import com.cloud.utils.component.ComponentContext; import com.cloud.vm.dao.UserVmDetailsDao; import junit.framework.TestCase; -import org.apache.cloudstack.annotation.dao.AnnotationDao; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.config.impl.ConfigurationVO; -import org.apache.cloudstack.resourcedetail.dao.UserIpAddressDetailsDao; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import javax.inject.Inject; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.nullable; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:/createNetworkOffering.xml") @@ -101,6 +104,9 @@ public class CreateNetworkOfferingTest extends TestCase { @Inject AnnotationDao annotationDao; + @Inject + PublicIpQuarantineDao publicIpQuarantineDao; + @Override @Before public void setUp() { diff --git a/server/src/test/resources/createNetworkOffering.xml b/server/src/test/resources/createNetworkOffering.xml index 0f558d11a7a..28e602720e8 100644 --- a/server/src/test/resources/createNetworkOffering.xml +++ b/server/src/test/resources/createNetworkOffering.xml @@ -72,4 +72,5 @@ + diff --git a/test/integration/smoke/test_quarantined_ips.py b/test/integration/smoke/test_quarantined_ips.py new file mode 100644 index 00000000000..42349fd2a53 --- /dev/null +++ b/test/integration/smoke/test_quarantined_ips.py @@ -0,0 +1,329 @@ +# 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. +import time + +from nose.plugins.attrib import attr + +from marvin.cloudstackAPI import updateConfiguration +from marvin.cloudstackException import CloudstackAPIException +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import Network, NetworkOffering, VpcOffering, VPC, PublicIPAddress +from marvin.lib.common import get_domain, get_zone + + +class Services: + """ Test Quarantine for public IPs + """ + + def __init__(self): + self.services = { + "root_domain": { + "name": "ROOT", + }, + "domain_admin": { + "username": "Domain admin", + "roletype": 2, + }, + "root_admin": { + "username": "Root admin", + "roletype": 1, + }, + "domain_vpc": { + "name": "domain-vpc", + "displaytext": "domain-vpc", + "cidr": "10.1.1.0/24", + }, + "domain_network": { + "name": "domain-network", + "displaytext": "domain-network", + }, + "root_vpc": { + "name": "root-vpc", + "displaytext": "root-vpc", + "cidr": "10.2.1.0/24", + }, + "root_network": { + "name": "root-network", + "displaytext": "root-network", + } + } + + +class TestQuarantineIPs(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestQuarantineIPs, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + + cls.services = Services().services + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + return + + def setUp(self): + self.domain_admin_apiclient = self.testClient.getUserApiClient(self.services["domain_admin"]["username"], + self.services["root_domain"]["name"], + self.services["domain_admin"]["roletype"]) + + self.admin_apiclient = self.testClient.getUserApiClient(self.services["root_admin"]["username"], + self.services["root_domain"]["name"], + self.services["root_admin"]["roletype"]) + + """ + Set public.ip.address.quarantine.duration to 60 minutes + """ + update_configuration_cmd = updateConfiguration.updateConfigurationCmd() + update_configuration_cmd.name = "public.ip.address.quarantine.duration" + update_configuration_cmd.value = "1" + self.apiclient.updateConfiguration(update_configuration_cmd) + + self.cleanup = [] + return + + def tearDown(self): + """ + Reset public.ip.address.quarantine.duration to 0 minutes + """ + update_configuration_cmd = updateConfiguration.updateConfigurationCmd() + update_configuration_cmd.name = "public.ip.address.quarantine.duration" + update_configuration_cmd.value = "0" + self.apiclient.updateConfiguration(update_configuration_cmd) + + super(TestQuarantineIPs, self).tearDown() + + def create_vpc(self, api_client, services): + # Get network offering + network_offering = NetworkOffering.list(api_client, name="DefaultIsolatedNetworkOfferingForVpcNetworks") + self.assertTrue(network_offering is not None and len(network_offering) > 0, "No VPC network offering") + + # Getting VPC offering + vpc_offering = VpcOffering.list(api_client, name="Default VPC offering") + self.assertTrue(vpc_offering is not None and len(vpc_offering) > 0, "No VPC offerings found") + + # Creating VPC + vpc = VPC.create( + apiclient=api_client, + services=services, + networkDomain="vpc.networkacl", + vpcofferingid=vpc_offering[0].id, + zoneid=self.zone.id, + domainid=self.domain.id, + start=False + ) + + self.cleanup.append(vpc) + self.assertTrue(vpc is not None, "VPC creation failed") + return vpc + + @attr(tags=["advanced", "basic"], required_hardware="false") + def test_only_owner_can_allocate_ip_in_quarantine_vpc(self): + """ Test allocate IP in quarantine to VPC. + """ + # Creating Domain Admin VPC + domain_vpc = self.create_vpc(self.domain_admin_apiclient, self.services["domain_vpc"]) + + # Allocating source nat first + PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + vpcid=domain_vpc.id) + + # Getting available public IP address + ip_address = PublicIPAddress.list(self.domain_admin_apiclient, state="Free", listall=True)[0].ipaddress + + self.debug( + f"creating public address with zone {self.zone.id} and vpc id {domain_vpc.id} and ip address {ip_address}.") + # Associating public IP address to Domain Admin account + public_ip = PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + vpcid=domain_vpc.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip, "Failed to Associate IP Address") + self.assertEqual(public_ip.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") + + self.debug(f"Disassociating public IP {public_ip.ipaddress.ipaddress}.") + public_ip.delete(self.domain_admin_apiclient) + + # Creating Root Admin VPC + root_vpc = self.create_vpc(self.admin_apiclient, self.services["root_vpc"]) + + self.debug(f"Trying to allocate the same IP address {ip_address} that is still in quarantine.") + + with self.assertRaises(CloudstackAPIException) as exception: + PublicIPAddress.create(self.admin_apiclient, + zoneid=self.zone.id, + vpcid=root_vpc.id, + ipaddress=ip_address) + self.assertIn(f"Failed to allocate public IP address [{ip_address}] as it is in quarantine.", + exception.exception.errorMsg) + + # Owner should be able to allocate its IP in quarantine + public_ip = PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + vpcid=domain_vpc.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip, "Failed to Associate IP Address") + self.assertEqual(public_ip.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") + + @attr(tags=["advanced", "basic"], required_hardware="false") + def test_another_user_can_allocate_ip_after_quarantined_has_ended_vpc(self): + """ Test allocate IP to VPC after quarantine has ended. + """ + # Creating Domain Admin VPC + domain_vpc = self.create_vpc(self.domain_admin_apiclient, self.services["domain_vpc"]) + + # Allocating source nat first + PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + vpcid=domain_vpc.id) + + # Getting available public IP address + ip_address = PublicIPAddress.list(self.domain_admin_apiclient, state="Free", listall=True)[0].ipaddress + + self.debug( + f"creating public address with zone {self.zone.id} and vpc id {domain_vpc.id} and ip address {ip_address}.") + # Associating public IP address to Domain Admin account + public_ip = PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + vpcid=domain_vpc.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip, "Failed to Associate IP Address") + self.assertEqual(public_ip.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") + + self.debug(f"Disassociating public IP {public_ip.ipaddress.ipaddress}.") + public_ip.delete(self.domain_admin_apiclient) + + # Creating Root Admin VPC + root_vpc = self.create_vpc(self.admin_apiclient, self.services["root_vpc"]) + + self.debug(f"Trying to allocate the same IP address {ip_address} after the quarantine duration.") + + time.sleep(60) + + public_ip_2 = PublicIPAddress.create(self.admin_apiclient, + zoneid=self.zone.id, + vpcid=root_vpc.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip_2, "Failed to Associate IP Address") + self.assertEqual(public_ip_2.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") + + @attr(tags=["advanced", "basic"], required_hardware="false") + def test_only_owner_can_allocate_ip_in_quarantine_network(self): + """ Test allocate IP in quarantine to network. + """ + network_offering = NetworkOffering.list(self.domain_admin_apiclient, + name="DefaultIsolatedNetworkOfferingWithSourceNatService") + domain_network = Network.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + services=self.services["domain_network"], + networkofferingid=network_offering[0].id) + self.cleanup.append(domain_network) + + # Allocating source nat first + PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + networkid=domain_network.id) + + # Getting available public IP address + ip_address = PublicIPAddress.list(self.domain_admin_apiclient, state="Free", listall=True)[0].ipaddress + + self.debug( + f"creating public address with zone {self.zone.id} and network id {domain_network.id} and ip address {ip_address}.") + # Associating public IP address to Domain Admin account + public_ip = PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + networkid=domain_network.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip, "Failed to Associate IP Address") + self.assertEqual(public_ip.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") + + self.debug(f"Disassociating public IP {public_ip.ipaddress.ipaddress}.") + public_ip.delete(self.domain_admin_apiclient) + + # Creating Root Admin network + root_network = Network.create(self.admin_apiclient, + zoneid=self.zone.id, + services=self.services["root_network"], + networkofferingid=network_offering[0].id) + self.cleanup.append(root_network) + self.debug(f"Trying to allocate the same IP address {ip_address} that is still in quarantine.") + + with self.assertRaises(CloudstackAPIException) as exception: + PublicIPAddress.create(self.admin_apiclient, + zoneid=self.zone.id, + networkid=root_network.id, + ipaddress=ip_address) + self.assertIn(f"Failed to allocate public IP address [{ip_address}] as it is in quarantine.", + exception.exception.errorMsg) + + # Owner should be able to allocate its IP in quarantine + public_ip = PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + networkid=domain_network.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip, "Failed to Associate IP Address") + self.assertEqual(public_ip.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") + + @attr(tags=["advanced", "basic"], required_hardware="false") + def test_another_user_can_allocate_ip_after_quarantined_has_ended_network(self): + """ Test allocate IP to network after quarantine has ended. + """ + network_offering = NetworkOffering.list(self.domain_admin_apiclient, + name="DefaultIsolatedNetworkOfferingWithSourceNatService") + domain_network = Network.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + services=self.services["domain_network"], + networkofferingid=network_offering[0].id) + self.cleanup.append(domain_network) + # Allocating source nat first + PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + networkid=domain_network.id) + + # Getting available public IP address + ip_address = PublicIPAddress.list(self.domain_admin_apiclient, state="Free", listall=True)[0].ipaddress + + self.debug( + f"creating public address with zone {self.zone.id} and network id {domain_network.id} and ip address {ip_address}.") + # Associating public IP address to Domain Admin account + public_ip = PublicIPAddress.create(self.domain_admin_apiclient, + zoneid=self.zone.id, + networkid=domain_network.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip, "Failed to Associate IP Address") + self.assertEqual(public_ip.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") + + self.debug(f"Disassociating public IP {public_ip.ipaddress.ipaddress}.") + public_ip.delete(self.domain_admin_apiclient) + + # Creating Root Admin VPC + root_network = Network.create(self.admin_apiclient, + zoneid=self.zone.id, + services=self.services["root_network"], + networkofferingid=network_offering[0].id) + self.cleanup.append(root_network) + + self.debug(f"Trying to allocate the same IP address {ip_address} after the quarantine duration.") + + time.sleep(60) + + public_ip_2 = PublicIPAddress.create(self.admin_apiclient, + zoneid=self.zone.id, + networkid=domain_network.id, + ipaddress=ip_address) + self.assertIsNotNone(public_ip_2, "Failed to Associate IP Address") + self.assertEqual(public_ip_2.ipaddress.ipaddress, ip_address, "Associated IP is not same as specified") diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index d46e544666c..c07d27ae06c 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -256,6 +256,9 @@ known_categories = { 'importVsphereStoragePolicies' : 'vSphere storage policies', 'listVsphereStoragePolicies' : 'vSphere storage policies', 'ConsoleEndpoint': 'Console Endpoint', + 'listQuarantinedIp': 'IP Quarantine', + 'updateQuarantinedIp': 'IP Quarantine', + 'removeQuarantinedIp': 'IP Quarantine', 'Shutdown': 'Shutdown' } diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index b0fd3198301..32b0e980407 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -1854,7 +1854,7 @@ class PublicIPAddress: if zoneid: cmd.zoneid = zoneid - elif "zoneid" in services: + elif services and "zoneid" in services: cmd.zoneid = services["zoneid"] if domainid: @@ -5105,7 +5105,7 @@ class VPC: @classmethod def create(cls, apiclient, services, vpcofferingid, zoneid, networkDomain=None, account=None, - domainid=None, **kwargs): + domainid=None, start=True, **kwargs): """Creates the virtual private connection (VPC)""" cmd = createVPC.createVPCCmd() @@ -5113,6 +5113,7 @@ class VPC: cmd.displaytext = "-".join([services["displaytext"], random_gen()]) cmd.vpcofferingid = vpcofferingid cmd.zoneid = zoneid + cmd.start = start if "cidr" in services: cmd.cidr = services["cidr"] if account: diff --git a/ui/src/views/network/IpAddressesTab.vue b/ui/src/views/network/IpAddressesTab.vue index 8b22ec66a33..37b5b210a96 100644 --- a/ui/src/views/network/IpAddressesTab.vue +++ b/ui/src/views/network/IpAddressesTab.vue @@ -453,8 +453,8 @@ export default { this.onCloseModal() }).catch(error => { this.$notification.error({ - message: `${this.$t('label.error')} ${error.response.status}`, - description: error.response.data.associateipaddressresponse.errortext || error.response.data.errorresponse.errortext, + message: this.$t('message.request.failed'), + description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message, duration: 0 }) }).finally(() => {