/*************************************************************************
* 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.config;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.Serializable;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceContext;
import javax.persistence.PrePersist;
import javax.persistence.Transient;
import org.apache.log4j.Logger;
import com.eucalyptus.blockstorage.Storage;
import com.eucalyptus.blockstorage.StorageManagers;
import com.eucalyptus.bootstrap.Hosts;
import com.eucalyptus.component.ServiceConfiguration;
import com.eucalyptus.component.ServiceConfigurations;
import com.eucalyptus.component.annotation.ComponentPart;
import com.eucalyptus.config.ComponentConfiguration;
import com.eucalyptus.configurable.ConfigurableClass;
import com.eucalyptus.configurable.ConfigurableField;
import com.eucalyptus.configurable.ConfigurableIdentifier;
import com.eucalyptus.configurable.ConfigurableProperty;
import com.eucalyptus.configurable.ConfigurablePropertyException;
import com.eucalyptus.configurable.MultiDatabasePropertyEntry;
import com.eucalyptus.configurable.PropertyChangeListener;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.system.BaseDirectory;
import com.eucalyptus.upgrade.Upgrades.EntityUpgrade;
import com.eucalyptus.upgrade.Upgrades.Version;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.Internets;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
@Entity
@PersistenceContext(name = "eucalyptus_config")
@ComponentPart(Storage.class)
@ConfigurableClass(root = "storage", alias = "basic", description = "Basic cluster controller configuration.", singleton = false, deferred = true)
public class StorageControllerConfiguration extends ComponentConfiguration implements Serializable {
@Transient
private static String DEFAULT_SERVICE_PATH = "/services/Storage";
@Transient
@ConfigurableIdentifier
private String propertyPrefix;
@ConfigurableField(description = "EBS Block Storage Manager to use for backend", displayName = "EBS Block Storage Manager",
changeListener = StorageBackendChangeListener.class)
@Column(name = "system_storage_ebs_backend")
private String blockStorageManager;
/*
* Available Backends is used *ONLY* for allowing the CLC to do sanity checks on the value being set for the block storage backend by the user via a
* modify-property call. This obviates the need to have the CLC call the SC somehow and check the set of valid values. The SC should set the value
* when the service is constructed.
*/
@Column(name = "available_storage_backends")
private String availableBackends;
/*
* Change Listener for san provider and backend config options. The semantics enforced are that they can be set once per registered SC. If the value
* has already been configured it will throw an exception if the user tries to reconfigure them, unless the user is simply specifying the same value
* (for idempotence)
*
* This is done to prevent data loss and database corruption that can occur if the user tries to change the backend after it already has some state.
*
* The semantics are that the value is valid if *any* of the SCs in the specified partition have proposed value listed as an available backend
* manager.
*/
public static class StorageBackendChangeListener implements PropertyChangeListener<String> {
@Override
public void fireChange(ConfigurableProperty t, String newValue) throws ConfigurablePropertyException {
String existingValue = (String) t.getValue();
if (existingValue != null && !"".equals(existingValue)) {
if (newValue != null && !newValue.equals(existingValue)) {
throw new ConfigurablePropertyException(
"Cannot change extant storage backend configuration. You must deregister all SCs in the partition before you can change the configuration value");
}
} else {
// Try to figure out the partition name for the request
String probablePartitionName = ((MultiDatabasePropertyEntry) t).getEntrySetName();
if (probablePartitionName == null) {
throw new ConfigurablePropertyException("Could not determing partition name from property to check validity");
}
String[] parts = probablePartitionName.split("\\.");
if (parts == null || parts.length == 0) {
throw new ConfigurablePropertyException("Could not determing partition name from property to check validity: " + probablePartitionName);
}
probablePartitionName = parts[0];
/*
* Look through the service configurations for each SC in the partition and see if the value is valid. This step must work if we don't allow
* the user to change it once set. The difficulty here is if 2 SCs are in an HA pair but have different backends installed (i.e. packages) The
* implemented semantic is that if the proposed value is valid in either SC, then allow the change.
*/
List<ServiceConfiguration> scConfigs = null;
try {
scConfigs = ServiceConfigurations.listPartition(Storage.class, probablePartitionName);
} catch (NoSuchElementException e) {
throw new ConfigurablePropertyException("No Storage Controller configurations found for partition: " + probablePartitionName);
}
final String proposedValue = newValue;
final Set<String> validEntries = Sets.newHashSet();
EntityTransaction tx = Entities.get(StorageControllerConfiguration.class);
try {
if (!Iterables.any(scConfigs, new Predicate<ServiceConfiguration>() {
@Override
public boolean apply(ServiceConfiguration config) {
if ( config.isVmLocal( ) || Hosts.isCoordinator( ) ) {
// Add locally discovered entries to the valid list
validEntries.addAll(StorageManagers.list());
}
if ( !config.isVmLocal( ) ) {
try {
// Remote SC, so check the db for the list of valid entries.
StorageControllerConfiguration scConfig = Entities.uniqueResult((StorageControllerConfiguration) config);
if ( scConfig.getAvailableBackends() != null ) {
for (String entry : Splitter.on(",").split(scConfig.getAvailableBackends())) {
validEntries.add(entry);
}
}
} catch (Exception e) {
return false;
}
}
return validEntries.contains(proposedValue);
}
})) {
// Nothing matched.
throw new ConfigurablePropertyException("Cannot modify " + t.getQualifiedName() + "." + t.getFieldName()
+ " new value is not a valid value. " + "Legal values are: "
+ Joiner.on(",").join(Sets.filter(validEntries, StorageManagers.SUPPORTED_PROVIDER_PREDICATE)));
}
} finally {
tx.rollback();
}
}
}
}
@PrePersist
private void updateRedundantConfig() {
// Checks to see if other SCs exist in the same partition and uses the same backend config if it exists.
if (this.blockStorageManager == null && this.getPartition() != null) {
for (ServiceConfiguration s : ServiceConfigurations.listPartition(Storage.class, this.getPartition())) {
StorageControllerConfiguration otherSc = (StorageControllerConfiguration) s;
this.blockStorageManager = otherSc.getBlockStorageManager() != null ? otherSc.getBlockStorageManager() : null;
}
}
}
public StorageControllerConfiguration() {
}
public StorageControllerConfiguration(String name) {
super.setName(name);
}
public StorageControllerConfiguration(String partition, String name, String hostName, Integer port) {
super(partition, name, hostName, port, DEFAULT_SERVICE_PATH);
}
public StorageControllerConfiguration(String partition, String name, String hostName, Integer port, String storageManager) {
super(partition, name, hostName, port, DEFAULT_SERVICE_PATH);
this.blockStorageManager = storageManager;
}
public String getBlockStorageManager() {
return this.blockStorageManager;
}
public void setBlockStorageManager(String m) {
this.blockStorageManager = m;
}
public String getPropertyPrefix() {
return this.getPartition();
}
public void setPropertyPrefix(String p) {
this.setPartition(p);
}
private static final String BLOCK_STORAGE_MANAGER_OVERLAY = "overlay";
private static final String BLOCK_STORAGE_MANAGER_DAS = "das";
private static final String BLOCK_STORAGE_MANAGER_EQUALLOGIC = "equallogic";
private static final String BLOCK_STORAGE_MANAGER_NETAPP = "netapp";
private static final Pattern EBS_STORAGE_MANAGER_PATTERN = Pattern.compile(".*-Debs\\.storage\\.manager=(\\w+).*");
private static final Pattern EBS_SAN_PROVIDER_PATTERN = Pattern.compile(".*-Debs\\.san\\.provider=(\\w+).*");
private static String matchParameter(Pattern pattern, String text) {
Matcher matcher = pattern.matcher(text);
if (matcher.matches()) {
return matcher.group(1);
}
return null;
}
public String getAvailableBackends() {
return availableBackends;
}
public void setAvailableBackends(String availableBackends) {
this.availableBackends = availableBackends;
}
@EntityUpgrade(entities = {StorageControllerConfiguration.class}, since = Version.v3_2_0, value = Storage.class)
public enum StorageControllerConfigurationUpgrade implements Predicate<Class> {
INSTANCE;
private static Logger LOG = Logger.getLogger(StorageControllerConfiguration.StorageControllerConfigurationUpgrade.class);
private static String loadLocalBlockStorageManagerConfig() throws Exception {
String manager = BLOCK_STORAGE_MANAGER_OVERLAY; // default
BufferedReader fileReader = new BufferedReader(new FileReader(BaseDirectory.HOME + "/etc/eucalyptus/eucalyptus.conf"));
String ebsStorageManager = null;
String ebsSanProvider = null;
String line;
while ((line = fileReader.readLine()) != null) {
line.trim();
if (line.startsWith("CLOUD_OPTS")) {
ebsStorageManager = matchParameter(EBS_STORAGE_MANAGER_PATTERN, line);
ebsSanProvider = matchParameter(EBS_SAN_PROVIDER_PATTERN, line);
break;
}
}
fileReader.close();
if (Strings.isNullOrEmpty(ebsStorageManager)) {
manager = BLOCK_STORAGE_MANAGER_OVERLAY;
} else if ("DASManager".equals(ebsStorageManager)) {
manager = BLOCK_STORAGE_MANAGER_DAS;
} else if ("OverlayManager".equals(ebsStorageManager)) {
manager = BLOCK_STORAGE_MANAGER_OVERLAY;
} else if ("SANManager".equals(ebsStorageManager)) {
if ("EquallogicProvider".equals(ebsSanProvider)) {
manager = BLOCK_STORAGE_MANAGER_EQUALLOGIC;
} else if ("NetappProvider".equals(ebsSanProvider)) {
manager = BLOCK_STORAGE_MANAGER_NETAPP;
} else {
LOG.error("Invalid SAN provider name: " + ebsSanProvider);
}
} else {
LOG.error("Invalid storage manager name: " + ebsStorageManager);
}
return manager;
}
@Override
public boolean apply(Class arg0) {
EntityTransaction db = Entities.get(StorageControllerConfiguration.class);
try {
// Get local IP addresses or host names
Set<String> localAddresses = Internets.getAllLocalHostNamesIps();
List<StorageControllerConfiguration> entities = Entities.query(new StorageControllerConfiguration());
for (StorageControllerConfiguration entry : entities) {
// This SC is running on the local machine, upgrade its block storage manager config
if (localAddresses.contains(entry.getHostName())) {
LOG.debug("Upgrading SC config " + entry.getPartition());
entry.setBlockStorageManager(loadLocalBlockStorageManagerConfig());
LOG.debug("Set storage manager " + entry.getBlockStorageManager() + " for SC " + entry.getPartition());
break;
}
}
db.commit();
return true;
} catch (Exception ex) {
db.rollback();
throw Exceptions.toUndeclared(ex);
}
}
}
}