package io.eguan.vold.model;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* Licensed 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.
* #L%
*/
import io.eguan.iscsisrv.IscsiDevice;
import io.eguan.iscsisrv.IscsiServer;
import io.eguan.iscsisrv.IscsiTarget;
import io.eguan.nbdsrv.NbdDevice;
import io.eguan.nbdsrv.NbdExport;
import io.eguan.nbdsrv.NbdServer;
import io.eguan.vvr.repository.core.api.Device;
import io.eguan.vvr.repository.core.api.FutureDevice;
import io.eguan.vvr.repository.core.api.FutureSnapshot;
import io.eguan.vvr.repository.core.api.FutureVoid;
import io.eguan.vvr.repository.core.api.Device.ReadWriteHandle;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import org.slf4j.Logger;
/**
* The class {@link Device} encapsulates a {@link VvrDevice} and is exported as a {@link DeviceMXBean}.
*
* @author oodrive
* @author llambert
* @author ebredzinski
* @author pwehrle
*/
final class VvrDevice implements DeviceMXBean {
private static final Logger LOGGER = Constants.LOGGER;
private static final class ProtocolDeviceImpl implements IscsiDevice, NbdDevice {
/** Read & write handle, require for reading and writing to/from a VVR device object */
private final ReadWriteHandle rwHandle;
/** <code>true</code> when the device is opened read-only */
private final boolean ro;
/** user-defined ISCSI block size */
private final int blockSize;
protected ProtocolDeviceImpl(final ReadWriteHandle rwHandle, final boolean ro, final int blockSize) {
super();
this.rwHandle = rwHandle;
this.ro = ro;
this.blockSize = blockSize;
}
@Override
public final boolean isReadOnly() {
return ro;
}
@Override
public final long getSize() {
return rwHandle.getSize();
}
@Override
public final int getBlockSize() {
if (blockSize > 0) {
return blockSize;
}
else {
return rwHandle.getBlockSize();
}
}
@Override
public final void read(final ByteBuffer bytes, final int length, final long storageIndex) throws IOException {
final int pos = bytes.position();
rwHandle.read(bytes, pos, length, storageIndex);
}
@Override
public final void write(final ByteBuffer bytes, final int length, final long storageIndex) throws IOException {
final int pos = bytes.position();
rwHandle.write(bytes, pos, length, storageIndex);
}
@Override
public final void trim(final long length, final long storageIndex) {
rwHandle.trim(length, storageIndex);
}
@Override
public final void close() throws IOException {
rwHandle.close();
}
}
/** Property name for saving the IQN of device in its own custom properties */
private final static String IQN_PROP_NAME = "iscsi_iqn";
/** Property name for saving the iSCSI alias name of device in its own custom properties */
private final static String IQN_ALIAS_PROP_NAME = "iscsi_alias";
/** Property name for saving the block size of device in its own custom properties */
private final static String BLOCK_SIZE_PROP_NAME = "iscsi_block_size";
/** Property name for saving the state 'active' of device in its own custom properties */
private final static String ACTIVE_PROP_NAME = "active";
/** Value set when the device is activated read-only */
private final static String ACTIVE_PROP_VALUE_RO = "ro";
/** Value set when the device is activated read-write */
private final static String ACTIVE_PROP_VALUE_RW = "rw";
/** Associated VVR device */
private final Device deviceInstance;
/** Associated iSCSI server */
private final IscsiServer iscsiServer;
/** Associated NBD server */
private final NbdServer nbdServer;
/** Current node, needed for management of activation */
private final UUID node;
/** Property for local activation handling, needed for management of activation */
private final String ACTIVE_PROP_NAME_NODE;
/** Lock guarding activation of the device */
private final ReadWriteLock activationLock = new ReentrantReadWriteLock();
/** Current local opened target handle */
@GuardedBy(value = "activationLock")
private ProtocolDeviceImpl protocolDeviceImpl;
/** Name of the NBD export. The device may be renamed after export. */
@GuardedBy(value = "activationLock")
private String nbdExportName;
private VvrDevice(final Device deviceInstance, final IscsiServer iscsiServer, final NbdServer nbdServer,
final UUID node) {
super();
this.deviceInstance = deviceInstance;
this.iscsiServer = iscsiServer;
this.nbdServer = nbdServer;
this.node = node;
this.ACTIVE_PROP_NAME_NODE = ACTIVE_PROP_NAME + this.node;
}
/**
* Create a new device. Initialize VOLD-defined values.
*
* @param deviceInstance
*/
final static void createVvrDevice(final Device deviceInstance) {
// Init device
final VvrDevice device = new VvrDevice(deviceInstance, null, null, null);
// Compute and store default IQN and alias
device.setIqn(getDefaultIqn(device));
device.setIscsiAlias(getDefaultAlias(device));
}
/**
* Load an existing device.
*
* @param deviceInstance
* @return new device instance
*/
final static VvrDevice loadVvrDevice(final Device deviceInstance, @Nonnull final IscsiServer iscsiServer,
@Nonnull final NbdServer nbdServer, @Nonnull final UUID node) {
// Init device
final VvrDevice result = new VvrDevice(deviceInstance, Objects.requireNonNull(iscsiServer),
Objects.requireNonNull(nbdServer), Objects.requireNonNull(node));
// Activate device?
result.handleStateOnLoad();
return result;
}
/**
* Compute the default iSCSI iqn for the given device.
*
* @param vvrDevice
* @return default iqn.
*/
private static final String getDefaultIqn(final VvrDevice vvrDevice) {
String name = vvrDevice.getName();
if (name == null || name.equals("")) {
name = vvrDevice.getUuid();
}
return Constants.IQN_PREFIX + name;
}
/**
* Compute the default iSCSI alias for the given device.
*
* @param vvrDevice
* @return default iSCSI alias.
*/
private static final String getDefaultAlias(final VvrDevice vvrDevice) {
return getDefaultIqn(vvrDevice);
}
/**
* Take care of activating a device if the according state was persisted.
*/
private final void handleStateOnLoad() {
activationLock.writeLock().lock();
try {
// Could be ro, rw or null
final String activeState = deviceInstance.getUserProperty(ACTIVE_PROP_NAME_NODE);
if (activeState != null) {
if (activeState.equals(ACTIVE_PROP_VALUE_RO)) {
doActivate(false);
}
else if (activeState.equals(ACTIVE_PROP_VALUE_RW)) {
doActivate(true);
}
}
}
finally {
activationLock.writeLock().unlock();
}
}
@Override
public final String getName() {
return deviceInstance.getName();
}
@Override
public final void setName(final String name) {
final FutureVoid futureTask = deviceInstance.setName(name);
if (futureTask == null) {
return;
}
try {
futureTask.get();
}
catch (InterruptedException | ExecutionException e) {
// propagate failure
throw new IllegalStateException(e);
}
}
@Override
public final String getDescription() {
return deviceInstance.getDescription();
}
@Override
public final void setDescription(final String description) {
final FutureVoid futureTask = deviceInstance.setDescription(description);
if (futureTask == null) {
return;
}
try {
futureTask.get();
}
catch (InterruptedException | ExecutionException e) {
// propagate failure
throw new IllegalStateException(e);
}
}
/*
* Should be getUuidString(), but, in this case, the JMX attribute would be 'uuidString'
*
* @see io.eguan.vold.model.DeviceMXBean#getUuid()
*/
@Override
public final String getUuid() {
return getUuidUuid().toString();
}
final UUID getUuidUuid() {
return deviceInstance.getUuid();
}
@Override
public final String getIqn() {
return deviceInstance.getUserProperty(IQN_PROP_NAME);
}
@Override
public final void setIqn(final String iqn) throws IllegalStateException {
final FutureVoid futureVoid = doSetIqnNoWait(iqn);
try {
futureVoid.get();
}
catch (InterruptedException | ExecutionException e) {
// Failed to edit the device
throw new IllegalStateException(e);
}
}
@Override
public final String setIqnNoWait(final String iqn) throws IllegalStateException {
final FutureVoid futureVoid = doSetIqnNoWait(iqn);
return futureVoid.getTaskId().toString();
}
private final FutureVoid doSetIqnNoWait(final String iqn) throws IllegalStateException {
activationLock.readLock().lock();
try {
if (isActive()) {
throw new IllegalStateException("IQN change while device is active is not allowed");
}
return setDeviceProperties(IQN_PROP_NAME, iqn);
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String getIscsiAlias() {
return deviceInstance.getUserProperty(IQN_ALIAS_PROP_NAME);
}
@Override
public final void setIscsiAlias(final String alias) throws IllegalStateException {
final FutureVoid futureVoid = doSetIscsiAliasNoWait(alias);
try {
futureVoid.get();
}
catch (InterruptedException | ExecutionException e) {
// Failed to edit the device
throw new IllegalStateException(e);
}
}
@Override
public final String setIscsiAliasNoWait(final String alias) throws IllegalStateException {
final FutureVoid futureVoid = doSetIscsiAliasNoWait(alias);
return futureVoid.getTaskId().toString();
}
private final FutureVoid doSetIscsiAliasNoWait(final String alias) throws IllegalStateException {
activationLock.readLock().lock();
try {
if (isActive()) {
throw new IllegalStateException("iSCSI alias change while device is active is not allowed");
}
return setDeviceProperties(IQN_ALIAS_PROP_NAME, alias);
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final int getIscsiBlockSize() {
if (deviceInstance.getUserProperty(BLOCK_SIZE_PROP_NAME) != null) {
return (Integer.parseInt(deviceInstance.getUserProperty(BLOCK_SIZE_PROP_NAME)));
}
else {
return 0;
}
}
@Override
public final void setIscsiBlockSize(final int blockSize) throws IllegalStateException {
final FutureVoid futureVoid = doSetIscsiBlockSizeNoWait(blockSize);
try {
futureVoid.get();
}
catch (InterruptedException | ExecutionException e) {
// Failed to edit the device
throw new IllegalStateException(e);
}
}
@Override
public final String setIscsiBlockSizeNoWait(final int blockSize) throws IllegalStateException {
final FutureVoid futureVoid = doSetIscsiBlockSizeNoWait(blockSize);
return futureVoid.getTaskId().toString();
}
private final FutureVoid doSetIscsiBlockSizeNoWait(final int blockSize) throws IllegalStateException {
activationLock.readLock().lock();
try {
if (isActive()) {
throw new IllegalStateException("iSCSI block size change while device is active is not allowed");
}
if (blockSize <= 0) {
return removeDeviceProperties(BLOCK_SIZE_PROP_NAME);
}
else {
return setDeviceProperties(BLOCK_SIZE_PROP_NAME, String.valueOf(blockSize));
}
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final long getSize() {
return deviceInstance.getSize();
}
@Override
public final void setSize(final long size) {
final FutureVoid futureVoid = doSetSize(size);
try {
futureVoid.get();
}
catch (InterruptedException | ExecutionException e) {
// Failed to edit the device
throw new IllegalStateException(e);
}
}
@Override
public final String setSizeNoWait(final long size) {
final FutureVoid futureVoid = doSetSize(size);
return futureVoid.getTaskId().toString();
}
private final FutureVoid doSetSize(final long size) {
activationLock.readLock().lock();
try {
checkModifyItem();
final int blockSize = deviceInstance.getBlockSize();
final long roundedSize = size - (size % blockSize);
return deviceInstance.setSize(roundedSize);
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public String getParent() {
return deviceInstance.getParent().toString();
}
@Override
public final boolean isActive() {
activationLock.readLock().lock();
try {
return deviceInstance.getUserProperty(ACTIVE_PROP_NAME) != null;
}
finally {
activationLock.readLock().unlock();
}
}
final boolean isActiveLocally() {
activationLock.readLock().lock();
try {
return deviceInstance.getUserProperty(ACTIVE_PROP_NAME_NODE) != null;
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final boolean isReadOnly() {
activationLock.readLock().lock();
try {
return ACTIVE_PROP_VALUE_RO.equals(deviceInstance.getUserProperty(ACTIVE_PROP_NAME));
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String activateRO() {
final FutureVoid futureVoid;
activationLock.writeLock().lock();
try {
// Activated RW somewhere?
if (ACTIVE_PROP_VALUE_RW.equals(deviceInstance.getUserProperty(ACTIVE_PROP_NAME))) {
throw new IllegalStateException("activated RW");
}
// Activate device
doActivate(false);
// Persist state
futureVoid = setDeviceProperties(ACTIVE_PROP_NAME, ACTIVE_PROP_VALUE_RO, ACTIVE_PROP_NAME_NODE,
ACTIVE_PROP_VALUE_RO);
}
finally {
activationLock.writeLock().unlock();
}
return futureVoid.getTaskId().toString();
}
@Override
public final String activateRW() {
final FutureVoid futureVoid;
activationLock.writeLock().lock();
try {
// Already activated?
if (isActive()) {
throw new IllegalStateException("activated");
}
// Activate device
doActivate(true);
// Persist state
futureVoid = setDeviceProperties(ACTIVE_PROP_NAME, ACTIVE_PROP_VALUE_RW, ACTIVE_PROP_NAME_NODE,
ACTIVE_PROP_VALUE_RW);
}
finally {
activationLock.writeLock().unlock();
}
return futureVoid.getTaskId().toString();
}
/**
* Open the device and create the iSCSI target.
*
* @param rw
* <code>true</code> to open read-write
*/
private final void doActivate(final boolean rw) {
// TODO create a local task to register target in background after the end of the activation
try {
deviceInstance.activate().get();
}
catch (InterruptedException | ExecutionException e) {
// Failed to activate the device
throw new IllegalStateException(e);
}
final ReadWriteHandle rwHandle = deviceInstance.open(rw);
// Implementation of protocol interface
protocolDeviceImpl = new ProtocolDeviceImpl(rwHandle, !rw, getIscsiBlockSize());
// iSCSI target
final IscsiTarget iScsiTarget = IscsiTarget.newIscsiTarget(getIqn(), getIscsiAlias(), protocolDeviceImpl);
iscsiServer.addTarget(iScsiTarget);
// NBD export
nbdExportName = getName();
final NbdExport nbdExport = new NbdExport(nbdExportName, protocolDeviceImpl);
nbdServer.addTarget(nbdExport);
}
@Override
public final String deActivate() {
final FutureVoid futureVoid;
activationLock.writeLock().lock();
try {
// Deactivate
doDeactivate();
// Persist state
// Must not remove property ACTIVE_PROP_NAME if the device is activated elsewhere
boolean activeLocalOnly = true;
final Map<String, String> deviceProperties = deviceInstance.getUserProperties();
final Iterator<String> keys = deviceProperties.keySet().iterator();
while (keys.hasNext() && activeLocalOnly) {
final String key = keys.next();
if (key.startsWith(ACTIVE_PROP_NAME)) {
activeLocalOnly = ACTIVE_PROP_NAME_NODE.equals(key) || ACTIVE_PROP_NAME.equals(key);
}
}
if (activeLocalOnly) {
futureVoid = removeDeviceProperties(ACTIVE_PROP_NAME, ACTIVE_PROP_NAME_NODE);
}
else {
futureVoid = removeDeviceProperties(ACTIVE_PROP_NAME_NODE);
}
}
finally {
activationLock.writeLock().unlock();
}
return futureVoid.getTaskId().toString();
}
/**
* Deactivate device. Do not persist state. Does nothing if the device is not activated.
*/
final void doDeactivate() {
activationLock.writeLock().lock();
try {
if (protocolDeviceImpl != null) {
// Remove from NBD server
try {
nbdServer.removeTarget(nbdExportName);
}
catch (final Throwable t) {
LOGGER.warn("Error while removing " + toString() + " from NBD server", t);
}
nbdExportName = null;
// Remove from iSCSI server
try {
iscsiServer.removeTarget(getIqn());
}
catch (final Throwable t) {
LOGGER.warn("Error while removing " + toString() + " from iSCSI server", t);
}
try {
protocolDeviceImpl.close();
}
catch (final Throwable t) {
LOGGER.warn("Error while closing " + toString(), t);
}
try {
deviceInstance.deactivate();
}
catch (final Throwable t) {
LOGGER.warn("Error while deactivating " + toString(), t);
}
// Reset field (state deactivated)
protocolDeviceImpl = null;
}
}
finally {
activationLock.writeLock().unlock();
}
}
/**
* Set the given properties in the device.
*
* @param keyValues
* key/value pairs
*/
private final FutureVoid setDeviceProperties(final String... keyValues) {
return deviceInstance.setUserProperties(keyValues);
}
/**
* Remove the given keys from the device properties.
*
* @param keys
* property keys
*/
private final FutureVoid removeDeviceProperties(final String... keys) {
return deviceInstance.unsetUserProperties(keys);
}
/**
* Check if the item can be changed on the current node. Need activation lock.
*/
private final void checkModifyItem() {
// No need to take a snapshot if the device is activated RO
if (ACTIVE_PROP_VALUE_RO.equals(deviceInstance.getUserProperty(ACTIVE_PROP_NAME))) {
throw new IllegalStateException("Activated read-only");
}
// Activated read-write?
if (ACTIVE_PROP_VALUE_RW.equals(deviceInstance.getUserProperty(ACTIVE_PROP_NAME))) {
if (!ACTIVE_PROP_VALUE_RW.equals(deviceInstance.getUserProperty(ACTIVE_PROP_NAME_NODE))) {
throw new IllegalStateException("Activated read-write");
}
}
}
@Override
public final String takeSnapshot() throws IllegalStateException {
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureSnapshot futureSnapshot = deviceInstance.createSnapshot();
return futureSnapshot.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String takeSnapshot(final String name) throws IllegalStateException {
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureSnapshot futureSnapshot = deviceInstance.createSnapshot(name);
return futureSnapshot.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String takeSnapshot(final String name, final String description) throws IllegalStateException {
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureSnapshot futureSnapshot = deviceInstance.createSnapshot(name, description);
return futureSnapshot.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String takeSnapshotUuid(final String uuid) throws IllegalStateException {
final UUID uuidObj = UUID.fromString(uuid);
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureSnapshot futureSnapshot = deviceInstance.createSnapshot(uuidObj);
return futureSnapshot.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String takeSnapshotUuid(final String name, final String uuid) throws IllegalStateException {
final UUID uuidObj = UUID.fromString(uuid);
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureSnapshot futureSnapshot = deviceInstance.createSnapshot(name, uuidObj);
return futureSnapshot.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String takeSnapshotUuid(final String name, final String description, final String uuid)
throws IllegalStateException {
final UUID uuidObj = UUID.fromString(uuid);
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureSnapshot futureSnapshot = deviceInstance.createSnapshot(name, description, uuidObj);
return futureSnapshot.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String delete() {
if (isActive()) {
throw new IllegalStateException("Active");
}
final FutureVoid future = deviceInstance.delete();
return future.getTaskId().toString();
}
@Override
public final String clone(final String name) {
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureDevice future = deviceInstance.clone(name);
try {
VvrDevice.createVvrDevice(future.get());
}
catch (final Exception e) {
throw new IllegalStateException("Failed to clone device", e);
}
return future.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String clone(final String name, final String description) {
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureDevice future = deviceInstance.clone(name, description);
try {
VvrDevice.createVvrDevice(future.get());
}
catch (final Exception e) {
throw new IllegalStateException("Failed to clone device", e);
}
return future.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String cloneUuid(final String name, final String uuid) {
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureDevice future = deviceInstance.clone(name, UUID.fromString(uuid));
try {
VvrDevice.createVvrDevice(future.get());
}
catch (final Exception e) {
throw new IllegalStateException("Failed to clone device", e);
}
return future.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String cloneUuid(final String name, final String description, final String uuid) {
activationLock.readLock().lock();
try {
checkModifyItem();
final FutureDevice future = deviceInstance.clone(name, description, UUID.fromString(uuid));
try {
VvrDevice.createVvrDevice(future.get());
}
catch (final Exception e) {
throw new IllegalStateException("Failed to clone device", e);
}
return future.getTaskId().toString();
}
finally {
activationLock.readLock().unlock();
}
}
@Override
public final String toString() {
return "VvrDevice[uuid=" + getUuid() + ",name=" + getName() + ",IQN=" + getIqn() + "]";
}
}