/************************************************************************* * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP * * 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/. * * 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; import java.util.List; import java.util.NoSuchElementException; import org.apache.log4j.Logger; import com.eucalyptus.blockstorage.entities.CHAPUserInfo; import com.eucalyptus.blockstorage.entities.DirectStorageInfo; import com.eucalyptus.blockstorage.entities.ISCSIMetaInfo; import com.eucalyptus.blockstorage.entities.ISCSIVolumeInfo; import com.eucalyptus.blockstorage.entities.LVMVolumeInfo; import com.eucalyptus.blockstorage.util.BlockStorageUtilSvc; import com.eucalyptus.blockstorage.util.BlockStorageUtilSvcImpl; import com.eucalyptus.blockstorage.util.StorageProperties; import com.eucalyptus.crypto.Crypto; import com.eucalyptus.entities.Entities; import com.eucalyptus.entities.TransactionException; import com.eucalyptus.entities.TransactionResource; import com.eucalyptus.util.EucalyptusCloudException; public class ISCSIManager implements StorageExportManager { private static Logger LOG = Logger.getLogger(ISCSIManager.class); protected TGTService tgtService; protected BlockStorageUtilSvc blockStorageUtilSvc; public ISCSIManager() { this.tgtService = new TGTServiceUsingTGTWrapper(); this.blockStorageUtilSvc = new BlockStorageUtilSvcImpl(); } public ISCSIManager(TGTService tgtService, BlockStorageUtilSvc blockStorageUtilSvc) { this.tgtService = tgtService; this.blockStorageUtilSvc = blockStorageUtilSvc; } @Override public void checkPreconditions() throws EucalyptusCloudException { Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); tgtService.precheckService(timeout); } @Override public void check() throws EucalyptusCloudException { Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); TGTWrapper.checkService(timeout); } public void addUser(String username, String password) throws EucalyptusCloudException { Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); tgtService.addUser(username, password, timeout); } public void deleteUser(String username) throws EucalyptusCloudException { Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); tgtService.deleteUser(username, timeout); } // Modified logic for implementing EUCA-3597 and EUCA-13029 public void exportTarget(String volumeId, int tid, String name, int lun, String path, String user) throws EucalyptusCloudException { LOG.debug("Exporting " + volumeId + " as target: " + tid + "," + name + "," + lun + "," + path + "," + user); checkAndAddUser(); Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); try { // Does the target ID exist? if (!tgtService.targetExists(volumeId, tid, null /*Don't check if our LUN exists*/, timeout)) { LOG.debug("Creating target " + tid + " for " + volumeId); tgtService.createTarget(volumeId, tid, name, timeout); } else { LOG.debug("Target " + tid + " already exists"); } // Does the LUN exist with our backing store path for our target ID? if (!tgtService.targetExists(volumeId, tid, path, timeout)) { LOG.debug("Creating lun " + lun + " for " + volumeId); tgtService.createLun(volumeId, tid, lun, path, timeout); } else { LOG.debug("Target " + tid + " with LUN " + lun + " and backing store path " + path + " already exists for " + volumeId); } // Is the target bound to our user? if (!tgtService.targetConfigured(volumeId, tid, path, timeout, user, false /*don't check initiators list*/)) { LOG.debug("Binding user " + user + " for " + volumeId); tgtService.bindUser(volumeId, user, tid, timeout); } else { LOG.debug("Target " + tid + " already bound to user " + user + " for " + volumeId); } // Is the target bound to the right initiators access list (ACL)? if (!tgtService.targetConfigured(volumeId, tid, path, timeout, null /*don't check user*/, true /*check initiators list*/)) { LOG.debug("Binding target " + tid + " initiators for " + volumeId); tgtService.bindTarget(volumeId, tid, timeout); } else { LOG.debug("Target " + tid + " initiators already bound for " + volumeId); } } catch (Exception e) { LOG.error("Failed creating target " + tid + " for " + volumeId); throw new EucalyptusCloudException(e); } } /** * Execute the tgt commands to remove the iscsi target. Removes the target for *all* hosts. Verifies that the target does indeed export the resource * of the given path with the given lun before unexport. * * @param tid * @param lun * @param path - the absolute path of the resource expected exported in the target. */ public void unexportTarget(String volumeId, int tid, int lun, String path) throws EucalyptusCloudException { final int opMaxRetry = 3; try { Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); LOG.debug("Unexport target: tid=" + tid + ",lun=" + lun + " for " + volumeId); if (tgtService.targetExists(volumeId, tid, path, timeout)) { LOG.info("Attempting to unexport target: " + tid); } else { LOG.info("Volume: " + volumeId + " Target: " + tid + " not found, cannot unexport it."); return; } LOG.debug("Unbinding target " + tid + " for " + volumeId); tgtService.unbindTarget(volumeId, tid, timeout); int retryCount = 0; do { try { LOG.debug("Deleting lun " + lun + " on target " + tid + " for " + volumeId); tgtService.deleteLun(volumeId, tid, lun, timeout); break; } catch (TGTWrapper.ResourceNotFoundException e) { LOG.warn("Resource not found when deleting lun for " + volumeId + ". Continuing unexport", e); break; } catch (EucalyptusCloudException e) { LOG.warn("Volume " + volumeId + " Unable to delete lun for target: " + tid); Thread.sleep(1000); continue; } } while (retryCount++ < opMaxRetry); if (retryCount >= opMaxRetry) { LOG.error("Volume: " + volumeId + " Gave up deleting the lun for: " + tid); } retryCount = 0; do { try { LOG.debug("Deleting target " + tid + " for " + volumeId); tgtService.deleteTarget(volumeId, tid, timeout, false); } catch (TGTWrapper.ResourceNotFoundException e) { // no-op LOG.warn("Resource not found when deleting target for volume " + volumeId + " Continuing.", e); } catch (EucalyptusCloudException e) { LOG.warn("Volume: " + volumeId + " Unable to delete target: " + tid, e); Thread.sleep(1000); continue; } if (tgtService.targetExists(volumeId, tid, null, timeout)) { LOG.warn("Volume: " + volumeId + " Target: " + tid + " still exists..."); Thread.sleep(1000); } else { break; } } while (retryCount++ < opMaxRetry); // Do a forcible delete of the target if (retryCount >= opMaxRetry && !tgtService.targetHasLun(volumeId, tid, lun, timeout)) { LOG.info("Forcibly deleting volume " + volumeId + " iscsi target " + tid); tgtService.deleteTarget(volumeId, tid, timeout, true); if (tgtService.targetExists(volumeId, tid, null, timeout)) { LOG.error("Volume: " + volumeId + " Target: " + tid + " still exists after forcible deletion"); throw new Exception("Failed to delete iscsi target " + tid + " for volume " + volumeId); } } } catch (Exception t) { LOG.error("Unexpected error encountered during unexport process for volume " + volumeId, t); } } @Override public void configure() { ISCSIMetaInfo metaInfo = new ISCSIMetaInfo(StorageProperties.NAME); try (TransactionResource tran = Entities.transactionFor(ISCSIMetaInfo.class)) { List<ISCSIMetaInfo> metaInfoList = Entities.query(metaInfo); if (metaInfoList.size() <= 0) { Entities.persist(metaInfo.init()); tran.commit(); } } catch (Exception e) { LOG.error(e); } checkAndAddUser(); } private void checkAndAddUser() { try (TransactionResource outter = Entities.transactionFor(CHAPUserInfo.class)) { CHAPUserInfo userInfo = Entities.uniqueResult(new CHAPUserInfo("eucalyptus")); outter.commit(); // check if account actually exists, if not create it. if (!checkUser("eucalyptus")) { try { addUser("eucalyptus", blockStorageUtilSvc.decryptSCTargetPassword(userInfo.getEncryptedPassword())); } catch (EucalyptusCloudException e1) { LOG.error(e1); return; } } } catch (NoSuchElementException ex) { boolean addUser = true; String encryptedPassword = null; try (TransactionResource inner = Entities.transactionFor(CHAPUserInfo.class)) { if (checkUser("eucalyptus")) { try { LOG.debug("No DB record found for chapuser although a eucalyptus account exists on SC. Looking for all records with chapuser eucalyptus"); CHAPUserInfo userInfo = new CHAPUserInfo("eucalyptus"); userInfo.setScName(null); CHAPUserInfo currentUserInfo = Entities.uniqueResult(userInfo); if (null != currentUserInfo && null != currentUserInfo.getEncryptedPassword()) { LOG.debug("Found a DB record, copying the password to the new record"); addUser = false; encryptedPassword = currentUserInfo.getEncryptedPassword(); } } catch (Exception e1) { LOG.debug("No old DB records found. The only way is to delete the chapuser and create a fresh account"); try { deleteUser("eucalyptus"); } catch (Exception e) { LOG.error("Failed to delete chapuser", e); } } } if (addUser) { // Windows iscsi initiator requires the password length to be 12-16 bytes String password = Crypto.getRandom( 16 ); password = password.substring(0, 16); try { addUser("eucalyptus", password); encryptedPassword = blockStorageUtilSvc.encryptSCTargetPassword(password); } catch (Exception e) { LOG.error("Failed to add chapuser to SC", e); return; } } try { Entities.persist(new CHAPUserInfo("eucalyptus", encryptedPassword)); } catch (Exception e) { LOG.error(e); } inner.commit(); } } catch (TransactionException e) { LOG.error(e); } } @Override public synchronized void allocateTarget(LVMVolumeInfo volumeInfo) throws EucalyptusCloudException { if (volumeInfo instanceof ISCSIVolumeInfo) { ISCSIVolumeInfo iscsiVolumeInfo = (ISCSIVolumeInfo) volumeInfo; LOG.debug("Allocate target: " + iscsiVolumeInfo); if (iscsiVolumeInfo.getTid() > -1) { LOG.info("Volume already associated with a tid: " + iscsiVolumeInfo.getTid()); return; } int tid = -1, storeNumber = -1; List<ISCSIMetaInfo> metaInfoList; try (TransactionResource tran = Entities.transactionFor(ISCSIMetaInfo.class)) { metaInfoList = Entities.query(new ISCSIMetaInfo(StorageProperties.NAME)); if (metaInfoList.size() > 0) { ISCSIMetaInfo foundMetaInfo = metaInfoList.get(0); storeNumber = foundMetaInfo.getStoreNumber(); tid = foundMetaInfo.getTid(); } tran.commit(); } // check if tid is in use int i = tid; Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); do { if (!tgtService.targetExists(volumeInfo.getVolumeId(), i, null, timeout)) { tid = i; break; } LOG.debug("Target TID " + i + " already exists, trying the next one"); i = (i + 1) % Integer.MAX_VALUE; } while (i != tid); LOG.debug("Volume " + iscsiVolumeInfo.getVolumeId() + " Target allocation found tid: " + tid); if (tid > 0) { try (TransactionResource tran = Entities.transactionFor(ISCSIMetaInfo.class)) { metaInfoList = Entities.query(new ISCSIMetaInfo(StorageProperties.NAME)); if (metaInfoList.size() > 0) { ISCSIMetaInfo foundMetaInfo = metaInfoList.get(0); foundMetaInfo.setStoreNumber(++storeNumber); foundMetaInfo.setTid(tid + 1); iscsiVolumeInfo.setStoreName(foundMetaInfo.getStorePrefix() + StorageProperties.NAME + ":store" + storeNumber); iscsiVolumeInfo.setStoreUser(foundMetaInfo.getStoreUser()); iscsiVolumeInfo.setTid(tid); // LUN cannot be 0 (some clients don't like that). iscsiVolumeInfo.setLun(1); } tran.commit(); } } else { iscsiVolumeInfo.setTid(-1); LOG.fatal("Unable to allocate ISCSI target id for volume " + iscsiVolumeInfo.getVolumeId()); } } } private boolean checkUser(String username) { try { Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); return tgtService.userExists(username, timeout); } catch (EucalyptusCloudException e) { LOG.error(e); return false; } } public String getEncryptedPassword() throws EucalyptusCloudException { try (TransactionResource tran = Entities.transactionFor(CHAPUserInfo.class)) { CHAPUserInfo userInfo = Entities.uniqueResult(new CHAPUserInfo("eucalyptus")); String encryptedPassword = userInfo.getEncryptedPassword(); tran.commit(); return blockStorageUtilSvc.encryptNodeTargetPassword(blockStorageUtilSvc.decryptSCTargetPassword(encryptedPassword), blockStorageUtilSvc.getPartitionForLocalService(Storage.class)); } catch (TransactionException | NoSuchElementException ex) { throw new EucalyptusCloudException("Unable to get CHAP password for: " + "eucalyptus"); } } /** * Unexport the volume from ISCSI and clean the db state to indicate that */ public void cleanup(LVMVolumeInfo volumeInfo) throws EucalyptusCloudException { if (volumeInfo instanceof ISCSIVolumeInfo) { ISCSIVolumeInfo volInfo = (ISCSIVolumeInfo) volumeInfo; try (TransactionResource tran = Entities.transactionFor(ISCSIVolumeInfo.class)) { ISCSIVolumeInfo volEntity = Entities.merge(volInfo); if (isExported(volEntity)) { unexportTarget(volEntity.getVolumeId(), volEntity.getTid(), volEntity.getLun(), volEntity.getAbsoluteLVPath()); // Verify it really is gone if (!isExported(volEntity)) { volEntity.setTid(-1); volEntity.setLun(-1); volEntity.setStoreName(null); volEntity.setStoreUser(null); } else { throw new EucalyptusCloudException("Unable to remove tid: " + volEntity.getTid()); } } else { LOG.debug("Target tid " + volEntity.getTid() + " is not exported for lun " + volEntity.getLun() + " for volume " + volEntity.getVolumeId()); // Ensure that db indicates the vol is not exported volEntity.setTid(-1); volEntity.setLun(-1); volEntity.setStoreName(null); volEntity.setStoreUser(null); } tran.commit(); } catch (Exception e) { LOG.error("Something went wrong, exiting iscsi cleanup for volume " + volumeInfo.getVolumeId()); } } else { LOG.error("Unknown volume type for volume " + volumeInfo.getVolumeId() + " - cannot cleanpup"); throw new EucalyptusCloudException("Unknown type for volumeInfo in ISCSI cleanup. Did not find ISCSIVolumeInfo as expected"); } } @Override public void stop() { tgtService.stop(); } @Override public boolean isExported(LVMVolumeInfo volumeInfo) throws EucalyptusCloudException { if (volumeInfo instanceof ISCSIVolumeInfo) { if (((ISCSIVolumeInfo) volumeInfo).getTid() > -1) { ISCSIVolumeInfo iscsiVolumeInfo = (ISCSIVolumeInfo) volumeInfo; Long timeout = DirectStorageInfo.getStorageInfo().getTimeoutInMillis(); return tgtService.targetExists(iscsiVolumeInfo.getVolumeId(), iscsiVolumeInfo.getTid(), iscsiVolumeInfo.getAbsoluteLVPath(), timeout); // CommandOutput output = execute(new String[] { ROOT_WRAP, "tgtadm", "--lld", "iscsi", "--op", "show", "--mode", "target", "--tid", // String.valueOf(iscsiVolumeInfo.getTid()) }, timeout); // if (StringUtils.isBlank(output.error)) { // return true; // } } } return false; } }