package io.airlift.airship.coordinator; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.io.CharStreams; import com.google.common.io.Files; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import io.airlift.airship.shared.AgentStatusRepresentation; import io.airlift.airship.shared.CoordinatorStatusRepresentation; import io.airlift.http.client.AsyncHttpClient; import io.airlift.http.client.Request; import io.airlift.http.client.Request.Builder; import io.airlift.http.client.StringResponseHandler.StringResponse; import io.airlift.json.JsonCodec; import io.airlift.log.Logger; import io.airlift.node.NodeInfo; import javax.annotation.Nullable; import javax.inject.Inject; import javax.ws.rs.core.UriBuilder; import java.io.File; import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import static com.google.common.base.Objects.firstNonNull; import static com.google.common.base.Predicates.notNull; import static com.google.common.base.Strings.isNullOrEmpty; import static io.airlift.airship.coordinator.ValidatingResponseHandler.validate; import static io.airlift.airship.shared.HttpUriBuilder.uriBuilderFrom; import static io.airlift.http.client.JsonResponseHandler.createJsonResponseHandler; import static io.airlift.http.client.Request.Builder.prepareGet; import static io.airlift.http.client.StringResponseHandler.createStringResponseHandler; public class StaticProvisioner implements Provisioner { private static final Logger log = Logger.get(StaticProvisioner.class); private final URI coordinatorsUri; private final URI agentsUri; private final NodeInfo nodeInfo; private final AsyncHttpClient httpClient; private final JsonCodec<CoordinatorStatusRepresentation> coordinatorCodec; private final JsonCodec<AgentStatusRepresentation> agentCodec; private final AtomicBoolean coordinatorsResourceIsUp = new AtomicBoolean(true); private final AtomicBoolean agentsResourceIsUp = new AtomicBoolean(true); private final Set<String> badAgentUris = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); @Inject public StaticProvisioner(StaticProvisionerConfig config, NodeInfo nodeInfo, @Global AsyncHttpClient httpClient, JsonCodec<CoordinatorStatusRepresentation> coordinatorCodec, JsonCodec<AgentStatusRepresentation> agentCodec) { this(config.getCoordinatorsUri(), config.getAgentsUri(), nodeInfo, httpClient, coordinatorCodec, agentCodec); } public StaticProvisioner(URI coordinatorsUri, URI agentsUri, NodeInfo nodeInfo, AsyncHttpClient httpClient, JsonCodec<CoordinatorStatusRepresentation> coordinatorCodec, JsonCodec<AgentStatusRepresentation> agentCodec) { Preconditions.checkNotNull(coordinatorsUri, "coordinatorsUri is null"); Preconditions.checkNotNull(agentsUri, "agentsUri is null"); Preconditions.checkNotNull(nodeInfo, "nodeInfo is null"); Preconditions.checkNotNull(httpClient, "httpClient is null"); Preconditions.checkNotNull(coordinatorCodec, "coordinatorCodec is null"); Preconditions.checkNotNull(agentCodec, "agentCodec is null"); this.nodeInfo = nodeInfo; this.httpClient = httpClient; this.coordinatorCodec = coordinatorCodec; this.agentCodec = agentCodec; this.agentsUri = agentsUri; String agentsUriScheme = agentsUri.getScheme().toLowerCase(); Preconditions.checkArgument(agentsUriScheme.equals("http") || agentsUriScheme.equals("https") || agentsUriScheme.equals("file"), "Agents uri must have a http, https, or file scheme"); this.coordinatorsUri = coordinatorsUri; String coordinatorsUriScheme = coordinatorsUri.getScheme().toLowerCase(); Preconditions.checkArgument(coordinatorsUriScheme.equals("http") || coordinatorsUriScheme.equals("https") || coordinatorsUriScheme.equals("file"), "Coordinators uri must have a http, https, or file scheme"); } @Override public List<Instance> listCoordinators() { List<String> lines = readLines("coordinators", coordinatorsUri, coordinatorsResourceIsUp); return ImmutableList.copyOf(Iterables.transform(lines, new Function<String, Instance>() { @Override public Instance apply(String coordinatorUri) { URI uri = UriBuilder.fromUri(coordinatorUri).path("/v1/coordinator").build(); Request request = Builder.prepareGet() .setUri(uri) .build(); String hostAndPort = uri.getHost() + ":" + uri.getPort(); try { CoordinatorStatusRepresentation coordinator = httpClient.execute(request, createJsonResponseHandler(coordinatorCodec)); return new Instance(coordinator.getInstanceId(), firstNonNull(coordinator.getInstanceType(), "unknown"), coordinator.getLocation(), coordinator.getSelf(), coordinator.getExternalUri()); } catch (Exception e) { return new Instance(hostAndPort, "unknown", null, uri, uri); } } })); } @Override public List<Instance> provisionCoordinators(String coordinatorConfigSpec, int coordinatorCount, String instanceType, String availabilityZone, String ami, String keyPair, String securityGroup, String provisioningScriptsArtifact) { throw new UnsupportedOperationException("Static provisioner does not support coordinator provisioning"); } @Override public List<Instance> listAgents() { List<String> lines = readLines("agents", agentsUri, agentsResourceIsUp); List<URI> agentUris = FluentIterable.from(lines) .transform(validAgentUri()) .filter(notNull()) .toList(); List<ListenableFuture<Instance>> futures = new ArrayList<>(); for (URI agentUri : agentUris) { futures.add(getAgentInstance(agentUri)); } return Futures.getUnchecked(Futures.allAsList(futures)); } private ListenableFuture<Instance> getAgentInstance(URI agentUri) { URI uri = uriBuilderFrom(agentUri).replacePath("/v1/agent").build(); Request request = prepareGet().setUri(uri).build(); SettableFuture<Instance> future = SettableFuture.create(); Futures.addCallback( httpClient.executeAsync(request, validate(createJsonResponseHandler(agentCodec))), agentStatusCallback(future, agentUri)); return future; } private FutureCallback<AgentStatusRepresentation> agentStatusCallback(final SettableFuture<Instance> future, final URI uri) { return new FutureCallback<AgentStatusRepresentation>() { @Override public void onSuccess(AgentStatusRepresentation agent) { future.set(new Instance(agent.getInstanceId(), firstNonNull(agent.getInstanceType(), "unknown"), agent.getLocation(), agent.getSelf(), agent.getExternalUri())); } @Override public void onFailure(Throwable t) { log.debug(t, "Failed to get agent status"); String hostAndPort = uri.getHost() + ":" + uri.getPort(); future.set(new Instance(hostAndPort, "unknown", null, uri, uri)); } }; } @Override public List<Instance> provisionAgents(String agentConfig, int agentCount, String instanceType, String availabilityZone, String ami, String keyPair, String securityGroup, String provisioningScriptsArtifact) { throw new UnsupportedOperationException("Static provisioner does not support agent provisioning"); } @Override public void terminateAgents(Iterable<String> instanceIds) { throw new UnsupportedOperationException("Static provisioner does not support agent termination"); } private List<String> readLines(String name, URI uri, AtomicBoolean resourceUp) { try { String contents; if (uri.getScheme().toLowerCase().startsWith("http")) { Builder requestBuilder = prepareGet() .setUri(uri) .setHeader("User-Agent", nodeInfo.getNodeId()); StringResponse stringResponse = httpClient.execute(requestBuilder.build(), createStringResponseHandler()); if (stringResponse.getStatusCode() != 200) { logServerError(resourceUp, "Error loading %s file from %s: statusCode=%s statusMessage=%s", name, uri, stringResponse.getStatusCode(), stringResponse.getStatusMessage()); return ImmutableList.of(); } contents = stringResponse.getBody(); } else { File file = new File(uri.getSchemeSpecificPart()); contents = Files.toString(file, Charsets.UTF_8); } List<String> lines = CharStreams.readLines(new StringReader(contents)); if (resourceUp.compareAndSet(false, true)) { log.info("Static provisioner connection for %s to %s succeeded", name, uri); } return lines; } catch (Exception e) { logServerError(resourceUp, "Error loading %s file from %s", name, uri); return ImmutableList.of(); } } private void logServerError(AtomicBoolean resourceUp, String message, Object... args) { if (resourceUp.compareAndSet(true, false)) { log.error(message, args); } } private Function<String, URI> validAgentUri() { return new Function<String, URI>() { @Nullable @Override public URI apply(String agentUri) { try { return validateAgentUri(agentUri); } catch (RuntimeException e) { log.error(e, "Agent URI is invalid: %s", agentUri); return null; } } }; } private URI validateAgentUri(String agentUri) { agentUri = agentUri.trim(); if (agentUri.isEmpty()) { return null; } URI uri; try { uri = new URI(agentUri); } catch (URISyntaxException e) { if (badAgentUris.add(agentUri)) { log.error("Agent URI is invalid: %s: %s", agentUri, e.getMessage()); } return null; } if ((!uri.getScheme().equalsIgnoreCase("http")) && (!uri.getScheme().equalsIgnoreCase("https"))) { log.warn("Agent URI scheme must be http or https: %s", agentUri); } if ((!isNullOrEmpty(uri.getPath())) && (!uri.getPath().equals("/"))) { if (badAgentUris.add(agentUri)) { log.warn("Agent URI should not have a path: %s", agentUri); } } return uriBuilderFrom(uri).replacePath("/").build(); } }