/******************************************************************************* *Copyright (c) 2009 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, only version 3 of the License. * * * This file 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., 130 Castilian * Dr., Goleta, CA 93101 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. *******************************************************************************/ /* * Author: chris grzegorczyk <grze@eucalyptus.com> */ package com.eucalyptus.blockstorage.san.common.entities; import static com.eucalyptus.upgrade.Upgrades.Version.v5_0_0; import groovy.sql.GroovyRowResult; import groovy.sql.Sql; import java.util.List; import java.util.TreeMap; import java.util.concurrent.Callable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Lob; import javax.persistence.PersistenceContext; import javax.persistence.PostLoad; import javax.persistence.PreUpdate; import javax.persistence.Table; import org.apache.log4j.Logger; import org.hibernate.annotations.Type; import com.eucalyptus.blockstorage.Storage; import com.eucalyptus.blockstorage.san.common.SANProperties; import com.eucalyptus.blockstorage.util.BlockStorageUtil; import com.eucalyptus.blockstorage.util.StorageProperties; import com.eucalyptus.configurable.ConfigurableClass; import com.eucalyptus.configurable.ConfigurableField; import com.eucalyptus.configurable.ConfigurableFieldType; import com.eucalyptus.configurable.ConfigurableIdentifier; import com.eucalyptus.configurable.ConfigurableInit; import com.eucalyptus.configurable.ConfigurableProperty; import com.eucalyptus.configurable.ConfigurablePropertyException; import com.eucalyptus.configurable.PropertyChangeListener; import com.eucalyptus.entities.AbstractPersistent; import com.eucalyptus.entities.Transactions; import com.eucalyptus.upgrade.Upgrades.DatabaseFilters; import com.eucalyptus.upgrade.Upgrades.PreUpgrade; import com.eucalyptus.util.EucalyptusCloudException; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.net.HostSpecifier; @Entity @PersistenceContext(name = "eucalyptus_storage") @Table(name = "san_info") @ConfigurableClass(root = "storage", alias = "san", description = "Basic storage controller configuration for SAN.", singleton = false, deferred = true) public class SANInfo extends AbstractPersistent { private static Logger LOG = Logger.getLogger(SANInfo.class); // The SAN controller management port address cache private static String currentSanHosts = null; private static TreeMap<String, Long> sanControllerAddresses = Maps.newTreeMap(); private static final Long ADDRESS_FAILURE_RETRY_INTERVAL_IN_MILLIS = 300 * 1000L; // 5 mins public static final String DEFAULT_CHAP_USER = "nouser"; public static final String DEFAULT_PATHS = "nopath"; @ConfigurableIdentifier @Column(name = "storage_name", unique = true) protected String name; @ConfigurableField(description = "Hostname for SAN device.", displayName = "SAN Host") @Column(name = "san_host") private String sanHost; @ConfigurableField(description = "Username for SAN device.", displayName = "SAN Username") @Column(name = "san_user") private String sanUser; @ConfigurableField(description = "Password for SAN device.", displayName = "SAN Password", type = ConfigurableFieldType.KEYVALUEHIDDEN) @Column(name = "san_password") @Type(type = "org.hibernate.type.StringClobType") @Lob private String sanPassword; @ConfigurableField(description = "User ID for CHAP authentication", displayName = "CHAP user", type = ConfigurableFieldType.KEYVALUE) @Column(name = "chap_user") private String chapUser; @ConfigurableField(description = "iSCSI Paths for NC. Default value is 'nopath'", displayName = "NC paths", type = ConfigurableFieldType.KEYVALUE, changeListener = PathsChangeListener.class, initial = DEFAULT_PATHS) @Column(name = "ncpaths") private String ncPaths; @ConfigurableField(description = "iSCSI Paths for SC. Default value is 'nopath'", displayName = "SC paths", type = ConfigurableFieldType.KEYVALUE, changeListener = PathsChangeListener.class, initial = DEFAULT_PATHS) @Column(name = "scpaths") private String scPaths; @ConfigurableField(description = "Prefix for resource name on SAN", displayName = "Resource Prefix", initial = "") @Column(name = "resource_prefix") private String resourcePrefix; @ConfigurableField(description = "Suffix for resource name on SAN", displayName = "Resource Suffix", initial = "") @Column(name = "resource_suffix") private String resourceSuffix; public SANInfo() { this.name = StorageProperties.NAME; } public SANInfo(final String name) { this.name = name; } public SANInfo(final String name, final String sanHost, final String sanUser, final String sanPassword) { this.name = name; this.sanHost = sanHost; this.sanUser = sanUser; this.sanPassword = sanPassword; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSanHost() { return sanHost; } public void setSanHost(String sanHost) { this.sanHost = sanHost; } public String getSanUser() { return sanUser; } public void setSanUser(String sanUser) { this.sanUser = sanUser; } public String getSanPassword() { try { return BlockStorageUtil.decryptSCTargetPassword(sanPassword); } catch (EucalyptusCloudException ex) { LOG.error(ex); return null; } } public void setSanPassword(String sanPassword) { try { this.sanPassword = BlockStorageUtil.encryptSCTargetPassword(sanPassword); } catch (EucalyptusCloudException ex) { LOG.error(ex); } } public String getChapUser() { return chapUser; } public void setChapUser(String chapUser) { this.chapUser = chapUser; } public String getNcPaths() { return this.ncPaths; } public void setNcPaths(String paths) { this.ncPaths = paths; } public String getScPaths() { return this.scPaths; } public void setScPaths(String scPaths) { this.scPaths = scPaths; } public String getResourcePrefix() { return resourcePrefix; } public void setResourcePrefix(String resourcePrefix) { this.resourcePrefix = resourcePrefix; } public String getResourceSuffix() { return resourceSuffix; } public void setResourceSuffix(String resourceSuffix) { this.resourceSuffix = resourceSuffix; } @PreUpdate @PostLoad public void setDefaults() { if (this.chapUser == null) { this.chapUser = DEFAULT_CHAP_USER; } } private static SANInfo newDefault() { return new SANInfo( ).init( ); } @ConfigurableInit public SANInfo init( ) { setSanHost(SANProperties.SAN_HOST); setSanUser(SANProperties.SAN_USERNAME); setSanPassword(SANProperties.SAN_PASSWORD); setChapUser(DEFAULT_CHAP_USER); setNcPaths(SANInfo.DEFAULT_PATHS); setScPaths(SANInfo.DEFAULT_PATHS); return this; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SANInfo other = (SANInfo) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public String toString() { return this.name; } public static SANInfo getStorageInfo() { SANInfo conf = null; try { conf = Transactions.find(new SANInfo()); } catch (Exception e) { LOG.warn("Storage information for " + StorageProperties.NAME + " not found. Loading defaults."); try { conf = Transactions.saveDirect(newDefault()); } catch (Exception e1) { try { conf = Transactions.find(new SANInfo()); } catch (Exception e2) { LOG.warn("Failed to persist and retrieve SANInfo entity"); } } } if (conf == null) { conf = newDefault(); } return conf; } private static void loadSanControllerAddressesIfChanged() { String hosts = getStorageInfo().getSanHost(); if (currentSanHosts == null || !currentSanHosts.equals(hosts)) { sanControllerAddresses.clear(); if (SANProperties.DUMMY_SAN_HOST.equals(hosts) || Strings.isNullOrEmpty(hosts)) { return; } for (String host : hosts.split(",")) { host = host.trim(); if (!Strings.isNullOrEmpty(host)) { sanControllerAddresses.put(host, 0L); } } currentSanHosts = hosts; } } /** * Get a usable SAN controller management port address: find the address whose last failure time is earlier than the retry limit. * * @return the working address. Empty if none is available. */ public static synchronized String getSanControllerAddress() { loadSanControllerAddressesIfChanged(); for (String addr : sanControllerAddresses.keySet()) { Long lastFailureTime = sanControllerAddresses.get(addr); if (System.currentTimeMillis() - lastFailureTime > ADDRESS_FAILURE_RETRY_INTERVAL_IN_MILLIS) { return addr; } } return null; } /** * Register an address failure. This will cause the next address retrieval to fail over to another address. * * @param address The failed address. */ public static synchronized void setSanControllerAddressFailure(String address) { sanControllerAddresses.put(address, System.currentTimeMillis()); } /** * The internal representation of an iSCSI path: * * Host iface -> SAN device port * * @author wenye * */ public static class Path { public String iface; // The host iface public String ip; // The SAN port IP public String sp; // The SAN device SP public String portId; // The SAN device SP port public String iqn; // The SAN port IQN public Path(String iface, String ip) { this.iface = iface; this.ip = ip; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append(iface).append(',').append(ip); return sb.toString(); } } public static class PathsChangeListener implements PropertyChangeListener { @Override public void fireChange(ConfigurableProperty t, Object newValue) throws ConfigurablePropertyException { try { if (!SANInfo.DEFAULT_PATHS.equals(newValue)) { parsePaths((String) newValue); } } catch (IllegalArgumentException e) { throw new ConfigurablePropertyException("Invalid paths: " + e, e); } } } /** * Parsing the iSCSI paths string into the internal representation. The iSCSI paths string format as in the system property value: * * <NC host interface name>:<device port IP address>,<NC host interface name>:<device port IP address>;... * * A typical example: * * iface0:192.168.25.182,iface1:10.109.25.186 * * Interface can be omitted (which means using default interface). So it will become: * * 192.168.25.182,10.109.25.186 * * @param paths Paths string * @return The internal paths structure * @throws IllegalArgumentException If the paths string has wrong format. Note that we only check syntax correctness. */ private static final String PATH_SEPARATOR = ","; private static final String PATH_FIELD_SEPARATOR = ":"; public static List<SANInfo.Path> parsePaths(String paths) { if (Strings.isNullOrEmpty(paths)) { throw new IllegalArgumentException("Empty paths"); } String[] splitPaths = paths.split(PATH_SEPARATOR); if (splitPaths == null || splitPaths.length < 1) { throw new IllegalArgumentException("Invalid paths " + paths); } List<SANInfo.Path> parsed = Lists.newArrayList(); for (String path : splitPaths) { String[] splitOnePath = path.split(PATH_FIELD_SEPARATOR); if (splitOnePath == null || splitOnePath.length < 1 || splitOnePath.length > 2) { throw new IllegalArgumentException("Invalid path " + path); } String iface; String ip; if (splitOnePath.length == 1) { iface = ""; ip = splitOnePath[0]; } else { iface = splitOnePath[0]; ip = splitOnePath[1]; } HostSpecifier.fromValid(ip); parsed.add(new SANInfo.Path(iface, ip)); } return parsed; } @PreUpgrade(since = v5_0_0, value = Storage.class) public static class RemoveTaskTimeout implements Callable<Boolean> { private static final Logger LOG = Logger.getLogger(RemoveTaskTimeout.class); @Override public Boolean call() throws Exception { Sql sql = null; String table = "san_info"; try { sql = DatabaseFilters.NEWVERSION.getConnection("eucalyptus_storage"); // check if the old column exists before removing it String column = "task_timeout"; List<GroovyRowResult> result = sql.rows(String.format("select column_name from information_schema.columns where table_name='%s' and column_name='%s'", table, column)); if (result != null && !result.isEmpty()) { // drop column if it exists LOG.info("Dropping column if it exists " + column); sql.execute(String.format("alter table %s drop column if exists %s", table, column)); } else { LOG.debug("Column " + column + " not found, nothing to drop"); } return Boolean.TRUE; } catch (Exception e) { LOG.warn("Failed to drop columns in table " + table, e); return Boolean.TRUE; } finally { if (sql != null) { sql.close(); } } } } }