From 1f9c79c9d5d3b639c0ab4939b716699260c2dc97 Mon Sep 17 00:00:00 2001 From: Aled Sage Date: Fri, 1 Aug 2014 15:13:33 +0100 Subject: [PATCH] Adds CloudExplorer utility --- .../main/java/brooklyn/cli/CloudExplorer.java | 486 ++++++++++++++++++ .../cli/src/main/java/brooklyn/cli/Main.java | 29 +- .../brooklyn/cli/CloudExplorerLiveTest.java | 209 ++++++++ usage/dist/src/main/dist/bin/cloud-explorer | 47 ++ 4 files changed, 761 insertions(+), 10 deletions(-) create mode 100644 usage/cli/src/main/java/brooklyn/cli/CloudExplorer.java create mode 100644 usage/cli/src/test/java/brooklyn/cli/CloudExplorerLiveTest.java create mode 100755 usage/dist/src/main/dist/bin/cloud-explorer diff --git a/usage/cli/src/main/java/brooklyn/cli/CloudExplorer.java b/usage/cli/src/main/java/brooklyn/cli/CloudExplorer.java new file mode 100644 index 0000000000..379f362130 --- /dev/null +++ b/usage/cli/src/main/java/brooklyn/cli/CloudExplorer.java @@ -0,0 +1,486 @@ +/* + * 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 brooklyn.cli; + +import static com.google.common.base.Preconditions.checkNotNull; +import io.airlift.command.Cli; +import io.airlift.command.Cli.CliBuilder; +import io.airlift.command.Command; +import io.airlift.command.Help; +import io.airlift.command.Option; +import io.airlift.command.ParseException; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jclouds.blobstore.BlobStore; +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.blobstore.domain.StorageMetadata; +import org.jclouds.compute.ComputeService; +import org.jclouds.compute.domain.ComputeMetadata; +import org.jclouds.compute.domain.Hardware; +import org.jclouds.compute.domain.Image; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.Template; +import org.jclouds.compute.options.TemplateOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.cli.Main.BrooklynCommand; +import brooklyn.cli.Main.BrooklynCommandCollectingArgs; +import brooklyn.cli.Main.HelpCommand; +import brooklyn.cli.Main.InfoCommand; +import brooklyn.location.Location; +import brooklyn.location.LocationDefinition; +import brooklyn.location.basic.LocationConfigKeys; +import brooklyn.location.cloud.CloudLocationConfig; +import brooklyn.location.jclouds.JcloudsLocation; +import brooklyn.location.jclouds.JcloudsUtil; +import brooklyn.management.internal.LocalManagementContext; +import brooklyn.util.exceptions.FatalConfigurationRuntimeException; +import brooklyn.util.exceptions.FatalRuntimeException; +import brooklyn.util.exceptions.UserFacingException; +import brooklyn.util.stream.Streams; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +/** + * Convenience for listing Cloud Compute and BlobStore details. + *

+ * For fuller functionality, consider instead the jclouds CLI or Ruby Fog CLI. + *

+ * The advantage of this utility is that it piggie-backs off the {@code brooklyn.property} credentials, + * so requires less additional credential configuration. It also gives brooklyn-specific information, + * such as which image will be used by default in a given cloud. + */ +public class CloudExplorer { + + private static final Logger log = LoggerFactory.getLogger(Main.class); + + // Error codes + public static final int SUCCESS = 0; + public static final int PARSE_ERROR = 1; + public static final int EXECUTION_ERROR = 2; + public static final int CONFIGURATION_ERROR = 3; + + public static void main(String... args) { + new CloudExplorer().execCli(args); + } + + public static abstract class JcloudsCommand extends BrooklynCommandCollectingArgs { + @Option(name = { "--all-locations" }, title = "all locations", + description = "All locations (i.e. all locations in brooklyn.properties for which there are credentials)") + public boolean allLocations; + + @Option(name = { "-l", "--location" }, title = "location spec", + description = "A location spec (e.g. referring to a named location in brooklyn.properties file)") + public String location; + + @Option(name = { "-y", "--yes" }, title = "auto-confirm", + description = "Automatically answer yes to any questions") + public boolean autoconfirm = false; + + protected abstract void doCall(JcloudsLocation loc, String indent) throws Exception; + + @Override + public Void call() throws Exception { + LocalManagementContext mgmt = new LocalManagementContext(); + List locs = Lists.newArrayList(); + try { + if (location != null && allLocations) { + throw new FatalConfigurationRuntimeException("Must not specify --location and --all-locations"); + } else if (location != null) { + JcloudsLocation loc = (JcloudsLocation) mgmt.getLocationRegistry().resolve(location); + locs.add(loc); + } else if (allLocations) { + // Find all named locations that point at different target clouds + Map definedLocations = mgmt.getLocationRegistry().getDefinedLocations(); + for (LocationDefinition locationDef : definedLocations.values()) { + Location loc = mgmt.getLocationRegistry().resolve(locationDef); + if (loc instanceof JcloudsLocation) { + boolean found = false; + for (JcloudsLocation existing : locs) { + if (equalTargets(existing, (JcloudsLocation) loc)) { + found = true; + break; + } + } + if (!found) { + locs.add((JcloudsLocation) loc); + } + } + } + } else { + throw new FatalConfigurationRuntimeException("Must specify one of --location or --all-locations"); + } + + for (JcloudsLocation loc : locs) { + stdout.println("Location {"); + stdout.println("\tprovider: "+loc.getProvider()); + stdout.println("\tdisplayName: "+loc.getDisplayName()); + stdout.println("\tidentity: "+loc.getIdentity()); + if (loc.getEndpoint() != null) stdout.println("\tendpoint: "+loc.getEndpoint()); + if (loc.getRegion() != null) stdout.println("\tregion: "+loc.getRegion()); + + try { + doCall(loc, "\t"); + } finally { + stdout.println("}"); + } + } + } finally { + mgmt.terminate(); + } + return null; + } + + @Override + public ToStringHelper string() { + return super.string() + .add("location", location); + } + + protected boolean equalTargets(JcloudsLocation loc1, JcloudsLocation loc2) { + return Objects.equal(loc1.getProvider(), loc2.getProvider()) + && Objects.equal(loc1.getIdentity(), loc2.getIdentity()) + && Objects.equal(loc1.getEndpoint(), loc2.getEndpoint()) + && Objects.equal(loc1.getRegion(), loc2.getRegion()); + } + + + protected boolean confirm(String msg, String indent) throws Exception { + if (autoconfirm) { + stdout.println(indent+"Auto-confirmed: "+msg); + return true; + } else { + stdout.println(indent+"Enter y/n. Are you sure you want to "+msg); + int in = stdin.read(); + boolean confirmed = (Character.toLowerCase(in) == 'y'); + if (confirmed) { + stdout.println(indent+"Confirmed; will "+msg); + } else { + stdout.println(indent+"Declined; will not "+msg); + } + return confirmed; + } + } + } + + public static abstract class ComputeCommand extends JcloudsCommand { + protected abstract void doCall(ComputeService computeService, String indent) throws Exception; + + @Override + protected void doCall(JcloudsLocation loc, String indent) throws Exception { + ComputeService computeService = loc.getComputeService(); + doCall(computeService, indent); + } + } + + @Command(name = "list-instances", description = "") + public static class ComputeListInstancesCommand extends ComputeCommand { + @Override + protected void doCall(ComputeService computeService, String indent) throws Exception { + failIfArguments(); + Set instances = computeService.listNodes(); + stdout.println(indent+"Instances {"); + for (ComputeMetadata instance : instances) { + stdout.println(indent+"\t"+instance); + } + stdout.println(indent+"}"); + } + } + + @Command(name = "list-images", description = "") + public static class ComputeListImagesCommand extends ComputeCommand { + @Override + protected void doCall(ComputeService computeService, String indent) throws Exception { + failIfArguments(); + Set images = computeService.listImages(); + stdout.println(indent+"Images {"); + for (Image image : images) { + stdout.println(indent+"\t"+image); + } + stdout.println(indent+"}"); + } + } + + @Command(name = "list-hardware-profiles", description = "") + public static class ComputeListHardwareProfilesCommand extends ComputeCommand { + @Override + protected void doCall(ComputeService computeService, String indent) throws Exception { + failIfArguments(); + Set hardware = computeService.listHardwareProfiles(); + stdout.println(indent+"Hardware Profiles {"); + for (Hardware image : hardware) { + stdout.println(indent+"\t"+image); + } + stdout.println(indent+"}"); + } + } + + @Command(name = "get-image", description = "") + public static class ComputeGetImageCommand extends ComputeCommand { + @Override + protected void doCall(ComputeService computeService, String indent) throws Exception { + if (arguments.isEmpty()) { + throw new ParseException("Requires at least one image-id arguments"); + } + + for (String imageId : arguments) { + Image image = computeService.getImage(imageId); + stdout.println(indent+"Image "+imageId+" {"); + stdout.println(indent+"\t"+image); + stdout.println(indent+"}"); + } + } + + @Override + public ToStringHelper string() { + return super.string() + .add("imageIds", arguments); + } + } + + @Command(name = "default-template", description = "") + public static class ComputeDefaultTemplateCommand extends JcloudsCommand { + @Override + protected void doCall(JcloudsLocation loc, String indent) throws Exception { + failIfArguments(); + ComputeService computeService = loc.getComputeService(); + + Template template = loc.buildTemplate(computeService, loc.getAllConfigBag()); + Image image = template.getImage(); + Hardware hardware = template.getHardware(); + org.jclouds.domain.Location location = template.getLocation(); + TemplateOptions options = template.getOptions(); + stdout.println(indent+"Default template {"); + stdout.println(indent+"\tImage: "+image); + stdout.println(indent+"\tHardware: "+hardware); + stdout.println(indent+"\tLocation: "+location); + stdout.println(indent+"\tOptions: "+options); + stdout.println(indent+"}"); + } + } + + @Command(name = "terminate-instances", description = "") + public static class ComputeTerminateInstancesCommand extends ComputeCommand { + @Override + protected void doCall(ComputeService computeService, String indent) throws Exception { + if (arguments.isEmpty()) { + throw new ParseException("Requires at least one instance-id arguments"); + } + + for (String instanceId : arguments) { + NodeMetadata instance = computeService.getNodeMetadata(instanceId); + if (instance == null) { + stderr.println(indent+"Cannot terminate instance; could not find "+instanceId); + } else { + boolean confirmed = confirm(indent, "terminate "+instanceId+" ("+instance+")"); + if (confirmed) { + computeService.destroyNode(instanceId); + } + } + } + } + + @Override + public ToStringHelper string() { + return super.string() + .add("instanceIds", arguments); + } + } + + public static abstract class BlobstoreCommand extends JcloudsCommand { + protected abstract void doCall(BlobStore blobstore, String indent) throws Exception; + + @Override + protected void doCall(JcloudsLocation loc, String indent) throws Exception { + String identity = checkNotNull(loc.getConfig(LocationConfigKeys.ACCESS_IDENTITY), "identity must not be null"); + String credential = checkNotNull(loc.getConfig(LocationConfigKeys.ACCESS_CREDENTIAL), "credential must not be null"); + String provider = checkNotNull(loc.getConfig(LocationConfigKeys.CLOUD_PROVIDER), "provider must not be null"); + String endpoint = loc.getConfig(CloudLocationConfig.CLOUD_ENDPOINT); + + BlobStoreContext context = JcloudsUtil.newBlobstoreContext(provider, endpoint, identity, credential, true); + try { + BlobStore blobStore = context.getBlobStore(); + doCall(blobStore, indent); + } finally { + context.close(); + } + } + } + + @Command(name = "list-containers", description = "") + public static class BlobstoreListContainersCommand extends BlobstoreCommand { + @Override + protected void doCall(BlobStore blobstore, String indent) throws Exception { + failIfArguments(); + Set containers = blobstore.list(); + stdout.println(indent+"Containers {"); + for (StorageMetadata container : containers) { + stdout.println(indent+"\t"+container); + } + stdout.println(indent+"}"); + } + } + + @Command(name = "list-container", description = "") + public static class BlobstoreListContainerCommand extends BlobstoreCommand { + @Override + protected void doCall(BlobStore blobStore, String indent) throws Exception { + if (arguments.isEmpty()) { + throw new ParseException("Requires at least one container-name arguments"); + } + + for (String containerName : arguments) { + Set contents = blobStore.list(containerName); + stdout.println(indent+"Container "+containerName+" {"); + for (StorageMetadata content : contents) { + stdout.println(indent+"\t"+content); + } + stdout.println(indent+"}"); + } + } + + @Override + public ToStringHelper string() { + return super.string() + .add("containers", arguments); + } + } + + @Command(name = "blob", description = "") + public static class BlobstoreGetBlobCommand extends BlobstoreCommand { + @Option(name = { "--container" }, title = "list contents of a given container", + description = "") + public String container; + + @Option(name = { "--blob" }, title = "retrieves the blog in the given container", + description = "") + public String blob; + + @Override + protected void doCall(BlobStore blobStore, String indent) throws Exception { + failIfArguments(); + Blob content = blobStore.getBlob(container, blob); + stdout.println(indent+"Blob "+container+" : " +blob +" {"); + stdout.println(indent+"\tHeaders {"); + for (Map.Entry entry : content.getAllHeaders().entries()) { + stdout.println(indent+"\t\t"+entry.getKey() + " = " + entry.getValue()); + } + stdout.println(indent+"\t}"); + stdout.println(indent+"\tmetadata : "+content.getMetadata()); + stdout.println(indent+"\tpayload : "+Streams.readFullyString(content.getPayload().openStream())); + stdout.println(indent+"}"); + } + + @Override + public ToStringHelper string() { + return super.string() + .add("container", container) + .add("blob", blob); + } + } + + /** method intended for overriding when the script filename is different + * @return the name of the script the user has invoked */ + protected String cliScriptName() { + return "cloud-explorer"; + } + + /** method intended for overriding when a different {@link Cli} is desired, + * or when the subclass wishes to change any of the arguments */ + protected CliBuilder cliBuilder() { + @SuppressWarnings({ "unchecked" }) + CliBuilder builder = Cli.builder(cliScriptName()) + .withDescription("Brooklyn Management Service") + .withCommands( + HelpCommand.class, + InfoCommand.class); + + builder.withGroup("compute") + .withDescription("Access cloud-compute details of a given cloud") + .withDefaultCommand(HelpCommand.class) + .withCommands(ImmutableList.>of( + ComputeListImagesCommand.class, + ComputeListHardwareProfilesCommand.class, + ComputeListInstancesCommand.class, + ComputeGetImageCommand.class, + ComputeDefaultTemplateCommand.class, + ComputeTerminateInstancesCommand.class)); + + builder.withGroup("blobstore") + .withDescription("Access cloud-blobstore details of a given cloud") + .withDefaultCommand(HelpCommand.class) + .withCommands(ImmutableList.>of( + BlobstoreListContainersCommand.class, + BlobstoreListContainerCommand.class, + BlobstoreGetBlobCommand.class)); + + return builder; + } + + protected void execCli(String ...args) { + execCli(cliBuilder().build(), args); + } + + protected void execCli(Cli parser, String ...args) { + try { + log.debug("Parsing command line arguments: {}", Arrays.asList(args)); + BrooklynCommand command = parser.parse(args); + log.debug("Executing command: {}", command); + command.call(); + System.exit(SUCCESS); + } catch (ParseException pe) { // looks like the user typed it wrong + System.err.println("Parse error: " + pe.getMessage()); // display + // error + System.err.println(getUsageInfo(parser)); // display cli help + System.exit(PARSE_ERROR); + } catch (FatalConfigurationRuntimeException e) { + log.error("Configuration error: "+e.getMessage(), e.getCause()); + System.err.println("Configuration error: " + e.getMessage()); + System.exit(CONFIGURATION_ERROR); + } catch (FatalRuntimeException e) { // anticipated non-configuration error + log.error("Startup error: "+e.getMessage(), e.getCause()); + System.err.println("Startup error: "+e.getMessage()); + System.exit(EXECUTION_ERROR); + } catch (Exception e) { // unexpected error during command execution + log.error("Execution error: " + e.getMessage(), e); + System.err.println("Execution error: " + e.getMessage()); + if (!(e instanceof UserFacingException)) + e.printStackTrace(); + System.exit(EXECUTION_ERROR); + } + } + + protected String getUsageInfo(Cli parser) { + StringBuilder help = new StringBuilder(); + help.append("\n"); + Help.help(parser.getMetadata(), Collections.emptyList(), help); + return help.toString(); + } + +} diff --git a/usage/cli/src/main/java/brooklyn/cli/Main.java b/usage/cli/src/main/java/brooklyn/cli/Main.java index 7dcfd73c56..23cecace70 100644 --- a/usage/cli/src/main/java/brooklyn/cli/Main.java +++ b/usage/cli/src/main/java/brooklyn/cli/Main.java @@ -29,10 +29,10 @@ import io.airlift.command.OptionType; import io.airlift.command.ParseException; -import java.io.BufferedReader; import java.io.Console; import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.URI; @@ -132,6 +132,15 @@ public static abstract class BrooklynCommand implements Callable { @Option(type = OptionType.GLOBAL, name = { "-q", "--quiet" }, description = "Quiet mode") public boolean quiet = false; + @VisibleForTesting + protected PrintStream stdout = System.out; + + @VisibleForTesting + protected PrintStream stderr = System.err; + + @VisibleForTesting + protected InputStream stdin = System.in; + public ToStringHelper string() { return Objects.toStringHelper(getClass()) .add("verbose", verbose) @@ -152,14 +161,14 @@ public static abstract class BrooklynCommandCollectingArgs extends BrooklynComma public List arguments = new ArrayList(); /** @return true iff there are arguments; it also sys.errs a warning in that case */ - protected boolean warnIfNoArguments() { + protected boolean warnIfArguments() { if (arguments.isEmpty()) return false; - System.err.println("Invalid subcommand arguments: "+Strings.join(arguments, " ")); + stderr.println("Invalid subcommand arguments: "+Strings.join(arguments, " ")); return true; } /** throw {@link ParseException} iff there are arguments */ - protected void failIfNoArguments() { + protected void failIfArguments() { if (arguments.isEmpty()) return ; throw new ParseException("Invalid subcommand arguments '"+Strings.join(arguments, " ")+"'"); } @@ -190,7 +199,7 @@ public static class InfoCommand extends BrooklynCommandCollectingArgs { @Override public Void call() throws Exception { if (log.isDebugEnabled()) log.debug("Invoked info command: {}", this); - warnIfNoArguments(); + warnIfArguments(); System.out.println(BANNER); System.out.println("Version: " + BrooklynVersion.get()); @@ -422,17 +431,17 @@ public static class LaunchCommand extends BrooklynCommandCollectingArgs { public Void call() throws Exception { // Configure launcher BrooklynLauncher launcher; - failIfNoArguments(); + failIfArguments(); try { if (log.isDebugEnabled()) log.debug("Invoked launch command {}", this); - if (!quiet) System.out.println(BANNER); + if (!quiet) stdout.println(BANNER); if (verbose) { if (app != null) { - System.out.println("Launching brooklyn app: " + app + " in " + locations); + stdout.println("Launching brooklyn app: " + app + " in " + locations); } else { - System.out.println("Launching brooklyn server (no app)"); + stdout.println("Launching brooklyn server (no app)"); } } @@ -661,7 +670,7 @@ protected void waitAfterLaunch(ManagementContext ctx) throws IOException { if (stopOnKeyPress) { // Wait for the user to type a key log.info("Server started. Press return to stop."); - System.in.read(); + stdin.read(); stopAllApps(ctx.getApplications()); } else { // Block forever so that Brooklyn doesn't exit (until someone does cntrl-c or kill) diff --git a/usage/cli/src/test/java/brooklyn/cli/CloudExplorerLiveTest.java b/usage/cli/src/test/java/brooklyn/cli/CloudExplorerLiveTest.java new file mode 100644 index 0000000000..2a314bb910 --- /dev/null +++ b/usage/cli/src/test/java/brooklyn/cli/CloudExplorerLiveTest.java @@ -0,0 +1,209 @@ +/* + * 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 brooklyn.cli; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; +import io.airlift.command.Cli; +import io.airlift.command.ParseCommandMissingException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.List; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import brooklyn.cli.Main.BrooklynCommand; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +public class CloudExplorerLiveTest { + + private String stdout; + private String stderr; + + @AfterMethod(alwaysRun=true) + public void tearDown() { + stdout = null; + stderr = null; + } + + @Test + public void testNoArgsThrowsException() throws Exception { + try { + call(new String[0]); + fail(); + } catch (ParseCommandMissingException e) { + // expected + } + } + + // A user running these tests might not have any instances; so don't assert that there will be one + @Test(groups={"Live", "Live-sanity"}) + public void testListInstances() throws Exception { + call("compute", "list-instances", "--location", "jclouds:aws-ec2:eu-west-1"); + + String errmsg = "stdout="+stdout+"; stderr="+stderr; + + List lines = assertAndStipSingleLocationHeader(stdout); + assertTrue(lines.get(0).equals("Instances {"), errmsg); + assertTrue(lines.get(lines.size()-1).equals("}"), errmsg); + assertTrue(stderr.isEmpty(), errmsg); + } + + @Test(groups={"Live", "Live-sanity"}) + public void testListImages() throws Exception { + call("compute", "list-images", "--location", "jclouds:softlayer:ams01"); + + String errmsg = "stdout="+stdout+"; stderr="+stderr; + + // FIXME Now has location details pre-amble; fix assertions + List lines = assertAndStipSingleLocationHeader(stdout); + assertTrue(lines.get(0).equals("Images {"), errmsg); + assertTrue(lines.get(lines.size()-1).equals("}"), errmsg); + assertTrue(stderr.isEmpty(), errmsg); + + List imageLines = lines.subList(1, lines.size()-1); + assertTrue(imageLines.size() > 0, errmsg); + assertTrue(imageLines.get(0).matches(".*id=.*providerId=.*os=.*description=.*"), "line="+imageLines.get(0)+"; "+errmsg); + } + + @Test(groups={"Live", "Live-sanity"}) + public void testListHardwareProfiles() throws Exception { + call("compute", "list-hardware-profiles", "--location", "jclouds:softlayer:ams01"); + + String errmsg = "stdout="+stdout+"; stderr="+stderr; + + // FIXME Now has location details pre-amble; fix assertions + List lines = assertAndStipSingleLocationHeader(stdout); + assertTrue(lines.get(0).equals("Hardware Profiles {"), errmsg); + assertTrue(lines.get(lines.size()-1).equals("}"), errmsg); + assertTrue(stderr.isEmpty(), errmsg); + + List hardwareProfileLines = lines.subList(1, lines.size()-1); + assertTrue(hardwareProfileLines.size() > 0, errmsg); + assertTrue(hardwareProfileLines.get(0).matches(".*cpu=.*memory=.*processors=.*"), "line="+hardwareProfileLines.get(0)+"; "+errmsg); + } + + @Test(groups={"Live", "Live-sanity"}) + public void testGetImage() throws Exception { + call("compute", "get-image", "--location", "jclouds:softlayer:ams01", "CENTOS_6_64"); + + String errmsg = "stdout="+stdout+"; stderr="+stderr; + + // FIXME Now has location details pre-amble; fix assertions + List lines = assertAndStipSingleLocationHeader(stdout); + assertTrue(lines.get(0).equals("Image CENTOS_6_64 {"), errmsg); + assertTrue(lines.get(lines.size()-1).equals("}"), errmsg); + assertTrue(stderr.isEmpty(), errmsg); + + List imageLines = lines.subList(1, lines.size()-1); + assertTrue(imageLines.size() > 0, errmsg); + assertTrue(imageLines.get(0).matches(".*id=.*providerId=.*os=.*description=.*"), "line="+imageLines.get(0)+"; "+errmsg); + } + + @Test(groups={"Live", "Live-sanity"}) + public void testGetDefaultTemplate() throws Exception { + call("compute", "default-template", "--location", "jclouds:softlayer:ams01"); + + String errmsg = "stdout="+stdout+"; stderr="+stderr; + + // FIXME Now has location details pre-amble; fix assertions + List lines = assertAndStipSingleLocationHeader(stdout); + assertTrue(lines.get(0).equals("Default template {"), errmsg); + assertTrue(lines.get(lines.size()-1).equals("}"), errmsg); + assertTrue(stderr.isEmpty(), errmsg); + + List imageLines = lines.subList(1, lines.size()-1); + assertTrue(imageLines.size() > 0, errmsg); + assertTrue(imageLines.get(0).matches("\tImage.*id=.*providerId=.*os=.*description=.*"), "line="+imageLines.get(0)+"; "+errmsg); + assertTrue(imageLines.get(1).matches("\tHardware.*cpu=.*memory=.*processors=.*"), "line="+imageLines.get(1)+"; "+errmsg); + assertTrue(imageLines.get(2).matches("\tLocation.*scope=.*"), "line="+imageLines.get(2)+"; "+errmsg); + assertTrue(imageLines.get(3).matches("\tOptions.*"), "line="+imageLines.get(3)+"; "+errmsg); + } + + /** + * Expects in brooklyn.properties: + * brooklyn.location.named.softlayer-swift-ams01=jclouds:swift:https://ams01.objectstorage.softlayer.net/auth/v1.0 + * brooklyn.location.named.softlayer-swift-ams01.identity=ABCDEFGH:myusername + * brooklyn.location.named.softlayer-swift-ams01.credential=1234567890... + */ + @Test(groups={"Live", "Live-sanity"}) + public void testListContainers() throws Exception { + call("blobstore", "list-containers", "--location", "named:softlayer-swift-ams01"); + + String errmsg = "stdout="+stdout+"; stderr="+stderr; + + // FIXME Now has location details pre-amble; fix assertions + List lines = assertAndStipSingleLocationHeader(stdout); + assertTrue(lines.get(0).equals("Containers {"), errmsg); + assertTrue(lines.get(lines.size()-1).equals("}"), errmsg); + assertTrue(stderr.isEmpty(), errmsg); + } + + protected void call(String... args) throws Exception { + call(new ByteArrayInputStream(new byte[0]), args); + } + + protected void call(InputStream instream, String... args) throws Exception { + ByteArrayOutputStream stdoutStream = new ByteArrayOutputStream(); + ByteArrayOutputStream stderrStream = new ByteArrayOutputStream(); + + Cli parser = new CloudExplorer().cliBuilder().build(); + + BrooklynCommand command = parser.parse(args); + command.stdout = new PrintStream(stdoutStream); + command.stderr = new PrintStream(stderrStream); + command.stdin = instream; + try { + command.call(); + } finally { + stdout = new String(stdoutStream.toByteArray()); + stderr = new String(stderrStream.toByteArray()); + } + } + + private List assertAndStipSingleLocationHeader(String stdout) { + List lines = ImmutableList.copyOf(Splitter.on("\n").omitEmptyStrings().split(stdout)); + + String errmsg = "lines="+lines; + + int nextLineCount = 0; + assertEquals(lines.get(nextLineCount++), "Location {", errmsg); + assertEquals(lines.get(lines.size()-1), "}", errmsg); + assertTrue(lines.get(nextLineCount++).startsWith("\tprovider: "), errmsg); + assertTrue(lines.get(nextLineCount++).startsWith("\tdisplayName: "), errmsg); + assertTrue(lines.get(nextLineCount++).startsWith("\tidentity: "), errmsg); + if (lines.get(nextLineCount).startsWith("\tendpoint: ")) nextLineCount++; + if (lines.get(nextLineCount).startsWith("\tregion: ")) nextLineCount++; + + List result = Lists.newArrayList(); + for (String line : lines.subList(nextLineCount, lines.size()-1)) { + assertTrue(line.startsWith("\t"), errmsg); + result.add(line.substring(1)); + } + return result; + } +} diff --git a/usage/dist/src/main/dist/bin/cloud-explorer b/usage/dist/src/main/dist/bin/cloud-explorer new file mode 100755 index 0000000000..822dbbedc6 --- /dev/null +++ b/usage/dist/src/main/dist/bin/cloud-explorer @@ -0,0 +1,47 @@ +#!/bin/bash +# +# 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. + +#set -x # debug + +ROOT=$(cd "$(dirname "$0")/.." && pwd -P) + +# discover BROOKLYN_HOME if not set, by attempting to resolve absolute path of this command (brooklyn) +if [ -z "$BROOKLYN_HOME" ] ; then + BROOKLYN_HOME=$(cd "$(dirname "$(readlink -f "$0" 2> /dev/null || readlink "$0" 2> /dev/null || echo "$0")")/.." && pwd) +fi + + +# use default memory settings, if not specified +if [ -z "${JAVA_OPTS}" ] ; then + JAVA_OPTS="-Xms256m -Xmx1g -XX:MaxPermSize=256m" +fi + +# set up the classpath +INITIAL_CLASSPATH=${BROOKLYN_HOME}/conf:${BROOKLYN_HOME}/lib/patch/*:${BROOKLYN_HOME}/lib/brooklyn/*:${BROOKLYN_HOME}/lib/dropins/* +# specify additional CP args in BROOKLYN_CLASSPATH +if [ ! -z "${BROOKLYN_CLASSPATH}" ]; then + INITIAL_CLASSPATH=${BROOKLYN_CLASSPATH}:${INITIAL_CLASSPATH} +fi + +# force resolution of localhost to be loopback, otherwise we hit problems +# TODO should be changed in code +JAVA_OPTS="-Dbrooklyn.location.localhost.address=127.0.0.1 ${JAVA_OPTS}" + +# run cloud-explorer +exec java ${JAVA_OPTS} -cp "${INITIAL_CLASSPATH}" brooklyn.cli.CloudExplorer "$@"