/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.location.byon;
import static org.apache.brooklyn.util.groovy.GroovyJavaMethods.truth;
import java.io.Closeable;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.api.location.MachineLocationCustomizer;
import org.apache.brooklyn.api.location.MachineProvisioningLocation;
import org.apache.brooklyn.api.location.NoMachinesAvailableException;
import org.apache.brooklyn.api.mgmt.LocationManager;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.location.AbstractLocation;
import org.apache.brooklyn.core.location.cloud.CloudLocationConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.CollectionFunctionals;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.WildcardGlobs;
import org.apache.brooklyn.util.text.WildcardGlobs.PhraseTreatment;
/**
* A provisioner of {@link MachineLocation}s which takes a list of machines it can connect to.
* The collection of initial machines should be supplied in the 'machines' flag in the constructor,
* for example a list of machines which can be SSH'd to.
*
* This can be extended to have a mechanism to make more machines to be available
* (override provisionMore and canProvisionMore).
*/
public class FixedListMachineProvisioningLocation<T extends MachineLocation> extends AbstractLocation
implements MachineProvisioningLocation<T>, Closeable {
// TODO Synchronization looks very wrong for accessing machines/inUse
// e.g. removeChild doesn't synchronize when doing machines.remove(...),
// and getMachines() returns the real sets risking
// ConcurrentModificationException in the caller if it iterates over them etc.
private static final Logger log = LoggerFactory.getLogger(FixedListMachineProvisioningLocation.class);
public static final ConfigKey<Function<Iterable<? extends MachineLocation>, MachineLocation>> MACHINE_CHOOSER =
ConfigKeys.newConfigKey(
new TypeToken<Function<Iterable<? extends MachineLocation>, MachineLocation>>() {},
"byon.machineChooser",
"For choosing which of the possible machines is chosen and returned by obtain()",
CollectionFunctionals.<MachineLocation>firstElement());
public static final ConfigKey<Collection<MachineLocationCustomizer>> MACHINE_LOCATION_CUSTOMIZERS = CloudLocationConfig.MACHINE_LOCATION_CUSTOMIZERS;
private final Object lock = new Object();
@SetFromFlag
protected Set<T> machines;
@SetFromFlag
protected Set<T> inUse;
@SetFromFlag
protected Set<T> pendingRemoval;
@SetFromFlag
protected Map<T, Map<String, Object>> origConfigs;
public FixedListMachineProvisioningLocation() {
this(Maps.newLinkedHashMap());
}
public FixedListMachineProvisioningLocation(Map properties) {
super(properties);
if (isLegacyConstruction()) {
init();
}
}
@Override
public void init() {
super.init();
Set<T> machinesCopy = MutableSet.of();
for (T location: machines) {
if (location==null) {
log.warn(""+this+" initialized with null location, removing (may be due to rebind with reference to an unmanaged location)");
} else {
Location parent = location.getParent();
if (parent == null) {
addChild(location);
}
machinesCopy.add(location);
}
}
if (!machinesCopy.equals(machines)) {
machines = machinesCopy;
}
}
@Override
public String toVerboseString() {
return Objects.toStringHelper(this).omitNullValues()
.add("id", getId()).add("name", getDisplayName())
.add("machinesAvailable", getAvailable()).add("machinesInUse", getInUse())
.toString();
}
@Override
public AbstractLocation configure(Map<?,?> properties) {
if (machines == null) machines = Sets.newLinkedHashSet();
if (inUse == null) inUse = Sets.newLinkedHashSet();
if (pendingRemoval == null) pendingRemoval = Sets.newLinkedHashSet();
if (origConfigs == null) origConfigs = Maps.newLinkedHashMap();
return super.configure(properties);
}
@SuppressWarnings("unchecked")
public FixedListMachineProvisioningLocation<T> newSubLocation(Map<?,?> newFlags) {
// TODO shouldn't have to copy config bag as it should be inherited (but currently it is not used inherited everywhere; just most places)
return getManagementContext().getLocationManager().createLocation(LocationSpec.create(getClass())
.parent(this)
.configure(config().getLocalBag().getAllConfig()) // FIXME Should this just be inherited?
.configure(newFlags));
}
@Override
public void close() {
for (T machine : machines) {
if (machine instanceof Closeable) Streams.closeQuietly((Closeable)machine);
}
}
public void addMachine(T machine) {
synchronized (lock) {
if (machines.contains(machine)) {
throw new IllegalArgumentException("Cannot add "+machine+" to "+toString()+", because already contained");
}
Location existingParent = ((Location)machine).getParent();
if (existingParent == null) {
addChild(machine);
}
machines.add(machine);
}
}
public void removeMachine(T machine) {
synchronized (lock) {
if (inUse.contains(machine)) {
pendingRemoval.add(machine);
} else {
machines.remove(machine);
pendingRemoval.remove(machine);
if (this.equals(machine.getParent())) {
removeChild((Location)machine);
}
}
}
}
protected Set<T> getMachines() {
return machines;
}
public Set<T> getAvailable() {
Set<T> a = Sets.newLinkedHashSet(machines);
a.removeAll(inUse);
return a;
}
public Set<T> getInUse() {
return Sets.newLinkedHashSet(inUse);
}
public Set<T> getAllMachines() {
return ImmutableSet.copyOf(machines);
}
@Override
public void addChild(Location child) {
super.addChild(child);
machines.add((T)child);
}
@Override
public boolean removeChild(Location child) {
if (inUse.contains(child)) {
throw new IllegalStateException("Child location "+child+" is in use; cannot remove from "+this);
}
machines.remove(child);
return super.removeChild(child);
}
protected boolean canProvisionMore() {
return false;
}
protected void provisionMore(int size) {
provisionMore(size, ImmutableMap.of());
}
protected void provisionMore(int size, Map<?,?> flags) {
throw new IllegalStateException("more not permitted");
}
public T obtain() throws NoMachinesAvailableException {
return obtain(Maps.<String,Object>newLinkedHashMap());
}
@Override
public T obtain(Map<?,?> flags) throws NoMachinesAvailableException {
T machine;
T desiredMachine = (T) flags.get("desiredMachine");
ConfigBag allflags = ConfigBag.newInstanceExtending(config().getBag()).putAll(flags);
Function<Iterable<? extends MachineLocation>, MachineLocation> chooser = allflags.get(MACHINE_CHOOSER);
synchronized (lock) {
Set<T> a = getAvailable();
if (a.isEmpty()) {
if (canProvisionMore()) {
provisionMore(1, allflags.getAllConfig());
a = getAvailable();
}
if (a.isEmpty())
throw new NoMachinesAvailableException("No machines available in "+toString());
}
if (desiredMachine != null) {
if (a.contains(desiredMachine)) {
machine = desiredMachine;
} else {
throw new IllegalStateException("Desired machine "+desiredMachine+" not available in "+toString()+"; "+
(inUse.contains(desiredMachine) ? "machine in use" : "machine unknown"));
}
} else {
machine = (T) chooser.apply(a);
if (!a.contains(machine)) {
throw new IllegalStateException("Machine chooser attempted to choose '"+machine+"' from outside the available set, in "+this);
}
}
inUse.add(machine);
updateMachineConfig(machine, flags);
}
for (MachineLocationCustomizer customizer : getMachineCustomizers(allflags)) {
customizer.customize(machine);
}
return machine;
}
@Override
public void release(T machine) {
ConfigBag machineConfig = ((ConfigurationSupportInternal)machine.config()).getBag();
for (MachineLocationCustomizer customizer : getMachineCustomizers(machineConfig)) {
customizer.preRelease(machine);
}
synchronized (lock) {
if (inUse.contains(machine) == false)
throw new IllegalStateException("Request to release machine "+machine+", but this machine is not currently allocated");
restoreMachineConfig(machine);
inUse.remove(machine);
if (pendingRemoval.contains(machine)) {
removeMachine(machine);
}
}
}
@Override
public Map<String,Object> getProvisioningFlags(Collection<String> tags) {
return Maps.<String,Object>newLinkedHashMap();
}
protected void updateMachineConfig(T machine, Map<?, ?> flags) {
if (origConfigs == null) {
// For backwards compatibility, where peristed state did not have this.
origConfigs = Maps.newLinkedHashMap();
}
Map<String, Object> strFlags = ConfigBag.newInstance(flags).getAllConfig();
Map<String, Object> origConfig = ((ConfigurationSupportInternal)machine.config()).getLocalBag().getAllConfig();
origConfigs.put(machine, origConfig);
requestPersist();
((ConfigurationSupportInternal)machine.config()).addToLocalBag(strFlags);
}
protected void restoreMachineConfig(MachineLocation machine) {
if (origConfigs == null) {
// For backwards compatibility, where peristed state did not have this.
origConfigs = Maps.newLinkedHashMap();
}
Map<String, Object> origConfig = origConfigs.remove(machine);
if (origConfig == null) return;
requestPersist();
Set<String> currentKeys = ((ConfigurationSupportInternal)machine.config()).getLocalBag().getAllConfig().keySet();
Set<String> newKeys = Sets.difference(currentKeys, origConfig.entrySet());
for (String key : newKeys) {
((ConfigurationSupportInternal)machine.config()).removeFromLocalBag(key);
}
((ConfigurationSupportInternal)machine.config()).addToLocalBag(origConfig);
}
@SuppressWarnings("unchecked")
private <K> K getConfigPreferringOverridden(ConfigKey<K> key, Map<?,?> overrides) {
K result = (K) overrides.get(key);
if (result == null) result = (K) overrides.get(key.getName());
if (result == null) result = getConfig(key);
return result;
}
protected Collection<MachineLocationCustomizer> getMachineCustomizers(ConfigBag setup) {
Collection<MachineLocationCustomizer> customizers = setup.get(MACHINE_LOCATION_CUSTOMIZERS);
return (customizers == null ? ImmutableList.<MachineLocationCustomizer>of() : customizers);
}
/**
* Facilitates fluent/programmatic style for constructing a fixed pool of machines.
* <pre>
* {@code
* new FixedListMachineProvisioningLocation.Builder()
* .user("alex")
* .keyFile("/Users/alex/.ssh/id_rsa")
* .addAddress("10.0.0.1")
* .addAddress("10.0.0.2")
* .addAddress("10.0.0.3")
* .addAddressMultipleTimes("me@127.0.0.1", 5)
* .build();
* }
* </pre>
*/
public static class Builder {
LocationManager lm;
String user;
String privateKeyPassphrase;
String privateKeyFile;
String privateKeyData;
File localTempDir;
List machines = Lists.newArrayList();
public Builder(LocationManager lm) {
this.lm = lm;
}
public Builder user(String user) {
this.user = user;
return this;
}
public Builder keyPassphrase(String keyPassphrase) {
this.privateKeyPassphrase = keyPassphrase;
return this;
}
public Builder keyFile(String keyFile) {
this.privateKeyFile = keyFile;
return this;
}
public Builder keyData(String keyData) {
this.privateKeyData = keyData;
return this;
}
public Builder localTempDir(File val) {
this.localTempDir = val;
return this;
}
/** adds the locations; user and keyfile set in the builder are _not_ applied to the machine
* (use add(String address) for that)
*/
public Builder add(SshMachineLocation location) {
machines.add(location);
return this;
}
public Builder addAddress(String address) {
return addAddresses(address);
}
public Builder addAddressMultipleTimes(String address, int n) {
for (int i=0; i<n; i++)
addAddresses(address);
return this;
}
public Builder addAddresses(String address1, String ...others) {
List<String> addrs = new ArrayList<String>();
addrs.addAll(WildcardGlobs.getGlobsAfterBraceExpansion("{"+address1+"}",
true /* numeric */, /* no quote support though */ PhraseTreatment.NOT_A_SPECIAL_CHAR, PhraseTreatment.NOT_A_SPECIAL_CHAR));
for (String address: others)
addrs.addAll(WildcardGlobs.getGlobsAfterBraceExpansion("{"+address+"}",
true /* numeric */, /* no quote support though */ PhraseTreatment.NOT_A_SPECIAL_CHAR, PhraseTreatment.NOT_A_SPECIAL_CHAR));
for (String addr: addrs)
add(createMachine(addr));
return this;
}
protected SshMachineLocation createMachine(String addr) {
if (lm==null)
return new SshMachineLocation(makeConfig(addr));
else
return lm.createLocation(makeConfig(addr), SshMachineLocation.class);
}
private Map makeConfig(String address) {
String user = this.user;
if (address.contains("@")) {
user = address.substring(0, address.indexOf("@"));
address = address.substring(address.indexOf("@")+1);
}
Map config = MutableMap.of("address", address);
if (truth(user)) {
config.put("user", user);
config.put("sshconfig.user", user);
}
if (truth(privateKeyPassphrase)) config.put("sshconfig.privateKeyPassphrase", privateKeyPassphrase);
if (truth(privateKeyFile)) config.put("sshconfig.privateKeyFile", privateKeyFile);
if (truth(privateKeyData)) config.put("sshconfig.privateKey", privateKeyData);
if (truth(localTempDir)) config.put("localTempDir", localTempDir);
return config;
}
@SuppressWarnings("unchecked")
public FixedListMachineProvisioningLocation<SshMachineLocation> build() {
if (lm==null)
return new FixedListMachineProvisioningLocation<SshMachineLocation>(MutableMap.builder()
.putIfNotNull("machines", machines)
.putIfNotNull("user", user)
.putIfNotNull("privateKeyPassphrase", privateKeyPassphrase)
.putIfNotNull("privateKeyFile", privateKeyFile)
.putIfNotNull("privateKeyData", privateKeyData)
.putIfNotNull("localTempDir", localTempDir)
.build());
else
return lm.createLocation(MutableMap.builder()
.putIfNotNull("machines", machines)
.putIfNotNull("user", user)
.putIfNotNull("privateKeyPassphrase", privateKeyPassphrase)
.putIfNotNull("privateKeyFile", privateKeyFile)
.putIfNotNull("privateKeyData", privateKeyData)
.putIfNotNull("localTempDir", localTempDir)
.build(),
FixedListMachineProvisioningLocation.class);
}
}
}