From a5a7be4c7e30edaf8db3f241ec03524755ba69d2 Mon Sep 17 00:00:00 2001 From: Nitin Mehta Date: Sat, 24 Mar 2012 13:59:59 +0530 Subject: [PATCH] Bug 10808: Add Upload Volume api and do validation. --- .../cloud/api/commands/UploadVolumeCmd.java | 145 ++++++++++++++++++ client/tomcatconf/commands.properties.in | 1 + .../com/cloud/storage/StorageManagerImpl.java | 113 ++++++++++++++ 3 files changed, 259 insertions(+) create mode 100755 api/src/com/cloud/api/commands/UploadVolumeCmd.java diff --git a/api/src/com/cloud/api/commands/UploadVolumeCmd.java b/api/src/com/cloud/api/commands/UploadVolumeCmd.java new file mode 100755 index 00000000000..672c91c54f8 --- /dev/null +++ b/api/src/com/cloud/api/commands/UploadVolumeCmd.java @@ -0,0 +1,145 @@ +package com.cloud.api.commands; + +import org.apache.log4j.Logger; + +import com.cloud.api.ApiConstants; +import com.cloud.api.BaseAsyncCreateCmd; +import com.cloud.api.IdentityMapper; +import com.cloud.api.Implementation; +import com.cloud.api.Parameter; +import com.cloud.api.ServerApiException; +import com.cloud.api.response.VolumeResponse; +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.UserContext; + +@Implementation(description="Uploads a data disk.", responseObject=VolumeResponse.class) +public class UploadVolumeCmd extends BaseAsyncCreateCmd { + public static final Logger s_logger = Logger.getLogger(UploadVolumeCmd.class.getName()); + private static final String s_name = "uploadvolumeresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name=ApiConstants.DISPLAY_TEXT, type=CommandType.STRING, required=true, description="the display text of the volume. This is usually used for display purposes.", length=4096) + private String displayText; + + @Parameter(name=ApiConstants.FORMAT, type=CommandType.STRING, required=true, description="the format for the volume. Possible values include QCOW2, RAW, and VHD.") + private String format; + + @Parameter(name=ApiConstants.HYPERVISOR, type=CommandType.STRING, required=true, description="the target hypervisor for the volume") + private String hypervisor; + + @Parameter(name=ApiConstants.NAME, type=CommandType.STRING, required=true, description="the name of the volume") + private String volumeName; + + @Parameter(name=ApiConstants.URL, type=CommandType.STRING, required=true, description="the URL of where the volume is hosted. Possible URL include http:// and https://") + private String url; + + @IdentityMapper(entityTableName="data_center") + @Parameter(name=ApiConstants.ZONE_ID, type=CommandType.LONG, required=true, description="the ID of the zone the volume is to be hosted on") + private Long zoneId; + + @IdentityMapper(entityTableName="domain") + @Parameter(name=ApiConstants.DOMAIN_ID, type=CommandType.LONG, description="an optional domainId. If the account parameter is used, domainId must also be used.") + private Long domainId; + + @Parameter(name=ApiConstants.ACCOUNT, type=CommandType.STRING, description="an optional accountName. Must be used with domainId.") + private String accountName; + + @Parameter(name=ApiConstants.CHECKSUM, type=CommandType.STRING, description="the MD5 checksum value of this volume") + private String checksum; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getDisplayText() { + return displayText; + } + + public String getFormat() { + return format; + } + + public String getHypervisor() { + return hypervisor; + } + + public String getVolumeName() { + return volumeName; + } + + public String getUrl() { + return url; + } + + public Long getZoneId() { + return zoneId; + } + + public Long getDomainId() { + return domainId; + } + + public String getAccountName() { + return accountName; + } + + public String getChecksum() { + return checksum; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void create() throws ResourceAllocationException { + + } + + @Override + public String getEntityTable() { + return "volumes"; + } + + @Override + public String getEventDescription() { + return "creating volume: " + getVolumeName(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VOLUME_CREATE; + } + + @Override + public void execute() throws ResourceUnavailableException, + InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + // TODO Auto-generated method stub + + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + Long accountId = finalyzeAccountId(accountName, domainId, null, true); + if (accountId == null) { + return UserContext.current().getCaller().getId(); + } + + return accountId; + } + +} diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in index 7d65341db99..a352ed87fe3 100755 --- a/client/tomcatconf/commands.properties.in +++ b/client/tomcatconf/commands.properties.in @@ -200,6 +200,7 @@ updateHostPassword=com.cloud.api.commands.UpdateHostPasswordCmd;1 #### volume commands attachVolume=com.cloud.api.commands.AttachVolumeCmd;15 +uploadVolume=com.cloud.api.commands.UploadVolumeCmd;15 detachVolume=com.cloud.api.commands.DetachVolumeCmd;15 createVolume=com.cloud.api.commands.CreateVolumeCmd;15 deleteVolume=com.cloud.api.commands.DeleteVolumeCmd;15 diff --git a/server/src/com/cloud/storage/StorageManagerImpl.java b/server/src/com/cloud/storage/StorageManagerImpl.java index de7dee23a64..db2971d1da9 100755 --- a/server/src/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/com/cloud/storage/StorageManagerImpl.java @@ -18,6 +18,8 @@ package com.cloud.storage; import java.math.BigDecimal; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; @@ -75,6 +77,7 @@ import com.cloud.api.commands.CreateVolumeCmd; import com.cloud.api.commands.DeletePoolCmd; import com.cloud.api.commands.ListVolumesCmd; import com.cloud.api.commands.UpdateStoragePoolCmd; +import com.cloud.api.commands.UploadVolumeCmd; import com.cloud.async.AsyncJobManager; import com.cloud.capacity.Capacity; import com.cloud.capacity.CapacityVO; @@ -1613,6 +1616,115 @@ public class StorageManagerImpl implements StorageManager, Manager, ClusterManag return _volsDao.findById(volume.getId()); } + + /* + * Just allocate a volume in the database, don't send the createvolume cmd to hypervisor. The volume will be finally + * created + * only when it's attached to a VM. + */ +// @Override + @DB + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", create = true) + public VolumeVO createVolumeEntry(UploadVolumeCmd cmd) throws ResourceAllocationException{ + Account caller = UserContext.current().getCaller(); + long ownerId = cmd.getEntityOwnerId(); + Long zoneId = cmd.getZoneId(); + String volumeName = cmd.getVolumeName(); + String url = cmd.getUrl(); + + validateVolume(caller, ownerId, zoneId, volumeName, url, cmd.getFormat()); + persistVolume(); + return null; + + } + + private boolean validateVolume(Account caller, long ownerId, Long zoneId, String volumeName, String url, String format) throws ResourceAllocationException{ + + // permission check + _accountMgr.checkAccess(caller, null, true, _accountMgr.getActiveAccountById(ownerId)); + + // Check that the resource limit for volumes won't be exceeded + _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(ownerId), ResourceType.volume); + + + // Verify that zone exists + DataCenterVO zone = _dcDao.findById(zoneId); + if (zone == null) { + throw new InvalidParameterValueException("Unable to find zone by id " + zoneId); + } + + // Check if zone is disabled + if (Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getType())) { + throw new PermissionDeniedException("Cannot perform this operation, Zone is currently disabled: " + zoneId); + } + + if (url.toLowerCase().contains("file://")) { + throw new InvalidParameterValueException("File:// type urls are currently unsupported"); + } + + String userSpecifiedName = volumeName; + if (userSpecifiedName == null) { + userSpecifiedName = getRandomVolumeName(); + } + if((!url.toLowerCase().endsWith("vhd"))&&(!url.toLowerCase().endsWith("vhd.zip")) + &&(!url.toLowerCase().endsWith("vhd.bz2"))&&(!url.toLowerCase().endsWith("vhd.gz")) + &&(!url.toLowerCase().endsWith("qcow2"))&&(!url.toLowerCase().endsWith("qcow2.zip")) + &&(!url.toLowerCase().endsWith("qcow2.bz2"))&&(!url.toLowerCase().endsWith("qcow2.gz")) + &&(!url.toLowerCase().endsWith("ova"))&&(!url.toLowerCase().endsWith("ova.zip")) + &&(!url.toLowerCase().endsWith("ova.bz2"))&&(!url.toLowerCase().endsWith("ova.gz")) + &&(!url.toLowerCase().endsWith("img"))&&(!url.toLowerCase().endsWith("raw"))){ + throw new InvalidParameterValueException("Please specify a valid " + format.toLowerCase()); + } + + if ((format.equalsIgnoreCase("vhd") && (!url.toLowerCase().endsWith("vhd") && !url.toLowerCase().endsWith("vhd.zip") && !url.toLowerCase().endsWith("vhd.bz2") && !url.toLowerCase().endsWith("vhd.gz") )) + || (format.equalsIgnoreCase("qcow2") && (!url.toLowerCase().endsWith("qcow2") && !url.toLowerCase().endsWith("qcow2.zip") && !url.toLowerCase().endsWith("qcow2.bz2") && !url.toLowerCase().endsWith("qcow2.gz") )) + || (format.equalsIgnoreCase("ova") && (!url.toLowerCase().endsWith("ova") && !url.toLowerCase().endsWith("ova.zip") && !url.toLowerCase().endsWith("ova.bz2") && !url.toLowerCase().endsWith("ova.gz"))) + || (format.equalsIgnoreCase("raw") && (!url.toLowerCase().endsWith("img") && !url.toLowerCase().endsWith("raw")))) { + throw new InvalidParameterValueException("Please specify a valid URL. URL:" + url + " is an invalid for the format " + format.toLowerCase()); + } + validateUrl(url); + + return false; + } + + private String validateUrl(String url){ + try { + URI uri = new URI(url); + if ((uri.getScheme() == null) || (!uri.getScheme().equalsIgnoreCase("http") + && !uri.getScheme().equalsIgnoreCase("https") && !uri.getScheme().equalsIgnoreCase("file"))) { + throw new IllegalArgumentException("Unsupported scheme for url: " + url); + } + + int port = uri.getPort(); + if (!(port == 80 || port == 443 || port == -1)) { + throw new IllegalArgumentException("Only ports 80 and 443 are allowed"); + } + String host = uri.getHost(); + try { + InetAddress hostAddr = InetAddress.getByName(host); + if (hostAddr.isAnyLocalAddress() || hostAddr.isLinkLocalAddress() || hostAddr.isLoopbackAddress() || hostAddr.isMulticastAddress()) { + throw new IllegalArgumentException("Illegal host specified in url"); + } + if (hostAddr instanceof Inet6Address) { + throw new IllegalArgumentException("IPV6 addresses not supported (" + hostAddr.getHostAddress() + ")"); + } + } catch (UnknownHostException uhe) { + throw new IllegalArgumentException("Unable to resolve " + host); + } + + return uri.toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URL " + url); + } + + } + + private boolean persistVolume() { + + return false; + } + + /* * Just allocate a volume in the database, don't send the createvolume cmd to hypervisor. The volume will be finally * created @@ -1714,6 +1826,7 @@ public class StorageManagerImpl implements StorageManager, Manager, ClusterManag * throw new UnsupportedServiceException("operation not supported, snapshot with id " + snapshotId + * " is created from ROOT volume"); * } + * */ }