mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
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:
parent
15e3a10f94
commit
6a53517d37
@ -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";
|
||||
|
||||
@ -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.
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -156,6 +156,8 @@ export default {
|
||||
switch (state.toLowerCase()) {
|
||||
case 'scheduled':
|
||||
return 'blue'
|
||||
case 'reserved':
|
||||
return 'orange'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@ -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 } }) }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -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'))
|
||||
|
||||
332
ui/src/views/network/ReservePublicIP.vue
Normal file
332
ui/src/views/network/ReservePublicIP.vue
Normal 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>
|
||||
@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user