http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsWinRmMachineLocation.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsWinRmMachineLocation.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsWinRmMachineLocation.java new file mode 100644 index 0000000..753d772 --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsWinRmMachineLocation.java @@ -0,0 +1,154 @@ +/* + * 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.jclouds; + +import static brooklyn.util.JavaGroovyEquivalents.groovyTruth; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Iterator; +import java.util.Set; + +import org.apache.brooklyn.location.basic.WinRmMachineLocation; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.Template; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.util.flags.SetFromFlag; +import brooklyn.util.net.Networking; + +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.net.HostAndPort; + +public class JcloudsWinRmMachineLocation extends WinRmMachineLocation implements JcloudsMachineLocation { + + private static final Logger LOG = LoggerFactory.getLogger(JcloudsWinRmMachineLocation.class); + + @SetFromFlag + JcloudsLocation jcloudsParent; + + @SetFromFlag + NodeMetadata node; + + @SetFromFlag + Template template; + + public JcloudsWinRmMachineLocation() { + } + + @Override + public String toVerboseString() { + return Objects.toStringHelper(this).omitNullValues() + .add("id", getId()).add("name", getDisplayName()) + .add("user", getUser()) + .add("address", getAddress()) + .add("port", getPort()) + .add("node", getNode()) + .add("jcloudsId", getJcloudsId()) + .add("privateAddresses", node.getPrivateAddresses()) + .add("publicAddresses", node.getPublicAddresses()) + .add("parentLocation", getParent()) + .add("osDetails", getOsDetails()) + .toString(); + } + + @Override + public int getPort() { + return getConfig(WINRM_PORT); + } + + @Override + public NodeMetadata getNode() { + return node; + } + + @Override + public Template getTemplate() { + return template; + } + + @Override + public JcloudsLocation getParent() { + return jcloudsParent; + } + + @Override + public String getHostname() { + InetAddress address = getAddress(); + return (address != null) ? address.getHostAddress() : null; + } + + @Override + public Set<String> getPublicAddresses() { + return node.getPublicAddresses(); + } + + @Override + public Set<String> getPrivateAddresses() { + return node.getPrivateAddresses(); + } + + @Override + public String getSubnetHostname() { + // TODO: TEMP FIX: WAS: + // String publicHostname = jcloudsParent.getPublicHostname(node, Optional.<HostAndPort>absent(), config().getBag()); + // but this causes a call to JcloudsUtil.getFirstReachableAddress, which searches for accessible SSH service. + // This workaround is good for public nodes but not private-subnet ones. + return getHostname(); + } + + @Override + public String getSubnetIp() { + Optional<String> privateAddress = getPrivateAddress(); + if (privateAddress.isPresent()) { + return privateAddress.get(); + } + + String hostname = jcloudsParent.getPublicHostname(node, Optional.<HostAndPort>absent(), config().getBag()); + if (hostname != null && !Networking.isValidIp4(hostname)) { + try { + return InetAddress.getByName(hostname).getHostAddress(); + } catch (UnknownHostException e) { + LOG.debug("Cannot resolve IP for hostname {} of machine {} (so returning hostname): {}", new Object[] {hostname, this, e}); + } + } + return hostname; + } + + protected Optional<String> getPrivateAddress() { + if (groovyTruth(node.getPrivateAddresses())) { + Iterator<String> pi = node.getPrivateAddresses().iterator(); + while (pi.hasNext()) { + String p = pi.next(); + // disallow local only addresses + if (Networking.isLocalOnly(p)) continue; + // other things may be public or private, but either way, return it + return Optional.of(p); + } + } + return Optional.absent(); + } + + @Override + public String getJcloudsId() { + return node.getId(); + } +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/SudoTtyFixingCustomizer.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/SudoTtyFixingCustomizer.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/SudoTtyFixingCustomizer.java new file mode 100644 index 0000000..d48a67c --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/SudoTtyFixingCustomizer.java @@ -0,0 +1,58 @@ +/* + * 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.jclouds; + +import org.apache.brooklyn.location.basic.SshMachineLocation; +import org.jclouds.compute.ComputeService; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; + +import brooklyn.util.task.DynamicTasks; +import brooklyn.util.task.ssh.SshTasks; +import brooklyn.util.task.ssh.SshTasks.OnFailingTask; + +/** + * Wraps Brooklyn's sudo-tty mitigations in a {@link JcloudsLocationCustomizer} for easy(-ish) consumption + * in YAML blueprints: + * + * <pre> + * name: My App + * brooklyn.config: + * provisioning.properties: + * customizerType: SudoTtyFixingCustomizer + * services: ... + * </pre> + * + * <p>This class should be seen as a temporary workaround and might disappear completely if/when Brooklyn takes care of this automatically. + * + * <p>See + * <a href='http://unix.stackexchange.com/questions/122616/why-do-i-need-a-tty-to-run-sudo-if-i-can-sudo-without-a-password'>http://unix.stackexchange.com/questions/122616/why-do-i-need-a-tty-to-run-sudo-if-i-can-sudo-without-a-password</a> + * for background. + */ +@Beta +public class SudoTtyFixingCustomizer extends BasicJcloudsLocationCustomizer { + + @Override + public void customize(JcloudsLocation location, ComputeService computeService, JcloudsMachineLocation machine) { + Preconditions.checkArgument(machine instanceof SshMachineLocation, "machine must be SshMachineLocation, but is %s", machine.getClass()); + DynamicTasks.queueIfPossible(SshTasks.dontRequireTtyForSudo((SshMachineLocation)machine, OnFailingTask.FAIL)).orSubmitAndBlock(); + DynamicTasks.waitForLast(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsLocationSecurityGroupCustomizer.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsLocationSecurityGroupCustomizer.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsLocationSecurityGroupCustomizer.java new file mode 100644 index 0000000..687a486 --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsLocationSecurityGroupCustomizer.java @@ -0,0 +1,563 @@ +/* + * 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.jclouds.networking; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.location.geo.LocalhostExternalIpLoader; +import org.apache.brooklyn.location.jclouds.JcloudsLocation; +import org.apache.brooklyn.location.jclouds.JcloudsLocationCustomizer; +import org.apache.brooklyn.location.jclouds.JcloudsMachineLocation; +import org.apache.brooklyn.location.jclouds.JcloudsSshMachineLocation; +import org.jclouds.aws.AWSResponseException; +import org.jclouds.compute.ComputeService; +import org.jclouds.compute.domain.SecurityGroup; +import org.jclouds.compute.domain.Template; +import org.jclouds.compute.extensions.SecurityGroupExtension; +import org.jclouds.domain.Location; +import org.jclouds.net.domain.IpPermission; +import org.jclouds.net.domain.IpProtocol; +import org.jclouds.providers.ProviderMetadata; +import org.jclouds.providers.Providers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.brooklyn.location.jclouds.BasicJcloudsLocationCustomizer; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.net.Cidr; +import brooklyn.util.task.Tasks; +import brooklyn.util.time.Duration; + +import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.base.Throwables; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +/** + * Configures custom security groups on Jclouds locations. + * + * @see SecurityGroupExtension is an optional extension to jclouds compute service. It allows the manipulation of + * {@link SecurityGroup}s. + * + * This customizer can be injected into {@link JcloudsLocation#obtainOnce} using + * It will be executed after the provisiioning of the {@link JcloudsMachineLocation} to apply app-specific + * customization related to the security groups. + * + * @since 0.7.0 + */ +@Beta +public class JcloudsLocationSecurityGroupCustomizer extends BasicJcloudsLocationCustomizer { + + private static final Logger LOG = LoggerFactory.getLogger(JcloudsLocationSecurityGroupCustomizer.class); + + // Caches instances of JcloudsLocationSecurityGroupCustomizer by application IDs. + private static final LoadingCache<String, JcloudsLocationSecurityGroupCustomizer> CUSTOMISERS = CacheBuilder.newBuilder() + .build(new CacheLoader<String, JcloudsLocationSecurityGroupCustomizer>() { + @Override + public JcloudsLocationSecurityGroupCustomizer load(final String appContext) throws Exception { + return new JcloudsLocationSecurityGroupCustomizer(appContext); + } + }); + + /** Caches the base security group that should be shared between all instances in the same Jclouds location */ + private final Cache<Location, SecurityGroup> sharedGroupCache = CacheBuilder.newBuilder().build(); + + /** Caches security groups unique to instances */ + private final Cache<String, SecurityGroup> uniqueGroupCache = CacheBuilder.newBuilder().build(); + + /** The context for this location customizer. */ + private final String applicationId; + + /** The CIDR for addresses that may SSH to machines. */ + private Supplier<Cidr> sshCidrSupplier; + + /** + * A predicate indicating whether the customiser can retry a request to add a security group + * or a rule after an throwable is thrown. + */ + private Predicate<Exception> isExceptionRetryable = Predicates.alwaysFalse(); + + protected JcloudsLocationSecurityGroupCustomizer(String applicationId) { + // Would be better to restrict with something like LocalhostExternalIpCidrSupplier, but + // we risk making machines inaccessible from Brooklyn when HA fails over. + this(applicationId, Suppliers.ofInstance(new Cidr("0.0.0.0/0"))); + } + + protected JcloudsLocationSecurityGroupCustomizer(String applicationId, Supplier<Cidr> sshCidrSupplier) { + this.applicationId = applicationId; + this.sshCidrSupplier = sshCidrSupplier; + } + + /** + * Gets the customizer for the given applicationId. Multiple calls to this method with the + * same application context will return the same JcloudsLocationSecurityGroupCustomizer instance. + * @param applicationId An identifier for the application the customizer is to be used for + * @return the unique customizer for the given context + */ + public static JcloudsLocationSecurityGroupCustomizer getInstance(String applicationId) { + return CUSTOMISERS.getUnchecked(applicationId); + } + + /** + * Gets a customizer for the given entity's application. Multiple calls to this method with entities + * in the same application will return the same JcloudsLocationSecurityGroupCustomizer instance. + * @param entity The entity the customizer is to be used for + * @return the unique customizer for the entity's owning application + */ + public static JcloudsLocationSecurityGroupCustomizer getInstance(Entity entity) { + return getInstance(entity.getApplicationId()); + } + + /** + * @param predicate + * A predicate whose return value indicates whether a request to add a security group + * or permission may be retried after its input {@link Exception} was thrown. + * @return this + */ + public JcloudsLocationSecurityGroupCustomizer setRetryExceptionPredicate(Predicate<Exception> predicate) { + this.isExceptionRetryable = checkNotNull(predicate, "predicate"); + return this; + } + + /** + * @param cidrSupplier A supplier returning a CIDR for hosts that are allowed to SSH to locations. + */ + public JcloudsLocationSecurityGroupCustomizer setSshCidrSupplier(Supplier<Cidr> cidrSupplier) { + this.sshCidrSupplier = checkNotNull(cidrSupplier, "cidrSupplier"); + return this; + } + + /** @see #addPermissionsToLocation(JcloudsSshMachineLocation, java.lang.Iterable) */ + public JcloudsLocationSecurityGroupCustomizer addPermissionsToLocation(final JcloudsMachineLocation location, IpPermission... permissions) { + addPermissionsToLocation(location, ImmutableList.copyOf(permissions)); + return this; + } + + /** @see #addPermissionsToLocation(JcloudsSshMachineLocation, java.lang.Iterable) */ + public JcloudsLocationSecurityGroupCustomizer addPermissionsToLocation(final JcloudsMachineLocation location, SecurityGroupDefinition securityGroupDefinition) { + addPermissionsToLocation(location, securityGroupDefinition.getPermissions()); + return this; + } + + /** + * Applies the given security group permissions to the given location. + * <p> + * Takes no action if the location's compute service does not have a security group extension. + * @param permissions The set of permissions to be applied to the location + * @param location Location to gain permissions + */ + public JcloudsLocationSecurityGroupCustomizer addPermissionsToLocation(final JcloudsMachineLocation location, final Iterable<IpPermission> permissions) { + ComputeService computeService = location.getParent().getComputeService(); + String nodeId = location.getNode().getId(); + addPermissionsToLocation(permissions, nodeId, computeService); + return this; + } + + /** + * Applies the given security group permissions to the given node with the given compute service. + * <p> + * Takes no action if the compute service does not have a security group extension. + * @param permissions The set of permissions to be applied to the node + * @param nodeId The id of the node to update + * @param computeService The compute service to use to apply the changes + */ + @VisibleForTesting + void addPermissionsToLocation(Iterable<IpPermission> permissions, final String nodeId, ComputeService computeService) { + if (!computeService.getSecurityGroupExtension().isPresent()) { + LOG.warn("Security group extension for {} absent; cannot update node {} with {}", + new Object[] {computeService, nodeId, permissions}); + return; + } + final SecurityGroupExtension securityApi = computeService.getSecurityGroupExtension().get(); + final String locationId = computeService.getContext().unwrap().getId(); + + // Expect to have two security groups on the node: one shared between all nodes in the location, + // that is cached in sharedGroupCache, and one created by Jclouds that is unique to the node. + // Relies on customize having been called before. This should be safe because the arguments + // needed to call this method are not available until post-instance creation. + SecurityGroup machineUniqueSecurityGroup; + Tasks.setBlockingDetails("Loading unique security group for node: " + nodeId); + try { + machineUniqueSecurityGroup = uniqueGroupCache.get(nodeId, new Callable<SecurityGroup>() { + @Override public SecurityGroup call() throws Exception { + SecurityGroup sg = getUniqueSecurityGroupForNodeCachingSharedGroupIfPreviouslyUnknown(nodeId, locationId, securityApi); + if (sg == null) { + throw new IllegalStateException("Failed to find machine-unique group on node: " + nodeId); + } + return sg; + } + }); + } catch (ExecutionException e) { + throw Throwables.propagate(new Exception(e.getCause())); + } finally { + Tasks.resetBlockingDetails(); + } + for (IpPermission permission : permissions) { + addPermission(permission, machineUniqueSecurityGroup, securityApi); + } + } + + /** + * Loads the security groups attached to the node with the given ID and returns the group + * that is unique to the node, per the application context. This method will also update + * {@link #sharedGroupCache} if no mapping for the shared group's location previously + * existed (e.g. Brooklyn was restarted and rebound to an existing application). + * + * Notice that jclouds will attach 2 securityGroups to the node if the locationId is `aws-ec2` so it needs to + * look for the uniqueSecurityGroup rather than the shared securityGroup. + * + * @param nodeId The id of the node in question + * @param locationId The id of the location in question + * @param securityApi The API to use to list security groups + * @return the security group unique to the given node, or null if one could not be determined. + */ + private SecurityGroup getUniqueSecurityGroupForNodeCachingSharedGroupIfPreviouslyUnknown(String nodeId, String locationId, SecurityGroupExtension securityApi) { + Set<SecurityGroup> groupsOnNode = securityApi.listSecurityGroupsForNode(nodeId); + SecurityGroup unique; + if (locationId.equals("aws-ec2")) { + if (groupsOnNode.size() != 2) { + LOG.warn("Expected to find two security groups on node {} in app {} (one shared, one unique). Found {}: {}", + new Object[]{nodeId, applicationId, groupsOnNode.size(), groupsOnNode}); + return null; + } + String expectedSharedName = getNameForSharedSecurityGroup(); + Iterator<SecurityGroup> it = groupsOnNode.iterator(); + SecurityGroup shared = it.next(); + if (shared.getName().endsWith(expectedSharedName)) { + unique = it.next(); + } else { + unique = shared; + shared = it.next(); + } + if (!shared.getName().endsWith(expectedSharedName)) { + LOG.warn("Couldn't determine which security group is shared between instances in app {}. Expected={}, found={}", + new Object[]{applicationId, expectedSharedName, groupsOnNode}); + return null; + } + // Shared entry might be missing if Brooklyn has rebound to an application + SecurityGroup old = sharedGroupCache.asMap().putIfAbsent(shared.getLocation(), shared); + LOG.info("Loaded unique security group for node {} (in {}): {}", + new Object[]{nodeId, applicationId, unique}); + if (old == null) { + LOG.info("Proactively set shared group for app {} to: {}", applicationId, shared); + } + return unique; + } + return Iterables.getOnlyElement(groupsOnNode); + } + + /** + * Replaces security groups configured on the given template with one that allows + * SSH access on port 22 and allows communication on all ports between machines in + * the same group. Security groups are reused when templates have equal + * {@link org.jclouds.compute.domain.Template#getLocation locations}. + * <p> + * This method is called by Brooklyn when obtaining machines, as part of the + * {@link JcloudsLocationCustomizer} contract. It + * should not be called from anywhere else. + * + * @param location The Brooklyn location that has called this method while obtaining a machine + * @param computeService The compute service being used by the location argument to provision a machine + * @param template The machine template created by the location argument + */ + @Override + public void customize(JcloudsLocation location, ComputeService computeService, Template template) { + if (!computeService.getSecurityGroupExtension().isPresent()) { + LOG.warn("Security group extension for {} absent; cannot configure security groups in context: {}", computeService, applicationId); + } else if (template.getLocation() == null) { + LOG.warn("No location has been set on {}; cannot configure security groups in context: {}", template, applicationId); + } else { + LOG.info("Configuring security groups on location {} in context {}", location, applicationId); + setSecurityGroupOnTemplate(location, template, computeService.getSecurityGroupExtension().get()); + } + } + + private void setSecurityGroupOnTemplate(final JcloudsLocation location, final Template template, final SecurityGroupExtension securityApi) { + SecurityGroup shared; + Tasks.setBlockingDetails("Loading security group shared by instances in " + template.getLocation() + + " in app " + applicationId); + try { + shared = sharedGroupCache.get(template.getLocation(), new Callable<SecurityGroup>() { + @Override public SecurityGroup call() throws Exception { + return getOrCreateSharedSecurityGroup(template.getLocation(), securityApi); + } + }); + } catch (ExecutionException e) { + throw Throwables.propagate(new Exception(e.getCause())); + } finally { + Tasks.resetBlockingDetails(); + } + + Set<String> originalGroups = template.getOptions().getGroups(); + template.getOptions().securityGroups(shared.getName()); + if (!originalGroups.isEmpty()) { + LOG.info("Replaced configured security groups: configured={}, replaced with={}", originalGroups, template.getOptions().getGroups()); + } else { + LOG.debug("Configured security groups at {} to: {}", location, template.getOptions().getGroups()); + } + } + + /** + * Loads the security group to be shared between nodes in the same application in the + * given Location. If no such security group exists it is created. + * + * @param location The location in which the security group will be found + * @param securityApi The API to use to list and create security groups + * @return the security group to share between instances in the given location in this application + */ + private SecurityGroup getOrCreateSharedSecurityGroup(Location location, SecurityGroupExtension securityApi) { + final String groupName = getNameForSharedSecurityGroup(); + // Could sort-and-search if straight search is too expensive + Optional<SecurityGroup> shared = Iterables.tryFind(securityApi.listSecurityGroupsInLocation(location), new Predicate<SecurityGroup>() { + @Override + public boolean apply(final SecurityGroup input) { + // endsWith because Jclouds prepends 'jclouds#' to security group names. + return input.getName().endsWith(groupName); + } + }); + if (shared.isPresent()) { + LOG.info("Found existing shared security group in {} for app {}: {}", + new Object[]{location, applicationId, groupName}); + return shared.get(); + } else { + LOG.info("Creating new shared security group in {} for app {}: {}", + new Object[]{location, applicationId, groupName}); + return createBaseSecurityGroupInLocation(groupName, location, securityApi); + } + } + + /** + * Creates a security group with rules to: + * <ul> + * <li>Allow SSH access on port 22 from the world</li> + * <li>Allow TCP, UDP and ICMP communication between machines in the same group</li> + * </ul> + * + * It needs to consider locationId as port ranges and groupId are cloud provider-dependent e.g openstack nova + * wants from 1-65535 while aws-ec2 accepts from 0-65535. + * + * + * @param groupName The name of the security group to create + * @param location The location in which the security group will be created + * @param securityApi The API to use to create the security group + * + * @return the created security group + */ + private SecurityGroup createBaseSecurityGroupInLocation(String groupName, Location location, SecurityGroupExtension securityApi) { + SecurityGroup group = addSecurityGroupInLocation(groupName, location, securityApi); + + Set<String> openstackNovaIds = getJcloudsLocationIds("openstack-nova"); + + String groupId = group.getProviderId(); + int fromPort = 0; + if (location.getParent() != null && Iterables.contains(openstackNovaIds, location.getParent().getId())) { + groupId = group.getId(); + fromPort = 1; + } + // Note: For groupName to work with GCE we also need to tag the machines with the same ID. + // See sourceTags section at https://developers.google.com/compute/docs/networking#firewalls + IpPermission.Builder allWithinGroup = IpPermission.builder() + .groupId(groupId) + .fromPort(fromPort) + .toPort(65535); + addPermission(allWithinGroup.ipProtocol(IpProtocol.TCP).build(), group, securityApi); + addPermission(allWithinGroup.ipProtocol(IpProtocol.UDP).build(), group, securityApi); + addPermission(allWithinGroup.ipProtocol(IpProtocol.ICMP).fromPort(-1).toPort(-1).build(), group, securityApi); + + IpPermission sshPermission = IpPermission.builder() + .fromPort(22) + .toPort(22) + .ipProtocol(IpProtocol.TCP) + .cidrBlock(getBrooklynCidrBlock()) + .build(); + addPermission(sshPermission, group, securityApi); + + return group; + } + + private Set<String> getJcloudsLocationIds(final String jcloudsApiId) { + Set<String> openstackNovaProviders = FluentIterable.from(Providers.all()) + .filter(new Predicate<ProviderMetadata>() { + @Override + public boolean apply(ProviderMetadata providerMetadata) { + return providerMetadata.getApiMetadata().getId().equals(jcloudsApiId); + } + }).transform(new Function<ProviderMetadata, String>() { + @Nullable + @Override + public String apply(ProviderMetadata input) { + return input.getId(); + } + }).toSet(); + + return new ImmutableSet.Builder<String>() + .addAll(openstackNovaProviders) + .add(jcloudsApiId) + .build(); + } + + protected SecurityGroup addSecurityGroupInLocation(final String groupName, final Location location, final SecurityGroupExtension securityApi) { + LOG.debug("Creating security group {} in {}", groupName, location); + Callable<SecurityGroup> callable = new Callable<SecurityGroup>() { + @Override + public SecurityGroup call() throws Exception { + return securityApi.createSecurityGroup(groupName, location); + } + }; + return runOperationWithRetry(callable); + } + + protected SecurityGroup addPermission(final IpPermission permission, final SecurityGroup group, final SecurityGroupExtension securityApi) { + LOG.debug("Adding permission to security group {}: {}", group.getName(), permission); + Callable<SecurityGroup> callable = new Callable<SecurityGroup>() { + @Override + public SecurityGroup call() throws Exception { + return securityApi.addIpPermission(permission, group); + } + }; + return runOperationWithRetry(callable); + } + + /** @return the CIDR block used to configure Brooklyn's in security groups */ + public String getBrooklynCidrBlock() { + return sshCidrSupplier.get().toString(); + } + + /** + * @return The name to be used by security groups that will be shared between machines + * in the same location for this instance's application context. + */ + @VisibleForTesting + String getNameForSharedSecurityGroup() { + return "brooklyn-" + applicationId.toLowerCase() + "-shared"; + } + + /** + * Invalidates all entries in {@link #sharedGroupCache} and {@link #uniqueGroupCache}. + * Use to simulate the effects of rebinding Brooklyn to a deployment. + */ + @VisibleForTesting + void clearSecurityGroupCaches() { + LOG.info("Clearing security group caches"); + sharedGroupCache.invalidateAll(); + uniqueGroupCache.invalidateAll(); + } + + /** + * Runs the given callable. Repeats until the operation succeeds or {@link #isExceptionRetryable} indicates + * that the request cannot be retried. + */ + protected <T> T runOperationWithRetry(Callable<T> operation) { + int backoff = 64; + Exception lastException = null; + for (int retries = 0; retries < 100; retries++) { + try { + return operation.call(); + } catch (Exception e) { + lastException = e; + if (isExceptionRetryable.apply(e)) { + LOG.debug("Attempt #{} failed to add security group: {}", retries + 1, e.getMessage()); + try { + Thread.sleep(backoff); + } catch (InterruptedException e1) { + throw Exceptions.propagate(e1); + } + backoff = backoff << 1; + } else { + break; + } + } + } + + throw new RuntimeException("Unable to add security group rule; repeated errors from provider", lastException); + } + + /** + * @return + * A predicate that is true if an exception contains an {@link org.jclouds.aws.AWSResponseException} + * whose error code is either <code>InvalidGroup.InUse</code>, <code>DependencyViolation</code> or + * <code>RequestLimitExceeded</code>. + */ + public static Predicate<Exception> newAwsExceptionRetryPredicate() { + return new AwsExceptionRetryPredicate(); + } + + private static class AwsExceptionRetryPredicate implements Predicate<Exception> { + // Error reference: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html + private static final Set<String> AWS_ERRORS_TO_RETRY = ImmutableSet.of( + "InvalidGroup.InUse", "DependencyViolation", "RequestLimitExceeded"); + + @Override + public boolean apply(Exception input) { + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + AWSResponseException exception = Exceptions.getFirstThrowableOfType(input, AWSResponseException.class); + if (exception != null) { + String code = exception.getError().getCode(); + return AWS_ERRORS_TO_RETRY.contains(code); + } + return false; + } + } + + /** + * A supplier of CIDRs that loads the external IP address of the localhost machine. + */ + private static class LocalhostExternalIpCidrSupplier implements Supplier<Cidr> { + + private volatile Cidr cidr; + + @Override + public Cidr get() { + Cidr local = cidr; + if (local == null) { + synchronized (this) { + local = cidr; + if (local == null) { + String externalIp = LocalhostExternalIpLoader.getLocalhostIpWithin(Duration.seconds(5)); + cidr = local = new Cidr(externalIp + "/32"); + } + } + } + return local; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsPortForwarderExtension.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsPortForwarderExtension.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsPortForwarderExtension.java new file mode 100644 index 0000000..00fd7f8 --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/JcloudsPortForwarderExtension.java @@ -0,0 +1,45 @@ +/* + * 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.jclouds.networking; + +import org.jclouds.compute.domain.NodeMetadata; + +import org.apache.brooklyn.location.access.BrooklynAccessUtils; +import org.apache.brooklyn.location.access.PortForwardManager; +import brooklyn.util.net.Cidr; +import brooklyn.util.net.Protocol; + +import com.google.common.base.Optional; +import com.google.common.net.HostAndPort; + +public interface JcloudsPortForwarderExtension { + + /** + * Opens port forwarding (e.g. DNAT or iptables port-forwarding) to reach the given given + * target port on this node (from the given cidr). + * + * This should also register the port with the {@link PortForwardManager}, via + * {@code portForwardManager.associate(node.getId(), result, targetPort)} so that + * subsequent calls to {@link BrooklynAccessUtils#getBrooklynAccessibleAddress(brooklyn.entity.Entity, int)} + * will know about the mapped port. + */ + public HostAndPort openPortForwarding(NodeMetadata node, int targetPort, Optional<Integer> optionalPublicPort, Protocol protocol, Cidr accessingCidr); + + public void closePortForwarding(NodeMetadata node, int targetPort, HostAndPort publicHostAndPort, Protocol protocol); +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupDefinition.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupDefinition.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupDefinition.java new file mode 100644 index 0000000..8b95d1e --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupDefinition.java @@ -0,0 +1,103 @@ +/* + * 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.jclouds.networking; + +import java.util.List; +import java.util.concurrent.Callable; + +import org.jclouds.aws.ec2.AWSEC2Api; +import org.jclouds.compute.ComputeServiceContext; +import org.jclouds.net.domain.IpPermission; +import org.jclouds.net.domain.IpProtocol; +import org.jclouds.net.util.IpPermissions; + +import brooklyn.util.collections.MutableList; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.net.Cidr; +import brooklyn.util.text.Identifiers; + +import com.google.common.annotations.Beta; + +/** WIP to define a security group in an up-front way, where subsequently it can be applied to a jclouds location */ +@Beta +public class SecurityGroupDefinition { + + private Callable<String> groupNameFactory = new Callable<String>() { public String call() { return "br-sg-"+Identifiers.makeRandomId(8); } }; + private List<IpPermission> ipPerms = MutableList.of(); + + public void createGroupInAwsRegion(ComputeServiceContext computeServiceContext, String region) { + AWSEC2Api ec2Client = computeServiceContext.unwrapApi(AWSEC2Api.class); + String sgId = ec2Client.getSecurityGroupApi().get().createSecurityGroupInRegionAndReturnId(region, getName(), "Brooklyn-managed security group "+getName()); + ec2Client.getSecurityGroupApi().get().authorizeSecurityGroupIngressInRegion(region, sgId, ipPerms); + } + + /** allows access to the given port on TCP from within the subnet */ + public SecurityGroupDefinition allowingInternalPort(int port) { + return allowing(IpPermissions.permit(IpProtocol.TCP).port(port)); + } + public SecurityGroupDefinition allowingInternalPorts(int port1, int port2, int ...ports) { + allowing(IpPermissions.permit(IpProtocol.TCP).port(port1)); + allowing(IpPermissions.permit(IpProtocol.TCP).port(port2)); + for (int port: ports) + allowing(IpPermissions.permit(IpProtocol.TCP).port(port)); + return this; + } + public SecurityGroupDefinition allowingInternalPortRange(int portRangeStart, int portRangeEnd) { + return allowing(IpPermissions.permit(IpProtocol.TCP).fromPort(portRangeStart).to(portRangeEnd)); + } + public SecurityGroupDefinition allowingInternalPing() { + return allowing(IpPermissions.permit(IpProtocol.ICMP)); + } + + public SecurityGroupDefinition allowingPublicPort(int port) { + return allowing(IpPermissions.permit(IpProtocol.TCP).port(port).originatingFromCidrBlock(Cidr.UNIVERSAL.toString())); + } + public SecurityGroupDefinition allowingPublicPorts(int port1, int port2, int ...ports) { + allowing(IpPermissions.permit(IpProtocol.TCP).port(port1).originatingFromCidrBlock(Cidr.UNIVERSAL.toString())); + allowing(IpPermissions.permit(IpProtocol.TCP).port(port2).originatingFromCidrBlock(Cidr.UNIVERSAL.toString())); + for (int port: ports) + allowing(IpPermissions.permit(IpProtocol.TCP).port(port).originatingFromCidrBlock(Cidr.UNIVERSAL.toString())); + return this; + } + public SecurityGroupDefinition allowingPublicPortRange(int portRangeStart, int portRangeEnd) { + return allowing(IpPermissions.permit(IpProtocol.TCP).fromPort(portRangeStart).to(portRangeEnd).originatingFromCidrBlock(Cidr.UNIVERSAL.toString())); + } + public SecurityGroupDefinition allowingPublicPing() { + return allowing(IpPermissions.permit(IpProtocol.ICMP).originatingFromCidrBlock(Cidr.UNIVERSAL.toString())); + } + + public SecurityGroupDefinition allowing(IpPermission permission) { + ipPerms.add(permission); + return this; + } + + // TODO use cloud machine namer + public SecurityGroupDefinition named(final String name) { + groupNameFactory = new Callable<String>() { public String call() { return name; } }; + return this; + } + public String getName() { + try { return groupNameFactory.call(); } + catch (Exception e) { throw Exceptions.propagate(e); } + } + + public Iterable<IpPermission> getPermissions() { + return ipPerms; + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupTool.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupTool.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupTool.java new file mode 100644 index 0000000..bf33f68 --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/networking/SecurityGroupTool.java @@ -0,0 +1,167 @@ +/* + * 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.jclouds.networking; + +import java.util.Set; + +import org.apache.brooklyn.location.jclouds.JcloudsLocation; +import org.apache.brooklyn.location.jclouds.JcloudsLocationConfig; +import org.jclouds.aws.ec2.AWSEC2Api; +import org.jclouds.aws.util.AWSUtils; +import org.jclouds.compute.domain.SecurityGroup; +import org.jclouds.compute.extensions.SecurityGroupExtension; +import org.jclouds.net.domain.IpPermission; +import org.jclouds.rest.ApiContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.text.Strings; + +import com.google.common.annotations.Beta; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +/** WIP to apply a security group to a jclouds endpoint. + * <p> + * sections of this code have been used but overall it is not yet in a working state, + * but merged May 2014 to make it easier to pick up if and when needed. + * (if not needed after several months this may simply be removed.) */ +@Beta +public class SecurityGroupTool { + + private static final Logger log = LoggerFactory.getLogger(SecurityGroupTool.class); + + protected final JcloudsLocation location; + protected final SecurityGroupDefinition sgDef; + + public SecurityGroupTool(JcloudsLocation location, SecurityGroupDefinition sgDef) { + this.location = Preconditions.checkNotNull(location); + this.sgDef = Preconditions.checkNotNull(sgDef); + } + + public String getName() { + return sgDef.getName(); + } + + public void apply() { + Optional<SecurityGroupExtension> sgExtO = location.getComputeService().getSecurityGroupExtension(); + if (!sgExtO.isPresent()) { + throw new IllegalStateException("Advanced networking not supported in this location ("+location+")"); + } + SecurityGroupExtension sgExt = sgExtO.get(); + + SecurityGroup sg = findSecurityGroupWithName(sgExt, getName()); + if (sg==null) { + // TODO initialize the location + org.jclouds.domain.Location sgLoc = null; + + // TODO record that we created it + // create it + try { + // FIXME this will always fail for providers which need a location, until we set it above + // https://github.com/brooklyncentral/brooklyn/pull/1343#discussion_r12275188 + sg = sgExt.createSecurityGroup(getName(), sgLoc); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + // check if someone else already created it + sg = findSecurityGroupWithName(sgExt, getName()); + if (sg==null) { + // no - so propagate error + throw Exceptions.propagate(e); + } else { + log.debug("Looks like parallel thread created security group "+getName()+"; ignoring error in our thread ("+e+") as we now have an SG"); + } + } + } + + if (sg==null) + throw new IllegalStateException("Unable to find or create security group ID for "+getName()); + + addPermissions(sgExt, sg); + } + + protected SecurityGroup findSecurityGroupWithName(SecurityGroupExtension sgExt, String name) { + Set<SecurityGroup> groups = sgExt.listSecurityGroups(); + // jclouds appends this sometimes so for portability let's add this + String nameAlt = name.startsWith("jclouds#") ? Strings.removeFromStart(name, "jclouds#") : "jclouds#"+name; + for (SecurityGroup g: groups) { + if (name.equals(g.getName())) return g; + if (nameAlt.equals(g.getName())) return g; + } + return null; + } + + protected void addPermissions(SecurityGroupExtension sgExt, SecurityGroup sg) { + + Object api = ((ApiContext<?>)location.getComputeService().getContext().unwrap()).getApi(); + if (api instanceof AWSEC2Api) { + // optimization for AWS where rules can be added all at once, and it cuts down Req Limit Exceeded problems! + String region = AWSUtils.getRegionFromLocationOrNull(sg.getLocation()); + String id = sg.getProviderId(); + + ((AWSEC2Api)api).getSecurityGroupApi().get().authorizeSecurityGroupIngressInRegion(region, id, sgDef.getPermissions()); + + } else { + for (IpPermission p: sgDef.getPermissions()) { + sgExt.addIpPermission(p, sg); + } + } + } + + + // TODO remove this method once we've confirmed the above works nicely (this is an early attempt) + protected void applyOldEc2(AWSEC2Api client) { + String region = location.getConfig(JcloudsLocationConfig.CLOUD_REGION_ID); + if (region==null) { + // TODO where does the default come from? + log.warn("No region set for "+location+"; assuming EC2"); + region = "us-east-1"; + } + + Set<org.jclouds.ec2.domain.SecurityGroup> groups = client.getSecurityGroupApi().get().describeSecurityGroupsInRegion(region, getName()); + String id = null; + if (groups.isEmpty()) { + // create it + try { + id = client.getSecurityGroupApi().get().createSecurityGroupInRegionAndReturnId(region , getName(), "Brooklyn-managed security group "+getName()); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + // check if someone else already created it! + groups = client.getSecurityGroupApi().get().describeSecurityGroupsInRegion(region, getName()); + if (groups.isEmpty()) { + // no - so propagate error + throw Exceptions.propagate(e); + } else { + log.debug("Looks like parallel thread created security group "+getName()+"; ignoring error in our thread ("+e+") as we now have an SG"); + } + } + } + if (!groups.isEmpty()) { + if (groups.size()>1) + log.warn("Multiple security groups matching '"+getName()+"' (using the first): "+groups); + id = groups.iterator().next().getId(); + } + if (id==null) + throw new IllegalStateException("Unable to find or create security group ID for "+getName()); + + client.getSecurityGroupApi().get().authorizeSecurityGroupIngressInRegion(region, id, sgDef.getPermissions()); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePool.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePool.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePool.java new file mode 100644 index 0000000..79a3dc8 --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePool.java @@ -0,0 +1,395 @@ +/* + * 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.jclouds.pool; + +import static org.apache.brooklyn.location.jclouds.pool.MachinePoolPredicates.compose; +import static org.apache.brooklyn.location.jclouds.pool.MachinePoolPredicates.matching; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.jclouds.compute.ComputeService; +import org.jclouds.compute.RunNodesException; +import org.jclouds.compute.domain.ComputeMetadata; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.Template; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +/** + * Contains details of machines detected at a given cloud (ComputeService), + * and records claims made against those machines via this pool. + * <p> + * Machine instances themselves are persisted and rescanned as new instances of this class are created. + * Claims however are specific to this instance of the class, i.e. <b>not</b> persisted. + * <p> + * This class is believed to be thread-safe. + * Refreshes to the remote detected machines are synchronized on the pool instance. + * Details of detected and claimed machines are also synchronized on the pool instance. + * (If it is necessary to claim machines whilst the pool is being rescanned, + * we can investigate a more sophisticated threading model. + * Access to some fields is clearly independent and uses a tighter synchonization + * strategy, e.g. templates. + * Synchronization of fields within a synch block on the class instance + * is permitted, but not the other way round, + * and synching on multiple fields is also not permitted.) + * <p> + * Callers wishing to guarantee results of e.g. ensureUnclaimed remaining available + * can synchronize on this class for the duration that they wish to have that guarantee + * (at the cost, of course, of any other threads being able to access this pool). + * <p> + * If underlying provisioning/destroying operations fail, the pool + * currently may be in an unknown state, currently. + * If more robustness is needed this can be added. + * + * @deprecated since 0.6.0; never used in production setting, and thus of dubious value; best avoided as unlikely to be supported in future versions + */ +@Deprecated +public class MachinePool { + + private static final Logger log = LoggerFactory.getLogger(MachinePool.class); + + protected final ComputeService computeService; + final AtomicBoolean refreshNeeded = new AtomicBoolean(true); + final List<ReusableMachineTemplate> templates = new ArrayList<ReusableMachineTemplate>(); + String poolName = null; + + /** all machines detected, less those in the black list */ + volatile MachineSet detectedMachines = new MachineSet(); + volatile MachineSet matchedMachines = new MachineSet(); + volatile MachineSet claimedMachines = new MachineSet(); + volatile MachineSet blacklistedMachines = new MachineSet(); + + public MachinePool(ComputeService computeService) { + this.computeService = computeService; + } + + protected synchronized void init() { + if (!refreshNeeded.get()) return; + refresh(); + } + + public void setPoolName(String poolName) { + if (poolName!=null) + log.warn("Changing pool name of "+this+" (from "+this.poolName+" to "+poolName+") is discouraged."); + this.poolName = poolName; + } + /** pool name is used as a group/label by jclouds, for convenience only; + * it has no special properties for detecting matching instances + * (use explicit tags on the templates, for that). + * defaults to name of pool class and user name. + * callers should set pool name before getting, if using a custom name. */ + public synchronized String getPoolName() { + if (poolName==null) + poolName = getClass().getSimpleName()+"-"+System.getProperty("user.name"); + return poolName; + } + + /** refreshes the pool of machines from the server (finding all instances matching the registered templates) */ + public synchronized void refresh() { + refreshNeeded.set(false); + Set<? extends ComputeMetadata> computes = computeService.listNodes(); + Set<NodeMetadata> nodes = new LinkedHashSet<NodeMetadata>(); + for (ComputeMetadata c: computes) { + if (c instanceof NodeMetadata) { + nodes.add((NodeMetadata)c); + } else { + // TODO should we try to fetch more info? + log.warn("MachinePool "+this+" ignoring non-Node record for remote machine: "+c); + } + } + + MachineSet allNewDetectedMachines = new MachineSet(nodes); + MachineSet newDetectedMachines = filterForAllowedMachines(allNewDetectedMachines); + MachineSet oldDetectedMachines = detectedMachines; + MachineSet newMatchedMachines = new MachineSet(); + detectedMachines = newDetectedMachines; + + MachineSet appearedMachinesIncludingBlacklist = allNewDetectedMachines.removed(oldDetectedMachines); + MachineSet appearedMachines = filterForAllowedMachines(appearedMachinesIncludingBlacklist); + if (appearedMachinesIncludingBlacklist.size()>appearedMachines.size()) + if (log.isDebugEnabled()) log.debug("Pool "+this+", ignoring "+(appearedMachinesIncludingBlacklist.size()-appearedMachines.size())+" disallowed"); + int matchedAppeared = 0; + for (NodeMetadata m: appearedMachines) { + if (m.getStatus() != NodeMetadata.Status.RUNNING) { + if (log.isDebugEnabled()) + log.debug("Pool "+this+", newly detected machine "+m+", not running ("+m.getStatus()+")"); + } else { + Set<ReusableMachineTemplate> ts = getTemplatesMatchingInstance(m); + if (!ts.isEmpty()) { + matchedAppeared++; + newMatchedMachines = newMatchedMachines.added(new MachineSet(m)); + if (log.isDebugEnabled()) + log.debug("Pool "+this+", newly detected machine "+m+", matches pool templates "+ts); + } else { + if (log.isDebugEnabled()) + log.debug("Pool "+this+", newly detected machine "+m+", does not match any pool templates"); + } + } + } + if (matchedAppeared>0) { + log.info("Pool "+this+" discovered "+matchedAppeared+" matching machines (of "+appearedMachines.size()+" total new; "+newDetectedMachines.size()+" total including claimed and unmatched)"); + } else { + if (log.isDebugEnabled()) + log.debug("Pool "+this+" discovered "+matchedAppeared+" matching machines (of "+appearedMachines.size()+" total new; "+newDetectedMachines.size()+" total including claimed and unmatched)"); + } + matchedMachines = newMatchedMachines; + } + + protected MachineSet filterForAllowedMachines(MachineSet input) { + return input.removed(blacklistedMachines); + } + + // TODO template registry and claiming from a template could be a separate responsibility + + protected ReusableMachineTemplate registerTemplate(ReusableMachineTemplate template) { + registerTemplates(template); + return template; + } + protected void registerTemplates(ReusableMachineTemplate ...templatesToReg) { + synchronized (templates) { + for (ReusableMachineTemplate template: templatesToReg) + templates.add(template); + } + } + + protected ReusableMachineTemplate newTemplate(String name) { + return registerTemplate(new ReusableMachineTemplate(name)); + } + + + public List<ReusableMachineTemplate> getTemplates() { + List<ReusableMachineTemplate> result; + synchronized (templates) { result = ImmutableList.copyOf(templates); } + return result; + } + + /** all machines matching any templates */ + public MachineSet all() { + init(); + return matchedMachines; + } + + /** machines matching any templates which have not been claimed */ + public MachineSet unclaimed() { + init(); + synchronized (this) { + return matchedMachines.removed(claimedMachines); + } + } + + /** returns all machines matching the given criteria (may be claimed) */ + @SuppressWarnings("unchecked") + public MachineSet all(Predicate<NodeMetadata> criterion) { + // To avoid generics complaints in callers caused by varargs, overload here + return all(new Predicate[] {criterion}); + } + + /** returns all machines matching the given criteria (may be claimed) */ + public MachineSet all(Predicate<NodeMetadata> ...ops) { + return new MachineSet(Iterables.filter(all(), compose(ops))); + } + + /** returns unclaimed machines matching the given criteria */ + @SuppressWarnings("unchecked") + public MachineSet unclaimed(Predicate<NodeMetadata> criterion) { + // To avoid generics complaints in callers caused by varargs, overload here + return unclaimed(new Predicate[] {criterion}); + } + + /** returns unclaimed machines matching the given criteria */ + public MachineSet unclaimed(Predicate<NodeMetadata> ...criteria) { + return new MachineSet(Iterables.filter(unclaimed(), compose(criteria))); + } + + /** creates machines if necessary so that this spec exists (may already be claimed however) + * returns a set of all matching machines, guaranteed non-empty + * (but possibly some are already claimed) */ + public MachineSet ensureExists(ReusableMachineTemplate template) { + return ensureExists(1, template); + } + + public synchronized void addToBlacklist(MachineSet newToBlacklist) { + setBlacklist(blacklistedMachines.added(newToBlacklist)); + } + + /** replaces the blacklist set; callers should generally perform a refresh() + * afterwards, to trigger re-detection of blacklisted machines + */ + public synchronized void setBlacklist(MachineSet newBlacklist) { + blacklistedMachines = newBlacklist; + detectedMachines = detectedMachines.removed(blacklistedMachines); + matchedMachines = matchedMachines.removed(blacklistedMachines); + } + + /** creates machines if necessary so that this spec exists (may already be claimed however); + * returns a set of all matching machines, of size at least count (but possibly some are already claimed). + * (the pool can change at any point, so this set is a best-effort but may be out of date. + * see javadoc comments on this class.) */ + public MachineSet ensureExists(int count, ReusableMachineTemplate template) { + MachineSet current; + current = all(matching(template)); + if (current.size() >= count) + return current; + //have to create more + MachineSet moreNeeded = create(count-current.size(), template); + return current.added(moreNeeded); + } + + /** creates machines if necessary so that this spec can subsequently be claimed; + * returns all such unclaimed machines, guaranteed to be non-empty. + * (the pool can change at any point, so this set is a best-effort but may be out of date. + * see javadoc comments on this class.) */ + public MachineSet ensureUnclaimed(ReusableMachineTemplate template) { + return ensureUnclaimed(1, template); + } + + /** creates machines if necessary so that this spec can subsequently be claimed; + * returns a set of at least count unclaimed machines */ + public MachineSet ensureUnclaimed(int count, ReusableMachineTemplate template) { + MachineSet current; + current = unclaimed(matching(template)); + if (current.size() >= count) + return current; + //have to create more + MachineSet moreNeeded = create(count-current.size(), template); + return current.added(moreNeeded); + } + + public Set<ReusableMachineTemplate> getTemplatesMatchingInstance(NodeMetadata nm) { + Set<ReusableMachineTemplate> result = new LinkedHashSet<ReusableMachineTemplate>(); + for (ReusableMachineTemplate t: getTemplates()) { + if (matching(t).apply(nm)) { + result.add(t); + } + } + return result; + } + + /** creates the given number of machines of the indicated template */ + public MachineSet create(int count, ReusableMachineTemplate template) { + Set<? extends NodeMetadata> nodes; + try { + Template t = template.newJcloudsTemplate(computeService); + if (log.isDebugEnabled()) log.debug("Creating "+count+" new instances of "+t); + nodes = computeService.createNodesInGroup(getPoolName(), count, t); + } catch (RunNodesException e) { + throw Throwables.propagate(e); + } + MachineSet result = new MachineSet(nodes); + registerNewNodes(result, template); + return result; + } + protected void registerNewNodes(MachineSet result, ReusableMachineTemplate template) { + for (NodeMetadata m: result) { + Set<ReusableMachineTemplate> ts = getTemplatesMatchingInstance(m); + if (ts.isEmpty()) { + log.error("Pool "+this+", created machine "+m+" from template "+template+", but no pool templates match!"); + } else { + if (log.isDebugEnabled()) + log.debug("Pool "+this+", created machine "+m+" from template "+template+", matching templates "+ts); + } + } + synchronized (this) { + detectedMachines = detectedMachines.added(result); + matchedMachines = matchedMachines.added(result); + } + } + + /** claims the indicated number of machines with the indicated spec, creating if necessary */ + public MachineSet claim(int count, ReusableMachineTemplate t) { + init(); + Set<NodeMetadata> claiming = new LinkedHashSet<NodeMetadata>(); + while (claiming.size() < count) { + MachineSet mm = ensureUnclaimed(count - claiming.size(), t); + for (NodeMetadata m : mm) { + synchronized (this) { + if (claiming.size() < count && !claimedMachines.contains(m)) { + claiming.add(m); + claimedMachines = claimedMachines.added(new MachineSet(m)); + } + } + } + } + MachineSet result = new MachineSet(claiming); + return result; + } + + + /** claims the indicated set of machines; + * throws exception if cannot all be claimed; + * returns the set passed in if successful */ + public MachineSet claim(MachineSet set) { + init(); + synchronized (this) { + MachineSet originalClaimed = claimedMachines; + claimedMachines = claimedMachines.added(set); + MachineSet newlyClaimed = claimedMachines.removed(originalClaimed); + if (newlyClaimed.size() != set.size()) { + //did not claim all; unclaim and fail + claimedMachines = originalClaimed; + MachineSet unavailable = set.removed(newlyClaimed); + throw new IllegalArgumentException("Could not claim all requested machines; failed to claim "+unavailable); + } + return newlyClaimed; + } + } + + public int unclaim(MachineSet set) { + init(); + synchronized (this) { + MachineSet originalClaimed = claimedMachines; + claimedMachines = claimedMachines.removed(set); + return originalClaimed.size() - claimedMachines.size(); + } + } + + + public int destroy(final MachineSet set) { + init(); + synchronized (this) { + detectedMachines = detectedMachines.removed(set); + matchedMachines = matchedMachines.removed(set); + claimedMachines = claimedMachines.removed(set); + } + Set<? extends NodeMetadata> destroyed = computeService.destroyNodesMatching(new Predicate<NodeMetadata>() { + @Override + public boolean apply(NodeMetadata input) { + return set.contains(input); + } + }); + synchronized (this) { + //in case a rescan happened while we were destroying + detectedMachines = detectedMachines.removed(set); + matchedMachines = matchedMachines.removed(set); + claimedMachines = claimedMachines.removed(set); + } + return destroyed.size(); + } + + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePoolPredicates.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePoolPredicates.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePoolPredicates.java new file mode 100644 index 0000000..a988ab6 --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachinePoolPredicates.java @@ -0,0 +1,149 @@ +/* + * 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.jclouds.pool; + +import java.util.Map; + +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.Processor; +import org.jclouds.domain.Location; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Throwables; + +public class MachinePoolPredicates { + + private static final Logger log = LoggerFactory.getLogger(MachinePoolPredicates.class); + + public static Predicate<NodeMetadata> except(final MachineSet removedItems) { + return new Predicate<NodeMetadata>() { + @Override + public boolean apply(NodeMetadata input) { + return !removedItems.contains(input); + } + }; + } + + public static Predicate<NodeMetadata> except(final Predicate<NodeMetadata> predicateToExclude) { + return Predicates.not(predicateToExclude); + } + + public static Predicate<NodeMetadata> matching(final ReusableMachineTemplate template) { + return new Predicate<NodeMetadata>() { + @Override + public boolean apply(NodeMetadata input) { + return matches(template, input); + } + }; + } + + public static Predicate<NodeMetadata> withTag(final String tag) { + return new Predicate<NodeMetadata>() { + @Override + public boolean apply(NodeMetadata input) { + return input.getTags().contains(tag); + } + }; + } + + public static Predicate<NodeMetadata> compose(final Predicate<NodeMetadata> ...predicates) { + return Predicates.and(predicates); + } + + /** True iff the node matches the criteria specified in this template. + * <p> + * NB: This only checks some of the most common fields, + * plus a hashcode (in strict mode). + * In strict mode you're practically guaranteed to match only machines created by this template. + * (Add a tag(uid) and you _will_ be guaranteed, strict mode or not.) + * <p> + * Outside strict mode, some things (OS and hypervisor) can fall through the gaps. + * But if that is a problem we can easily add them in. + * <p> + * (Caveat: If explicit Hardware, Image, and/or Template were specified in the template, + * then the hash code probably will not detect it.) + **/ + public static boolean matches(ReusableMachineTemplate template, NodeMetadata m) { + try { + // tags and user metadata + + if (! m.getTags().containsAll( template.getTags(false) )) return false; + + if (! isSubMapOf(template.getUserMetadata(false), m.getUserMetadata())) return false; + + + // common hardware parameters + + if (template.getMinRam()!=null && m.getHardware().getRam() < template.getMinRam()) return false; + + if (template.getMinCores()!=null) { + double numCores = 0; + for (Processor p: m.getHardware().getProcessors()) numCores += p.getCores(); + if (numCores+0.001 < template.getMinCores()) return false; + } + + if (template.getIs64bit()!=null) { + if (m.getOperatingSystem().is64Bit() != template.getIs64bit()) return false; + } + + if (template.getOsFamily()!=null) { + if (m.getOperatingSystem() == null || + !template.getOsFamily().equals(m.getOperatingSystem().getFamily())) return false; + } + if (template.getOsNameMatchesRegex()!=null) { + if (m.getOperatingSystem() == null || m.getOperatingSystem().getName()==null || + !m.getOperatingSystem().getName().matches(template.getOsNameMatchesRegex())) return false; + } + + if (template.getLocationId()!=null) { + if (!isLocationContainedIn(m.getLocation(), template.getLocationId())) return false; + } + + // TODO other TemplateBuilder fields and TemplateOptions + + return true; + + } catch (Exception e) { + log.warn("Error (rethrowing) trying to match "+m+" against "+template+": "+e, e); + throw Throwables.propagate(e); + } + } + + private static boolean isLocationContainedIn(Location location, String locationId) { + if (location==null) return false; + if (locationId.equals(location.getId())) return true; + return isLocationContainedIn(location.getParent(), locationId); + } + + public static boolean isSubMapOf(Map<String, String> sub, Map<String, String> bigger) { + for (Map.Entry<String, String> e: sub.entrySet()) { + if (e.getValue()==null) { + if (!bigger.containsKey(e.getKey())) return false; + if (bigger.get(e.getKey())!=null) return false; + } else { + if (!e.getValue().equals(bigger.get(e.getKey()))) return false; + } + } + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachineSet.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachineSet.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachineSet.java new file mode 100644 index 0000000..53116ab --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/pool/MachineSet.java @@ -0,0 +1,98 @@ +/* + * 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.jclouds.pool; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.annotation.concurrent.Immutable; + +import org.jclouds.compute.domain.NodeMetadata; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +@Immutable +public class MachineSet implements Iterable<NodeMetadata> { + + final Set<NodeMetadata> members; + + public MachineSet(Iterable<? extends NodeMetadata> m) { + members = ImmutableSet.copyOf(m); + } + public MachineSet(NodeMetadata ...nodes) { + members = ImmutableSet.copyOf(nodes); + } + + @Override + public Iterator<NodeMetadata> iterator() { + return members.iterator(); + } + + public MachineSet removed(MachineSet toRemove) { + Set<NodeMetadata> s = new LinkedHashSet<NodeMetadata>(members); + for (NodeMetadata m: toRemove) s.remove(m); + return new MachineSet(s); + } + public MachineSet added(MachineSet toAdd) { + Set<NodeMetadata> s = new LinkedHashSet<NodeMetadata>(members); + for (NodeMetadata m: toAdd) s.add(m); + return new MachineSet(s); + } + + @SuppressWarnings("unchecked") + public MachineSet filtered(Predicate<NodeMetadata> criterion) { + // To avoid generics complaints in callers caused by varargs, overload here + return filtered(new Predicate[] {criterion}); + } + + public MachineSet filtered(Predicate<NodeMetadata> ...criteria) { + return new MachineSet(Iterables.filter(members, MachinePoolPredicates.compose(criteria))); + } + + public int size() { + return members.size(); + } + + public boolean isEmpty() { + return members.isEmpty(); + } + + public boolean contains(NodeMetadata input) { + return members.contains(input); + } + + @Override + public String toString() { + return members.toString(); + } + + @Override + public int hashCode() { + return members.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof MachineSet) && (members.equals( ((MachineSet)obj).members )); + } + +}
