package io.airlift.airship.cli; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import io.airlift.airship.coordinator.AgentProvisioningRepresentation; import io.airlift.airship.coordinator.CoordinatorProvisioningRepresentation; import io.airlift.airship.shared.AgentLifecycleState; import io.airlift.airship.shared.AgentStatusRepresentation; import io.airlift.airship.shared.Assignment; import io.airlift.airship.shared.AssignmentRepresentation; import io.airlift.airship.shared.CoordinatorLifecycleState; import io.airlift.airship.shared.CoordinatorStatusRepresentation; import io.airlift.airship.shared.SlotLifecycleState; import io.airlift.airship.shared.SlotStatusRepresentation; import io.airlift.airship.shared.UpgradeVersions; import io.airlift.http.client.BodyGenerator; import io.airlift.http.client.FullJsonResponseHandler.JsonResponse; import io.airlift.http.client.HttpClient; import io.airlift.http.client.Request; import io.airlift.http.client.jetty.JettyHttpClient; import io.airlift.json.JsonCodec; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.util.List; import java.util.Map; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static io.airlift.airship.cli.CommanderResponse.createCommanderResponse; import static io.airlift.airship.cli.HttpCommander.TextBodyGenerator.textBodyGenerator; import static io.airlift.airship.shared.HttpUriBuilder.uriBuilderFrom; import static io.airlift.airship.shared.VersionsUtil.AIRSHIP_AGENTS_VERSION_HEADER; import static io.airlift.airship.shared.VersionsUtil.AIRSHIP_FORCE_HEADER; import static io.airlift.airship.shared.VersionsUtil.AIRSHIP_SLOTS_VERSION_HEADER; import static io.airlift.http.client.FullJsonResponseHandler.createFullJsonResponseHandler; import static io.airlift.http.client.JsonBodyGenerator.jsonBodyGenerator; import static io.airlift.http.client.JsonResponseHandler.createJsonResponseHandler; public class HttpCommander implements Commander { private static final JsonCodec<List<SlotStatusRepresentation>> SLOTS_CODEC = JsonCodec.listJsonCodec(SlotStatusRepresentation.class); private static final JsonCodec<AssignmentRepresentation> ASSIGNMENT_CODEC = JsonCodec.jsonCodec(AssignmentRepresentation.class); private static final JsonCodec<UpgradeVersions> UPGRADE_VERSIONS_CODEC = JsonCodec.jsonCodec(UpgradeVersions.class); private static final JsonCodec<List<CoordinatorStatusRepresentation>> COORDINATORS_CODEC = JsonCodec.listJsonCodec(CoordinatorStatusRepresentation.class); private static final JsonCodec<CoordinatorProvisioningRepresentation> COORDINATOR_PROVISIONING_CODEC = JsonCodec.jsonCodec(CoordinatorProvisioningRepresentation.class); private static final JsonCodec<AgentStatusRepresentation> AGENT_CODEC = JsonCodec.jsonCodec(AgentStatusRepresentation.class); private static final JsonCodec<List<AgentStatusRepresentation>> AGENTS_CODEC = JsonCodec.listJsonCodec(AgentStatusRepresentation.class); private static final JsonCodec<AgentProvisioningRepresentation> AGENT_PROVISIONING_CODEC = JsonCodec.jsonCodec(AgentProvisioningRepresentation.class); private final HttpClient client; private final URI coordinatorUri; private final boolean useInternalAddress; public HttpCommander(URI coordinatorUri, boolean useInternalAddress) throws IOException { Preconditions.checkNotNull(coordinatorUri, "coordinatorUri is null"); this.coordinatorUri = coordinatorUri; this.client = new JettyHttpClient(); this.useInternalAddress = useInternalAddress; } @Override public CommanderResponse<List<SlotStatusRepresentation>> show(SlotFilter slotFilter) { URI uri = slotFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("/v1/slot")); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); JsonResponse<List<SlotStatusRepresentation>> response = client.execute(request, createFullJsonResponseHandler(SLOTS_CODEC)); return createCommanderResponse(response.getHeader(AIRSHIP_SLOTS_VERSION_HEADER), response.getValue()); } @Override public List<SlotStatusRepresentation> install(AgentFilter agentFilter, int count, Assignment assignment, String expectedVersion) { URI uri = agentFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("/v1/slot").addParameter("limit", String.valueOf(count))); Request.Builder requestBuilder = Request.Builder.preparePost() .setUri(uri) .setHeader("Content-Type", "application/json") .setBodyGenerator(jsonBodyGenerator(ASSIGNMENT_CODEC, AssignmentRepresentation.from(assignment))); if (expectedVersion != null) { requestBuilder.setHeader(AIRSHIP_SLOTS_VERSION_HEADER, expectedVersion); } List<SlotStatusRepresentation> slots = client.execute(requestBuilder.build(), createJsonResponseHandler(SLOTS_CODEC)); return slots; } @Override public List<SlotStatusRepresentation> upgrade(SlotFilter slotFilter, UpgradeVersions upgradeVersions, String expectedVersion, boolean force) { URI uri = slotFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("/v1/slot/assignment")); Request.Builder requestBuilder = Request.Builder.preparePost() .setUri(uri) .setHeader("Content-Type", "application/json") .setBodyGenerator(jsonBodyGenerator(UPGRADE_VERSIONS_CODEC, upgradeVersions)); if (expectedVersion != null) { requestBuilder.setHeader(AIRSHIP_SLOTS_VERSION_HEADER, expectedVersion); } if (force) { requestBuilder.setHeader(AIRSHIP_FORCE_HEADER, "true"); } List<SlotStatusRepresentation> slots = client.execute(requestBuilder.build(), createJsonResponseHandler(SLOTS_CODEC)); return slots; } @Override public List<SlotStatusRepresentation> setState(SlotFilter slotFilter, SlotLifecycleState state, String expectedVersion) { URI uri = slotFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("/v1/slot/lifecycle")); Request.Builder requestBuilder = Request.Builder.preparePut() .setUri(uri) .setBodyGenerator(textBodyGenerator(state.name())); if (expectedVersion != null) { requestBuilder.setHeader(AIRSHIP_SLOTS_VERSION_HEADER, expectedVersion); } List<SlotStatusRepresentation> slots = client.execute(requestBuilder.build(), createJsonResponseHandler(SLOTS_CODEC)); return slots; } @Override public List<SlotStatusRepresentation> terminate(SlotFilter slotFilter, String expectedVersion) { URI uri = slotFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("/v1/slot")); Request.Builder requestBuilder = Request.Builder.prepareDelete().setUri(uri); if (expectedVersion != null) { requestBuilder.setHeader(AIRSHIP_SLOTS_VERSION_HEADER, expectedVersion); } List<SlotStatusRepresentation> slots = client.execute(requestBuilder.build(), createJsonResponseHandler(SLOTS_CODEC)); return slots; } @Override public List<SlotStatusRepresentation> resetExpectedState(SlotFilter slotFilter, String expectedVersion) { URI uri = slotFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("/v1/slot/expected-state")); Request.Builder requestBuilder = Request.Builder.prepareDelete().setUri(uri); if (expectedVersion != null) { requestBuilder.setHeader(AIRSHIP_SLOTS_VERSION_HEADER, expectedVersion); } List<SlotStatusRepresentation> slots = client.execute(requestBuilder.build(), createJsonResponseHandler(SLOTS_CODEC)); return slots; } @Override public boolean ssh(SlotFilter slotFilter, String command) { URI uri = slotFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("/v1/slot")); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); List<SlotStatusRepresentation> slots = client.execute(request, createJsonResponseHandler(SLOTS_CODEC)); if (slots.isEmpty()) { return false; } SlotStatusRepresentation slot = slots.get(0); String host; if (useInternalAddress) { host = slot.getInternalHost(); } else { host = slot.getExternalHost(); } Exec.execRemote(host, slot.getInstallPath(), command); return true; } @Override public List<CoordinatorStatusRepresentation> showCoordinators(CoordinatorFilter coordinatorFilter) { URI uri = coordinatorFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("v1/admin/coordinator")); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); List<CoordinatorStatusRepresentation> coordinators = client.execute(request, createJsonResponseHandler(COORDINATORS_CODEC)); return coordinators; } @Override public List<CoordinatorStatusRepresentation> provisionCoordinators(String coordinatorConfig, int coordinatorCount, String instanceType, String availabilityZone, String ami, String keyPair, String securityGroup, String provisioningScriptsArtifact, boolean waitForStartup) { URI uri = uriBuilderFrom(coordinatorUri).replacePath("v1/admin/coordinator").build(); CoordinatorProvisioningRepresentation coordinatorProvisioning = new CoordinatorProvisioningRepresentation( coordinatorConfig, coordinatorCount, instanceType, availabilityZone, ami, keyPair, securityGroup, provisioningScriptsArtifact); Request request = Request.Builder.preparePost() .setUri(uri) .setHeader("Content-Type", "application/json") .setBodyGenerator(jsonBodyGenerator(COORDINATOR_PROVISIONING_CODEC, coordinatorProvisioning)) .build(); List<CoordinatorStatusRepresentation> coordinators = client.execute(request, createJsonResponseHandler(COORDINATORS_CODEC)); if (waitForStartup) { List<String> instanceIds = newArrayList(); for (CoordinatorStatusRepresentation coordinator : coordinators) { instanceIds.add(coordinator.getInstanceId()); } coordinators = waitForCoordinatorsToStart(instanceIds); } return coordinators; } private List<CoordinatorStatusRepresentation> waitForCoordinatorsToStart(List<String> instanceIds) { for (int loop = 0; true; loop++) { try { URI uri = uriBuilderFrom(coordinatorUri).replacePath("v1/admin/coordinator").build(); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); List<CoordinatorStatusRepresentation> coordinators = client.execute(request, createJsonResponseHandler(COORDINATORS_CODEC)); Map<String, CoordinatorStatusRepresentation> runningCoordinators = newHashMap(); for (CoordinatorStatusRepresentation coordinator : coordinators) { if (coordinator.getState() == CoordinatorLifecycleState.ONLINE) { runningCoordinators.put(coordinator.getInstanceId(), coordinator); } } if (runningCoordinators.keySet().containsAll(instanceIds)) { WaitUtils.clearWaitMessage(); runningCoordinators.keySet().retainAll(instanceIds); return ImmutableList.copyOf(runningCoordinators.values()); } } catch (Exception ignored) { } WaitUtils.wait(loop); } } @Override public boolean sshCoordinator(CoordinatorFilter coordinatorFilter, String command) { URI uri = coordinatorFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("v1/admin/coordinator")); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); List<CoordinatorStatusRepresentation> coordinators = client.execute(request, createJsonResponseHandler(COORDINATORS_CODEC)); if (coordinators.isEmpty()) { return false; } Exec.execRemote(coordinators.get(0).getExternalHost(), command); return true; } @Override public CommanderResponse<List<AgentStatusRepresentation>> showAgents(AgentFilter agentFilter) { URI uri = agentFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("v1/admin/agent")); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); JsonResponse<List<AgentStatusRepresentation>> response = client.execute(request, createFullJsonResponseHandler(AGENTS_CODEC)); if (response.getStatusCode() != 200) { throw new RuntimeException(response.getStatusMessage()); } return CommanderResponse.createCommanderResponse(response.getHeader(AIRSHIP_AGENTS_VERSION_HEADER), response.getValue()); } @Override public List<AgentStatusRepresentation> provisionAgents(String agentConfig, int agentCount, String instanceType, String availabilityZone, String ami, String keyPair, String securityGroup, String provisioningScriptsArtifact, boolean waitForStartup) { URI uri = uriBuilderFrom(coordinatorUri).replacePath("v1/admin/agent").build(); AgentProvisioningRepresentation agentProvisioning = new AgentProvisioningRepresentation( agentConfig, agentCount, instanceType, availabilityZone, ami, keyPair, securityGroup, provisioningScriptsArtifact); Request request = Request.Builder.preparePost() .setUri(uri) .setHeader("Content-Type", "application/json") .setBodyGenerator(jsonBodyGenerator(AGENT_PROVISIONING_CODEC, agentProvisioning)) .build(); List<AgentStatusRepresentation> agents = client.execute(request, createJsonResponseHandler(AGENTS_CODEC)); if (waitForStartup) { List<String> instanceIds = newArrayList(); for (AgentStatusRepresentation agent : agents) { instanceIds.add(agent.getInstanceId()); } agents = waitForAgentsToStart(instanceIds); } return agents; } private List<AgentStatusRepresentation> waitForAgentsToStart(List<String> instanceIds) { for (int loop = 0; true; loop++) { try { URI uri = uriBuilderFrom(coordinatorUri).replacePath("v1/admin/agent").build(); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); List<AgentStatusRepresentation> agents = client.execute(request, createJsonResponseHandler(AGENTS_CODEC)); Map<String, AgentStatusRepresentation> runningAgents = newHashMap(); for (AgentStatusRepresentation agent : agents) { if (agent.getState() == AgentLifecycleState.ONLINE) { runningAgents.put(agent.getInstanceId(), agent); } } if (runningAgents.keySet().containsAll(instanceIds)) { WaitUtils.clearWaitMessage(); runningAgents.keySet().retainAll(instanceIds); return ImmutableList.copyOf(runningAgents.values()); } } catch (Exception ignored) { } WaitUtils.wait(loop); } } @Override public AgentStatusRepresentation terminateAgent(String agentId) { URI uri = uriBuilderFrom(coordinatorUri).replacePath("v1/admin/agent").build(); Request request = Request.Builder.prepareDelete() .setUri(uri) .setBodyGenerator(textBodyGenerator(agentId)) .build(); AgentStatusRepresentation agents = client.execute(request, createJsonResponseHandler(AGENT_CODEC)); return agents; } @Override public boolean sshAgent(AgentFilter agentFilter, String command) { URI uri = agentFilter.toUri(uriBuilderFrom(coordinatorUri).replacePath("v1/admin/agent")); Request request = Request.Builder.prepareGet() .setUri(uri) .build(); List<AgentStatusRepresentation> agents = client.execute(request, createJsonResponseHandler(AGENTS_CODEC)); if (agents.isEmpty()) { return false; } Exec.execRemote(agents.get(0).getExternalHost(), command); return true; } public static class TextBodyGenerator implements BodyGenerator { public static TextBodyGenerator textBodyGenerator(String instance) { return new TextBodyGenerator(instance); } private byte[] text; private TextBodyGenerator(String text) { this.text = text.getBytes(Charsets.UTF_8); } @Override public void write(OutputStream out) throws Exception { out.write(text); } } }