New feature: Reserve and release Public IPs (#6046)

* Reserve and release a public IP

* Update #6046: show orange color for Reserved public ip

* Update #6046 reserve IP: fix ui conflicts

* Update #6046: fix resource count

* Update #6046: associate Reserved public IP to network

* Update #6046: fix unit tests

* Update #6046: fix ui bugs

* Update #6046: make api/ui available for domain admin and users
This commit is contained in:
Wei Zhou 2022-03-17 18:35:40 +01:00 committed by GitHub
parent 15e3a10f94
commit 6a53517d37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 819 additions and 14 deletions

View File

@ -133,6 +133,7 @@ public class EventTypes {
// Network Events
public static final String EVENT_NET_IP_ASSIGN = "NET.IPASSIGN";
public static final String EVENT_NET_IP_RELEASE = "NET.IPRELEASE";
public static final String EVENT_NET_IP_RESERVE = "NET.IPRESERVE";
public static final String EVENT_NET_IP_UPDATE = "NET.IPUPDATE";
public static final String EVENT_PORTABLE_IP_ASSIGN = "PORTABLE.IPASSIGN";
public static final String EVENT_PORTABLE_IP_RELEASE = "PORTABLE.IPRELEASE";

View File

@ -41,6 +41,7 @@ public interface IpAddress extends ControlledEntity, Identity, InternalIdentity,
Allocating, // The IP Address is being propagated to other network elements and is not ready for use yet.
Allocated, // The IP address is in used.
Releasing, // The IP address is being released for other network elements and is not ready for allocation.
Reserved, // The IP address is reserved and is not ready for allocation.
Free // The IP address is ready to be allocated.
}

View File

@ -59,6 +59,10 @@ public interface NetworkService {
IpAddress allocateIP(Account ipOwner, long zoneId, Long networkId, Boolean displayIp, String ipaddress) throws ResourceAllocationException, InsufficientAddressCapacityException,
ConcurrentOperationException;
IpAddress reserveIpAddress(Account account, Boolean displayIp, Long ipAddressId) throws ResourceAllocationException;
boolean releaseReservedIpAddress(long ipAddressId) throws InsufficientAddressCapacityException;
boolean releaseIpAddress(long ipAddressId) throws InsufficientAddressCapacityException;
IpAddress allocatePortableIP(Account ipOwner, int regionId, Long zoneId, Long networkId, Long vpcId) throws ResourceAllocationException,

View File

@ -0,0 +1,104 @@
// 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 org.apache.log4j.Logger;
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.IPAddressResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.IpAddress;
@APICommand(name = "releaseIpAddress",
description = "Releases an IP address from the account.",
since = "4.17",
responseObject = SuccessResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class ReleaseIPAddrCmd extends BaseCmd {
public static final Logger s_logger = Logger.getLogger(ReleaseIPAddrCmd.class.getName());
private static final String s_name = "releaseipaddressresponse";
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = IPAddressResponse.class, required = true, description = "the ID of the public IP address"
+ " to release")
private Long id;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getIpAddressId() {
return id;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
IpAddress ip = getIpAddress(id);
if (ip == null) {
throw new InvalidParameterValueException("Unable to find IP address by ID=" + id);
}
return ip.getAccountId();
}
@Override
public void execute() throws InsufficientAddressCapacityException {
CallContext.current().setEventDetails("IP ID: " + getIpAddressId());
boolean result = _networkService.releaseReservedIpAddress(getIpAddressId());
if (result) {
SuccessResponse response = new SuccessResponse(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to release IP address");
}
}
private IpAddress getIpAddress(long id) {
IpAddress ip = _entityMgr.findById(IpAddress.class, id);
if (ip == null) {
throw new InvalidParameterValueException("Unable to find IP address by ID=" + id);
} else {
return ip;
}
}
}

View File

@ -0,0 +1,164 @@
// 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 org.apache.log4j.Logger;
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.ResponseObject.ResponseView;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.IPAddressResponse;
import org.apache.cloudstack.api.response.ProjectResponse;
import org.apache.cloudstack.context.CallContext;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.IpAddress;
import com.cloud.projects.Project;
import com.cloud.user.Account;
@APICommand(name = "reserveIpAddress",
description = "Reserve a public IP to an account.",
since = "4.17",
responseObject = IPAddressResponse.class,
responseView = ResponseView.Restricted,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class ReserveIPAddrCmd extends BaseCmd implements UserCmd {
public static final Logger s_logger = Logger.getLogger(ReserveIPAddrCmd.class.getName());
private static final String s_name = "reserveipaddressresponse";
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ACCOUNT,
type = CommandType.STRING,
description = "the account to reserve with this IP address")
private String accountName;
@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.UUID,
entityType = DomainResponse.class,
description = "the ID of the domain to reserve with this IP address")
private Long domainId;
@Parameter(name = ApiConstants.PROJECT_ID,
type = CommandType.UUID,
entityType = ProjectResponse.class,
description = "the ID of the project to reserve with this IP address")
private Long projectId;
@Parameter(name = ApiConstants.FOR_DISPLAY,
type = CommandType.BOOLEAN,
description = "an optional field, whether to the display the IP to the end user or not",
authorized = {RoleType.Admin})
private Boolean display;
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
entityType = IPAddressResponse.class,
required = true,
description = "the ID of the public IP address to reserve")
private Long id;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getAccountName() {
if (accountName != null) {
return accountName;
}
return CallContext.current().getCallingAccount().getAccountName();
}
public long getDomainId() {
if (domainId != null) {
return domainId;
}
return CallContext.current().getCallingAccount().getDomainId();
}
public Long getIpAddressId() {
return id;
}
@Override
public boolean isDisplay() {
if (display == null)
return true;
else
return display;
}
@Override
public long getEntityOwnerId() {
Account caller = CallContext.current().getCallingAccount();
if (accountName != null && domainId != null) {
Account account = _accountService.finalizeOwner(caller, accountName, domainId, projectId);
return account.getId();
} else if (projectId != null) {
Project project = _projectService.getProject(projectId);
if (project != null) {
if (project.getState() == Project.State.Active) {
return project.getProjectAccountId();
} else {
throw new PermissionDeniedException("Can't add resources to the project with specified projectId in state=" + project.getState() +
" as it's no longer active");
}
} else {
throw new InvalidParameterValueException("Unable to find project by ID");
}
}
return caller.getAccountId();
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public String getCommandName() {
return s_name;
}
@Override
public void execute() throws ResourceUnavailableException, ResourceAllocationException, ConcurrentOperationException {
IpAddress result = _networkService.reserveIpAddress(_accountService.getAccount(getEntityOwnerId()), isDisplay(), getIpAddressId());
if (result != null) {
IPAddressResponse ipResponse = _responseGenerator.createIPAddressResponse(getResponseView(), result);
ipResponse.setResponseName(getCommandName());
setResponseObject(ipResponse);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to reserve IP address");
}
}
}

View File

@ -92,4 +92,6 @@ public interface IPAddressDao extends GenericDao<IPAddressVO, Long> {
List<IPAddressVO> listByAssociatedVmId(long vmId);
IPAddressVO findByVmIdAndNetworkId(long networkId, long vmId);
IPAddressVO findByAccountIdAndZoneIdAndStateAndIpAddress(long accountId, long dcId, State state, String ipAddress);
}

View File

@ -62,6 +62,7 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
@Inject
protected VlanDao _vlanDao;
protected GenericSearchBuilder<IPAddressVO, Long> CountFreePublicIps;
protected SearchBuilder<IPAddressVO> PublicIpSearchByAccountAndState;
@Inject
ResourceTagDao _tagsDao;
@Inject
@ -138,6 +139,7 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
AllocatedIpCountForAccount.and("allocated", AllocatedIpCountForAccount.entity().getAllocatedTime(), Op.NNULL);
AllocatedIpCountForAccount.and().op("network", AllocatedIpCountForAccount.entity().getAssociatedWithNetworkId(), Op.NNULL);
AllocatedIpCountForAccount.or("vpc", AllocatedIpCountForAccount.entity().getVpcId(), Op.NNULL);
AllocatedIpCountForAccount.or("state", AllocatedIpCountForAccount.entity().getState(), Op.EQ);
AllocatedIpCountForAccount.cp();AllocatedIpCountForAccount.done();
CountFreePublicIps = createSearchBuilder(Long.class);
@ -152,6 +154,13 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
DeleteAllExceptGivenIp = createSearchBuilder();
DeleteAllExceptGivenIp.and("vlanDbId", DeleteAllExceptGivenIp.entity().getVlanId(), Op.EQ);
DeleteAllExceptGivenIp.and("ip", DeleteAllExceptGivenIp.entity().getAddress(), Op.NEQ);
PublicIpSearchByAccountAndState = createSearchBuilder();
PublicIpSearchByAccountAndState.and("accountId", PublicIpSearchByAccountAndState.entity().getAllocatedToAccountId(), Op.EQ);
PublicIpSearchByAccountAndState.and("dcId", PublicIpSearchByAccountAndState.entity().getDataCenterId(), Op.EQ);
PublicIpSearchByAccountAndState.and("state", PublicIpSearchByAccountAndState.entity().getState(), Op.EQ);
PublicIpSearchByAccountAndState.and("allocated", PublicIpSearchByAccountAndState.entity().getAllocatedTime(), Op.NNULL);
PublicIpSearchByAccountAndState.and("ipAddress", PublicIpSearchByAccountAndState.entity().getAddress(), Op.EQ);
}
@Override
@ -368,6 +377,7 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
public long countAllocatedIPsForAccount(long accountId) {
SearchCriteria<Long> sc = AllocatedIpCountForAccount.create();
sc.setParameters("account", accountId);
sc.setParameters("state", State.Reserved);
return customSearch(sc, null).get(0);
}
@ -480,4 +490,14 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
sc.setParameters("state", state);
return listBy(sc);
}
@Override
public IPAddressVO findByAccountIdAndZoneIdAndStateAndIpAddress(long accountId, long dcId, State state, String ipAddress) {
SearchCriteria<IPAddressVO> sc = PublicIpSearchByAccountAndState.create();
sc.setParameters("accountId", accountId);
sc.setParameters("dcId", dcId);
sc.setParameters("state", state);
sc.setParameters("ipAddress", ipAddress);
return findOneBy(sc);
}
}

View File

@ -1239,6 +1239,13 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
s_logger.debug("Associate IP address lock acquired");
}
if (ipaddress != null) {
IPAddressVO ipAddr = _ipAddressDao.findByAccountIdAndZoneIdAndStateAndIpAddress(ipOwner.getId(), zone.getId(), State.Reserved, ipaddress);
if (ipAddr != null) {
return PublicIp.createFromAddrAndVlan(ipAddr, _vlanDao.findById(ipAddr.getVlanId()));
}
}
ip = Transaction.execute(new TransactionCallbackWithException<PublicIp, InsufficientAddressCapacityException>() {
@Override
public PublicIp doInTransaction(TransactionStatus status) throws InsufficientAddressCapacityException {
@ -1401,7 +1408,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
}
if (ipToAssoc.getAssociatedWithNetworkId() != null) {
s_logger.debug("IP " + ipToAssoc + " is already associated with network id" + networkId);
s_logger.debug("IP " + ipToAssoc + " is already associated with network id=" + networkId);
return ipToAssoc;
}
@ -1459,6 +1466,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
IPAddressVO ip = _ipAddressDao.findById(ipId);
//update ip address with networkId
ip.setState(State.Allocated);
ip.setAssociatedWithNetworkId(networkId);
ip.setSourceNat(isSourceNat);
_ipAddressDao.update(ipId, ip);

View File

@ -28,6 +28,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -61,6 +62,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.messagebus.MessageBus;
import org.apache.cloudstack.framework.messagebus.PublishScope;
import org.apache.cloudstack.network.element.InternalLoadBalancerElementService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
@ -68,14 +70,18 @@ import com.cloud.api.ApiDBUtils;
import com.cloud.configuration.Config;
import com.cloud.configuration.ConfigurationManager;
import com.cloud.configuration.Resource;
import com.cloud.dc.AccountVlanMapVO;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.DataCenterVnetVO;
import com.cloud.dc.DomainVlanMapVO;
import com.cloud.dc.Vlan.VlanType;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.AccountVlanMapDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.DataCenterVnetDao;
import com.cloud.dc.dao.DomainVlanMapDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.deploy.DeployDestination;
import com.cloud.domain.Domain;
@ -84,6 +90,7 @@ import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.AccountLimitException;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientCapacityException;
@ -303,6 +310,10 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
@Inject
AccountGuestVlanMapDao _accountGuestVlanMapDao;
@Inject
AccountVlanMapDao _accountVlanMapDao;
@Inject
DomainVlanMapDao _domainVlanMapDao;
@Inject
VpcDao _vpcDao;
@Inject
NetworkACLDao _networkACLDao;
@ -915,6 +926,98 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
return nicSecIp;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_NET_IP_RESERVE, eventDescription = "reserving Ip", async = false)
public IpAddress reserveIpAddress(Account account, Boolean displayIp, Long ipAddressId) throws ResourceAllocationException {
IPAddressVO ipVO = _ipAddressDao.findById(ipAddressId);
if (ipVO == null) {
throw new InvalidParameterValueException("Unable to find IP address by ID=" + ipAddressId);
}
// verify permissions
Account caller = CallContext.current().getCallingAccount();
_accountMgr.checkAccess(caller, null, true, account);
VlanVO vlan = _vlanDao.findById(ipVO.getVlanId());
if (!vlan.getVlanType().equals(VlanType.VirtualNetwork)) {
throw new IllegalArgumentException("only ip addresses that belong to a virtual network may be reserved.");
}
if (ipVO.isPortable()) {
throw new InvalidParameterValueException("Unable to reserve a portable IP.");
}
if (State.Reserved.equals(ipVO.getState())) {
if (account.getId() == ipVO.getAccountId()) {
s_logger.info(String.format("IP address %s has already been reserved for account %s", ipVO.getAddress(), account));
return ipVO;
}
throw new InvalidParameterValueException("Unable to reserve a IP because it has already been reserved for another account.");
}
if (!State.Free.equals(ipVO.getState())) {
throw new InvalidParameterValueException("Unable to reserve a IP in " + ipVO.getState() + " state.");
}
Long ipDedicatedDomainId = getIpDedicatedDomainId(ipVO.getVlanId());
if (ipDedicatedDomainId != null && !ipDedicatedDomainId.equals(account.getDomainId())) {
throw new InvalidParameterValueException("Unable to reserve a IP because it is dedicated to another domain.");
}
Long ipDedicatedAccountId = getIpDedicatedAccountId(ipVO.getVlanId());
if (ipDedicatedAccountId != null && !ipDedicatedAccountId.equals(account.getAccountId())) {
throw new InvalidParameterValueException("Unable to reserve a IP because it is dedicated to another account.");
}
if (ipDedicatedAccountId == null) {
// Check that the maximum number of public IPs for the given accountId will not be exceeded
try {
_resourceLimitMgr.checkResourceLimit(account, Resource.ResourceType.public_ip);
} catch (ResourceAllocationException ex) {
s_logger.warn("Failed to allocate resource of type " + ex.getResourceType() + " for account " + account);
throw new AccountLimitException("Maximum number of public IP addresses for account: " + account.getAccountName() + " has been exceeded.");
}
}
List<AccountVlanMapVO> maps = _accountVlanMapDao.listAccountVlanMapsByVlan(ipVO.getVlanId());
ipVO.setAllocatedTime(new Date());
ipVO.setAllocatedToAccountId(account.getAccountId());
ipVO.setAllocatedInDomainId(account.getDomainId());
ipVO.setState(State.Reserved);
if (displayIp != null) {
ipVO.setDisplay(displayIp);
}
ipVO = _ipAddressDao.persist(ipVO);
if (ipDedicatedAccountId == null) {
_resourceLimitMgr.incrementResourceCount(account.getId(), Resource.ResourceType.public_ip);
}
return ipVO;
}
private Long getIpDedicatedAccountId(Long vlanId) {
List<AccountVlanMapVO> accountVlanMaps = _accountVlanMapDao.listAccountVlanMapsByVlan(vlanId);
if (CollectionUtils.isNotEmpty(accountVlanMaps)) {
return accountVlanMaps.get(0).getAccountId();
}
return null;
}
private Long getIpDedicatedDomainId(Long vlanId) {
List<DomainVlanMapVO> domainVlanMaps = _domainVlanMapDao.listDomainVlanMapsByVlan(vlanId);
if (CollectionUtils.isNotEmpty(domainVlanMaps)) {
return domainVlanMaps.get(0).getDomainId();
}
return null;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_NET_IP_RELEASE, eventDescription = "releasing Reserved Ip", async = false)
public boolean releaseReservedIpAddress(long ipAddressId) throws InsufficientAddressCapacityException {
IPAddressVO ipVO = _ipAddressDao.findById(ipAddressId);
if (ipVO == null) {
throw new InvalidParameterValueException("Unable to find IP address by ID=" + ipAddressId);
}
if (ipVO.isPortable()) {
throw new InvalidParameterValueException("Unable to release a portable IP, please use disassociateIpAddress instead");
}
if (State.Allocated.equals(ipVO.getState())) {
throw new InvalidParameterValueException("Unable to release a public IP in Allocated state, please use disassociateIpAddress instead");
}
return releaseIpAddressInternal(ipAddressId);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_NET_IP_RELEASE, eventDescription = "disassociating Ip", async = true)
public boolean releaseIpAddress(long ipAddressId) throws InsufficientAddressCapacityException {
@ -961,6 +1064,15 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
throwInvalidIdException("Can't release system IP address with specified id", ipVO.getUuid(), "systemIpAddrId");
}
if (State.Reserved.equals(ipVO.getState())) {
_ipAddressDao.unassignIpAddress(ipVO.getId());
Long ipDedicatedAccountId = getIpDedicatedAccountId(ipVO.getVlanId());
if (ipDedicatedAccountId == null) {
_resourceLimitMgr.decrementResourceCount(ipVO.getAccountId(), Resource.ResourceType.public_ip);
}
return true;
}
boolean success = _ipAddrMgr.disassociatePublicIpAddress(ipAddressId, userId, caller);
if (success) {

View File

@ -316,6 +316,8 @@ 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.ReleaseIPAddrCmd;
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.affinitygroup.CreateAffinityGroupCmd;
import org.apache.cloudstack.api.command.user.affinitygroup.DeleteAffinityGroupCmd;
@ -2147,13 +2149,13 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
final String state = cmd.getState();
Boolean isAllocated = cmd.isAllocatedOnly();
if (isAllocated == null) {
if (state != null && state.equalsIgnoreCase(IpAddress.State.Free.name())) {
if (state != null && (state.equalsIgnoreCase(IpAddress.State.Free.name()) || state.equalsIgnoreCase(IpAddress.State.Reserved.name()))) {
isAllocated = Boolean.FALSE;
} else {
isAllocated = Boolean.TRUE; // default
}
} else {
if (state != null && state.equalsIgnoreCase(IpAddress.State.Free.name())) {
if (state != null && (state.equalsIgnoreCase(IpAddress.State.Free.name()) || state.equalsIgnoreCase(IpAddress.State.Reserved.name()))) {
if (isAllocated) {
throw new InvalidParameterValueException("Conflict: allocatedonly is true but state is Free");
}
@ -2242,7 +2244,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
buildParameters(sb, cmd, vlanType == VlanType.VirtualNetwork ? true : isAllocated);
SearchCriteria<IPAddressVO> sc = sb.create();
setParameters(sc, cmd, vlanType);
setParameters(sc, cmd, vlanType, isAllocated);
if (isAllocated || (vlanType == VlanType.VirtualNetwork && (caller.getType() != Account.Type.ADMIN || cmd.getDomainId() != null))) {
_accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
@ -2305,7 +2307,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
sb2.and("ids", sb2.entity().getId(), SearchCriteria.Op.IN);
SearchCriteria<IPAddressVO> sc2 = sb2.create();
setParameters(sc2, cmd, vlanType);
setParameters(sc2, cmd, vlanType, isAllocated);
sc2.setParameters("ids", freeAddrIds.toArray());
addrs.addAll(_publicIpAddressDao.search(sc2, searchFilter)); // Allocated + Free
}
@ -2369,7 +2371,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
}
}
private void setParameters(SearchCriteria<IPAddressVO> sc, final ListPublicIpAddressesCmd cmd, VlanType vlanType) {
private void setParameters(SearchCriteria<IPAddressVO> sc, final ListPublicIpAddressesCmd cmd, VlanType vlanType, Boolean isAllocated) {
final Object keyword = cmd.getKeyword();
final Long physicalNetworkId = cmd.getPhysicalNetworkId();
final Long sourceNetworkId = cmd.getNetworkId();
@ -2437,6 +2439,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
if (state != null) {
sc.setParameters("state", state);
} else if (isAllocated != null && isAllocated) {
sc.setParameters("state", IpAddress.State.Allocated);
}
sc.setParameters( "forsystemvms", false);
@ -3199,6 +3203,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(ListProjectAccountsCmd.class);
cmdList.add(AssociateIPAddrCmd.class);
cmdList.add(DisassociateIPAddrCmd.class);
cmdList.add(ReserveIPAddrCmd.class);
cmdList.add(ReleaseIPAddrCmd.class);
cmdList.add(ListPublicIpAddressesCmd.class);
cmdList.add(CreateAutoScalePolicyCmd.class);
cmdList.add(CreateAutoScaleVmGroupCmd.class);

View File

@ -166,6 +166,16 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches
return null;
}
@Override
public IpAddress reserveIpAddress(Account account, Boolean displayIp, Long ipAddressId) throws ResourceAllocationException {
return null;
}
@Override
public boolean releaseReservedIpAddress(long ipAddressId) throws InsufficientAddressCapacityException {
return false;
}
@Override
public IpAddress allocatePortableIP(Account ipOwner, int regionId, Long zoneId, Long networkId, Long vpcId) throws ResourceAllocationException,
InsufficientAddressCapacityException, ConcurrentOperationException {

View File

@ -248,6 +248,7 @@
"label.action.remove.host": "Remove Host",
"label.action.remove.host.processing": "Removing Host....",
"label.action.remove.vm": "Release VM",
"label.action.reserve.ip": "Reserve Public IP",
"label.action.reset.password": "Reset Password",
"label.action.reset.password.processing": "Resetting Password....",
"label.action.resize.volume": "Resize Volume",
@ -975,6 +976,7 @@
"label.forceencap": "Force UDP Encapsulation of ESP Packets",
"label.forgedtransmits": "Forged Transmits",
"label.format": "Format",
"label.free": "Free",
"label.french.azerty.keyboard": "French AZERTY keyboard",
"label.friday": "Friday",
"label.from": "from",
@ -1874,6 +1876,7 @@
"label.required": "Required",
"label.requireshvm": "HVM",
"label.requiresupgrade": "Requires Upgrade",
"label.reserved": "Reserved",
"label.reserved.system.gateway": "Reserved system gateway",
"label.reserved.system.ip": "Reserved System IP",
"label.reserved.system.netmask": "Reserved system netmask",
@ -2573,6 +2576,7 @@
"message.action.recover.volume": "Please confirm that you would like to recover this volume.",
"message.action.release.ip": "Please confirm that you want to release this IP.",
"message.action.remove.host": "Please confirm that you want to remove this host.",
"message.action.reserve.ip": "Please confirm that you want to reserve this IP.",
"message.action.reset.password.off": "Your instance currently does not support this feature.",
"message.action.reset.password.warning": "Your instance must be stopped before attempting to change its current password.",
"message.action.restore.instance": "Please confirm that you want to restore this instance.",
@ -3204,6 +3208,7 @@
"message.publicip.state.allocating": "The IP Address is being propagated to other network elements and is not ready for use yet.",
"message.publicip.state.free": "The IP address is ready to be allocated.",
"message.publicip.state.releasing": "The IP address is being released for other network elements and is not ready for allocation.",
"message.publicip.state.reserved": "The IP address is reserved and not ready for use.",
"message.question.are.you.sure.you.want.to.add": "Are you sure you want to add",
"message.read.accept.license.agreements": "Please read and accept the terms for the license agreements.",
"message.read.admin.guide.scaling.up": "Please read the dynamic scaling section in the admin guide before scaling up.",

View File

@ -156,6 +156,8 @@ export default {
switch (state.toLowerCase()) {
case 'scheduled':
return 'blue'
case 'reserved':
return 'orange'
default:
return null
}

View File

@ -275,6 +275,7 @@ export default {
resourceType: 'PublicIpAddress',
columns: ['ipaddress', 'state', 'associatednetworkname', 'virtualmachinename', 'allocated', 'account', 'zonename'],
details: ['ipaddress', 'id', 'associatednetworkname', 'virtualmachinename', 'networkid', 'issourcenat', 'isstaticnat', 'virtualmachinename', 'vmipaddress', 'vlan', 'allocated', 'account', 'zonename'],
filters: ['allocated', 'reserved', 'free'],
component: shallowRef(() => import('@/views/network/PublicIpResource.vue')),
tabs: [{
name: 'details',
@ -311,7 +312,7 @@ export default {
label: 'label.action.enable.static.nat',
docHelp: 'adminguide/networking_and_traffic.html#enabling-or-disabling-static-nat',
dataView: true,
show: (record) => { return !record.virtualmachineid && !record.issourcenat },
show: (record) => { return record.state === 'Allocated' && !record.virtualmachineid && !record.issourcenat },
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/network/EnableStaticNat.vue')))
},
@ -337,7 +338,27 @@ export default {
message: 'message.action.release.ip',
docHelp: 'adminguide/networking_and_traffic.html#releasing-an-ip-address-alloted-to-a-vpc',
dataView: true,
show: (record) => { return !record.issourcenat },
show: (record) => { return record.state === 'Allocated' && !record.issourcenat },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'reserveIpAddress',
icon: 'lock-outlined',
label: 'label.action.reserve.ip',
dataView: true,
show: (record) => { return record.state === 'Free' },
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/network/ReservePublicIP.vue')))
},
{
api: 'releaseIpAddress',
icon: 'delete-outlined',
label: 'label.action.release.ip',
message: 'message.action.release.ip',
dataView: true,
show: (record) => { return record.state === 'Reserved' },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }

View File

@ -55,7 +55,8 @@
:placeholder="$t('label.filterby')"
:value="$route.query.filter || (projectView && $route.name === 'vm' ||
['Admin', 'DomainAdmin'].includes($store.getters.userInfo.roletype) && ['vm', 'iso', 'template'].includes($route.name)
? 'all' : ['guestnetwork'].includes($route.name) ? 'all' : 'self')"
? 'all' : ['publicip'].includes($route.name)
? 'allocated': ['guestnetwork'].includes($route.name) ? 'all' : 'self')"
style="min-width: 100px; margin-left: 10px"
@change="changeFilter"
showSearch
@ -851,6 +852,9 @@ export default {
delete params.id
params.name = this.$route.params.id
}
if (['listPublicIpAddresses'].includes(this.apiName)) {
params.allocatedonly = false
}
if (this.$route.path.startsWith('/vmsnapshot/')) {
params.vmsnapshotid = this.$route.params.id
} else if (this.$route.path.startsWith('/ldapsetting/')) {
@ -1494,6 +1498,8 @@ export default {
} else {
query.type = filter
}
} else if (this.$route.name === 'publicip') {
query.state = filter
} else if (this.$route.name === 'vm') {
if (filter === 'self') {
query.account = this.$store.getters.userInfo.account

View File

@ -137,7 +137,7 @@
}" >
<a-select-option
v-for="ip in listPublicIpAddress"
:key="ip.ipaddress">{{ ip.ipaddress }}</a-select-option>
:key="ip.ipaddress">{{ ip.ipaddress }} ({{ ip.state }})</a-select-option>
</a-select>
</a-form-item>
<div :span="24" class="action-button">
@ -367,7 +367,7 @@ export default {
}).catch(error => {
this.$notification.error({
message: `${this.$t('label.error')} ${error.response.status}`,
description: error.response.data.errorresponse.errortext,
description: error.response.data.associateipaddressresponse.errortext || error.response.data.errorresponse.errortext,
duration: 0
})
}).finally(() => {
@ -447,9 +447,10 @@ export default {
try {
const listPublicIpAddress = await this.fetchListPublicIpAddress()
listPublicIpAddress.forEach(item => {
if (item.state === 'Free') {
if (item.state === 'Free' || item.state === 'Reserved') {
this.listPublicIpAddress.push({
ipaddress: item.ipaddress
ipaddress: item.ipaddress,
state: item.state
})
}
})

View File

@ -113,6 +113,11 @@ export default {
this.loading = false
},
async filterTabs () {
// Public IPs in Free state have nothing
if (['Free', 'Reserved'].includes(this.resource.state)) {
this.tabs = this.defaultTabs
return
}
// VPC IPs with source nat have only VPN
if (this.resource && this.resource.vpcid && this.resource.issourcenat) {
this.tabs = this.defaultTabs.concat(this.$route.meta.tabs.filter(tab => tab.name === 'vpn'))

View File

@ -0,0 +1,332 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<div class="form" v-ctrl-enter="submitData">
<div v-if="loading" class="loading">
<loading-outlined style="color: #1890ff;" />
</div>
<a-alert type="warning" style="margin-bottom: 20px">
<template #message>
<label v-html="$t('message.action.reserve.ip')"></label>
</template>
</a-alert>
<div class="form__item">
<p class="form__label">{{ $t('label.accounttype') }}</p>
<a-select
v-model:value="selectedAccountType"
v-focus="true"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}">
<a-select-option :value="$t('label.account')">{{ $t('label.account') }}</a-select-option>
<a-select-option :value="$t('label.project')">{{ $t('label.project') }}</a-select-option>
</a-select>
</div>
<div class="form__item" v-if="isAdminOrDomainAdmin()" >
<p class="form__label"><span class="required">*</span>{{ $t('label.domain') }}</p>
<a-select
@change="changeDomain"
v-model:value="selectedDomain"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="domain in domains" :key="domain.name" :value="domain.id" :label="domain.path || domain.name || domain.description">
<span>
<resource-icon v-if="domain && domain.icon" :image="domain.icon.base64image" size="1x" style="margin-right: 5px"/>
<block-outlined v-else style="margin-right: 5px" />
{{ domain.path || domain.name || domain.description }}
</span>
</a-select-option>
</a-select>
</div>
<template v-if="selectedAccountType === $t('label.account')">
<div class="form__item" v-if="isAdminOrDomainAdmin()">
<p class="form__label"><span class="required">*</span>{{ $t('label.account') }}</p>
<a-select
@change="changeAccount"
v-model:value="selectedAccount"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="account in accounts" :key="account.name" :value="account.name">
<span>
<resource-icon v-if="account && account.icon" :image="account.icon.base64image" size="1x" style="margin-right: 5px"/>
<team-outlined v-else style="margin-right: 5px" />
{{ account.name }}
</span>
</a-select-option>
</a-select>
<span v-if="accountError" class="required">{{ $t('label.required') }}</span>
</div>
</template>
<template v-else>
<div class="form__item">
<p class="form__label"><span class="required">*</span>{{ $t('label.project') }}</p>
<a-select
@change="changeProject"
v-model:value="selectedProject"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="project in projects" :key="project.id" :value="project.id" :label="project.name">
<span>
<resource-icon v-if="project && project.icon" :image="project.icon.base64image" size="1x" style="margin-right: 5px"/>
<project-outlined v-else style="margin-right: 5px" />
{{ project.name }}
</span>
</a-select-option>
</a-select>
<span v-if="projectError" class="required">{{ $t('label.required') }}</span>
</div>
</template>
<div class="submit-btn">
<a-button @click="closeAction">
{{ $t('label.cancel') }}
</a-button>
<a-button type="primary" @click="submitData" ref="submit">
{{ $t('label.submit') }}
</a-button>
</div>
</div>
</div>
</template>
<script>
import { api } from '@/api'
import ResourceIcon from '@/components/view/ResourceIcon'
import { isAdminOrDomainAdmin } from '@/role'
export default {
name: 'AssignInstance',
props: {
resource: {
type: Object,
required: true
}
},
components: {
ResourceIcon
},
inject: ['parentFetchData'],
data () {
return {
domains: [],
accounts: [],
projects: [],
selectedAccountType: 'Account',
selectedDomain: null,
selectedAccount: null,
selectedProject: null,
accountError: false,
projectError: false,
loading: false
}
},
created () {
if (isAdminOrDomainAdmin()) {
this.fetchData()
} else {
this.fetchProjects()
}
},
methods: {
isAdminOrDomainAdmin () {
return isAdminOrDomainAdmin()
},
fetchData () {
this.loading = true
api('listDomains', {
response: 'json',
listAll: true,
showicon: true,
details: 'min'
}).then(response => {
this.domains = response.listdomainsresponse.domain || []
this.selectedDomain = this.domains[0].id
this.fetchAccounts()
this.fetchProjects()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
},
fetchAccounts () {
this.loading = true
api('listAccounts', {
response: 'json',
domainId: this.selectedDomain,
showicon: true,
state: 'Enabled',
isrecursive: false
}).then(response => {
this.accounts = response.listaccountsresponse.account || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
},
fetchProjects () {
this.loading = true
api('listProjects', {
response: 'json',
domainId: this.selectedDomain,
state: 'Active',
showicon: true,
details: 'min',
isrecursive: false
}).then(response => {
this.projects = response.listprojectsresponse.project || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
},
changeDomain () {
this.selectedAccount = null
this.selectedProject = null
this.fetchAccounts()
this.fetchProjects()
},
changeAccount () {
this.selectedProject = null
},
changeProject () {
this.selectedAccount = null
},
closeAction () {
this.$emit('close-action')
},
submitData () {
if (this.loading) return
let variableKey = ''
let variableValue = ''
if (this.selectedAccountType === 'Account') {
if (!this.selectedAccount && isAdminOrDomainAdmin()) {
this.accountError = true
return
}
variableKey = 'account'
variableValue = this.selectedAccount
} else if (this.selectedAccountType === 'Project') {
if (!this.selectedProject) {
this.projectError = true
return
}
variableKey = 'projectid'
variableValue = this.selectedProject
}
this.loading = true
api('reserveIpAddress', {
response: 'json',
id: this.resource.id,
domainid: this.selectedDomain,
[variableKey]: variableValue
}).then(response => {
this.$notification.success({
message: this.$t('label.action.reserve.ip')
})
this.closeAction()
this.$emit('refresh-data')
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped lang="scss">
.form {
width: 85vw;
@media (min-width: 760px) {
width: 500px;
}
display: flex;
flex-direction: column;
&__item {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 10px;
}
&__label {
display: flex;
font-weight: bold;
margin-bottom: 5px;
}
}
.submit-btn {
margin-top: 10px;
align-self: flex-end;
button {
margin-left: 10px;
}
}
.required {
margin-right: 2px;
color: red;
font-size: 0.7rem;
}
.loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
}
</style>

View File

@ -12,6 +12,7 @@
"state.error": "Error",
"message.publicip.state.allocated": "Allocated",
"message.publicip.state.created": "Created",
"message.publicip.state.reserved": "Reserved",
"message.vmsnapshot.state.active": "Active",
"message.vm.state.active": "Active",
"message.volume.state.active": "Active",
@ -141,4 +142,4 @@
"component": {}
}
]
}
}