/************************************************************************* * 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.objectstorage.entities; import com.eucalyptus.bootstrap.Hosts; import com.eucalyptus.configurable.ConfigurableInit; import com.eucalyptus.configurable.PropertyChangeListeners; import com.eucalyptus.upgrade.Upgrades.EntityUpgrade; import groovy.sql.GroovyRowResult; import groovy.sql.Sql; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import javax.annotation.Nullable; import javax.persistence.*; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.eucalyptus.component.Components; import com.eucalyptus.component.ServiceConfiguration; import com.eucalyptus.component.ServiceConfigurations; import com.eucalyptus.configurable.ConfigurableClass; import com.eucalyptus.configurable.ConfigurableField; import com.eucalyptus.configurable.ConfigurableFieldType; import com.eucalyptus.configurable.ConfigurableProperty; import com.eucalyptus.configurable.ConfigurablePropertyException; import com.eucalyptus.configurable.PropertyChangeListener; import com.eucalyptus.entities.AbstractPersistent; import com.eucalyptus.entities.Entities; import com.eucalyptus.entities.TransactionResource; import com.eucalyptus.entities.Transactions; import com.eucalyptus.objectstorage.ObjectStorage; import com.eucalyptus.objectstorage.providers.ObjectStorageProviders; import com.eucalyptus.storage.config.CacheableConfiguration; import com.eucalyptus.upgrade.Upgrades; import com.eucalyptus.upgrade.Upgrades.DatabaseFilters; import com.eucalyptus.util.Exceptions; import com.eucalyptus.walrus.entities.WalrusInfo; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; 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; /** * The OSG global configuration parameters. These are common for all OSG instances. */ @Entity @PersistenceContext(name = "eucalyptus_osg") @Table(name = "osg_config") @ConfigurableClass(root = "ObjectStorage", description = "Object Storage Gateway configuration.", deferred = true) public class ObjectStorageGlobalConfiguration extends AbstractPersistent implements CacheableConfiguration<ObjectStorageGlobalConfiguration> { private static final Logger LOG = Logger.getLogger(ObjectStorageGlobalConfiguration.class); private static final int DEFAULT_MAX_BUCKETS_PER_ACCOUNT = 100; private static final int DEFAULT_MAX_METADATA_REQUEST_SIZE = 1024 * 300; // 300 KB private static final int DEFAULT_PUT_TIMEOUT_HOURS = 168; // An upload not marked completed or deleted in 24 hours from record creation will be // considered 'failed' private static final int DEFAULT_CLEANUP_INTERVAL_SEC = 60; // 60 seconds between cleanup tasks. private static final String DEFAULT_BUCKET_NAMING_SCHEME = "extended"; private static final Boolean DEFAULT_COPY_UNSUPPORTED_STRATEGY = Boolean.FALSE; private static final int DEFAULT_MAX_TAGS = 50; @Override public ObjectStorageGlobalConfiguration getLatest() { return getConfiguration(); } @Column( name = "max_metadata_request_size" ) @ConfigurableField(description = "Maximum allowed size of metadata request bodies", displayName = "Maximum allowed size of metadata requests", initialInt = DEFAULT_MAX_METADATA_REQUEST_SIZE ) protected Integer max_metadata_request_size; @Column( name = "max_buckets_per_account" ) @ConfigurableField(description = "Maximum number of buckets per account", displayName = "Maximum buckets per account", initialInt = DEFAULT_MAX_BUCKETS_PER_ACCOUNT ) protected Integer max_buckets_per_account; @Column( name = "max_total_reporting_capacity_gb" ) @ConfigurableField( description = "Total ObjectStorage storage capacity for Objects soley for reporting usage percentage. Not a size restriction. No enforcement of this value", displayName = "ObjectStorage object capacity (GB)", initialInt = Integer.MAX_VALUE ) protected Integer max_total_reporting_capacity_gb; @Column( name = "failed_put_timeout_hrs" ) @ConfigurableField(description = "Number of hours to wait for object PUT operations to be allowed to complete before cleanup.", displayName = "Object PUT failure cleanup (Hours)", initialInt = DEFAULT_PUT_TIMEOUT_HOURS ) protected Integer failed_put_timeout_hrs; @Column( name = "cleanup_task_interval_seconds" ) @ConfigurableField(description = "Interval, in seconds, at which cleanup tasks are initiated for removing old/stale objects.", displayName = "Cleanup interval (seconds)", initialInt = DEFAULT_CLEANUP_INTERVAL_SEC ) protected Integer cleanup_task_interval_seconds; @Column( name = "bucket_creation_wait_interval_seconds" ) @ConfigurableField( description = "Interval, in seconds, during which buckets in creating-state are valid. After this interval, the operation is assumed failed.", displayName = "Operation wait interval (seconds)", initialInt = DEFAULT_CLEANUP_INTERVAL_SEC ) protected Integer bucket_creation_wait_interval_seconds; @Column( name = "bucket_naming_restrictions" ) @ConfigurableField(description = "The S3 bucket naming restrictions to enforce. Values are 'dns-compliant' or 'extended'. " + "Default is 'extended'. dns_compliant is non-US region S3 names, extended is for US-Standard Region naming. " + "See http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html", displayName = "Bucket Naming restrictions", changeListener = BucketNamingRestrictionsValidator.class, initial = DEFAULT_BUCKET_NAMING_SCHEME ) protected String bucket_naming_restrictions; @Column( name = "dogetputoncopyfail" ) @ConfigurableField(description = "Should provider client attempt a GET / PUT when backend does not support Copy operation", displayName = "attempt GET/PUT on Copy fail", type = ConfigurableFieldType.BOOLEAN, initial = "false" ) protected Boolean doGetPutOnCopyFail; @Column( name = "providerclient" ) @ConfigurableField(description = "Object Storage Provider client to use for backend", displayName = "Object Storage Provider Client", changeListener = ObjectStorageProviderChangeListener.class) protected String providerClient; // configured by user to specify which back-end client to use @Column( name = "bucket_reserved_cnames" ) @ConfigurableField(description = "List of host names that may not be used as bucket cnames") protected String bucket_reserved_cnames; @Column( name = "max_tags" ) @ConfigurableField( initial = "50", description = "Maximum number of user defined tags for a bucket.", changeListener = PropertyChangeListeners.IsNonNegativeInteger.class) protected Integer max_tags; @ConfigurableInit protected ObjectStorageGlobalConfiguration initializeDefaults() { this.setBucket_creation_wait_interval_seconds(DEFAULT_CLEANUP_INTERVAL_SEC); this.setBucket_naming_restrictions(DEFAULT_BUCKET_NAMING_SCHEME); this.setCleanup_task_interval_seconds(DEFAULT_CLEANUP_INTERVAL_SEC); this.setDoGetPutOnCopyFail(DEFAULT_COPY_UNSUPPORTED_STRATEGY); this.setFailed_put_timeout_hrs(DEFAULT_PUT_TIMEOUT_HOURS); this.setMax_buckets_per_account(DEFAULT_MAX_BUCKETS_PER_ACCOUNT); this.setMax_metadata_request_size(DEFAULT_MAX_METADATA_REQUEST_SIZE); this.setMax_total_reporting_capacity_gb(Integer.MAX_VALUE); this.setMax_tags(DEFAULT_MAX_TAGS); return this; } /** * Validator for values to set restrictions to */ public static class BucketNamingRestrictionsValidator implements PropertyChangeListener { @Override public void fireChange(ConfigurableProperty t, Object newValue) throws ConfigurablePropertyException { String proposed = (String) newValue; if (Strings.isNullOrEmpty(proposed) || (!"extended".equals(proposed) && !"dns-compliant".equals(proposed))) { throw Exceptions.toUndeclared("Invalid property value: " + proposed + " Acceptabled values are: 'extended' and 'dns-compliant'", new NoSuchElementException(proposed)); } }; } public Integer getMax_metadata_request_size() { return max_metadata_request_size != null ? max_metadata_request_size : DEFAULT_MAX_METADATA_REQUEST_SIZE; } public void setMax_metadata_request_size(Integer max_metadata_request_size) { this.max_metadata_request_size = max_metadata_request_size; } public Integer getMax_buckets_per_account() { return max_buckets_per_account; } public void setMax_buckets_per_account(Integer max_buckets_per_account) { this.max_buckets_per_account = max_buckets_per_account; } public Integer getMax_total_reporting_capacity_gb() { return max_total_reporting_capacity_gb; } public void setMax_total_reporting_capacity_gb(Integer max_total_reporting_capacity_gb) { this.max_total_reporting_capacity_gb = max_total_reporting_capacity_gb; } public Integer getFailed_put_timeout_hrs() { return failed_put_timeout_hrs; } public void setFailed_put_timeout_hrs(Integer failed_put_timeout_hrs) { this.failed_put_timeout_hrs = failed_put_timeout_hrs; } public Integer getCleanup_task_interval_seconds() { return cleanup_task_interval_seconds; } public void setCleanup_task_interval_seconds(Integer cleanup_task_interval_seconds) { this.cleanup_task_interval_seconds = cleanup_task_interval_seconds; } public Integer getBucket_creation_wait_interval_seconds() { return bucket_creation_wait_interval_seconds; } public void setBucket_creation_wait_interval_seconds(Integer bucket_creation_wait_interval_seconds) { this.bucket_creation_wait_interval_seconds = bucket_creation_wait_interval_seconds; } public String getBucket_naming_restrictions() { return bucket_naming_restrictions; } public void setBucket_naming_restrictions(String bucket_naming_restrictions) { this.bucket_naming_restrictions = bucket_naming_restrictions; } public Boolean getDoGetPutOnCopyFail() { return doGetPutOnCopyFail; } public void setDoGetPutOnCopyFail(Boolean doGetPutOnCopyFail) { this.doGetPutOnCopyFail = doGetPutOnCopyFail; } public String getProviderClient() { return providerClient; } public void setProviderClient(String providerClient) { this.providerClient = providerClient; } public String getBucket_reserved_cnames( ) { return bucket_reserved_cnames; } public void setBucket_reserved_cnames( final String bucket_reserved_cnames ) { this.bucket_reserved_cnames = bucket_reserved_cnames; } public Integer getMax_tags( ) { return MoreObjects.firstNonNull( max_tags, 10 ); // use old hard-coded limit if null } public void setMax_tags( final Integer max_tags ) { this.max_tags = max_tags; } @PrePersist @PreUpdate public void updateDefaults() { if (max_metadata_request_size == null) { max_metadata_request_size = DEFAULT_MAX_METADATA_REQUEST_SIZE; } if (max_tags==null) { max_tags = 10; // initialize to old hard-coded limit } } /** * Gets this config from the DB. May throw an exception on db failure * * @return * @throws Exception */ public static ObjectStorageGlobalConfiguration getConfiguration() { try { try { return Transactions.find(new ObjectStorageGlobalConfiguration()); } catch (NoSuchElementException e) { // Initialize; LOG.info("No extant S3 provider configuration found. Initializing defaults"); return Transactions.saveDirect(new ObjectStorageGlobalConfiguration().initializeDefaults()); } } catch (Throwable f) { throw Exceptions.toUndeclared("Failed getting and/or initializing OSG global configuration", f); } } @Override public String toString() { String value = "[OSG Global configuration: " + "MaxTotalCapacity=" + max_total_reporting_capacity_gb + " , " + "MaxBucketsPerAccount=" + max_buckets_per_account + " , " + "FailedPutTimeoutHrs=" + failed_put_timeout_hrs + " , " + "CleanupTaskIntervalSec=" + cleanup_task_interval_seconds + " , " + "BucketCreationWaitIntervalSec=" + bucket_creation_wait_interval_seconds + " , " + "BucketNamingRestrictions=" + bucket_naming_restrictions + " , " + "DoGetPutOnCopyFail=" + doGetPutOnCopyFail + " , " + "ProviderClient=" + providerClient + "]"; return value; } /** * Upgrade code to copy walrus config into osg * * @throws Exception */ @EntityUpgrade(entities = {ObjectStorageGlobalConfiguration.class}, since = Upgrades.Version.v4_0_0, value = ObjectStorage.class) public static enum OSGConfigUpgrade implements Predicate<Class> { INSTANCE; @Override public boolean apply(@Nullable Class arg0) { try (TransactionResource trans = Entities.transactionFor(arg0)) { // Set defaults to the values from Walrus in 3.4.x ObjectStorageGlobalConfiguration config = new ObjectStorageGlobalConfiguration().initializeDefaults(); config = Entities.merge(config); WalrusInfo walrusConfig = WalrusInfo.getWalrusInfo(); config.setMax_buckets_per_account(walrusConfig.getStorageMaxBucketsPerAccount()); config.setBucket_naming_restrictions((walrusConfig.getBucketNamesRequireDnsCompliance() ? "dns-compliant" : "extended")); config.setProviderClient("walrus"); // set the provider client to walrus since its an upgrade to 4.0.0 trans.commit(); return true; } catch (Exception e) { LOG.error("Error saving upgrade global osg configuration", e); throw Exceptions.toUndeclared("Error upgrading walrus config to OSG config", e); } } } /** * Upgrade code to copy global provider client to osg config * */ @EntityUpgrade(entities = {ObjectStorageGlobalConfiguration.class}, since = Upgrades.Version.v4_1_0, value = ObjectStorage.class) public static enum ProviderClientUpgrade implements Predicate<Class> { INSTANCE; private static final Logger LOG = Logger.getLogger(ProviderClientUpgrade.class); @Override public boolean apply(@Nullable Class arg0) { try (TransactionResource tr = Entities.transactionFor(arg0)) { // look for global config ObjectStorageGlobalConfiguration osgc = null; try { osgc = Entities.uniqueResult(new ObjectStorageGlobalConfiguration()); } catch (NoSuchElementException e) { osgc = new ObjectStorageGlobalConfiguration().initializeDefaults(); Entities.persist(osgc); // create global config with defaults } // check if provider client is set if (StringUtils.isNotBlank(osgc.getProviderClient())) { // This could be the case if cloud was upgraded from 3.4.x release. Upgrade logic to 4.0.0 (OSGConfigUpgrade) may have populated the // provider client. Don't overwrite the provider client, just move on LOG.info("Nothing to upgrade as objectstorage provider client is already configured to " + osgc.getProviderClient()); } else { // Cloud was upgraded from 4.0.x release, look for provider client property in static global properties table and copy it String prevClient = getStaticProviderClient(); if (StringUtils.isNotBlank(prevClient)) { LOG.info("Found global objectstorage.providerclient=" + prevClient + ". Copying value to providerClient of ObjectStorageGlobalConfiguration entity"); osgc.setProviderClient(prevClient); } else { // else block should never be hit LOG.info("Global objectstorage.providerclient not found. Defaulting providerClient of ObjectStorageGlobalConfiguration entity to walrus"); osgc.setProviderClient("walrus"); } } tr.commit(); return true; } catch (Exception e) { LOG.error("Error upgrading global osg configuration", e); throw Exceptions.toUndeclared("Error upgrading global osg configuration", e); } } /* * Get the value of objectstorage.providerclient stored in config_static_property table. Had to use sql directly as StaticDatabasePropertyEntry * has a private constructor that restricts its usage for lookup/delete operations */ private String getStaticProviderClient() { String propertyValue = null; Sql sql = null; try { sql = DatabaseFilters.NEWVERSION.getConnection("eucalyptus_config"); String query = "select config_static_field_value from config_static_property where config_static_prop_name='objectstorage.providerclient'"; try { // Look for static property objectstorage.providerclient List<GroovyRowResult> result = sql.rows(query); if (result != null && result.size() == 1 && result.get(0) != null) { propertyValue = (String) result.get(0).getProperty("config_static_field_value"); // Delete the static property query = "delete from config_static_property where config_static_prop_name='objectstorage.providerclient'"; sql.execute(query); } else { // static property not found, nothing to return here } } catch (Exception e) { LOG.warn("Failed to execute query: " + query, e); } } catch (Exception e) { LOG.warn("Failed to connect to database", e); } finally { try { if (sql != null) { sql.close(); } } catch (Exception e) { LOG.warn("Failed to close database connection", e); } } return propertyValue; } } @EntityUpgrade(entities = {ObjectStorageGlobalConfiguration.class}, since = Upgrades.Version.v4_4_0, value = ObjectStorage.class) public enum OSG44ConfigUpgrade implements Predicate<Class> { INSTANCE; @Override public boolean apply(@Nullable Class arg0) { try (TransactionResource trans = Entities.transactionFor(arg0)) { ObjectStorageGlobalConfiguration config; try { config = Entities.uniqueResult(new ObjectStorageGlobalConfiguration()); } catch (NoSuchElementException e) { config = new ObjectStorageGlobalConfiguration().initializeDefaults(); } config.updateDefaults(); Entities.persist(config); trans.commit(); return true; } catch (Exception e) { String msg = "Error saving OSG 4.4 configuration upgrade"; LOG.error(msg, e); throw Exceptions.toUndeclared(msg, e); } } } @EntityUpgrade(entities = ObjectStorageGlobalConfiguration.class, since = Upgrades.Version.v5_0_0, value = ObjectStorage.class) public enum OSG50ConfigUpgrade implements Predicate<Class> { INSTANCE; @Override public boolean apply( @Nullable Class arg0 ) { try ( TransactionResource trans = Entities.transactionFor( ObjectStorageGlobalConfiguration.class ) ) { final Optional<ObjectStorageGlobalConfiguration> config = Entities.criteriaQuery( ObjectStorageGlobalConfiguration.class ).uniqueResultOption( ); config.transform( it -> { it.setBucket_reserved_cnames( "*" ); return it; } ); trans.commit( ); return true; } catch (Exception e) { String msg = "Error saving OSG 5.0 configuration upgrade"; LOG.error( msg, e ); throw Exceptions.toUndeclared(msg, e); } } } /** * Change listener for the osg provider client setting. * * @author zhill * */ public static class ObjectStorageProviderChangeListener implements PropertyChangeListener<String> { /* * Ensures that the proposed value is valid based on the set of valid values for OSGs Additional DB lookup required for remote OSGs where the CLC * doesn't have the OSG bits installed and therefore doesn't have the same view of the set of valid values. (non-Javadoc) * * @see com.eucalyptus.configurable.PropertyChangeListener#fireChange(com.eucalyptus.configurable.ConfigurableProperty, java.lang.Object) */ @Override public void fireChange(ConfigurableProperty t, String newValue) throws ConfigurablePropertyException { String existingValue = (String) t.getValue(); List<ServiceConfiguration> objConfigs = null; try { objConfigs = ServiceConfigurations.list(ObjectStorage.class); } catch (NoSuchElementException e) { throw new ConfigurablePropertyException("No ObjectStorage configurations found"); } final String proposedValue = newValue; final Set<String> validEntries = Sets.newHashSet(); try (TransactionResource tr = Entities.transactionFor(ObjectStorageConfiguration.class)) { boolean match = Components.services( ObjectStorage.class ).find( config -> { if ( config.isVmLocal( ) || Hosts.isCoordinator( ) ) { // Add locally discovered entries to the valid list validEntries.addAll(ObjectStorageProviders.list()); } if ( !config.isVmLocal( ) ) { // Remote OSG, so check the db for the list of valid entries. try { final ObjectStorageConfiguration objConfig = Entities.uniqueResult((ObjectStorageConfiguration) config); if ( objConfig.getAvailableClients() != null ) { for ( final String entry : Splitter.on(",").split(objConfig.getAvailableClients()) ) { validEntries.add(entry); } } } catch (Exception ignore) { } } return validEntries.contains(proposedValue); } ).isDefined( ); tr.commit(); if (!match) { // Nothing matched. throw new ConfigurablePropertyException("Cannot modify " + t.getQualifiedName() + "." + t.getFieldName() + " new value is not a valid value. " + "Legal values are: " + Joiner.on(",").join(validEntries)); } else { // matching provider client found } } } } }