diff --git a/CHANGES.txt b/CHANGES.txt index 7964b54f..68941b37 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,8 @@ Trunk (unreleased changes) WHIRR-100. Create a binary distribution of Whirr. (tomwhite) + WHIRR-73. Add a list command to the CLI. (tomwhite) + IMPROVEMENTS WHIRR-89. Support maven 3 builds. (Adrian Cole via tomwhite) diff --git a/cli/src/main/java/org/apache/whirr/cli/Main.java b/cli/src/main/java/org/apache/whirr/cli/Main.java index 645e648c..86d2ad75 100644 --- a/cli/src/main/java/org/apache/whirr/cli/Main.java +++ b/cli/src/main/java/org/apache/whirr/cli/Main.java @@ -18,6 +18,8 @@ package org.apache.whirr.cli; +import com.google.common.collect.Maps; + import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; @@ -28,11 +30,10 @@ import org.apache.whirr.cli.command.DestroyClusterCommand; import org.apache.whirr.cli.command.LaunchClusterCommand; +import org.apache.whirr.cli.command.ListClusterCommand; import org.apache.whirr.cli.command.VersionCommand; import org.apache.whirr.service.ServiceFactory; -import com.google.common.collect.Maps; - /** * The entry point for the Whirr CLI. */ @@ -51,31 +52,41 @@ public class Main { int run(InputStream in, PrintStream out, PrintStream err, List list) throws Exception { if (list.isEmpty()) { - out.println("Usage: whirr COMMAND [ARGS]"); - out.println("where COMMAND may be one of:"); - out.println(); - for (Command command : commandMap.values()) { - out.printf("%" + maxLen + "s %s\n", command.getName(), - command.getDescription()); - } - out.println(); - out.println("Available services:"); - ServiceFactory serviceFactory = new ServiceFactory(); - for (String serviceName : - new TreeSet(serviceFactory.availableServices())) { - out.println(" " + serviceName); - } + printUsage(out); return -1; } Command command = commandMap.get(list.get(0)); + if (command == null) { + err.printf("Unrecognized command '%s'\n", list.get(0)); + err.println(); + printUsage(err); + return -1; + } return command.run(in, out, err, list.subList(1, list.size())); } - + + private void printUsage(PrintStream stream) { + stream.println("Usage: whirr COMMAND [ARGS]"); + stream.println("where COMMAND may be one of:"); + stream.println(); + for (Command command : commandMap.values()) { + stream.printf("%" + maxLen + "s %s\n", command.getName(), + command.getDescription()); + } + stream.println(); + stream.println("Available services:"); + ServiceFactory serviceFactory = new ServiceFactory(); + for (String serviceName : new TreeSet( + serviceFactory.availableServices())) { + stream.println(" " + serviceName); + } + } public static void main(String... args) throws Exception { Main main = new Main( new VersionCommand(), new LaunchClusterCommand(), - new DestroyClusterCommand() + new DestroyClusterCommand(), + new ListClusterCommand() ); int rc = main.run(System.in, System.out, System.err, Arrays.asList(args)); System.exit(rc); diff --git a/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java b/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java new file mode 100644 index 00000000..722e8f29 --- /dev/null +++ b/cli/src/main/java/org/apache/whirr/cli/command/ListClusterCommand.java @@ -0,0 +1,89 @@ +/** + * 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.whirr.cli.command; + +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.List; +import java.util.Set; + +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import org.apache.whirr.service.ClusterSpec; +import org.apache.whirr.service.Service; +import org.apache.whirr.service.ServiceFactory; +import org.jclouds.compute.domain.NodeMetadata; + +/** + * A command to list the nodes in a cluster. + */ +public class ListClusterCommand extends AbstractClusterSpecCommand { + + public ListClusterCommand() throws IOException { + this(new ServiceFactory()); + } + + public ListClusterCommand(ServiceFactory factory) { + super("list-cluster", "List the nodes in a cluster.", factory); + } + + @Override + public int run(InputStream in, PrintStream out, PrintStream err, + List args) throws Exception { + + OptionSet optionSet = parser.parse(args.toArray(new String[0])); + + if (!optionSet.nonOptionArguments().isEmpty()) { + printUsage(parser, err); + return -1; + } + try { + ClusterSpec clusterSpec = getClusterSpec(optionSet); + + Service service = factory.create(clusterSpec.getServiceName()); + Set nodes = service.getNodes(clusterSpec); + for (NodeMetadata node : nodes) { + out.println(Joiner.on('\t').join(node.getId(), node.getImageId(), + getFirstAddress(node.getPublicAddresses()), + getFirstAddress(node.getPrivateAddresses()), + node.getState(), node.getLocation().getId())); + } + return 0; + } catch (IllegalArgumentException e) { + err.println(e.getMessage()); + printUsage(parser, err); + return -1; + } + } + + private String getFirstAddress(Set addresses) { + return addresses.isEmpty() ? "" : Iterables.get(addresses, 0); + } + + private void printUsage(OptionParser parser, PrintStream stream) throws IOException { + stream.println("Usage: whirr list-cluster [OPTIONS]"); + stream.println(); + parser.printHelpOn(stream); + } +} diff --git a/cli/src/test/java/org/apache/whirr/cli/MainTest.java b/cli/src/test/java/org/apache/whirr/cli/MainTest.java index 62fb5ec5..63fb2f07 100644 --- a/cli/src/test/java/org/apache/whirr/cli/MainTest.java +++ b/cli/src/test/java/org/apache/whirr/cli/MainTest.java @@ -60,6 +60,16 @@ public void testNoArgs() throws Exception { assertThat(bytes.toString(), containsString("test-command test description")); } + @Test + public void testUnrecognizedCommand() throws Exception { + Main main = new Main(new TestCommand()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PrintStream err = new PrintStream(bytes); + int rc = main.run(null, null, err, Lists.newArrayList("bogus-command")); + assertThat(rc, is(-1)); + assertThat(bytes.toString(), containsString("Unrecognized command 'bogus-command'")); + } + @Test public void testCommand() throws Exception { Command command = mock(Command.class); diff --git a/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java b/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java new file mode 100644 index 00000000..ba7c9712 --- /dev/null +++ b/cli/src/test/java/org/apache/whirr/cli/command/ListClusterCommandTest.java @@ -0,0 +1,115 @@ +/** + * 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.whirr.cli.command; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Collections; +import java.util.Set; + +import org.apache.whirr.service.ClusterSpec; +import org.apache.whirr.service.Service; +import org.apache.whirr.service.ServiceFactory; +import org.hamcrest.Matcher; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.NodeState; +import org.jclouds.compute.domain.internal.NodeMetadataImpl; +import org.jclouds.domain.LocationScope; +import org.jclouds.domain.internal.LocationImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.internal.matchers.StringContains; + +public class ListClusterCommandTest { + + private ByteArrayOutputStream outBytes; + private PrintStream out; + private ByteArrayOutputStream errBytes; + private PrintStream err; + + @Before + public void setUp() { + outBytes = new ByteArrayOutputStream(); + out = new PrintStream(outBytes); + errBytes = new ByteArrayOutputStream(); + err = new PrintStream(errBytes); + } + + @Test + public void testInsufficientOptions() throws Exception { + ListClusterCommand command = new ListClusterCommand(); + int rc = command.run(null, null, err, Collections.emptyList()); + assertThat(rc, is(-1)); + assertThat(errBytes.toString(), containsUsageString()); + } + + private Matcher containsUsageString() { + return StringContains.containsString("Usage: whirr list-cluster [OPTIONS]"); + } + + @Test + public void testAllOptions() throws Exception { + + ServiceFactory factory = mock(ServiceFactory.class); + Service service = mock(Service.class); + when(factory.create((String) any())).thenReturn(service); + NodeMetadata node1 = new NodeMetadataImpl(null, "name1", "id1", + new LocationImpl(LocationScope.PROVIDER, "location-id1", + "location-desc1", null), + null, Collections.emptyMap(), null, null, "image-id", + null, NodeState.RUNNING, + Lists.newArrayList("100.0.0.1"), + Lists.newArrayList("10.0.0.1"), null); + NodeMetadata node2 = new NodeMetadataImpl(null, "name2", "id2", + new LocationImpl(LocationScope.PROVIDER, "location-id2", + "location-desc2", null), + null, Collections.emptyMap(), null, null, "image-id", + null, NodeState.RUNNING, + Lists.newArrayList("100.0.0.2"), + Lists.newArrayList("10.0.0.2"), null); + when(service.getNodes((ClusterSpec) any())).thenReturn( + (Set) Sets.newLinkedHashSet(Lists.newArrayList(node1, node2))); + + ListClusterCommand command = new ListClusterCommand(factory); + + int rc = command.run(null, out, null, Lists.newArrayList( + "--service-name", "test-service", + "--cluster-name", "test-cluster", + "--identity", "myusername")); + + assertThat(rc, is(0)); + + assertThat(outBytes.toString(), is( + "id1\timage-id\t100.0.0.1\t10.0.0.1\tRUNNING\tlocation-id1\n" + + "id2\timage-id\t100.0.0.2\t10.0.0.2\tRUNNING\tlocation-id2\n")); + + verify(factory).create("test-service"); + + } +} diff --git a/core/src/main/java/org/apache/whirr/service/Service.java b/core/src/main/java/org/apache/whirr/service/Service.java index c242371e..ade139d3 100644 --- a/core/src/main/java/org/apache/whirr/service/Service.java +++ b/core/src/main/java/org/apache/whirr/service/Service.java @@ -20,9 +20,15 @@ import static org.jclouds.compute.predicates.NodePredicates.withTag; +import com.google.common.base.Predicate; + import java.io.IOException; +import java.util.Set; import org.jclouds.compute.ComputeService; +import org.jclouds.compute.domain.ComputeMetadata; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.NodeState; /** * This class represents a service that a client wants to use. This class is @@ -58,5 +64,32 @@ public void destroyCluster(ClusterSpec clusterSpec) throws IOException { ComputeServiceContextBuilder.build(clusterSpec).getComputeService(); computeService.destroyNodesMatching(withTag(clusterSpec.getClusterName())); } + + public Set getNodes(ClusterSpec clusterSpec) + throws IOException { + ComputeService computeService = + ComputeServiceContextBuilder.build(clusterSpec).getComputeService(); + return computeService.listNodesDetailsMatching( + runningWithTag(clusterSpec.getClusterName())); + } + + public static Predicate runningWithTag(final String tag) { + return new Predicate() { + @Override + public boolean apply(ComputeMetadata computeMetadata) { + // Not all list calls return NodeMetadata (e.g. VCloud) + if (computeMetadata instanceof NodeMetadata) { + NodeMetadata nodeMetadata = (NodeMetadata) computeMetadata; + return tag.equals(nodeMetadata.getTag()) + && nodeMetadata.getState() == NodeState.RUNNING; + } + return false; + } + @Override + public String toString() { + return "runningWithTag(" + tag + ")"; + } + }; + } } diff --git a/services/cassandra/src/main/java/org/apache/whirr/service/cassandra/CassandraService.java b/services/cassandra/src/main/java/org/apache/whirr/service/cassandra/CassandraService.java index c5d30ad6..5225f022 100644 --- a/services/cassandra/src/main/java/org/apache/whirr/service/cassandra/CassandraService.java +++ b/services/cassandra/src/main/java/org/apache/whirr/service/cassandra/CassandraService.java @@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.apache.whirr.service.RunUrlBuilder.runUrls; import static org.jclouds.compute.options.TemplateOptions.Builder.runScript; -import static org.jclouds.compute.predicates.NodePredicates.runningWithTag; import static org.jclouds.io.Payloads.newStringPayload; import com.google.common.base.Function; @@ -55,6 +54,7 @@ import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.Template; import org.jclouds.compute.domain.TemplateBuilder; +import org.jclouds.compute.predicates.NodePredicates; import org.jclouds.io.Payload; import org.jclouds.ssh.ExecResponse; @@ -115,7 +115,8 @@ public Cluster launchCluster(ClusterSpec clusterSpec) throws IOException { try { Map responses = computeService .runScriptOnNodesMatching( - runningWithTag(clusterSpec.getClusterName()), configureScript); + NodePredicates.runningWithTag(clusterSpec.getClusterName()), + configureScript); assert responses.size() > 0 : "no nodes matched " + clusterSpec.getClusterName(); } catch (RunScriptOnNodesException e) { diff --git a/services/zookeeper/src/main/java/org/apache/whirr/service/zookeeper/ZooKeeperService.java b/services/zookeeper/src/main/java/org/apache/whirr/service/zookeeper/ZooKeeperService.java index 667ef5e3..c2c91cee 100644 --- a/services/zookeeper/src/main/java/org/apache/whirr/service/zookeeper/ZooKeeperService.java +++ b/services/zookeeper/src/main/java/org/apache/whirr/service/zookeeper/ZooKeeperService.java @@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.apache.whirr.service.RunUrlBuilder.runUrls; import static org.jclouds.compute.options.TemplateOptions.Builder.runScript; -import static org.jclouds.compute.predicates.NodePredicates.runningWithTag; import static org.jclouds.io.Payloads.newStringPayload; import com.google.common.base.Function; @@ -53,6 +52,7 @@ import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.Template; import org.jclouds.compute.domain.TemplateBuilder; +import org.jclouds.compute.predicates.NodePredicates; import org.jclouds.io.Payload; public class ZooKeeperService extends Service { @@ -107,7 +107,8 @@ public ZooKeeperCluster launchCluster(ClusterSpec clusterSpec) throws IOExceptio Payload configureScript = newStringPayload(runUrls(clusterSpec.getRunUrlBase(), "apache/zookeeper/post-configure " + servers)); try { - computeService.runScriptOnNodesMatching(runningWithTag(clusterSpec.getClusterName()), configureScript); + computeService.runScriptOnNodesMatching(NodePredicates.runningWithTag( + clusterSpec.getClusterName()), configureScript); } catch (RunScriptOnNodesException e) { // TODO: retry throw new IOException(e);