/*************************************************************************
* Copyright 2009-2012 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
*
* This file may incorporate work covered under the following copyright
* and permission notice:
*
* Software License Agreement (BSD License)
*
* Copyright (c) 2008, Regents of the University of California
* All rights reserved.
*
* Redistribution and use of this software in source and binary forms,
* with or without modification, are permitted provided that the
* following conditions are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE
* THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL,
* COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE,
* AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING
* IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA,
* SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY,
* WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION,
* REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO
* IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT
* NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS.
************************************************************************/
package com.eucalyptus.blockstorage.entities;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.PersistenceContext;
import javax.persistence.Table;
import org.apache.log4j.Logger;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import com.eucalyptus.blockstorage.util.BlockStorageUtil;
import com.eucalyptus.blockstorage.util.StorageProperties;
import com.eucalyptus.entities.AbstractPersistent;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.util.EucalyptusCloudException;
import com.google.common.base.Function;
@Entity
@PersistenceContext(name = "eucalyptus_storage")
@Table(name = "Volumes")
public class VolumeInfo extends AbstractPersistent {
private static final Logger LOG = Logger.getLogger(VolumeInfo.class);
@Column(name = "volume_user_name")
private String userName;
@Column(name = "volume_name", unique = true)
private String volumeId;
@Column(name = "sc_name")
private String scName;
@Column(name = "size")
private Integer size; // in GB
@Column(name = "status")
private String status;
@Column(name = "create_time")
private Date createTime;
@Column(name = "zone")
private String zone;
@Column(name = "snapshot_id")
private String snapshotId;
@Column(name = "deletion_time")
private Date deletionTime;
@NotFound(action = NotFoundAction.IGNORE)
@OneToMany(fetch = FetchType.LAZY, mappedBy = "volume", orphanRemoval = true, cascade = CascadeType.ALL)
private List<VolumeToken> attachmentTokens;
public VolumeInfo() {
this.scName = StorageProperties.NAME;
}
public VolumeInfo(String volumeId) {
this();
this.volumeId = volumeId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getVolumeId() {
return volumeId;
}
public void setVolumeId(String volumeId) {
this.volumeId = volumeId;
}
public String getScName() {
return scName;
}
public void setScName(String scName) {
this.scName = scName;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public String getZone() {
return zone;
}
public void setZone(String zone) {
this.zone = zone;
}
public String getSnapshotId() {
return snapshotId;
}
public void setSnapshotId(String snapshotId) {
this.snapshotId = snapshotId;
}
public Date getDeletionTime() {
return deletionTime;
}
public void setDeletionTime(Date deletionTime) {
this.deletionTime = deletionTime;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
VolumeInfo other = (VolumeInfo) obj;
if (scName == null) {
if (other.scName != null)
return false;
} else if (!scName.equals(other.scName))
return false;
if (volumeId == null) {
if (other.volumeId != null)
return false;
} else if (!volumeId.equals(other.volumeId))
return false;
return true;
}
@Override
public int hashCode() {
return scName.hashCode() + volumeId.hashCode();
}
public List<VolumeToken> getAttachmentTokens() {
return attachmentTokens;
}
public void setAttachmentTokens(List<VolumeToken> attachmentTokens) {
this.attachmentTokens = attachmentTokens;
}
private static String decryptToken(String tokenCipher) throws EucalyptusCloudException {
// Decrypt the token with the Cloud's private key.
try {
return BlockStorageUtil.decryptWithCloud(tokenCipher);
} catch (EucalyptusCloudException e) {
LOG.error("Failed to decrypt token: " + tokenCipher);
throw e;
}
}
/**
* Returns the last 4 chars prefixed by '*' to meet the same length as rawToken. For printing the token in the logs. Just shows '**********ad4e'
*
* @param rawToken
* @return
*/
private static String redactToken(String rawToken) {
String tokenSuffix = new String(rawToken).substring(rawToken.length() - 4);
StringBuilder redact = new StringBuilder();
for (int i = 0; i < rawToken.length() - 4; i++) {
redact.append('*');
}
redact.append(tokenSuffix);
return redact.toString();
}
/**
* Returns either the single valid attachment token for the volume, or null if none exists. Throws an exception if multiple valid tokens are found
*
* @return
* @throws EucalyptusCloudException
*/
public VolumeToken getAttachmentTokenIfValid(final String encryptedTokenValue) throws EucalyptusCloudException {
try {
String decryptedToken = decryptToken(encryptedTokenValue);
VolumeToken tok = getCurrentValidToken();
if (tok == null) {
LOG.warn("Got request for attachment token, no valid token found for volume " + this.getVolumeId());
throw new EucalyptusCloudException("No valid token found");
} else {
if (!decryptedToken.equals(tok.getToken())) {
LOG.error("Token requested is not valid token for volume " + this.getVolumeId() + " request token: " + redactToken(decryptedToken)
+ " current token: " + redactToken(tok.getToken()));
throw new EucalyptusCloudException("Requested token is not the current valid token");
} else {
return tok;
}
}
} catch (Exception e) {
LOG.error("Error checking for valid attachment token", e);
throw new EucalyptusCloudException(e);
}
}
/**
* Return the valid token if it exists or null if not.
*
* @return
*/
public VolumeToken getCurrentValidToken() throws EucalyptusCloudException {
Function<VolumeInfo, VolumeToken> getValidToken = new Function<VolumeInfo, VolumeToken>() {
@Override
public VolumeToken apply(VolumeInfo src) {
try {
VolumeInfo volEntity = Entities.merge(src);
for (VolumeToken tok : volEntity.getAttachmentTokens()) {
if (tok.getIsValid()) {
return tok;
}
}
} catch (Exception e) {
LOG.trace("Failed while looking up valid token found for volume " + src.getVolumeId(), e);
}
return null;
}
};
return Entities.asTransaction(VolumeInfo.class, getValidToken).apply(this);
}
/**
* Get an attachment token, if valid one exists return it, otherwise create a new one. This should only be used in very specific cases. Normally,
* the semantics are to either explicitly create a new token or check an existing one.
*
* @param ip
* @param iqn
* @return
* @throws EucalyptusCloudException
*/
public synchronized VolumeToken getOrCreateAttachmentToken() throws EucalyptusCloudException {
VolumeToken token = null;
Function<VolumeInfo, VolumeToken> checkAddToken = new Function<VolumeInfo, VolumeToken>() {
@Override
public VolumeToken apply(VolumeInfo src) {
src = Entities.merge(src);
try {
if (src.getCurrentValidToken() != null) {
return src.getCurrentValidToken();
} else {
VolumeToken tok = VolumeToken.generate(src);
Entities.flush(tok);
src.getAttachmentTokens().add(tok);
return tok;
}
} catch (Exception e) {
LOG.error("Error creating new volume token for " + volumeId);
}
return null;
}
};
token = Entities.asTransaction(VolumeInfo.class, checkAddToken).apply(this);
return token;
}
/**
* Invalidates the export if it exists on the current valid token and invalidates the token if there are no remaining exports on the token.
*
* Throws an exception if no valid token exists, if the export is already invalid, or invalidation fails.
*
* @param tokenValue - the token string to use to lookup an active token
* @param nodeIp - the node IP to use for lookup of export record
* @param nodeIqn - the iqn for the lookup of export record
*/
public void invalidateExport(final String tokenValue, final String nodeIp, final String nodeIqn) throws EucalyptusCloudException {
Function<VolumeInfo, VolumeToken> invalidateToken = new Function<VolumeInfo, VolumeToken>() {
@Override
public VolumeToken apply(VolumeInfo src) {
try {
VolumeInfo volEntity = Entities.merge(src);
VolumeToken token = volEntity.getAttachmentTokenIfValid(tokenValue);
token.invalidateExport(nodeIp, nodeIqn);
Entities.flush(token);
return token;
} catch (Exception e) {
LOG.error("Could not invalidate requested export with token " + tokenValue + " to " + nodeIp + " with iqn " + nodeIqn);
}
return null;
}
};
if (Entities.asTransaction(VolumeInfo.class, invalidateToken).apply(this) == null) {
throw new EucalyptusCloudException("Failed on invalidation of token");
}
}
public boolean cleanupOnDeletion() {
if (deletionTime != null) {
if (System.currentTimeMillis() > (deletionTime.getTime() + TimeUnit.MILLISECONDS.convert(
StorageInfo.getStorageInfo().getVolExpiration(), TimeUnit.HOURS))) {
return true;
}
}
return false;
}
}