http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java b/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java new file mode 100644 index 0000000..3860eb5 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java @@ -0,0 +1,360 @@ +/* + * 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.basic; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.location.MachineDetails; +import org.apache.brooklyn.location.OsDetails; +import org.apache.commons.codec.binary.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.net.HostAndPort; +import com.google.common.reflect.TypeToken; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import org.apache.brooklyn.location.MachineLocation; +import org.apache.brooklyn.location.access.PortForwardManager; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.stream.Streams; +import brooklyn.util.time.Duration; +import brooklyn.util.time.Time; +import io.cloudsoft.winrm4j.winrm.WinRmTool; +import io.cloudsoft.winrm4j.winrm.WinRmToolResponse; + +public class WinRmMachineLocation extends AbstractLocation implements MachineLocation { + + private static final Logger LOG = LoggerFactory.getLogger(WinRmMachineLocation.class); + + // FIXME Respect `port` config when using {@link WinRmTool} + public static final ConfigKey<Integer> WINRM_PORT = ConfigKeys.newIntegerConfigKey( + "port", + "WinRM port to use when connecting to the remote machine", + 5985); + + // TODO merge with {link SshTool#PROP_USER} and {@link SshMachineLocation#user} + public static final ConfigKey<String> USER = ConfigKeys.newStringConfigKey("user", + "Username to use when connecting to the remote machine"); + + // TODO merge with {link SshTool#PROP_PASSWORD} + public static final ConfigKey<String> PASSWORD = ConfigKeys.newStringConfigKey("password", + "Password to use when connecting to the remote machine"); + + public static final ConfigKey<Integer> COPY_FILE_CHUNK_SIZE_BYTES = ConfigKeys.newIntegerConfigKey("windows.copy.file.size.bytes", + "Size of file chunks (in bytes) to be used when copying a file to the remote server", 1024); + + public static final ConfigKey<InetAddress> ADDRESS = ConfigKeys.newConfigKey( + InetAddress.class, + "address", + "Address of the remote machine"); + + public static final ConfigKey<Integer> EXECUTION_ATTEMPTS = ConfigKeys.newIntegerConfigKey( + "windows.exec.attempts", + "Number of attempts to execute a remote command", + 1); + + // TODO See SshTool#PROP_SSH_TRIES, where it was called "sshTries"; remove duplication? Merge into one well-named thing? + public static final ConfigKey<Integer> EXEC_TRIES = ConfigKeys.newIntegerConfigKey( + "execTries", + "Max number of times to attempt WinRM operations", + 10); + + public static final ConfigKey<Iterable<String>> PRIVATE_ADDRESSES = ConfigKeys.newConfigKey( + new TypeToken<Iterable<String>>() {}, + "privateAddresses", + "Private addresses of this machine, e.g. those within the private network", + null); + + public static final ConfigKey<Map<Integer, String>> TCP_PORT_MAPPINGS = ConfigKeys.newConfigKey( + new TypeToken<Map<Integer, String>>() {}, + "tcpPortMappings", + "NAT'ed ports, giving the mapping from private TCP port to a public host:port", + null); + + @Override + public InetAddress getAddress() { + return getConfig(ADDRESS); + } + + @Override + public OsDetails getOsDetails() { + return null; + } + + @Override + public MachineDetails getMachineDetails() { + return null; + } + + @Nullable + @Override + public String getHostname() { + InetAddress address = getAddress(); + return (address != null) ? address.getHostAddress() : null; + } + + @Nullable + protected String getHostAndPort() { + String host = getHostname(); + return (host == null) ? null : host + ":" + config().get(WINRM_PORT); + } + + @Override + public Set<String> getPublicAddresses() { + InetAddress address = getAddress(); + return (address == null) ? ImmutableSet.<String>of() : ImmutableSet.of(address.getHostAddress()); + } + + @Override + public Set<String> getPrivateAddresses() { + Iterable<String> result = getConfig(PRIVATE_ADDRESSES); + return (result == null) ? ImmutableSet.<String>of() : ImmutableSet.copyOf(result); + } + + public WinRmToolResponse executeScript(String script) { + return executeScript(ImmutableList.of(script)); + } + + public WinRmToolResponse executeScript(List<String> script) { + int execTries = getRequiredConfig(EXEC_TRIES); + Collection<Throwable> exceptions = Lists.newArrayList(); + for (int i = 0; i < execTries; i++) { + try { + return executeScriptNoRetry(script); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (i == (execTries+1)) { + LOG.info("Propagating WinRM exception (attempt "+(i+1)+" of "+execTries+")", e); + } else if (i == 0) { + LOG.warn("Ignoring WinRM exception and retrying (attempt "+(i+1)+" of "+execTries+")", e); + } else { + LOG.debug("Ignoring WinRM exception and retrying (attempt "+(i+1)+" of "+execTries+")", e); + } + exceptions.add(e); + } + } + throw Exceptions.propagate("failed to execute shell script", exceptions); + } + + protected WinRmToolResponse executeScriptNoRetry(List<String> script) { + WinRmTool winRmTool = WinRmTool.connect(getHostAndPort(), getUser(), getPassword()); + WinRmToolResponse response = winRmTool.executeScript(script); + return response; + } + + public WinRmToolResponse executePsScript(String psScript) { + return executePsScript(ImmutableList.of(psScript)); + } + + public WinRmToolResponse executePsScript(List<String> psScript) { + int execTries = getRequiredConfig(EXEC_TRIES); + Collection<Throwable> exceptions = Lists.newArrayList(); + for (int i = 0; i < execTries; i++) { + try { + return executePsScriptNoRetry(psScript); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (i == (execTries+1)) { + LOG.info("Propagating WinRM exception (attempt "+(i+1)+" of "+execTries+")", e); + } else if (i == 0) { + LOG.warn("Ignoring WinRM exception and retrying after 5 seconds (attempt "+(i+1)+" of "+execTries+")", e); + Time.sleep(Duration.FIVE_SECONDS); + } else { + LOG.debug("Ignoring WinRM exception and retrying after 5 seconds (attempt "+(i+1)+" of "+execTries+")", e); + Time.sleep(Duration.FIVE_SECONDS); + } + exceptions.add(e); + } + } + throw Exceptions.propagate("failed to execute powershell script", exceptions); + } + + public WinRmToolResponse executePsScriptNoRetry(List<String> psScript) { + WinRmTool winRmTool = WinRmTool.connect(getHostAndPort(), getUser(), getPassword()); + WinRmToolResponse response = winRmTool.executePs(psScript); + return response; + } + + public int copyTo(File source, String destination) { + FileInputStream sourceStream = null; + try { + sourceStream = new FileInputStream(source); + return copyTo(sourceStream, destination); + } catch (FileNotFoundException e) { + throw Exceptions.propagate(e); + } finally { + if (sourceStream != null) { + Streams.closeQuietly(sourceStream); + } + } + } + + public int copyTo(InputStream source, String destination) { + executePsScript(ImmutableList.of("rm -ErrorAction SilentlyContinue " + destination)); + try { + int chunkSize = getConfig(COPY_FILE_CHUNK_SIZE_BYTES); + byte[] inputData = new byte[chunkSize]; + int bytesRead; + int expectedFileSize = 0; + while ((bytesRead = source.read(inputData)) > 0) { + byte[] chunk; + if (bytesRead == chunkSize) { + chunk = inputData; + } else { + chunk = Arrays.copyOf(inputData, bytesRead); + } + executePsScript(ImmutableList.of("If ((!(Test-Path " + destination + ")) -or ((Get-Item '" + destination + "').length -eq " + + expectedFileSize + ")) {Add-Content -Encoding Byte -path " + destination + + " -value ([System.Convert]::FromBase64String(\"" + new String(Base64.encodeBase64(chunk)) + "\"))}")); + expectedFileSize += bytesRead; + } + + return 0; + } catch (java.io.IOException e) { + throw Exceptions.propagate(e); + } + } + + @Override + public void init() { + super.init(); + + // Register any pre-existing port-mappings with the PortForwardManager + Map<Integer, String> tcpPortMappings = getConfig(TCP_PORT_MAPPINGS); + if (tcpPortMappings != null) { + PortForwardManager pfm = (PortForwardManager) getManagementContext().getLocationRegistry().resolve("portForwardManager(scope=global)"); + for (Map.Entry<Integer, String> entry : tcpPortMappings.entrySet()) { + int targetPort = entry.getKey(); + HostAndPort publicEndpoint = HostAndPort.fromString(entry.getValue()); + if (!publicEndpoint.hasPort()) { + throw new IllegalArgumentException("Invalid portMapping ('"+entry.getValue()+"') for port "+targetPort+" in machine "+this); + } + pfm.associate(publicEndpoint.getHostText(), publicEndpoint, this, targetPort); + } + } + } + public String getUser() { + return config().get(USER); + } + + private String getPassword() { + return config().get(PASSWORD); + } + + private <T> T getRequiredConfig(ConfigKey<T> key) { + return checkNotNull(getConfig(key), "key %s must be set", key); + } + + public static String getDefaultUserMetadataString() { + // Using an encoded command obviates the need to escape + String unencodePowershell = Joiner.on("\r\n").join(ImmutableList.of( + // Allow TS connections + "$RDP = Get-WmiObject -Class Win32_TerminalServiceSetting -ComputerName $env:computername -Namespace root\\CIMV2\\TerminalServices -Authentication PacketPrivacy", + "$RDP.SetAllowTSConnections(1,1)", + "Set-ExecutionPolicy Unrestricted -Force", + // Set unlimited values for remote execution limits + "Set-Item WSMan:\\localhost\\Shell\\MaxConcurrentUsers 100", + "Set-Item WSMan:\\localhost\\Shell\\MaxMemoryPerShellMB 0", + "Set-Item WSMan:\\localhost\\Shell\\MaxProcessesPerShell 0", + "Set-Item WSMan:\\localhost\\Shell\\MaxShellsPerUser 0", + "New-ItemProperty \"HKLM:\\System\\CurrentControlSet\\Control\\LSA\" -Name \"SuppressExtendedProtection\" -Value 1 -PropertyType \"DWord\"", + // The following allows scripts to re-authenticate with local credential - this is required + // as certain operations cannot be performed with remote credentials + "$allowed = @('WSMAN/*')", + "$key = 'hklm:\\SOFTWARE\\Policies\\Microsoft\\Windows\\CredentialsDelegation'", + "if (!(Test-Path $key)) {", + " md $key", + "}", + "New-ItemProperty -Path $key -Name AllowFreshCredentials -Value 1 -PropertyType Dword -Force", + "New-ItemProperty -Path $key -Name AllowFreshCredentialsWhenNTLMOnly -Value 1 -PropertyType Dword -Force", + "$credKey = Join-Path $key 'AllowFreshCredentials'", + "if (!(Test-Path $credKey)) {", + " md $credkey", + "}", + "$ntlmKey = Join-Path $key 'AllowFreshCredentialsWhenNTLMOnly'", + "if (!(Test-Path $ntlmKey)) {", + " md $ntlmKey", + "}", + "$i = 1", + "$allowed |% {", + " # Script does not take into account existing entries in this key", + " New-ItemProperty -Path $credKey -Name $i -Value $_ -PropertyType String -Force", + " New-ItemProperty -Path $ntlmKey -Name $i -Value $_ -PropertyType String -Force", + " $i++", + "}" + )); + + String encoded = new String(Base64.encodeBase64(unencodePowershell.getBytes(Charsets.UTF_16LE))); + return "winrm quickconfig -q & " + + "winrm set winrm/config/service/auth @{Basic=\"true\"} & " + + "winrm set winrm/config/service/auth @{CredSSP=\"true\"} & " + + "winrm set winrm/config/client/auth @{CredSSP=\"true\"} & " + + "winrm set winrm/config/client @{AllowUnencrypted=\"true\"} & " + + "winrm set winrm/config/service @{AllowUnencrypted=\"true\"} & " + + "winrm set winrm/config/winrs @{MaxConcurrentUsers=\"100\"} & " + + "winrm set winrm/config/winrs @{MaxMemoryPerShellMB=\"0\"} & " + + "winrm set winrm/config/winrs @{MaxProcessesPerShell=\"0\"} & " + + "winrm set winrm/config/winrs @{MaxShellsPerUser=\"0\"} & " + + "netsh advfirewall firewall add rule name=RDP dir=in protocol=tcp localport=3389 action=allow profile=any & " + + "netsh advfirewall firewall add rule name=WinRM dir=in protocol=tcp localport=5985 action=allow profile=any & " + + "powershell -EncodedCommand " + encoded; + /* TODO: Find out why scripts with new line characters aren't working on AWS. The following appears as if it *should* + work but doesn't - the script simply isn't run. By connecting to the machine via RDP, you can get the script + from 'http://169.254.169.254/latest/user-data', and running it at the command prompt works, but for some + reason the script isn't run when the VM is provisioned + */ +// return Joiner.on("\r\n").join(ImmutableList.of( +// "winrm quickconfig -q", +// "winrm set winrm/config/service/auth @{Basic=\"true\"}", +// "winrm set winrm/config/client @{AllowUnencrypted=\"true\"}", +// "winrm set winrm/config/service @{AllowUnencrypted=\"true\"}", +// "netsh advfirewall firewall add rule name=RDP dir=in protocol=tcp localport=3389 action=allow profile=any", +// "netsh advfirewall firewall add rule name=WinRM dir=in protocol=tcp localport=5985 action=allow profile=any", +// // Using an encoded command necessitates the need to escape. The unencoded command is as follows: +// // $RDP = Get-WmiObject -Class Win32_TerminalServiceSetting -ComputerName $env:computername -Namespace root\CIMV2\TerminalServices -Authentication PacketPrivacy +// // $Result = $RDP.SetAllowTSConnections(1,1) +// "powershell -EncodedCommand JABSAEQAUAAgAD0AIABHAGUAdAAtAFcAbQBpAE8AYgBqAGUAYwB0ACAALQBDAGwAYQBzAHMAI" + +// "ABXAGkAbgAzADIAXwBUAGUAcgBtAGkAbgBhAGwAUwBlAHIAdgBpAGMAZQBTAGUAdAB0AGkAbgBnACAALQBDAG8AbQBwA" + +// "HUAdABlAHIATgBhAG0AZQAgACQAZQBuAHYAOgBjAG8AbQBwAHUAdABlAHIAbgBhAG0AZQAgAC0ATgBhAG0AZQBzAHAAY" + +// "QBjAGUAIAByAG8AbwB0AFwAQwBJAE0AVgAyAFwAVABlAHIAbQBpAG4AYQBsAFMAZQByAHYAaQBjAGUAcwAgAC0AQQB1A" + +// "HQAaABlAG4AdABpAGMAYQB0AGkAbwBuACAAUABhAGMAawBlAHQAUAByAGkAdgBhAGMAeQANAAoAJABSAGUAcwB1AGwAd" + +// "AAgAD0AIAAkAFIARABQAC4AUwBlAHQAQQBsAGwAbwB3AFQAUwBDAG8AbgBuAGUAYwB0AGkAbwBuAHMAKAAxACwAMQApAA==" +// )); + } +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java b/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java new file mode 100644 index 0000000..d399b92 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java @@ -0,0 +1,83 @@ +/* + * 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.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.brooklyn.api.management.ManagementContext; + +import org.apache.brooklyn.location.Location; + +import com.google.common.annotations.Beta; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +@Beta +public abstract class AbstractAvailabilityZoneExtension implements AvailabilityZoneExtension { + + protected final ManagementContext managementContext; + protected final AtomicReference<List<Location>> subLocations = new AtomicReference<List<Location>>(); + private final Object mutex = new Object(); + + public AbstractAvailabilityZoneExtension(ManagementContext managementContext) { + this.managementContext = checkNotNull(managementContext, "managementContext"); + } + + @Override + public List<Location> getSubLocations(int max) { + List<Location> all = getAllSubLocations(); + return all.subList(0, Math.min(max, all.size())); + } + + @Override + public List<Location> getSubLocationsByName(Predicate<? super String> namePredicate, int max) { + List<Location> result = Lists.newArrayList(); + List<Location> all = getAllSubLocations(); + for (Location loc : all) { + if (isNameMatch(loc, namePredicate)) { + result.add(loc); + } + } + return Collections.<Location>unmodifiableList(result); + } + + @Override + public List<Location> getAllSubLocations() { + synchronized (mutex) { + if (subLocations.get() == null) { + List<Location> result = doGetAllSubLocations(); + subLocations.set(ImmutableList.copyOf(result)); + } + } + return subLocations.get(); + } + + /** + * <strong>Note</strong> this method can be called while synchronized on {@link #mutex}. + */ + // TODO bad pattern, as this will likely call alien code (such as asking cloud provider?!) + protected abstract List<Location> doGetAllSubLocations(); + + protected abstract boolean isNameMatch(Location loc, Predicate<? super String> namePredicate); +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractCloudMachineProvisioningLocation.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractCloudMachineProvisioningLocation.java b/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractCloudMachineProvisioningLocation.java new file mode 100644 index 0000000..13707b5 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractCloudMachineProvisioningLocation.java @@ -0,0 +1,97 @@ +/* + * 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.cloud; + +import java.util.Collection; +import java.util.Map; + +import org.apache.brooklyn.location.LocationSpec; +import org.apache.brooklyn.location.MachineLocation; +import org.apache.brooklyn.location.MachineProvisioningLocation; +import org.apache.brooklyn.location.basic.AbstractLocation; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.internal.ssh.SshTool; + +public abstract class AbstractCloudMachineProvisioningLocation extends AbstractLocation +implements MachineProvisioningLocation<MachineLocation>, CloudLocationConfig +{ + public AbstractCloudMachineProvisioningLocation() { + super(); + } + + /** typically wants at least ACCESS_IDENTITY and ACCESS_CREDENTIAL */ + public AbstractCloudMachineProvisioningLocation(Map<?,?> conf) { + super(conf); + } + + /** uses reflection to create an object of the same type, assuming a Map constructor; + * subclasses can extend and downcast the result */ + @Override + public AbstractCloudMachineProvisioningLocation newSubLocation(Map<?,?> newFlags) { + return newSubLocation(getClass(), newFlags); + } + + public AbstractCloudMachineProvisioningLocation newSubLocation(Class<? extends AbstractCloudMachineProvisioningLocation> type, Map<?,?> newFlags) { + // TODO should be able to use ConfigBag.newInstanceExtending; would require moving stuff around to api etc + // TODO was previously `return LocationCreationUtils.newSubLocation(newFlags, this)`; need to retest on CloudStack etc + return getManagementContext().getLocationManager().createLocation(LocationSpec.create(type) + .parent(this) + .configure(config().getLocalBag().getAllConfig()) // FIXME Should this just be inherited? + .configure(newFlags)); + } + + @Override + public Map<String, Object> getProvisioningFlags(Collection<String> tags) { + if (tags.size() > 0) { + LOG.warn("Location {}, ignoring provisioning tags {}", this, tags); + } + return MutableMap.<String, Object>of(); + } + + // ---------------- utilities -------------------- + + protected ConfigBag extractSshConfig(ConfigBag setup, ConfigBag alt) { + ConfigBag sshConfig = new ConfigBag(); + + if (setup.containsKey(PASSWORD)) { + sshConfig.put(SshTool.PROP_PASSWORD, setup.get(PASSWORD)); + } else if (alt.containsKey(PASSWORD)) { + sshConfig.put(SshTool.PROP_PASSWORD, alt.get(PASSWORD)); + } + + if (setup.containsKey(PRIVATE_KEY_DATA)) { + sshConfig.put(SshTool.PROP_PRIVATE_KEY_DATA, setup.get(PRIVATE_KEY_DATA)); + } else if (setup.containsKey(PRIVATE_KEY_FILE)) { + sshConfig.put(SshTool.PROP_PRIVATE_KEY_FILE, setup.get(PRIVATE_KEY_FILE)); + } else if (alt.containsKey(PRIVATE_KEY_DATA)) { + sshConfig.put(SshTool.PROP_PRIVATE_KEY_DATA, alt.get(PRIVATE_KEY_DATA)); + } + + if (setup.containsKey(PRIVATE_KEY_PASSPHRASE)) { + // NB: not supported in jclouds (but it is by our ssh tool) + sshConfig.put(SshTool.PROP_PRIVATE_KEY_PASSPHRASE, setup.get(PRIVATE_KEY_PASSPHRASE)); + } + + // TODO extract other SshTool properties ? + + return sshConfig; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/AvailabilityZoneExtension.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/AvailabilityZoneExtension.java b/core/src/main/java/org/apache/brooklyn/location/cloud/AvailabilityZoneExtension.java new file mode 100644 index 0000000..1580440 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/AvailabilityZoneExtension.java @@ -0,0 +1,54 @@ +/* + * 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.cloud; + +import java.util.List; + +import brooklyn.entity.group.DynamicCluster; +import org.apache.brooklyn.location.Location; +import org.apache.brooklyn.location.basic.MultiLocation; + +import com.google.common.annotations.Beta; +import com.google.common.base.Predicate; + +/** + * For a location that has sub-zones within it (e.g. an AWS region has availability zones that can be + * mapped as sub-locations), this extension interface allows those to be accessed and used. + * For some well-known clouds, the availability zones are automatically set, although for others they may + * have to be configured explicitly. The "multi:(locs,...)" location descriptor (cf {@link MultiLocation}) allows + * this to be down at runtime. + * <p> + * Note that only entities which are explicitly aware of the {@link AvailabilityZoneExtension} + * will use availability zone information. For example {@link DynamicCluster} + * <p> + * Implementers are strongly encouraged to extend {@link AbstractAvailabilityZoneExtension} + * which has useful behaviour, rather than attempt to implement this interface directly. + * + * @since 0.6.0 + */ +@Beta +public interface AvailabilityZoneExtension { + + List<Location> getAllSubLocations(); + + List<Location> getSubLocations(int max); + + List<Location> getSubLocationsByName(Predicate<? super String> namePredicate, int max); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/CloudLocationConfig.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/CloudLocationConfig.java b/core/src/main/java/org/apache/brooklyn/location/cloud/CloudLocationConfig.java new file mode 100644 index 0000000..9682994 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/CloudLocationConfig.java @@ -0,0 +1,116 @@ +/* + * 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.cloud; + +import java.util.Collection; + +import com.google.common.annotations.Beta; +import com.google.common.reflect.TypeToken; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.event.basic.BasicConfigKey; +import org.apache.brooklyn.location.MachineLocationCustomizer; +import org.apache.brooklyn.location.basic.LocationConfigKeys; +import brooklyn.util.flags.SetFromFlag; + +public interface CloudLocationConfig { + + public static final ConfigKey<String> CLOUD_ENDPOINT = LocationConfigKeys.CLOUD_ENDPOINT; + public static final ConfigKey<String> CLOUD_REGION_ID = LocationConfigKeys.CLOUD_REGION_ID; + public static final ConfigKey<String> CLOUD_AVAILABILITY_ZONE_ID = LocationConfigKeys.CLOUD_AVAILABILITY_ZONE_ID; + + @SetFromFlag("identity") + public static final ConfigKey<String> ACCESS_IDENTITY = LocationConfigKeys.ACCESS_IDENTITY; + @SetFromFlag("credential") + public static final ConfigKey<String> ACCESS_CREDENTIAL = LocationConfigKeys.ACCESS_CREDENTIAL; + + public static final ConfigKey<String> USER = LocationConfigKeys.USER; + + public static final ConfigKey<String> PASSWORD = LocationConfigKeys.PASSWORD; + public static final ConfigKey<String> PUBLIC_KEY_FILE = LocationConfigKeys.PUBLIC_KEY_FILE; + public static final ConfigKey<String> PUBLIC_KEY_DATA = LocationConfigKeys.PUBLIC_KEY_DATA; + public static final ConfigKey<String> PRIVATE_KEY_FILE = LocationConfigKeys.PRIVATE_KEY_FILE; + public static final ConfigKey<String> PRIVATE_KEY_DATA = LocationConfigKeys.PRIVATE_KEY_DATA; + public static final ConfigKey<String> PRIVATE_KEY_PASSPHRASE = LocationConfigKeys.PRIVATE_KEY_PASSPHRASE; + + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PUBLIC_KEY_FILE = LocationConfigKeys.LEGACY_PUBLIC_KEY_FILE; + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PUBLIC_KEY_DATA = LocationConfigKeys.LEGACY_PUBLIC_KEY_DATA; + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PRIVATE_KEY_FILE = LocationConfigKeys.LEGACY_PRIVATE_KEY_FILE; + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PRIVATE_KEY_DATA = LocationConfigKeys.LEGACY_PRIVATE_KEY_DATA; + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PRIVATE_KEY_PASSPHRASE = LocationConfigKeys.LEGACY_PRIVATE_KEY_PASSPHRASE; + + // default is just shy of common 64-char boundary, leaving 4 chars plus our salt allowance (default 4+1) which allows up to -12345678 by jclouds + public static final ConfigKey<Integer> VM_NAME_MAX_LENGTH = ConfigKeys.newIntegerConfigKey( + "vmNameMaxLength", "Maximum length of VM name", 60); + + public static final ConfigKey<Integer> VM_NAME_SALT_LENGTH = ConfigKeys.newIntegerConfigKey( + "vmNameSaltLength", "Number of characters to use for a random identifier inserted in hostname " + + "to uniquely identify machines", 4); + + public static final ConfigKey<String> WAIT_FOR_SSHABLE = ConfigKeys.newStringConfigKey("waitForSshable", + "Whether and how long to wait for a newly provisioned VM to be accessible via ssh; " + + "if 'false', won't check; if 'true' uses default duration; otherwise accepts a time string e.g. '5m' (the default) or a number of milliseconds", "5m"); + + public static final ConfigKey<String> WAIT_FOR_WINRM_AVAILABLE = ConfigKeys.newStringConfigKey("waitForWinRmAvailable", + "Whether and how long to wait for a newly provisioned VM to be accessible via WinRm; " + + "if 'false', won't check; if 'true' uses default duration; otherwise accepts a time string e.g. '30m' (the default) or a number of milliseconds", "30m"); + + public static final ConfigKey<Boolean> LOG_CREDENTIALS = ConfigKeys.newBooleanConfigKey( + "logCredentials", + "Whether to log credentials of a new VM - strongly recommended never be used in production, as it is a big security hole!", + false); + + public static final ConfigKey<Object> CALLER_CONTEXT = LocationConfigKeys.CALLER_CONTEXT; + + public static final ConfigKey<Boolean> DESTROY_ON_FAILURE = ConfigKeys.newBooleanConfigKey("destroyOnFailure", "Whether to destroy the VM if provisioningLocation.obtain() fails", true); + + public static final ConfigKey<Object> INBOUND_PORTS = new BasicConfigKey<Object>(Object.class, "inboundPorts", + "Inbound ports to be applied when creating a VM, on supported clouds " + + "(either a single port as a String, or an Iterable<Integer> or Integer[])", null); + @Beta + public static final ConfigKey<Object> ADDITIONAL_INBOUND_PORTS = new BasicConfigKey<Object>(Object.class, "required.ports", + "Required additional ports to be applied when creating a VM, on supported clouds " + + "(either a single port as an Integer, or an Iterable<Integer> or Integer[])", null); + + public static final ConfigKey<Boolean> OS_64_BIT = ConfigKeys.newBooleanConfigKey("os64Bit", + "Whether to require 64-bit OS images (true), 32-bit images (false), or either (null)"); + + public static final ConfigKey<Object> MIN_RAM = new BasicConfigKey<Object>(Object.class, "minRam", + "Minimum amount of RAM, either as string (4gb) or number of MB (4096), for use in selecting the machine/hardware profile", null); + + public static final ConfigKey<Integer> MIN_CORES = new BasicConfigKey<Integer>(Integer.class, "minCores", + "Minimum number of cores, for use in selecting the machine/hardware profile", null); + + public static final ConfigKey<Object> MIN_DISK = new BasicConfigKey<Object>(Object.class, "minDisk", + "Minimum size of disk, either as string (100gb) or number of GB (100), for use in selecting the machine/hardware profile", null); + + public static final ConfigKey<String> DOMAIN_NAME = new BasicConfigKey<String>(String.class, "domainName", + "DNS domain where the host should be created, e.g. yourdomain.com (selected clouds only)", null); + + @SuppressWarnings("serial") + public static final ConfigKey<Collection<MachineLocationCustomizer>> MACHINE_LOCATION_CUSTOMIZERS = ConfigKeys.newConfigKey( + new TypeToken<Collection<MachineLocationCustomizer>>() {}, + "machineCustomizers", "Optional machine customizers"); +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java b/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java new file mode 100644 index 0000000..b06b88a --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java @@ -0,0 +1,151 @@ +/* + * 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.cloud.names; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.trait.HasShortName; + +import org.apache.brooklyn.location.cloud.CloudLocationConfig; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.text.Identifiers; +import brooklyn.util.text.Strings; + +import com.google.common.annotations.Beta; +import com.google.common.base.CharMatcher; + +/** + * Implements <b>most</b> of {@link CloudMachineNamer}, + * leaving just one method -- {@link #generateNewIdOfLength(int)} -- + * for subclasses to provide. + * <p> + * {@link CloudLocationConfig#VM_NAME_MAX_LENGTH} is used to find the VM length, + * unless {@link #getCustomMaxNameLength(ConfigBag)} is overridden or + * {@link #setDefaultMachineNameMaxLength(int)} invoked on the instance supplied. + */ +public abstract class AbstractCloudMachineNamer implements CloudMachineNamer { + + int defaultMachineNameMaxLength = CloudLocationConfig.VM_NAME_MAX_LENGTH.getDefaultValue(); + int defaultMachineNameSaltLength = CloudLocationConfig.VM_NAME_SALT_LENGTH.getDefaultValue(); + protected String separator = "-"; + + public String generateNewMachineUniqueName(ConfigBag setup) { + return generateNewIdReservingLength(setup, 0); + } + + public String generateNewMachineUniqueNameFromGroupId(ConfigBag setup, String groupId) { + int availSaltLength = getMaxNameLength(setup) - (groupId.length() + separator.length()); + int requestedSaltLength = getLengthForMachineUniqueNameSalt(setup, false); + if (availSaltLength <= 0 || requestedSaltLength <= 0) { + return groupId; + } + + return sanitize(groupId + separator + Identifiers.makeRandomId(Math.min(requestedSaltLength, availSaltLength))).toLowerCase(); + } + + public String generateNewGroupId(ConfigBag setup) { + return sanitize(generateNewIdReservingLength(setup, getLengthForMachineUniqueNameSalt(setup, true))).toLowerCase(); + } + + protected String generateNewIdReservingLength(ConfigBag setup, int lengthToReserve) { + int len = getMaxNameLength(setup); + // decrement by e.g. 9 chars because jclouds adds that (dash plus 8 for hex id) + len -= lengthToReserve; + if (len<=0) return ""; + return Strings.maxlen(generateNewIdOfLength(setup, len), len); + } + + /** Method for subclasses to provide to construct the context-specific part of an identifier, + * for use in {@link #generateNewGroupId()} and {@link #generateNewMachineUniqueName()}. + * + * @param maxLengthHint an indication of the maximum length permitted for the ID generated, + * supplied for implementations which wish to use this information to decide what to truncate. + * (This class will truncate any return values longer than this.) + */ + protected abstract String generateNewIdOfLength(ConfigBag setup, int maxLengthHint); + + /** Returns the max length of a VM name for the cloud specified in setup; + * this value is typically decremented by 9 to make room for jclouds labels; + * delegates to {@link #getCustomMaxNameLength()} when + * {@link CloudLocationConfig#VM_NAME_MAX_LENGTH} is not set */ + public int getMaxNameLength(ConfigBag setup) { + if (setup.containsKey(CloudLocationConfig.VM_NAME_MAX_LENGTH)) { + // if a length is set explicitly, use that (but intercept default behaviour) + return setup.get(CloudLocationConfig.VM_NAME_MAX_LENGTH); + } + + Integer custom = getCustomMaxNameLength(setup); + if (custom!=null) return custom; + + // return the default + return defaultMachineNameMaxLength; + } + + // sometimes we create salt string, sometimes jclouds does + public int getLengthForMachineUniqueNameSalt(ConfigBag setup, boolean includeSeparator) { + int saltLen; + if (setup.containsKey(CloudLocationConfig.VM_NAME_SALT_LENGTH)) { + saltLen = setup.get(CloudLocationConfig.VM_NAME_SALT_LENGTH); + } else { + // default value comes from key, but custom default can be set + saltLen = defaultMachineNameSaltLength; + } + + if (saltLen>0 && includeSeparator) + saltLen += separator.length(); + + return saltLen; + } + + public AbstractCloudMachineNamer setDefaultMachineNameMaxLength(int defaultMaxLength) { + this.defaultMachineNameMaxLength = defaultMaxLength; + return this; + } + + /** Number of chars to use or reserve for the machine identifier when constructing a group identifier; + * jclouds for instance uses "-" plus 8 */ + public AbstractCloudMachineNamer setDefaultMachineNameSeparatorAndSaltLength(String separator, int defaultMachineUniqueNameSaltLength) { + this.separator = separator; + this.defaultMachineNameSaltLength = defaultMachineUniqueNameSaltLength; + return this; + } + + /** Method for overriding to provide custom logic when an explicit config key is not set for the machine length. */ + public Integer getCustomMaxNameLength(ConfigBag setup) { + return null; + } + + protected static String shortName(Object x) { + if (x instanceof HasShortName) { + return ((HasShortName)x).getShortName(); + } + if (x instanceof Entity) { + return ((Entity)x).getDisplayName(); + } + return x.toString(); + } + + @Beta //probably won't live here long-term + public static String sanitize(String s) { + return CharMatcher.inRange('A', 'Z') + .or(CharMatcher.inRange('a', 'z')) + .or(CharMatcher.inRange('0', '9')) + .negate() + .trimAndCollapseFrom(s, '-'); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java b/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java new file mode 100644 index 0000000..818053d --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java @@ -0,0 +1,92 @@ +/* + * 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.cloud.names; + +import org.apache.brooklyn.api.entity.Application; +import org.apache.brooklyn.api.entity.Entity; + +import org.apache.brooklyn.location.cloud.CloudLocationConfig; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.text.Identifiers; +import brooklyn.util.text.StringShortener; +import brooklyn.util.text.Strings; + +/** + * Standard implementation of {@link CloudMachineNamer}, + * which looks at several of the properties of the context (entity) + * and is clever about abbreviating them. */ +public class BasicCloudMachineNamer extends AbstractCloudMachineNamer { + + @Override + protected String generateNewIdOfLength(ConfigBag setup, int len) { + Object context = setup.peek(CloudLocationConfig.CALLER_CONTEXT); + Entity entity = null; + if (context instanceof Entity) entity = (Entity) context; + + StringShortener shortener = Strings.shortener().separator("-"); + shortener.append("system", "brooklyn"); + + // randId often not necessary, as an 8-char hex identifier is added later (in jclouds? can we override?) + // however it can be useful to have this early in the string, to prevent collisions in places where it is abbreviated + shortener.append("randId", Identifiers.makeRandomId(4)); + + String user = System.getProperty("user.name"); + if (!"brooklyn".equals(user)) + // include user; unless the user is 'brooklyn', as 'brooklyn-brooklyn-' is just silly! + shortener.append("user", user); + + if (entity!=null) { + Application app = entity.getApplication(); + if (app!=null) { + shortener.append("app", shortName(app)) + .append("appId", app.getId()); + } + shortener.append("entity", shortName(entity)) + .append("entityId", entity.getId()); + } else if (context!=null) { + shortener.append("context", context.toString()); + } + + shortener.truncate("user", 12) + .truncate("app", 16) + .truncate("entity", 16) + .truncate("appId", 4) + .truncate("entityId", 4) + .truncate("context", 12); + + shortener.canTruncate("user", 8) + .canTruncate("app", 5) + .canTruncate("entity", 5) + .canTruncate("system", 2) + .canTruncate("app", 3) + .canTruncate("entity", 3) + .canRemove("app") + .canTruncate("user", 4) + .canRemove("entity") + .canTruncate("context", 4) + .canTruncate("randId", 2) + .canRemove("user") + .canTruncate("appId", 2) + .canRemove("appId"); + + String s = shortener.getStringOfMaxLength(len); + return sanitize(s).toLowerCase(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java new file mode 100644 index 0000000..eaf7198 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java @@ -0,0 +1,62 @@ +/* + * 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.cloud.names; + +import org.apache.brooklyn.api.entity.Entity; + +import org.apache.brooklyn.location.Location; +import org.apache.brooklyn.location.cloud.CloudLocationConfig; +import brooklyn.util.config.ConfigBag; + +/** + * Interface used to construct names for individual cloud machines and for groups of machines. + * <p> + * Implementations <b>must</b> provide a constructor which takes a single argument, + * being the {@link ConfigBag} for the context where the machine is being created + * (usually a {@link Location}). + * <p> + * With that bag, the config key {@link CloudLocationConfig#CALLER_CONTEXT} + * typically contains the {@link Entity} for which the machine is being created. + */ +public interface CloudMachineNamer { + + /** + * Generate a name for a new machine, based on context. + * <p> + * The name should normally be unique, as a context might produce multiple machines, + * for example basing it partially on information from the context but also including some random salt. + */ + public String generateNewMachineUniqueName(ConfigBag setup); + /** + * Generate a name stem for a group of machines, based on context. + * <p> + * The name does not need to be unique, as uniqueness will be applied by {@link #generateNewMachineUniqueNameFromGroupId(String)}. + */ + public String generateNewGroupId(ConfigBag setup); + + /** + * Generate a unique name from the given name stem. + * <p> + * The name stem is normally based on context information so the usual + * function of this method is to apply a suffix which helps to uniquely distinguish between machines + * in cases where the same name stem ({@link #generateNewGroupId()}) is used for multiple machines. + */ + public String generateNewMachineUniqueNameFromGroupId(ConfigBag setup, String groupId); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java new file mode 100644 index 0000000..472adde --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java @@ -0,0 +1,73 @@ +/* + * 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.cloud.names; + +import java.util.Map; + +import org.apache.brooklyn.api.entity.Entity; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.entity.basic.EntityInternal; +import org.apache.brooklyn.location.cloud.CloudLocationConfig; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.text.Strings; +import brooklyn.util.text.TemplateProcessor; + +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; + +/** Provides a machine namer which looks at a location config key {@link #MACHINE_NAME_TEMPLATE} + * to construct the hostname. + * For instance, setting this to <code>${config.entity_hostname}</code> + * will take the hostname from an <code>entity_hostname</code> key passed as entity <code>brooklyn.config</code>. + * <p> + * Note that this is not jclouds aware, so jclouds-specific cloud max lengths are not observed with this class. + */ +public class CustomMachineNamer extends BasicCloudMachineNamer { + + public static final ConfigKey<String> MACHINE_NAME_TEMPLATE = ConfigKeys.newStringConfigKey("custom.machine.namer.machine", + "Freemarker template format for custom machine name", "${entity.displayName}"); + @SuppressWarnings("serial") + public static final ConfigKey<Map<String, ?>> EXTRA_SUBSTITUTIONS = ConfigKeys.newConfigKey(new TypeToken<Map<String, ?>>() {}, + "custom.machine.namer.substitutions", "Additional substitutions to be used in the template", ImmutableMap.<String, Object>of()); + + @Override + protected String generateNewIdOfLength(ConfigBag setup, int len) { + Object context = setup.peek(CloudLocationConfig.CALLER_CONTEXT); + Entity entity = null; + if (context instanceof Entity) { + entity = (Entity) context; + } + + String template = setup.get(MACHINE_NAME_TEMPLATE); + + String processed; + if (entity == null) { + processed = TemplateProcessor.processTemplateContents(template, setup.get(EXTRA_SUBSTITUTIONS)); + } else { + processed = TemplateProcessor.processTemplateContents(template, (EntityInternal)entity, setup.get(EXTRA_SUBSTITUTIONS)); + } + + processed = Strings.removeFromStart(processed, "#ftl\n"); + + return sanitize(processed); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java b/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java new file mode 100644 index 0000000..78e6d87 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java @@ -0,0 +1,51 @@ +/* + * 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.dynamic; + +import org.apache.brooklyn.api.entity.Entity; + +import com.google.common.annotations.Beta; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import org.apache.brooklyn.location.Location; +import brooklyn.util.flags.SetFromFlag; + +/** + * A location that is created and owned by an entity at runtime. + * <p> + * The lifecycle of the location is managed by the owning entity. + * + * @param E the entity type + * @param L the location type + */ +@Beta +public interface DynamicLocation<E extends Entity & LocationOwner<L, E>, L extends Location & DynamicLocation<E, L>> { + + @SetFromFlag("owner") + ConfigKey<Entity> OWNER = + ConfigKeys.newConfigKey(Entity.class, "owner", "The entity owning this location"); + + @SetFromFlag("maxLocations") + ConfigKey<Integer> MAX_SUB_LOCATIONS = + ConfigKeys.newIntegerConfigKey("maxLocations", "The maximum number of sub-locations that can be created; 0 for unlimited", 0); + + E getOwner(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java b/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java new file mode 100644 index 0000000..a2b0bfb --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java @@ -0,0 +1,86 @@ +/* + * 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.dynamic; + +import java.util.Map; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.event.AttributeSensor; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.event.basic.BasicAttributeSensorAndConfigKey; +import brooklyn.event.basic.Sensors; +import org.apache.brooklyn.location.Location; +import org.apache.brooklyn.location.LocationDefinition; +import brooklyn.util.flags.SetFromFlag; + +import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; + +/** + * An entity that owns a particular location. + * <p> + * The entity should be able to dynamically create an instance of the required type of location, and will manage + * the lifecycle of the location in parallel with its own. + * + * @param L the location type + * @param E the entity type + */ +@Beta +public interface LocationOwner<L extends Location & DynamicLocation<E, L>, E extends Entity & LocationOwner<L, E>> { + + @SetFromFlag("locationPrefix") + ConfigKey<String> LOCATION_NAME_PREFIX = ConfigKeys.newStringConfigKey( + "entity.dynamicLocation.prefix", "The name prefix for the location owned by this entity", "dynamic"); + + @SetFromFlag("locationSuffix") + ConfigKey<String> LOCATION_NAME_SUFFIX = ConfigKeys.newStringConfigKey( + "entity.dynamicLocation.suffix", "The name suffix for the location owned by this entity"); + + @SetFromFlag("locationName") + BasicAttributeSensorAndConfigKey<String> LOCATION_NAME = new BasicAttributeSensorAndConfigKey<String>(String.class, + "entity.dynamicLocation.name", "The name of the location owned by this entity (default is auto-generated using prefix and suffix keys)"); + + ConfigKey<Map<String, Object>> LOCATION_FLAGS = ConfigKeys.newConfigKey(new TypeToken<Map<String, Object>>() { }, + "entity.dynamicLocation.flags", "Extra creation flags for the Location owned by this entity", + ImmutableMap.<String, Object>of()); + + AttributeSensor<Location> DYNAMIC_LOCATION = Sensors.newSensor(Location.class, + "entity.dynamicLocation", "The location owned by this entity"); + + AttributeSensor<String> LOCATION_SPEC = Sensors.newStringSensor( + "entity.dynamicLocation.spec", "The specification string for the location owned by this entity"); + + AttributeSensor<Boolean> DYNAMIC_LOCATION_STATUS = Sensors.newBooleanSensor( + "entity.dynamicLocation.status", "The status of the location owned by this entity"); + + AttributeSensor<LocationDefinition> LOCATION_DEFINITION = Sensors.newSensor( + LocationDefinition.class, "entity.dynamicLocation.definition", "The location definition for the location owned by this entity"); + + L getDynamicLocation(); + + L createLocation(Map<String, ?> flags); + + boolean isLocationAvailable(); + + void deleteLocation(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java b/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java new file mode 100644 index 0000000..0ff6e8c --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java @@ -0,0 +1,105 @@ +/* + * 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.geo; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.util.net.Networking; + +/** @deprecated Mar 2015 - the API has changed; GetLocation now discouraged for free access, and valuepairs.txt not supported */ +@Deprecated +public class GeoBytesHostGeoLookup implements HostGeoLookup { + + public static final Logger log = LoggerFactory.getLogger(GeoBytesHostGeoLookup.class); + + /* + curl "http://www.geobytes.com/IpLocator.htm?GetLocation&template=valuepairs.txt&IpAddress=geobytes.com" + known=1 + countryid=254 + country=United States + fips104=US + iso2=US + iso3=USA + ison=840 + internet=US + comment= + regionid=142 + region=Maryland + code=MD + adm1code= + cityid=8909 + city=Baltimore + latitude=39.2894 + longitude=-76.6384 + timezone=-05:00 + dmaid=512 + dma=512 + market=Baltimore + certainty=78 + isproxy=false + mapbytesremaining=Free + */ + + public String getPropertiesLookupUrlForPublicIp(String ip) { + return "http://www.geobytes.com/IpLocator.htm?GetLocation&template=valuepairs.txt&IpAddress="+ip.trim(); + } + + public String getPropertiesLookupUrlForLocalhost() { + return "http://www.geobytes.com/IpLocator.htm?GetLocation&template=valuepairs.txt"; + } + + /** returns URL to get properties for the given address (assuming localhost if address is on a subnet) */ + public String getPropertiesLookupUrlFor(InetAddress address) { + if (Networking.isPrivateSubnet(address)) return getPropertiesLookupUrlForLocalhost(); + return getPropertiesLookupUrlForPublicIp(address.getHostAddress()); + } + + private static boolean LOGGED_GEO_LOOKUP_UNAVAILABLE = false; + + public HostGeoInfo getHostGeoInfo(InetAddress address) throws MalformedURLException, IOException { + String url = getPropertiesLookupUrlFor(address); + if (log.isDebugEnabled()) + log.debug("Geo info lookup for "+address+" at "+url); + Properties props = new Properties(); + try { + props.load( new URL(url).openStream() ); + HostGeoInfo geo = new HostGeoInfo(address.getHostName(), props.getProperty("city")+" ("+props.getProperty("iso2")+")", + Double.parseDouble(props.getProperty("latitude")), Double.parseDouble(props.getProperty("longitude"))); + log.info("Geo info lookup for "+address+" returned: "+geo); + return geo; + } catch (Exception e) { + // may be web not available, or gateway giving us funny crap + if (log.isDebugEnabled()) + log.debug("Geo info lookup for "+address+" failed: "+e); + if (!LOGGED_GEO_LOOKUP_UNAVAILABLE) { + LOGGED_GEO_LOOKUP_UNAVAILABLE = true; + log.info("Geo info lookup unavailable (for "+address+"; cause "+e+")"); + } + return null; + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java b/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java new file mode 100644 index 0000000..9d3c630 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java @@ -0,0 +1,25 @@ +/* + * 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.geo; + +public interface HasHostGeoInfo { + + HostGeoInfo getHostGeoInfo(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java new file mode 100644 index 0000000..d159a95 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java @@ -0,0 +1,206 @@ +/* + * 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.geo; + +import java.io.Serializable; +import java.net.InetAddress; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.location.AddressableLocation; +import org.apache.brooklyn.location.Location; +import org.apache.brooklyn.location.basic.AbstractLocation; +import org.apache.brooklyn.location.basic.LocationConfigKeys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.flags.TypeCoercions; +import brooklyn.util.guava.Maybe; +import brooklyn.util.internal.BrooklynSystemProperties; + +import com.google.common.base.Objects; + +/** + * Encapsulates geo-IP information for a given host. + */ +public class HostGeoInfo implements Serializable { + + private static final long serialVersionUID = -5866759901535266181L; + + public static final Logger log = LoggerFactory.getLogger(HostGeoInfo.class); + + /** the IP address */ + public final String address; + + public final String displayName; + + public final double latitude; + public final double longitude; + + private static Maybe<HostGeoLookup> cachedLookup = null; + + public static HostGeoInfo create(String address, String displayName, double latitude, double longitude) { + return new HostGeoInfo(address, displayName, latitude, longitude); + } + + public static HostGeoInfo fromIpAddress(InetAddress address) { + try { + HostGeoLookup lookup = getDefaultLookup(); + if (lookup!=null) + return lookup.getHostGeoInfo(address); + } catch (Exception e) { + if (log.isDebugEnabled()) + log.debug("unable to look up geo DNS info for "+address, e); + } + return null; + } + + @Nullable + public static HostGeoLookup getDefaultLookup() throws InstantiationException, IllegalAccessException, ClassNotFoundException { + if (cachedLookup==null) { + cachedLookup = Maybe.of(findHostGeoLookupImpl()); + } + return cachedLookup.get(); + } + + public static void clearCachedLookup() { + cachedLookup = null; + } + + /** returns null if cannot be set */ + public static HostGeoInfo fromLocation(Location l) { + if (l==null) return null; + + Location la = l; + HostGeoInfo resultFromLocation = null; + while (la!=null) { + if (la instanceof HasHostGeoInfo) { + resultFromLocation = ((HasHostGeoInfo)l).getHostGeoInfo(); + if (resultFromLocation!=null) break; + } + la = la.getParent(); + } + if (resultFromLocation!=null && l==la) { + // from the location + return resultFromLocation; + } + // resultFromLocation may be inherited, in which case we will copy it later + + InetAddress address = findIpAddress(l); + Object latitude = l.getConfig(LocationConfigKeys.LATITUDE); + Object longitude = l.getConfig(LocationConfigKeys.LONGITUDE); + + if (resultFromLocation!=null && (latitude == null || longitude == null)) { + latitude = resultFromLocation.latitude; + longitude = resultFromLocation.longitude; + } + if (address!=null && (latitude == null || longitude == null)) { + HostGeoInfo geo = fromIpAddress(address); + if (geo==null) return null; + latitude = geo.latitude; + longitude = geo.longitude; + } + + if (latitude==null || longitude==null) + return null; + + Exception error=null; + try { + latitude = TypeCoercions.castPrimitive(latitude, Double.class); + longitude = TypeCoercions.castPrimitive(longitude, Double.class); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + error = e; + } + if (error!=null || !(latitude instanceof Double) || !(longitude instanceof Double)) + throw new IllegalArgumentException("Location "+l+" specifies invalid type of lat/long: " + + "lat="+latitude+" (type "+(latitude==null ? null : latitude.getClass())+"); " + + "lon="+longitude+" (type "+(longitude==null ? null : longitude.getClass())+")", error); + + HostGeoInfo result = new HostGeoInfo(address!=null ? address.getHostAddress() : null, l.getDisplayName(), (Double) latitude, (Double) longitude); + if (l instanceof AbstractLocation) { + ((AbstractLocation)l).setHostGeoInfo(result); + } + return result; + } + + private static HostGeoLookup findHostGeoLookupImpl() throws InstantiationException, IllegalAccessException, ClassNotFoundException { + String type = BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getValue(); + /* utrace seems more accurate than geobytes, and it gives a report of how many tokens are left; + * but maxmind if it's installed locally is even better (does not require remote lookup), + * so use it if available */ + if (type==null) { + if (MaxMind2HostGeoLookup.getDatabaseReader()!=null) + return new MaxMind2HostGeoLookup(); + log.debug("Using Utrace remote for geo lookup because MaxMind2 is not available"); + return new UtraceHostGeoLookup(); + } + if (type.isEmpty()) return null; + return (HostGeoLookup) Class.forName(type).newInstance(); + } + + public static HostGeoInfo fromEntity(Entity e) { + for (Location l : e.getLocations()) { + HostGeoInfo hgi = fromLocation(l); + if (hgi != null) + return hgi; + } + return null; + } + + public static InetAddress findIpAddress(Location l) { + if (l == null) + return null; + if (l instanceof AddressableLocation) + return ((AddressableLocation) l).getAddress(); + return findIpAddress(l.getParent()); + } + + public HostGeoInfo(String address, String displayName, double latitude, double longitude) { + this.address = address; + this.displayName = displayName==null ? "" : displayName; + this.latitude = latitude; + this.longitude = longitude; + } + + public String getAddress() { + return address; + } + + @Override + public String toString() { + return "HostGeoInfo["+displayName+": "+(address!=null ? address : "(no-address)")+" at ("+latitude+","+longitude+")]"; + } + + @Override + public boolean equals(Object o) { + // Slight cheat: only includes the address + displayName field (displayName to allow overloading localhost etc) + return (o instanceof HostGeoInfo) && Objects.equal(address, ((HostGeoInfo) o).address) + && Objects.equal(displayName, ((HostGeoInfo) o).displayName); + } + + @Override + public int hashCode() { + // Slight cheat: only includes the address + displayName field (displayName to allow overloading localhost etc) + return Objects.hashCode(address, displayName); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java new file mode 100644 index 0000000..db58276 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java @@ -0,0 +1,27 @@ +/* + * 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.geo; + +import java.net.InetAddress; + +public interface HostGeoLookup { + + public HostGeoInfo getHostGeoInfo(InetAddress address) throws Exception; + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java b/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java new file mode 100644 index 0000000..1d31f0f --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java @@ -0,0 +1,178 @@ +/* + * 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.geo; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.util.ResourceUtils; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.text.StringPredicates; +import brooklyn.util.time.Duration; +import brooklyn.util.time.Durations; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Predicates; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +public class LocalhostExternalIpLoader { + + public static final Logger LOG = LoggerFactory.getLogger(LocalhostExternalIpLoader.class); + + private static final AtomicBoolean retrievingLocalExternalIp = new AtomicBoolean(false); + private static final CountDownLatch triedLocalExternalIp = new CountDownLatch(1); + private static volatile String localExternalIp; + + private static class IpLoader implements Callable<String> { + private static final Pattern ipPattern = Pattern.compile( + "\\b((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\b"); + final String url; + + protected IpLoader(String url) { + this.url = url; + } + + @Override + public String call() { + String response = ResourceUtils.create(LocalhostExternalIpLoader.class) + .getResourceAsString(url).trim(); + return postProcessResponse(response); + } + + String postProcessResponse(String response) { + Matcher matcher = ipPattern.matcher(response); + boolean matched = matcher.find(); + if (!matched) { + LOG.error("No IP address matched in output from {}: {}", url, response); + return null; + } else { + return matcher.group(); + } + } + } + + @VisibleForTesting + static List<String> getIpAddressWebsites() { + String file = new ResourceUtils(LocalhostExternalIpLoader.class) + .getResourceAsString("classpath://brooklyn/location/geo/external-ip-address-resolvers.txt"); + Iterable<String> lines = Splitter.on('\n') + .omitEmptyStrings() + .trimResults() + .split(file); + List<String> urls = Lists.newArrayList(Iterables.filter(lines, Predicates.not(StringPredicates.startsWith("#")))); + Collections.shuffle(urls); + return urls; + } + + @VisibleForTesting + static String getIpAddressFrom(String url) { + return new IpLoader(url).call(); + } + + /** As {@link #getLocalhostIpWithin(Duration)} but returning 127.0.0.1 if not accessible */ + public static String getLocalhostIpQuicklyOrDefault() { + String result = doLoad(Duration.seconds(2)); + if (result==null) return "127.0.0.1"; + return result; + } + + /** As {@link #getLocalhostIpWithin(Duration)} but without the time limit cut-off, failing if the load gives an error. */ + public static String getLocalhostIpWaiting() { + return getLocalhostIpWithin(null); + } + + /** + * Attempts to load the public IP address of localhost, failing if the load + * does not complete within the given duration. + * @return The public IP address of localhost + */ + public static String getLocalhostIpWithin(Duration timeout) { + String result = doLoad(timeout); + if (result == null) { + throw new IllegalStateException("Unable to retrieve external IP for localhost; network may be down or slow or remote service otherwise not responding"); + } + return result; + } + + /** + * Requests URLs returned by {@link #getIpAddressWebsites()} until one returns an IP address. + * The address is assumed to be the external IP address of localhost. + * @param blockFor The maximum duration to wait for the IP address to be resolved. + * An indefinite way if null. + * @return A string in IPv4 format, or null if no such address could be ascertained. + */ + private static String doLoad(Duration blockFor) { + if (localExternalIp != null) { + return localExternalIp; + } + + final List<String> candidateUrls = getIpAddressWebsites(); + if (candidateUrls.isEmpty()) { + LOG.debug("No candidate URLs to use to determine external IP of localhost"); + return null; + } + + // do in private thread, otherwise blocks for 30s+ on dodgy network! + // (we can skip it if someone else is doing it, we have synch lock so we'll get notified) + if (retrievingLocalExternalIp.compareAndSet(false, true)) { + new Thread() { + public void run() { + for (String url : candidateUrls) { + try { + LOG.debug("Looking up external IP of this host from {} in private thread {}", url, Thread.currentThread()); + localExternalIp = new IpLoader(url).call(); + LOG.debug("Finished looking up external IP of this host from {} in private thread, result {}", url, localExternalIp); + break; + } catch (Throwable t) { + LOG.debug("Unable to look up external IP of this host from {}, probably offline {})", url, t); + } finally { + retrievingLocalExternalIp.set(false); + triedLocalExternalIp.countDown(); + } + } + } + }.start(); + } + + try { + if (blockFor!=null) { + Durations.await(triedLocalExternalIp, blockFor); + } else { + triedLocalExternalIp.await(); + } + } catch (InterruptedException e) { + throw Exceptions.propagate(e); + } + if (localExternalIp == null) { + return null; + } + return localExternalIp; + } + +}
