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 // Network Events
public static final String EVENT_NET_IP_ASSIGN = "NET.IPASSIGN"; 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_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_NET_IP_UPDATE = "NET.IPUPDATE";
public static final String EVENT_PORTABLE_IP_ASSIGN = "PORTABLE.IPASSIGN"; public static final String EVENT_PORTABLE_IP_ASSIGN = "PORTABLE.IPASSIGN";
public static final String EVENT_PORTABLE_IP_RELEASE = "PORTABLE.IPRELEASE"; 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. 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. Allocated, // The IP address is in used.
Releasing, // The IP address is being released for other network elements and is not ready for allocation. 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. 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, IpAddress allocateIP(Account ipOwner, long zoneId, Long networkId, Boolean displayIp, String ipaddress) throws ResourceAllocationException, InsufficientAddressCapacityException,
ConcurrentOperationException; ConcurrentOperationException;
IpAddress reserveIpAddress(Account account, Boolean displayIp, Long ipAddressId) throws ResourceAllocationException;
boolean releaseReservedIpAddress(long ipAddressId) throws InsufficientAddressCapacityException;
boolean releaseIpAddress(long ipAddressId) throws InsufficientAddressCapacityException; boolean releaseIpAddress(long ipAddressId) throws InsufficientAddressCapacityException;
IpAddress allocatePortableIP(Account ipOwner, int regionId, Long zoneId, Long networkId, Long vpcId) throws ResourceAllocationException, 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); List<IPAddressVO> listByAssociatedVmId(long vmId);
IPAddressVO findByVmIdAndNetworkId(long networkId, 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 @Inject
protected VlanDao _vlanDao; protected VlanDao _vlanDao;
protected GenericSearchBuilder<IPAddressVO, Long> CountFreePublicIps; protected GenericSearchBuilder<IPAddressVO, Long> CountFreePublicIps;
protected SearchBuilder<IPAddressVO> PublicIpSearchByAccountAndState;
@Inject @Inject
ResourceTagDao _tagsDao; ResourceTagDao _tagsDao;
@Inject @Inject
@ -138,6 +139,7 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
AllocatedIpCountForAccount.and("allocated", AllocatedIpCountForAccount.entity().getAllocatedTime(), Op.NNULL); AllocatedIpCountForAccount.and("allocated", AllocatedIpCountForAccount.entity().getAllocatedTime(), Op.NNULL);
AllocatedIpCountForAccount.and().op("network", AllocatedIpCountForAccount.entity().getAssociatedWithNetworkId(), Op.NNULL); AllocatedIpCountForAccount.and().op("network", AllocatedIpCountForAccount.entity().getAssociatedWithNetworkId(), Op.NNULL);
AllocatedIpCountForAccount.or("vpc", AllocatedIpCountForAccount.entity().getVpcId(), Op.NNULL); AllocatedIpCountForAccount.or("vpc", AllocatedIpCountForAccount.entity().getVpcId(), Op.NNULL);
AllocatedIpCountForAccount.or("state", AllocatedIpCountForAccount.entity().getState(), Op.EQ);
AllocatedIpCountForAccount.cp();AllocatedIpCountForAccount.done(); AllocatedIpCountForAccount.cp();AllocatedIpCountForAccount.done();
CountFreePublicIps = createSearchBuilder(Long.class); CountFreePublicIps = createSearchBuilder(Long.class);
@ -152,6 +154,13 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
DeleteAllExceptGivenIp = createSearchBuilder(); DeleteAllExceptGivenIp = createSearchBuilder();
DeleteAllExceptGivenIp.and("vlanDbId", DeleteAllExceptGivenIp.entity().getVlanId(), Op.EQ); DeleteAllExceptGivenIp.and("vlanDbId", DeleteAllExceptGivenIp.entity().getVlanId(), Op.EQ);
DeleteAllExceptGivenIp.and("ip", DeleteAllExceptGivenIp.entity().getAddress(), Op.NEQ); 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 @Override
@ -368,6 +377,7 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
public long countAllocatedIPsForAccount(long accountId) { public long countAllocatedIPsForAccount(long accountId) {
SearchCriteria<Long> sc = AllocatedIpCountForAccount.create(); SearchCriteria<Long> sc = AllocatedIpCountForAccount.create();
sc.setParameters("account", accountId); sc.setParameters("account", accountId);
sc.setParameters("state", State.Reserved);
return customSearch(sc, null).get(0); return customSearch(sc, null).get(0);
} }
@ -480,4 +490,14 @@ public class IPAddressDaoImpl extends GenericDaoBase<IPAddressVO, Long> implemen
sc.setParameters("state", state); sc.setParameters("state", state);
return listBy(sc); 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"); 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>() { ip = Transaction.execute(new TransactionCallbackWithException<PublicIp, InsufficientAddressCapacityException>() {
@Override @Override
public PublicIp doInTransaction(TransactionStatus status) throws InsufficientAddressCapacityException { public PublicIp doInTransaction(TransactionStatus status) throws InsufficientAddressCapacityException {
@ -1401,7 +1408,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
} }
if (ipToAssoc.getAssociatedWithNetworkId() != null) { 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; return ipToAssoc;
} }
@ -1459,6 +1466,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
IPAddressVO ip = _ipAddressDao.findById(ipId); IPAddressVO ip = _ipAddressDao.findById(ipId);
//update ip address with networkId //update ip address with networkId
ip.setState(State.Allocated);
ip.setAssociatedWithNetworkId(networkId); ip.setAssociatedWithNetworkId(networkId);
ip.setSourceNat(isSourceNat); ip.setSourceNat(isSourceNat);
_ipAddressDao.update(ipId, ip); _ipAddressDao.update(ipId, ip);

View File

@ -28,6 +28,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; 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.MessageBus;
import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.framework.messagebus.PublishScope;
import org.apache.cloudstack.network.element.InternalLoadBalancerElementService; import org.apache.cloudstack.network.element.InternalLoadBalancerElementService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@ -68,14 +70,18 @@ import com.cloud.api.ApiDBUtils;
import com.cloud.configuration.Config; import com.cloud.configuration.Config;
import com.cloud.configuration.ConfigurationManager; import com.cloud.configuration.ConfigurationManager;
import com.cloud.configuration.Resource; import com.cloud.configuration.Resource;
import com.cloud.dc.AccountVlanMapVO;
import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType; import com.cloud.dc.DataCenter.NetworkType;
import com.cloud.dc.DataCenterVO; import com.cloud.dc.DataCenterVO;
import com.cloud.dc.DataCenterVnetVO; import com.cloud.dc.DataCenterVnetVO;
import com.cloud.dc.DomainVlanMapVO;
import com.cloud.dc.Vlan.VlanType; import com.cloud.dc.Vlan.VlanType;
import com.cloud.dc.VlanVO; import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.AccountVlanMapDao;
import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.DataCenterVnetDao; import com.cloud.dc.dao.DataCenterVnetDao;
import com.cloud.dc.dao.DomainVlanMapDao;
import com.cloud.dc.dao.VlanDao; import com.cloud.dc.dao.VlanDao;
import com.cloud.deploy.DeployDestination; import com.cloud.deploy.DeployDestination;
import com.cloud.domain.Domain; import com.cloud.domain.Domain;
@ -84,6 +90,7 @@ import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent; import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes; import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils; import com.cloud.event.UsageEventUtils;
import com.cloud.exception.AccountLimitException;
import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InsufficientCapacityException;
@ -303,6 +310,10 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
@Inject @Inject
AccountGuestVlanMapDao _accountGuestVlanMapDao; AccountGuestVlanMapDao _accountGuestVlanMapDao;
@Inject @Inject
AccountVlanMapDao _accountVlanMapDao;
@Inject
DomainVlanMapDao _domainVlanMapDao;
@Inject
VpcDao _vpcDao; VpcDao _vpcDao;
@Inject @Inject
NetworkACLDao _networkACLDao; NetworkACLDao _networkACLDao;
@ -915,6 +926,98 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
return nicSecIp; 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 @Override
@ActionEvent(eventType = EventTypes.EVENT_NET_IP_RELEASE, eventDescription = "disassociating Ip", async = true) @ActionEvent(eventType = EventTypes.EVENT_NET_IP_RELEASE, eventDescription = "disassociating Ip", async = true)
public boolean releaseIpAddress(long ipAddressId) throws InsufficientAddressCapacityException { 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"); 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); boolean success = _ipAddrMgr.disassociatePublicIpAddress(ipAddressId, userId, caller);
if (success) { 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.AssociateIPAddrCmd;
import org.apache.cloudstack.api.command.user.address.DisassociateIPAddrCmd; 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.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.address.UpdateIPAddrCmd;
import org.apache.cloudstack.api.command.user.affinitygroup.CreateAffinityGroupCmd; 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.DeleteAffinityGroupCmd;
@ -2147,13 +2149,13 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
final String state = cmd.getState(); final String state = cmd.getState();
Boolean isAllocated = cmd.isAllocatedOnly(); Boolean isAllocated = cmd.isAllocatedOnly();
if (isAllocated == null) { 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; isAllocated = Boolean.FALSE;
} else { } else {
isAllocated = Boolean.TRUE; // default isAllocated = Boolean.TRUE; // default
} }
} else { } 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) { if (isAllocated) {
throw new InvalidParameterValueException("Conflict: allocatedonly is true but state is Free"); 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); buildParameters(sb, cmd, vlanType == VlanType.VirtualNetwork ? true : isAllocated);
SearchCriteria<IPAddressVO> sc = sb.create(); 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))) { if (isAllocated || (vlanType == VlanType.VirtualNetwork && (caller.getType() != Account.Type.ADMIN || cmd.getDomainId() != null))) {
_accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); _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); sb2.and("ids", sb2.entity().getId(), SearchCriteria.Op.IN);
SearchCriteria<IPAddressVO> sc2 = sb2.create(); SearchCriteria<IPAddressVO> sc2 = sb2.create();
setParameters(sc2, cmd, vlanType); setParameters(sc2, cmd, vlanType, isAllocated);
sc2.setParameters("ids", freeAddrIds.toArray()); sc2.setParameters("ids", freeAddrIds.toArray());
addrs.addAll(_publicIpAddressDao.search(sc2, searchFilter)); // Allocated + Free 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 Object keyword = cmd.getKeyword();
final Long physicalNetworkId = cmd.getPhysicalNetworkId(); final Long physicalNetworkId = cmd.getPhysicalNetworkId();
final Long sourceNetworkId = cmd.getNetworkId(); final Long sourceNetworkId = cmd.getNetworkId();
@ -2437,6 +2439,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
if (state != null) { if (state != null) {
sc.setParameters("state", state); sc.setParameters("state", state);
} else if (isAllocated != null && isAllocated) {
sc.setParameters("state", IpAddress.State.Allocated);
} }
sc.setParameters( "forsystemvms", false); sc.setParameters( "forsystemvms", false);
@ -3199,6 +3203,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(ListProjectAccountsCmd.class); cmdList.add(ListProjectAccountsCmd.class);
cmdList.add(AssociateIPAddrCmd.class); cmdList.add(AssociateIPAddrCmd.class);
cmdList.add(DisassociateIPAddrCmd.class); cmdList.add(DisassociateIPAddrCmd.class);
cmdList.add(ReserveIPAddrCmd.class);
cmdList.add(ReleaseIPAddrCmd.class);
cmdList.add(ListPublicIpAddressesCmd.class); cmdList.add(ListPublicIpAddressesCmd.class);
cmdList.add(CreateAutoScalePolicyCmd.class); cmdList.add(CreateAutoScalePolicyCmd.class);
cmdList.add(CreateAutoScaleVmGroupCmd.class); cmdList.add(CreateAutoScaleVmGroupCmd.class);

View File

@ -166,6 +166,16 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches
return null; 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 @Override
public IpAddress allocatePortableIP(Account ipOwner, int regionId, Long zoneId, Long networkId, Long vpcId) throws ResourceAllocationException, public IpAddress allocatePortableIP(Account ipOwner, int regionId, Long zoneId, Long networkId, Long vpcId) throws ResourceAllocationException,
InsufficientAddressCapacityException, ConcurrentOperationException { InsufficientAddressCapacityException, ConcurrentOperationException {

View File

@ -248,6 +248,7 @@
"label.action.remove.host": "Remove Host", "label.action.remove.host": "Remove Host",
"label.action.remove.host.processing": "Removing Host....", "label.action.remove.host.processing": "Removing Host....",
"label.action.remove.vm": "Release VM", "label.action.remove.vm": "Release VM",
"label.action.reserve.ip": "Reserve Public IP",
"label.action.reset.password": "Reset Password", "label.action.reset.password": "Reset Password",
"label.action.reset.password.processing": "Resetting Password....", "label.action.reset.password.processing": "Resetting Password....",
"label.action.resize.volume": "Resize Volume", "label.action.resize.volume": "Resize Volume",
@ -975,6 +976,7 @@
"label.forceencap": "Force UDP Encapsulation of ESP Packets", "label.forceencap": "Force UDP Encapsulation of ESP Packets",
"label.forgedtransmits": "Forged Transmits", "label.forgedtransmits": "Forged Transmits",
"label.format": "Format", "label.format": "Format",
"label.free": "Free",
"label.french.azerty.keyboard": "French AZERTY keyboard", "label.french.azerty.keyboard": "French AZERTY keyboard",
"label.friday": "Friday", "label.friday": "Friday",
"label.from": "from", "label.from": "from",
@ -1874,6 +1876,7 @@
"label.required": "Required", "label.required": "Required",
"label.requireshvm": "HVM", "label.requireshvm": "HVM",
"label.requiresupgrade": "Requires Upgrade", "label.requiresupgrade": "Requires Upgrade",
"label.reserved": "Reserved",
"label.reserved.system.gateway": "Reserved system gateway", "label.reserved.system.gateway": "Reserved system gateway",
"label.reserved.system.ip": "Reserved System IP", "label.reserved.system.ip": "Reserved System IP",
"label.reserved.system.netmask": "Reserved system netmask", "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.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.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.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.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.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.", "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.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.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.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.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.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.", "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()) { switch (state.toLowerCase()) {
case 'scheduled': case 'scheduled':
return 'blue' return 'blue'
case 'reserved':
return 'orange'
default: default:
return null return null
} }

View File

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

View File

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

View File

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

View File

@ -113,6 +113,11 @@ export default {
this.loading = false this.loading = false
}, },
async filterTabs () { 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 // VPC IPs with source nat have only VPN
if (this.resource && this.resource.vpcid && this.resource.issourcenat) { if (this.resource && this.resource.vpcid && this.resource.issourcenat) {
this.tabs = this.defaultTabs.concat(this.$route.meta.tabs.filter(tab => tab.name === 'vpn')) 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", "state.error": "Error",
"message.publicip.state.allocated": "Allocated", "message.publicip.state.allocated": "Allocated",
"message.publicip.state.created": "Created", "message.publicip.state.created": "Created",
"message.publicip.state.reserved": "Reserved",
"message.vmsnapshot.state.active": "Active", "message.vmsnapshot.state.active": "Active",
"message.vm.state.active": "Active", "message.vm.state.active": "Active",
"message.volume.state.active": "Active", "message.volume.state.active": "Active",