/*************************************************************************
* Copyright 2009-2014 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.
************************************************************************/
package com.eucalyptus.database.activities;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.log4j.Logger;
import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.principal.AccountIdentifiers;
import com.eucalyptus.autoscaling.common.msgs.AutoScalingGroupType;
import com.eucalyptus.autoscaling.common.msgs.DescribeAutoScalingGroupsResponseType;
import com.eucalyptus.autoscaling.common.msgs.Instance;
import com.eucalyptus.autoscaling.common.msgs.TagDescription;
import com.eucalyptus.bootstrap.Bootstrap;
import com.eucalyptus.bootstrap.Bootstrapper;
import com.eucalyptus.bootstrap.DatabaseInfo;
import com.eucalyptus.bootstrap.DependsLocal;
import com.eucalyptus.bootstrap.Provides;
import com.eucalyptus.bootstrap.RunDuring;
import com.eucalyptus.cloudwatch.common.CloudWatch;
import com.eucalyptus.cloudwatch.common.CloudWatchBackend;
import com.eucalyptus.component.Topology;
import com.eucalyptus.component.id.Eucalyptus;
import com.eucalyptus.compute.common.ClusterInfoType;
import com.eucalyptus.compute.common.RunningInstancesItemType;
import com.eucalyptus.compute.common.Volume;
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.configurable.PropertyDirectory;
import com.eucalyptus.crypto.Crypto;
import com.eucalyptus.entities.PersistenceContexts;
import com.eucalyptus.event.ClockTick;
import com.eucalyptus.event.EventListener;
import com.eucalyptus.event.Listeners;
import com.eucalyptus.resources.PropertyChangeListeners;
import com.eucalyptus.resources.client.AutoScalingClient;
import com.eucalyptus.resources.client.Ec2Client;
import com.eucalyptus.scripting.Groovyness;
import com.eucalyptus.system.Threads;
import com.eucalyptus.util.EucalyptusCloudException;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
// Configuration properties disabled for EUCA-12016
//@ConfigurableClass(root = "services.database.worker", description = "Parameters controlling database information.", singleton = true)
public class DatabaseServerProperties {
private static Logger LOG = Logger.getLogger(DatabaseServerProperties.class);
@ConfigurableField(displayName = "configured", description = "Configure DB service so a VM can be launched."
+ "If something goes south with the service there is a chance that setting it to false and back to true would solve issues",
initial = "false", readonly = false, type = ConfigurableFieldType.BOOLEAN,
changeListener = EnabledChangeListener.class)
public static boolean CONFIGURED = false;
@ConfigurableField(displayName = "image", description = "EMI containing database server",
initial = "NULL", readonly = false, type = ConfigurableFieldType.KEYVALUE,
changeListener = PropertyChangeListeners.EmiChangeListener.class)
public static String IMAGE = "NULL";
@ConfigurableField(displayName = "volume", description = "volume containing database files",
initial = "NULL", readonly = false, type = ConfigurableFieldType.KEYVALUE,
changeListener = VolumeChangeListener.class)
public static String VOLUME = "NULL";
@ConfigurableField(displayName = "instance_type", description = "instance type for database server",
initial = "m1.small", readonly = false, type = ConfigurableFieldType.KEYVALUE,
changeListener = PropertyChangeListeners.InstanceTypeChangeListener.class)
public static String INSTANCE_TYPE = "m1.small";
@ConfigurableField(displayName = "availability_zones", description = "availability zones for database server",
initial = "", readonly = false, type = ConfigurableFieldType.KEYVALUE,
changeListener = PropertyChangeListeners.AvailabilityZonesChangeListener.class)
public static String AVAILABILITY_ZONES = "";
@ConfigurableField(displayName = "keyname", description = "keyname to use when debugging database server",
readonly = false, type = ConfigurableFieldType.KEYVALUE,
changeListener = KeyNameChangeListener.class)
public static String KEYNAME = null;
@ConfigurableField(displayName = "ntp_server", description = "address of the NTP server used by database server",
readonly = false, type = ConfigurableFieldType.KEYVALUE,
changeListener = PropertyChangeListeners.NTPServerChangeListener.class)
public static String NTP_SERVER = null;
@ConfigurableField(displayName = "expiration_days", description = "days after which the VMs expire",
readonly = false, initial = "180", type = ConfigurableFieldType.KEYVALUE,
changeListener = PropertyChangeListeners.PositiveNumberChangeListener.class)
public static String EXPIRATION_DAYS = "180";
@ConfigurableField(displayName = "init_script", description = "bash script that will be executed before service"
+ "configuration and start up", readonly = false, type = ConfigurableFieldType.KEYVALUE)
public static String INIT_SCRIPT = null;
@Provides(CloudWatchBackend.class)
@RunDuring(Bootstrap.Stage.Final)
@DependsLocal(CloudWatchBackend.class)
public static class CloudWatchBackendPropertyBootstrapper extends
DatabaseServerPropertyBootstrapper {
}
@Provides(CloudWatch.class)
@RunDuring(Bootstrap.Stage.Final)
@DependsLocal(CloudWatch.class)
public static class CloudWatchPropertyBootstrapper extends
DatabaseServerPropertyBootstrapper {
}
public abstract static class DatabaseServerPropertyBootstrapper extends
Bootstrapper.Simple {
@Override
public boolean check() throws Exception {
if ("localhost"
.equals(DatabaseInfo.getDatabaseInfo().getAppendOnlyHost()))
return true;
String vmHost = null;
try {
final DatabaseInfo dbInfo = DatabaseInfo.getDatabaseInfo();
// get the host addr
vmHost = dbInfo.getAppendOnlyHost();
// get the port
final String port = dbInfo.getAppendOnlyPort();
final String userName = dbInfo.getAppendOnlyUser();
final String password = dbInfo.getAppendOnlyPassword();
// get jdbc url and ping
final String jdbcUrl = EventHandlerChainEnableVmDatabase.WaitOnDb
.getJdbcUrlWithSsl(vmHost, Integer.parseInt(port));
if (EventHandlerChainEnableVmDatabase.WaitOnDb.pingDatabase(jdbcUrl,
userName, password))
return true;
else
return false;
} catch (final Exception ex) {
LOG.warn("Error pinging append-only database at " + vmHost);
return false;
}
}
@Override
public boolean enable() throws Exception {
synchronized (DatabaseServerPropertyBootstrapper.class) {
if (PersistenceContexts.remoteConnected())
return true;
try {
Groovyness.run("setup_persistence_remote.groovy");
LOG.info("Remote persistence contexts are initialized");
} catch (final Exception ex) {
LOG.error("Failed to setup remote persistence contexts", ex);
return false;
}
return true;
}
}
}
public static final String DEFAULT_LAUNCHER_TAG = "euca-internal-db-servers";
public static final String REPORTING_DB_NAME = "euca-internal-remote-reporting";
private static AtomicBoolean launchLock = new AtomicBoolean();
private static final String DB_INSTANCE_IDENTIFIER = "postgresql";
private static final int DB_PORT = 5432;
private static class CreateDBRunner implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
try {
String masterPassword = DatabaseInfo.getDatabaseInfo()
.getAppendOnlyPassword();
if (masterPassword == null || masterPassword.length() <= 0)
masterPassword = Crypto.generateAlphanumericId(25).toLowerCase();
final String masterUserName = "eucalyptus";
boolean vmCreated = false;
try {
// creates a random password for master db user
final NewDBInstanceEvent evt = new NewDBInstanceEvent(Accounts
.lookupSystemAccountByAlias(AccountIdentifiers.DATABASE_SYSTEM_ACCOUNT).getUserId());
evt.setMasterUserName(masterUserName);
evt.setMasterUserPassword(masterPassword);
evt.setDbInstanceIdentifier(DB_INSTANCE_IDENTIFIER);
evt.setPort(DB_PORT);
evt.setDbName(REPORTING_DB_NAME);
DatabaseEventListeners.getInstance().fire(evt);
vmCreated = true;
} catch (final Exception e) {
LOG.error("failed to create a database vm", e);
vmCreated = false;
}
if (!vmCreated)
return false;
try {
final EnableDBInstanceEvent evt = new EnableDBInstanceEvent(Accounts
.lookupSystemAccountByAlias(AccountIdentifiers.DATABASE_SYSTEM_ACCOUNT).getUserId());
evt.setMasterUserName(masterUserName);
evt.setMasterUserPassword(masterPassword);
evt.setDbInstanceIdentifier(DB_INSTANCE_IDENTIFIER);
evt.setPort(DB_PORT);
DatabaseEventListeners.getInstance().fire(evt);
} catch (final Exception e) {
LOG.error("failed to enable remote database", e);
throw e;
}
} catch (final Exception ex) {
return false;
} finally {
launchLock.set(false);
}
LOG.info("New remote database is created");
return true;
}
}
private static class DeleteDBRunner implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
try {
final DisableDBInstanceEvent evt = new DisableDBInstanceEvent(Accounts
.lookupSystemAccountByAlias(AccountIdentifiers.DATABASE_SYSTEM_ACCOUNT).getUserId());
evt.setDbInstanceIdentifier(DB_INSTANCE_IDENTIFIER);
DatabaseEventListeners.getInstance().fire(evt);
} catch (final Exception e) {
launchLock.set(false);
return false;
}
LOG.info("Remote database is disabled");
try {
final DeleteDBInstanceEvent evt = new DeleteDBInstanceEvent(Accounts
.lookupSystemAccountByAlias(AccountIdentifiers.DATABASE_SYSTEM_ACCOUNT).getUserId());
evt.setDbInstanceIdentifier(DB_INSTANCE_IDENTIFIER);
DatabaseEventListeners.getInstance().fire(evt);
} catch (final Exception e) {
LOG.error("failed to handle DeleteDbInstanceEvent", e);
return false;
} finally {
launchLock.set(false);
}
LOG.info("Database worker stack is destroyed");
return true;
}
}
public static class KeyNameChangeListener implements PropertyChangeListener<String> {
@Override
public void fireChange(ConfigurableProperty t, String keyname)
throws ConfigurablePropertyException {
if(t.getValue()!=null && t.getValue().equals(keyname))
return;
if (keyname == null || keyname.isEmpty())
return;
try {
Ec2Client.getInstance().describeKeyPairs(Accounts.lookupSystemAccountByAlias(
AccountIdentifiers.DATABASE_SYSTEM_ACCOUNT ).getUserId( ),
Lists.newArrayList(keyname));
} catch (final Exception e) {
throw new ConfigurablePropertyException("Could not change key name due to: "
+ e.getMessage() + ". Are you using keypair that belongs to "
+ AccountIdentifiers.DATABASE_SYSTEM_ACCOUNT + " account?");
}
}
}
public static class EnabledChangeListener implements
PropertyChangeListener<String> {
@Override
public void fireChange(ConfigurableProperty t, String newValue)
throws ConfigurablePropertyException {
try {
if ("false".equalsIgnoreCase(newValue)
&& "true".equalsIgnoreCase(t.getValue())) {
// / disable vm-based database
if (!launchLock.compareAndSet(false, true))
throw new ConfigurablePropertyException(
"the property is currently being updated");
try {
Callable<Boolean> disableDbRun = new DeleteDBRunner();
Threads.enqueue(Eucalyptus.class, DatabaseServerProperties.class,
disableDbRun);
} catch (final Exception ex) {
throw ex;
}
} else if ("true".equalsIgnoreCase(newValue)
&& "false".equalsIgnoreCase(t.getValue())) {
// / enable vm-based database
if (!launchLock.compareAndSet(false, true))
throw new ConfigurablePropertyException(
"the property is currently being updated");
try {
Callable<Boolean> newDbRun = new CreateDBRunner();
Threads.enqueue(Eucalyptus.class, DatabaseServerProperties.class,
newDbRun);
} catch (final Exception ex) {
throw ex;
}
} else
; // do nothing
} catch (final Exception e) {
throw new ConfigurablePropertyException(
"Could not toggle database server", e);
}
}
}
public static class VolumeChangeListener implements
PropertyChangeListener<String> {
@Override
public void fireChange(ConfigurableProperty t, String volumeId)
throws ConfigurablePropertyException {
if (t.getValue() != null && t.getValue().equals(volumeId))
return;
try {
if (!volumeId.toLowerCase().startsWith("vol-"))
throw new EucalyptusCloudException("Invalid volume id");
final List<Volume> volumes = Ec2Client.getInstance().describeVolumes(
null, Lists.newArrayList(volumeId));
if (volumes == null
|| volumes.size() != 1
|| !(volumeId.equals(volumes.get(0).getVolumeId()) && "available"
.equals(volumes.get(0).getStatus()))) {
throw new EucalyptusCloudException("There is no volume with id "
+ volumeId + " in available status");
}
} catch (final Exception e) {
throw new ConfigurablePropertyException(
"Could not change VOLUME ID due to: " + e.getMessage());
}
}
}
static final Set<String> configuredZones = Sets.newHashSet();
public static List<String> listConfiguredZones() throws Exception {
if (configuredZones.size() <= 0) {
List<String> allZones = Lists.newArrayList();
try {
final List<ClusterInfoType> clusters = Ec2Client.getInstance()
.describeAvailabilityZones(null, false);
for (final ClusterInfoType c : clusters)
allZones.add(c.getZoneName());
} catch (final Exception ex) {
throw new Exception("failed to lookup availability zones", ex);
}
if (AVAILABILITY_ZONES != null && AVAILABILITY_ZONES.length() > 0) {
if (AVAILABILITY_ZONES.contains(",")) {
final String[] tokens = AVAILABILITY_ZONES.split(",");
for (final String zone : tokens) {
if (allZones.contains(zone))
configuredZones.add(zone);
}
} else {
if (allZones.contains(AVAILABILITY_ZONES))
configuredZones.add(AVAILABILITY_ZONES);
}
} else {
configuredZones.addAll(allZones);
}
}
return Lists.newArrayList(configuredZones);
}
public static class RemoteDatabaseChecker implements EventListener<ClockTick> {
static final int CHECK_EVERY_SECONDS = 60;
static Date lastChecked = null;
public static void register() {
Listeners.register(ClockTick.class, new RemoteDatabaseChecker());
}
@Override
public void fireEvent(ClockTick event) {
if (!(Bootstrap.isOperational() && Topology
.isEnabledLocally(Eucalyptus.class)))
return;
if (Topology.isEnabled(CloudWatchBackend.class))
return;
if (lastChecked == null) {
lastChecked = new Date();
} else {
int elapsedSec = (int) (((new Date()).getTime() - lastChecked.getTime()) / 1000.0);
if (elapsedSec < CHECK_EVERY_SECONDS) {
return;
}
lastChecked = new Date();
}
try {
final ConfigurableProperty hostProp = PropertyDirectory
.getPropertyEntry("services.database.appendonlyhost");
if ("localhost".equals(hostProp.getValue()))
return;
} catch (final Exception ex) {
return;
}
// describe autoscaling group and finds the instances
final List<String> instances = Lists.newArrayList();
String asgName = null;
try {
final List<TagDescription> tags = AutoScalingClient.getInstance()
.describeAutoScalingTags(null);
for (final TagDescription tag : tags) {
if (DEFAULT_LAUNCHER_TAG.equals(tag.getValue())) {
asgName = tag.getResourceId();
break;
}
}
} catch (final Exception ex) {
return; // ASG not created yet; do nothing.
}
if (asgName == null)
return;
AutoScalingGroupType asgType = null;
try {
final DescribeAutoScalingGroupsResponseType resp = AutoScalingClient
.getInstance().describeAutoScalingGroups(null,
Lists.newArrayList(asgName));
if (resp.getDescribeAutoScalingGroupsResult() != null
&& resp.getDescribeAutoScalingGroupsResult().getAutoScalingGroups() != null
&& resp.getDescribeAutoScalingGroupsResult().getAutoScalingGroups()
.getMember() != null
&& resp.getDescribeAutoScalingGroupsResult().getAutoScalingGroups()
.getMember().size() > 0) {
asgType = resp.getDescribeAutoScalingGroupsResult()
.getAutoScalingGroups().getMember().get(0);
}
if (asgType.getInstances() != null
&& asgType.getInstances().getMember() != null)
instances.addAll(Collections2.transform(asgType.getInstances()
.getMember(), new Function<Instance, String>() {
@Override
public String apply(Instance arg0) {
return arg0.getInstanceId();
}
}));
} catch (final Exception ex) {
LOG.warn("Can't find autoscaling group named " + asgName);
return;
}
// get the ip address of the running instance
final List<String> runningIps = Lists.newArrayList();
try {
final List<RunningInstancesItemType> ec2Instances = Ec2Client
.getInstance().describeInstances(null, instances);
for (final RunningInstancesItemType inst : ec2Instances) {
if ("running".equals(inst.getStateName())) {
runningIps.add(inst.getIpAddress());
}
}
} catch (final Exception ex) {
LOG.warn("Can't get the ip address of the running instance", ex);
return;
}
if (runningIps.size() > 1) {
LOG.warn("There are more than 1 instances running remote databases.");
} else if (runningIps.size() == 0) {
return;
}
final String instanceIp = runningIps.get(0);
if (instanceIp == null || instanceIp.length() <= 0) {
LOG.warn("Invalid IP address for the instance running remote databases.");
return;
}
// see if the ip address matches with the property
// if not update the property
try {
final ConfigurableProperty hostProp = PropertyDirectory
.getPropertyEntry("services.database.appendonlyhost");
final String curHost = hostProp.getValue();
if ("localhost".equals(curHost))
return;
else if (!instanceIp.equals(curHost)) {
hostProp.setValue(instanceIp);
LOG.info("Updated the property services.database.appendonlyhost to "
+ instanceIp);
}
} catch (final Exception ex) {
LOG.error(
"Failed to update the property: services.database.appendonlyhost",
ex);
}
}
}
}