Return-Path: X-Original-To: apmail-brooklyn-commits-archive@minotaur.apache.org Delivered-To: apmail-brooklyn-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id B8C0118E1F for ; Wed, 5 Aug 2015 20:56:43 +0000 (UTC) Received: (qmail 71902 invoked by uid 500); 5 Aug 2015 20:56:43 -0000 Delivered-To: apmail-brooklyn-commits-archive@brooklyn.apache.org Received: (qmail 71879 invoked by uid 500); 5 Aug 2015 20:56:43 -0000 Mailing-List: contact commits-help@brooklyn.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@brooklyn.incubator.apache.org Delivered-To: mailing list commits@brooklyn.incubator.apache.org Received: (qmail 71870 invoked by uid 99); 5 Aug 2015 20:56:43 -0000 Received: from Unknown (HELO spamd4-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 05 Aug 2015 20:56:43 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd4-us-west.apache.org (ASF Mail Server at spamd4-us-west.apache.org) with ESMTP id F3334C0045 for ; Wed, 5 Aug 2015 20:56:42 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd4-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: 0.791 X-Spam-Level: X-Spam-Status: No, score=0.791 tagged_above=-999 required=6.31 tests=[KAM_ASCII_DIVIDERS=0.8, T_RP_MATCHES_RCVD=-0.01, URIBL_BLOCKED=0.001] autolearn=disabled Received: from mx1-us-west.apache.org ([10.40.0.8]) by localhost (spamd4-us-west.apache.org [10.40.0.11]) (amavisd-new, port 10024) with ESMTP id Equ_PSevY2nu for ; Wed, 5 Aug 2015 20:56:06 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-us-west.apache.org (ASF Mail Server at mx1-us-west.apache.org) with SMTP id EC3542100B for ; Wed, 5 Aug 2015 20:56:05 +0000 (UTC) Received: (qmail 69497 invoked by uid 99); 5 Aug 2015 20:56:05 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 05 Aug 2015 20:56:05 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 9CD65E050A; Wed, 5 Aug 2015 20:56:05 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: hadrian@apache.org To: commits@brooklyn.incubator.apache.org Date: Wed, 05 Aug 2015 20:56:07 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [03/20] incubator-brooklyn git commit: Package rename to org.apache.brooklyn: usage-cli http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/74ee6aac/usage/cli/src/main/java/org/apache/brooklyn/cli/CloudExplorer.java ---------------------------------------------------------------------- diff --git a/usage/cli/src/main/java/org/apache/brooklyn/cli/CloudExplorer.java b/usage/cli/src/main/java/org/apache/brooklyn/cli/CloudExplorer.java new file mode 100644 index 0000000..044042a --- /dev/null +++ b/usage/cli/src/main/java/org/apache/brooklyn/cli/CloudExplorer.java @@ -0,0 +1,381 @@ +/* + * 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.cli; + +import static com.google.common.base.Preconditions.checkNotNull; +import io.airlift.command.Command; +import io.airlift.command.Option; +import io.airlift.command.ParseException; + +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 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.stream.Streams; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +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 { + + public static abstract class JcloudsCommand extends AbstractMain.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.config().getBag()); + 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); + 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); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/74ee6aac/usage/cli/src/main/java/org/apache/brooklyn/cli/ItemLister.java ---------------------------------------------------------------------- diff --git a/usage/cli/src/main/java/org/apache/brooklyn/cli/ItemLister.java b/usage/cli/src/main/java/org/apache/brooklyn/cli/ItemLister.java new file mode 100644 index 0000000..dc25447 --- /dev/null +++ b/usage/cli/src/main/java/org/apache/brooklyn/cli/ItemLister.java @@ -0,0 +1,273 @@ +/* + * 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.cli; + +import io.airlift.command.Command; +import io.airlift.command.Option; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.basic.BrooklynObject; +import org.apache.brooklyn.catalog.Catalog; +import org.apache.brooklyn.cli.lister.ClassFinder; +import org.apache.brooklyn.cli.lister.ItemDescriptors; + +import brooklyn.entity.Entity; +import brooklyn.entity.proxying.ImplementedBy; +import brooklyn.location.Location; +import brooklyn.location.LocationResolver; +import brooklyn.policy.Enricher; +import brooklyn.policy.Policy; +import brooklyn.util.ResourceUtils; +import brooklyn.util.collections.MutableSet; +import brooklyn.util.net.Urls; +import brooklyn.util.os.Os; +import brooklyn.util.text.Strings; +import brooklyn.util.text.TemplateProcessor; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.io.Files; + +public class ItemLister { + + private static final Logger LOG = LoggerFactory.getLogger(ItemLister.class); + private static final String BASE = "brooklyn/item-lister"; + private static final String BASE_TEMPLATES = BASE+"/"+"templates"; + private static final String BASE_STATICS = BASE+"/"+"statics"; + + @Command(name = "list-objects", description = "List Brooklyn objects (Entities, Policies, Enrichers and Locations)") + public static class ListAllCommand extends AbstractMain.BrooklynCommandCollectingArgs { + + @Option(name = { "--jars" }, title = "Jars", description = "Jars to scan. If a file (not a url) pointing at a directory, will include all files in that directory") + public List jars = Lists.newLinkedList(); + + @Option(name = { "--type-regex" }, title = "Regex for types to list") + public String typeRegex; + + @Option(name = { "--catalog-only" }, title = "Whether to only list items annotated with @Catalog") + public boolean catalogOnly = true; + + @Option(name = { "--ignore-impls" }, title = "Ignore Entity implementations, where there is an Entity interface with @ImplementedBy") + public boolean ignoreImpls = false; + + @Option(name = { "--headings-only" }, title = "Whether to only show name/type, and not config keys etc") + public boolean headingsOnly = false; + + @Option(name = { "--output-folder" }, title = "Folder to save output") + public String outputFolder; + + @SuppressWarnings("unchecked") + @Override + public Void call() throws Exception { + List urls = getUrls(); + LOG.info("Retrieving objects from "+urls); + + // TODO Remove duplication from separate ListPolicyCommand etc + List> entityTypes = getTypes(urls, Entity.class); + List> policyTypes = getTypes(urls, Policy.class); + List> enricherTypes = getTypes(urls, Enricher.class); + List> locationTypes = getTypes(urls, Location.class, Boolean.FALSE); + + Map result = ImmutableMap.builder() + .put("entities", ItemDescriptors.toItemDescriptors(entityTypes, headingsOnly, "name")) + .put("policies", ItemDescriptors.toItemDescriptors(policyTypes, headingsOnly, "name")) + .put("enrichers", ItemDescriptors.toItemDescriptors(enricherTypes, headingsOnly, "name")) + .put("locations", ItemDescriptors.toItemDescriptors(locationTypes, headingsOnly, "type")) + .put("locationResolvers", ItemDescriptors.toItemDescriptors(ImmutableList.copyOf(ServiceLoader.load(LocationResolver.class)), true)) + .build(); + + String json = toJson(result); + + if (outputFolder == null) { + System.out.println(json); + } else { + LOG.info("Outputting item list (size "+itemCount+") to " + outputFolder); + String outputPath = Os.mergePaths(outputFolder, "index.html"); + String parentDir = (new File(outputPath).getParentFile()).getAbsolutePath(); + mkdir(parentDir, "entities"); + mkdir(parentDir, "policies"); + mkdir(parentDir, "enrichers"); + mkdir(parentDir, "locations"); + mkdir(parentDir, "locationResolvers"); //TODO nothing written here yet... + + mkdir(parentDir, "style"); + mkdir(Os.mergePaths(parentDir, "style"), "js"); + mkdir(Os.mergePaths(parentDir, "style", "js"), "catalog"); + + Files.write("var items = " + json, new File(Os.mergePaths(outputFolder, "items.js")), Charsets.UTF_8); + ResourceUtils resourceUtils = ResourceUtils.create(this); + + // root - just loads the above JSON + copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "brooklyn-object-list.html", "index.html"); + + // statics - structure mirrors docs (not for any real reason however... the json is usually enough for our docs) + copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "common.js"); + copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "items.css"); + copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "style/js/underscore-min.js"); + copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "style/js/underscore-min.map"); + copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "style/js/catalog/bloodhound.js"); + + // now make pages for each item + + List> entities = (List>) result.get("entities"); + String entityTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "entity.html")); + for (Map entity : entities) { + String type = (String) entity.get("type"); + String name = (String) entity.get("name"); + String entityHtml = TemplateProcessor.processTemplateContents(entityTemplateHtml, ImmutableMap.of("type", type, "name", name)); + Files.write(entityHtml, new File(Os.mergePaths(outputFolder, "entities", type + ".html")), Charsets.UTF_8); + } + + List> policies = (List>) result.get("policies"); + String policyTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "policy.html")); + for (Map policy : policies) { + String type = (String) policy.get("type"); + String name = (String) policy.get("name"); + String policyHtml = TemplateProcessor.processTemplateContents(policyTemplateHtml, ImmutableMap.of("type", type, "name", name)); + Files.write(policyHtml, new File(Os.mergePaths(outputFolder, "policies", type + ".html")), Charsets.UTF_8); + } + + List> enrichers = (List>) result.get("enrichers"); + String enricherTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "enricher.html")); + for (Map enricher : enrichers) { + String type = (String) enricher.get("type"); + String name = (String) enricher.get("name"); + String enricherHtml = TemplateProcessor.processTemplateContents(enricherTemplateHtml, ImmutableMap.of("type", type, "name", name)); + Files.write(enricherHtml, new File(Os.mergePaths(outputFolder, "enrichers", type + ".html")), Charsets.UTF_8); + } + + List> locations = (List>) result.get("locations"); + String locationTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "location.html")); + for (Map location : locations) { + String type = (String) location.get("type"); + String locationHtml = TemplateProcessor.processTemplateContents(locationTemplateHtml, ImmutableMap.of("type", type)); + Files.write(locationHtml, new File(Os.mergePaths(outputFolder, "locations", type + ".html")), Charsets.UTF_8); + } + LOG.info("Finished outputting item list to " + outputFolder); + } + return null; + } + + private void copyFromItemListerClasspathBaseStaticsToOutputDir(ResourceUtils resourceUtils, String item) throws IOException { + copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, item, item); + } + private void copyFromItemListerClasspathBaseStaticsToOutputDir(ResourceUtils resourceUtils, String item, String dest) throws IOException { + String js = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_STATICS, item)); + Files.write(js, new File(Os.mergePaths(outputFolder, dest)), Charsets.UTF_8); + } + + private void mkdir(String rootDir, String dirName) { + (new File(Os.mergePaths(rootDir, dirName))).mkdirs(); + } + + protected List getUrls() throws MalformedURLException { + List urls = Lists.newArrayList(); + if (jars.isEmpty()) { + String classpath = System.getenv("INITIAL_CLASSPATH"); + if (Strings.isNonBlank(classpath)) { + List entries = Splitter.on(":").omitEmptyStrings().trimResults().splitToList(classpath); + for (String entry : entries) { + if (entry.endsWith(".jar") || entry.endsWith("/*")) { + urls.addAll(ClassFinder.toJarUrls(entry.replace("/*", ""))); + } + } + } else { + throw new IllegalArgumentException("No Jars to process"); + } + } else { + for (String jar : jars) { + List expanded = ClassFinder.toJarUrls(jar); + if (expanded.isEmpty()) + LOG.warn("No jars found at: "+jar); + urls.addAll(expanded); + } + } + return urls; + } + + private List> getTypes(List urls, Class type) { + return getTypes(urls, type, null); + } + + int itemCount = 0; + + private List> getTypes(List urls, Class type, Boolean catalogOnlyOverride) { + FluentIterable> fluent = FluentIterable.from(ClassFinder.findClasses(urls, type)); + if (typeRegex != null) { + fluent = fluent.filter(ClassFinder.withClassNameMatching(typeRegex)); + } + if (catalogOnlyOverride == null ? catalogOnly : catalogOnlyOverride) { + fluent = fluent.filter(ClassFinder.withAnnotation(Catalog.class)); + } + List> filtered = fluent.toList(); + Collection> result; + if (ignoreImpls) { + result = MutableSet.copyOf(filtered); + for (Class clazz : filtered) { + ImplementedBy implementedBy = clazz.getAnnotation(ImplementedBy.class); + if (implementedBy != null) { + result.remove(implementedBy.value()); + } + } + } else { + result = filtered; + } + itemCount += result.size(); + return ImmutableList.copyOf(result); + } + + private String toJson(Object obj) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS) + .enable(SerializationFeature.INDENT_OUTPUT) + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + + // Only serialise annotated fields + .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + return objectMapper.writeValueAsString(obj); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/74ee6aac/usage/cli/src/main/java/org/apache/brooklyn/cli/Main.java ---------------------------------------------------------------------- diff --git a/usage/cli/src/main/java/org/apache/brooklyn/cli/Main.java b/usage/cli/src/main/java/org/apache/brooklyn/cli/Main.java new file mode 100644 index 0000000..cd827e0 --- /dev/null +++ b/usage/cli/src/main/java/org/apache/brooklyn/cli/Main.java @@ -0,0 +1,987 @@ +/* + * 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.cli; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.Console; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import brooklyn.BrooklynVersion; +import brooklyn.basic.BrooklynTypes; +import org.apache.brooklyn.catalog.BrooklynCatalog; +import org.apache.brooklyn.catalog.CatalogItem; +import org.apache.brooklyn.catalog.CatalogItem.CatalogItemType; +import org.apache.brooklyn.cli.CloudExplorer.BlobstoreGetBlobCommand; +import org.apache.brooklyn.cli.CloudExplorer.BlobstoreListContainerCommand; +import org.apache.brooklyn.cli.CloudExplorer.BlobstoreListContainersCommand; +import org.apache.brooklyn.cli.CloudExplorer.ComputeDefaultTemplateCommand; +import org.apache.brooklyn.cli.CloudExplorer.ComputeGetImageCommand; +import org.apache.brooklyn.cli.CloudExplorer.ComputeListHardwareProfilesCommand; +import org.apache.brooklyn.cli.CloudExplorer.ComputeListImagesCommand; +import org.apache.brooklyn.cli.CloudExplorer.ComputeListInstancesCommand; +import org.apache.brooklyn.cli.CloudExplorer.ComputeTerminateInstancesCommand; +import org.apache.brooklyn.cli.ItemLister.ListAllCommand; + +import brooklyn.catalog.internal.CatalogInitialization; +import brooklyn.entity.Application; +import brooklyn.entity.Entity; +import brooklyn.entity.basic.AbstractApplication; +import brooklyn.entity.basic.AbstractEntity; +import brooklyn.entity.basic.ApplicationBuilder; +import brooklyn.entity.basic.Entities; +import brooklyn.entity.basic.StartableApplication; +import brooklyn.entity.proxying.EntitySpec; +import brooklyn.entity.rebind.persister.BrooklynPersistenceUtils; +import brooklyn.entity.rebind.persister.PersistMode; +import brooklyn.entity.rebind.transformer.CompoundTransformer; +import brooklyn.entity.trait.Startable; +import brooklyn.launcher.BrooklynLauncher; +import brooklyn.launcher.BrooklynServerDetails; +import brooklyn.launcher.config.StopWhichAppsOnShutdown; +import brooklyn.management.ManagementContext; +import brooklyn.management.Task; +import brooklyn.management.ha.HighAvailabilityMode; +import brooklyn.management.ha.OsgiManager; +import brooklyn.rest.security.PasswordHasher; +import brooklyn.rest.util.ShutdownHandler; +import brooklyn.util.ResourceUtils; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.exceptions.FatalConfigurationRuntimeException; +import brooklyn.util.exceptions.FatalRuntimeException; +import brooklyn.util.exceptions.UserFacingException; +import brooklyn.util.guava.Maybe; +import brooklyn.util.javalang.Enums; +import brooklyn.util.net.Networking; +import brooklyn.util.text.Identifiers; +import brooklyn.util.text.StringEscapes.JavaStringEscapes; +import brooklyn.util.text.Strings; +import brooklyn.util.time.Duration; +import groovy.lang.GroovyClassLoader; +import groovy.lang.GroovyShell; +import io.airlift.command.Cli; +import io.airlift.command.Cli.CliBuilder; +import io.airlift.command.Command; +import io.airlift.command.Option; + +/** + * This class is the primary CLI for brooklyn. + * Run with the `help` argument for help. + *

+ * This class is designed for subclassing, with subclasses typically: + *

  • providing their own static {@link #main(String...)} (of course) which need simply invoke + * {@link #execCli(String[])} with the arguments + *
  • returning their CLI name (e.g. "start.sh") in an overridden {@link #cliScriptName()} + *
  • providing an overridden {@link LaunchCommand} via {@link #cliLaunchCommand()} if desired + *
  • providing any other CLI customisations by overriding {@link #cliBuilder()} + * (typically calling the parent and then customizing the builder) + *
  • populating a custom catalog using {@link LaunchCommand#populateCatalog(BrooklynCatalog)} + */ +public class Main extends AbstractMain { + + private static final Logger log = LoggerFactory.getLogger(Main.class); + + public static void main(String... args) { + log.debug("Launching Brooklyn via CLI, with "+Arrays.toString(args)); + BrooklynVersion.INSTANCE.logSummary(); + new Main().execCli(args); + } + + @Command(name = "generate-password", description = "Generates a hashed web-console password") + public static class GeneratePasswordCommand extends BrooklynCommandCollectingArgs { + + @Option(name = { "--user" }, title = "username", required = true) + public String user; + + @Option(name = { "--stdin" }, title = "read password from stdin, instead of console", + description = "Before using stdin, read http://stackoverflow.com/a/715681/1393883 for discussion of security!") + public boolean useStdin; + + @Override + public Void call() throws Exception { + checkCanReadPassword(); + + System.out.print("Enter password: "); + System.out.flush(); + String password = readPassword(); + if (Strings.isBlank(password)) { + throw new UserFacingException("Password must not be blank; aborting"); + } + + System.out.print("Re-enter password: "); + System.out.flush(); + String password2 = readPassword(); + if (!password.equals(password2)) { + throw new UserFacingException("Passwords did not match; aborting"); + } + + String salt = Identifiers.makeRandomId(4); + String sha256password = PasswordHasher.sha256(salt, new String(password)); + + System.out.println(); + System.out.println("Please add the following to your brooklyn.properties:"); + System.out.println(); + System.out.println("brooklyn.webconsole.security.users="+user); + System.out.println("brooklyn.webconsole.security.user."+user+".salt="+salt); + System.out.println("brooklyn.webconsole.security.user."+user+".sha256="+sha256password); + + return null; + } + + private void checkCanReadPassword() { + if (useStdin) { + // yes; always + } else { + Console console = System.console(); + if (console == null) { + throw new FatalConfigurationRuntimeException("No console; cannot get password securely; aborting"); + } + } + } + + private String readPassword() throws IOException { + if (useStdin) { + return readLine(System.in); + } else { + return new String(System.console().readPassword()); + } + } + + private String readLine(InputStream in) throws IOException { + StringBuilder result = new StringBuilder(); + char c; + while ((c = (char)in.read()) != '\n') { + result.append(c); + } + return result.toString(); + } + } + + @Command(name = "launch", description = "Starts a server, optionally with applications") + public static class LaunchCommand extends BrooklynCommandCollectingArgs { + + @Option(name = { "--localBrooklynProperties" }, title = "local brooklyn.properties file", + description = "Load the given properties file, specific to this launch (appending to and overriding global properties)") + public String localBrooklynProperties; + + @Option(name = { "--noGlobalBrooklynProperties" }, title = "do not use any global brooklyn.properties file found", + description = "Do not use the default global brooklyn.properties file found") + public boolean noGlobalBrooklynProperties = false; + + @Option(name = { "-a", "--app" }, title = "application class or file", + description = "The Application to start. " + + "For example, my.AppName, file://my/app.yaml, or classpath://my/AppName.groovy -- " + + "note that a BROOKLYN_CLASSPATH environment variable may be required to " + + "load classes from other locations") + public String app; + + @Beta + @Option(name = { "-s", "--script" }, title = "script URI", + description = "EXPERIMENTAL. URI for a Groovy script to parse and load." + + " This script will run before starting the app.") + public String script = null; + + @Option(name = { "-l", "--location", "--locations" }, title = "location list", + description = "Specifies the locations where the application will be launched. " + + "You can specify more than one location as a comma-separated list of values " + + "(or as a JSON array, if the values are complex)") + public String locations; + + @Option(name = { "--catalogInitial" }, title = "catalog initial bom URI", + description = "Specifies a catalog.bom URI to be used to populate the initial catalog, " + + "loaded on first run, or when persistence is off/empty or the catalog is reset") + public String catalogInitial; + + @Option(name = { "--catalogReset" }, + description = "Specifies that any catalog items which have been persisted should be cleared") + public boolean catalogReset; + + @Option(name = { "--catalogAdd" }, title = "catalog bom URI to add", + description = "Specifies a catalog.bom to be added to the catalog") + public String catalogAdd; + + @Option(name = { "--catalogForce" }, + description = "Specifies that catalog items added via the CLI should be forcibly added, " + + "replacing any identical versions already registered (use with care!)") + public boolean catalogForce; + + @Option(name = { "-p", "--port" }, title = "port number", + description = "Use this port for the brooklyn management web console and REST API; " + + "default is 8081+ for http, 8443+ for https.") + public String port; + + @Option(name = { "--https" }, + description = "Launch the web console on https") + public boolean useHttps = false; + + @Option(name = { "-nc", "--noConsole" }, + description = "Do not start the web console or REST API") + public boolean noConsole = false; + + @Option(name = { "-b", "--bindAddress" }, + description = "Specifies the IP address of the NIC to bind the Brooklyn Management Console to") + public String bindAddress = null; + + @Option(name = { "-pa", "--publicAddress" }, + description = "Specifies the IP address or hostname that the Brooklyn Management Console will be available on") + public String publicAddress = null; + + @Option(name = { "--noConsoleSecurity" }, + description = "Whether to disable authentication and security filters for the web console (for use when debugging on a secure network or bound to localhost)") + public Boolean noConsoleSecurity = false; + + @Option(name = { "--startupContinueOnWebErrors" }, + description = "Continue on web subsystem failures during startup " + + "(default is to abort if the web API fails to start, as management access is not normally possible)") + public boolean startupContinueOnWebErrors = false; + + @Option(name = { "--startupFailOnPersistenceErrors" }, + description = "Fail on persistence/HA subsystem failures during startup " + + "(default is to continue, so errors can be viewed via the API)") + public boolean startupFailOnPersistenceErrors = false; + + @Option(name = { "--startupFailOnCatalogErrors" }, + description = "Fail on catalog subsystem failures during startup " + + "(default is to continue, so errors can be viewed via the API)") + public boolean startupFailOnCatalogErrors = false; + + @Option(name = { "--startupFailOnManagedAppsErrors" }, + description = "Fail startup on errors deploying of managed apps specified via the command line " + + "(default is to continue, so errors can be viewed via the API)") + public boolean startupFailOnManagedAppsErrors = false; + + @Beta + @Option(name = { "--startBrooklynNode" }, + description = "Start a BrooklynNode entity representing this Brooklyn instance") + public boolean startBrooklynNode = false; + + // Note in some cases, you can get java.util.concurrent.RejectedExecutionException + // if shutdown is not co-ordinated, looks like: {@linktourl https://gist.github.com/47066f72d6f6f79b953e} + @Beta + @Option(name = { "-sk", "--stopOnKeyPress" }, + description = "Shutdown immediately on user text entry after startup (useful for debugging and demos)") + public boolean stopOnKeyPress = false; + + final static String STOP_WHICH_APPS_ON_SHUTDOWN = "--stopOnShutdown"; + protected final static String STOP_ALL = "all"; + protected final static String STOP_ALL_IF_NOT_PERSISTED = "allIfNotPersisted"; + protected final static String STOP_NONE = "none"; + protected final static String STOP_THESE = "these"; + protected final static String STOP_THESE_IF_NOT_PERSISTED = "theseIfNotPersisted"; + static { Enums.checkAllEnumeratedIgnoreCase(StopWhichAppsOnShutdown.class, STOP_ALL, STOP_ALL_IF_NOT_PERSISTED, STOP_NONE, STOP_THESE, STOP_THESE_IF_NOT_PERSISTED); } + + @Option(name = { STOP_WHICH_APPS_ON_SHUTDOWN }, + allowedValues = { STOP_ALL, STOP_ALL_IF_NOT_PERSISTED, STOP_NONE, STOP_THESE, STOP_THESE_IF_NOT_PERSISTED }, + description = "Which managed applications to stop on shutdown. Possible values are:\n"+ + "all: stop all apps\n"+ + "none: leave all apps running\n"+ + "these: stop the apps explicitly started on this command line, but leave others started subsequently running\n"+ + "theseIfNotPersisted: stop the apps started on this command line IF persistence is not enabled, otherwise leave all running\n"+ + "allIfNotPersisted: stop all apps IF persistence is not enabled, otherwise leave all running") + public String stopWhichAppsOnShutdown = STOP_THESE_IF_NOT_PERSISTED; + + @Option(name = { "--exitAndLeaveAppsRunningAfterStarting" }, + description = "Once the application to start (from --app) is running exit the process, leaving any entities running. " + + "Can be used in combination with --persist auto --persistenceDir to attach to the running app at a later time.") + public boolean exitAndLeaveAppsRunningAfterStarting = false; + + final static String PERSIST_OPTION = "--persist"; + protected final static String PERSIST_OPTION_DISABLED = "disabled"; + protected final static String PERSIST_OPTION_AUTO = "auto"; + protected final static String PERSIST_OPTION_REBIND = "rebind"; + protected final static String PERSIST_OPTION_CLEAN = "clean"; + static { Enums.checkAllEnumeratedIgnoreCase(PersistMode.class, PERSIST_OPTION_DISABLED, PERSIST_OPTION_AUTO, PERSIST_OPTION_REBIND, PERSIST_OPTION_CLEAN); } + + // TODO currently defaults to disabled; want it to default to on, when we're ready + // TODO how to force a line-split per option?! + // Looks like java.io.airlift.airline.UsagePrinter is splitting the description by word, and + // wrapping it automatically. + // See https://github.com/airlift/airline/issues/30 + @Option(name = { PERSIST_OPTION }, + allowedValues = { PERSIST_OPTION_DISABLED, PERSIST_OPTION_AUTO, PERSIST_OPTION_REBIND, PERSIST_OPTION_CLEAN }, + title = "persistence mode", + description = + "The persistence mode. Possible values are: \n"+ + "disabled: will not read or persist any state; \n"+ + "auto: will rebind to any existing state, or start up fresh if no state; \n"+ + "rebind: will rebind to the existing state, or fail if no state available; \n"+ + "clean: will start up fresh (removing any existing state)") + public String persist = PERSIST_OPTION_DISABLED; + + @Option(name = { "--persistenceDir" }, title = "persistence dir", + description = "The directory to read/write persisted state (or container name if using an object store)") + public String persistenceDir; + + @Option(name = { "--persistenceLocation" }, title = "persistence location", + description = "The location spec for an object store to read/write persisted state") + public String persistenceLocation; + + final static String HA_OPTION = "--highAvailability"; + protected final static String HA_OPTION_DISABLED = "disabled"; + protected final static String HA_OPTION_AUTO = "auto"; + protected final static String HA_OPTION_MASTER = "master"; + protected final static String HA_OPTION_STANDBY = "standby"; + protected final static String HA_OPTION_HOT_STANDBY = "hot_standby"; + protected final static String HA_OPTION_HOT_BACKUP = "hot_backup"; + static { Enums.checkAllEnumeratedIgnoreCase(HighAvailabilityMode.class, HA_OPTION_AUTO, HA_OPTION_DISABLED, HA_OPTION_MASTER, HA_OPTION_STANDBY, HA_OPTION_HOT_STANDBY, HA_OPTION_HOT_BACKUP); } + + @Option(name = { HA_OPTION }, allowedValues = { HA_OPTION_DISABLED, HA_OPTION_AUTO, HA_OPTION_MASTER, HA_OPTION_STANDBY, HA_OPTION_HOT_STANDBY, HA_OPTION_HOT_BACKUP }, + title = "high availability mode", + description = + "The high availability mode. Possible values are: \n"+ + "disabled: management node works in isolation - will not cooperate with any other standby/master nodes in management plane; \n"+ + "auto: will look for other management nodes, and will allocate itself as standby or master based on other nodes' states; \n"+ + "master: will startup as master - if there is already a master then fails immediately; \n"+ + "standby: will start up as lukewarm standby with no state - if there is not already a master then fails immediately, " + + "and if there is a master which subsequently fails, this node can promote itself; \n"+ + "hot_standby: will start up as hot standby in read-only mode - if there is not already a master then fails immediately, " + + "and if there is a master which subseuqently fails, this node can promote itself; \n"+ + "hot_backup: will start up as hot backup in read-only mode - no master is required, and this node will not become a master" + ) + public String highAvailability = HA_OPTION_AUTO; + + @VisibleForTesting + protected ManagementContext explicitManagementContext; + + @Override + public Void call() throws Exception { + // Configure launcher + BrooklynLauncher launcher; + AppShutdownHandler shutdownHandler = new AppShutdownHandler(); + failIfArguments(); + try { + if (log.isDebugEnabled()) log.debug("Invoked launch command {}", this); + + if (!quiet) stdout.println(banner); + + if (verbose) { + if (app != null) { + stdout.println("Launching brooklyn app: " + app + " in " + locations); + } else { + stdout.println("Launching brooklyn server (no app)"); + } + } + + PersistMode persistMode = computePersistMode(); + HighAvailabilityMode highAvailabilityMode = computeHighAvailabilityMode(persistMode); + + StopWhichAppsOnShutdown stopWhichAppsOnShutdownMode = computeStopWhichAppsOnShutdown(); + + computeLocations(); + + ResourceUtils utils = ResourceUtils.create(this); + GroovyClassLoader loader = new GroovyClassLoader(getClass().getClassLoader()); + + // First, run a setup script if the user has provided one + if (script != null) { + execGroovyScript(utils, loader, script); + } + + launcher = createLauncher(); + + CatalogInitialization catInit = new CatalogInitialization(catalogInitial, catalogReset, catalogAdd, catalogForce); + catInit.addPopulationCallback(new Function() { + @Override + public Void apply(CatalogInitialization catInit) { + try { + populateCatalog(catInit.getManagementContext().getCatalog()); + } catch (Throwable e) { + catInit.handleException(e, "overridden main class populate catalog"); + } + + // Force load of catalog (so web console is up to date) + confirmCatalog(catInit); + return null; + } + }); + catInit.setFailOnStartupErrors(startupFailOnCatalogErrors); + launcher.catalogInitialization(catInit); + + launcher.persistMode(persistMode); + launcher.persistenceDir(persistenceDir); + launcher.persistenceLocation(persistenceLocation); + + launcher.highAvailabilityMode(highAvailabilityMode); + + launcher.stopWhichAppsOnShutdown(stopWhichAppsOnShutdownMode); + launcher.shutdownHandler(shutdownHandler); + + computeAndSetApp(launcher, utils, loader); + + customize(launcher); + + } catch (FatalConfigurationRuntimeException e) { + throw e; + } catch (Exception e) { + throw new FatalConfigurationRuntimeException("Fatal error configuring Brooklyn launch: "+e.getMessage(), e); + } + + // Launch server + try { + launcher.start(); + } catch (FatalRuntimeException e) { + // rely on caller logging this propagated exception + throw e; + } catch (Exception e) { + // for other exceptions we log it, possibly redundantly but better too much than too little + Exceptions.propagateIfFatal(e); + log.error("Error launching brooklyn: "+Exceptions.collapseText(e), e); + try { + launcher.terminate(); + } catch (Exception e2) { + log.warn("Subsequent error during termination: "+e2); + log.debug("Details of subsequent error during termination: "+e2, e2); + } + Exceptions.propagate(e); + } + + BrooklynServerDetails server = launcher.getServerDetails(); + ManagementContext ctx = server.getManagementContext(); + + if (verbose) { + Entities.dumpInfo(launcher.getApplications()); + } + + if (!exitAndLeaveAppsRunningAfterStarting) { + waitAfterLaunch(ctx, shutdownHandler); + } + + // will call mgmt.terminate() in BrooklynShutdownHookJob + return null; + } + + /** can be overridden by subclasses which need to customize the launcher and/or management */ + protected void customize(BrooklynLauncher launcher) { + } + + protected void computeLocations() { + boolean hasLocations = !Strings.isBlank(locations); + if (app != null) { + if (hasLocations && isYamlApp()) { + log.info("YAML app combined with command line locations; YAML locations will take precedence; this behaviour may change in subsequent versions"); + } else if (!hasLocations && isYamlApp()) { + log.info("No locations supplied; defaulting to locations defined in YAML (if any)"); + } else if (!hasLocations) { + log.info("No locations supplied; starting with no locations"); + } + } else if (hasLocations) { + log.error("Locations specified without any applications; ignoring locations"); + } + } + + protected boolean isYamlApp() { + return app != null && app.endsWith(".yaml"); + } + + protected PersistMode computePersistMode() { + Maybe persistMode = Enums.valueOfIgnoreCase(PersistMode.class, persist); + if (!persistMode.isPresent()) { + if (Strings.isBlank(persist)) { + throw new FatalConfigurationRuntimeException("Persist mode must not be blank"); + } else { + throw new FatalConfigurationRuntimeException("Illegal persist setting: "+persist); + } + } + + if (persistMode.get() == PersistMode.DISABLED) { + if (Strings.isNonBlank(persistenceDir)) + throw new FatalConfigurationRuntimeException("Cannot specify persistenceDir when persist is disabled"); + if (Strings.isNonBlank(persistenceLocation)) + throw new FatalConfigurationRuntimeException("Cannot specify persistenceLocation when persist is disabled"); + } + return persistMode.get(); + } + + protected HighAvailabilityMode computeHighAvailabilityMode(PersistMode persistMode) { + Maybe highAvailabilityMode = Enums.valueOfIgnoreCase(HighAvailabilityMode.class, highAvailability); + if (!highAvailabilityMode.isPresent()) { + if (Strings.isBlank(highAvailability)) { + throw new FatalConfigurationRuntimeException("High availability mode must not be blank"); + } else { + throw new FatalConfigurationRuntimeException("Illegal highAvailability setting: "+highAvailability); + } + } + + if (highAvailabilityMode.get() != HighAvailabilityMode.DISABLED) { + if (persistMode == PersistMode.DISABLED) { + if (highAvailabilityMode.get() == HighAvailabilityMode.AUTO) + return HighAvailabilityMode.DISABLED; + throw new FatalConfigurationRuntimeException("Cannot specify highAvailability when persistence is disabled"); + } else if (persistMode == PersistMode.CLEAN && + (highAvailabilityMode.get() == HighAvailabilityMode.STANDBY + || highAvailabilityMode.get() == HighAvailabilityMode.HOT_STANDBY + || highAvailabilityMode.get() == HighAvailabilityMode.HOT_BACKUP)) { + throw new FatalConfigurationRuntimeException("Cannot specify highAvailability "+highAvailabilityMode.get()+" when persistence is CLEAN"); + } + } + return highAvailabilityMode.get(); + } + + protected StopWhichAppsOnShutdown computeStopWhichAppsOnShutdown() { + boolean isDefault = STOP_THESE_IF_NOT_PERSISTED.equals(stopWhichAppsOnShutdown); + if (exitAndLeaveAppsRunningAfterStarting && isDefault) { + return StopWhichAppsOnShutdown.NONE; + } else { + return Enums.valueOfIgnoreCase(StopWhichAppsOnShutdown.class, stopWhichAppsOnShutdown).get(); + } + } + + @VisibleForTesting + /** forces the launcher to use the given management context, when programmatically invoked; + * mainly used when testing to inject a safe (and fast) mgmt context */ + public void useManagementContext(ManagementContext mgmt) { + explicitManagementContext = mgmt; + } + + protected BrooklynLauncher createLauncher() { + BrooklynLauncher launcher; + launcher = BrooklynLauncher.newInstance(); + launcher.localBrooklynPropertiesFile(localBrooklynProperties) + .ignorePersistenceErrors(!startupFailOnPersistenceErrors) + .ignoreCatalogErrors(!startupFailOnCatalogErrors) + .ignoreWebErrors(startupContinueOnWebErrors) + .ignoreAppErrors(!startupFailOnManagedAppsErrors) + .locations(Strings.isBlank(locations) ? ImmutableList.of() : JavaStringEscapes.unwrapJsonishListIfPossible(locations)); + + launcher.webconsole(!noConsole); + if (useHttps) { + // true sets it; false (not set) leaves it blank and falls back to config key + // (no way currently to override config key, but that could be added) + launcher.webconsoleHttps(useHttps); + } + launcher.webconsolePort(port); + + if (noGlobalBrooklynProperties) { + log.debug("Configuring to disable global brooklyn.properties"); + launcher.globalBrooklynPropertiesFile(null); + } + if (noConsoleSecurity) { + log.info("Configuring to disable console security"); + launcher.installSecurityFilter(false); + } + if (startBrooklynNode) { + log.info("Configuring BrooklynNode entity startup"); + launcher.startBrooklynNode(true); + } + if (Strings.isNonEmpty(bindAddress)) { + log.debug("Configuring bind address as "+bindAddress); + launcher.bindAddress(Networking.getInetAddressWithFixedName(bindAddress)); + } + if (Strings.isNonEmpty(publicAddress)) { + log.debug("Configuring public address as "+publicAddress); + launcher.publicAddress(Networking.getInetAddressWithFixedName(publicAddress)); + } + if (explicitManagementContext!=null) { + log.debug("Configuring explicit management context "+explicitManagementContext); + launcher.managementContext(explicitManagementContext); + } + return launcher; + } + + /** method intended for subclassing, to add custom items to the catalog */ + protected void populateCatalog(BrooklynCatalog catalog) { + // nothing else added here + } + + protected void confirmCatalog(CatalogInitialization catInit) { + // Force load of catalog (so web console is up to date) + Stopwatch time = Stopwatch.createStarted(); + BrooklynCatalog catalog = catInit.getManagementContext().getCatalog(); + Iterable> items = catalog.getCatalogItems(); + for (CatalogItem item: items) { + try { + if (item.getCatalogItemType()==CatalogItemType.TEMPLATE) { + // skip validation of templates, they might contain instructions, + // and additionally they might contain multiple items in which case + // the validation below won't work anyway (you need to go via a deployment plan) + } else { + Object spec = catalog.createSpec(item); + if (spec instanceof EntitySpec) { + BrooklynTypes.getDefinedEntityType(((EntitySpec)spec).getType()); + } + log.debug("Catalog loaded spec "+spec+" for item "+item); + } + } catch (Throwable throwable) { + catInit.handleException(throwable, item); + } + } + log.debug("Catalog (size "+Iterables.size(items)+") confirmed in "+Duration.of(time)); + // nothing else added here + } + + /** convenience for subclasses to specify that an app should run, + * throwing the right (caught) error if another app has already been specified */ + protected void setAppToLaunch(String className) { + if (app!=null) { + if (app.equals(className)) return; + throw new FatalConfigurationRuntimeException("Cannot specify app '"+className+"' when '"+app+"' is already specified; " + + "remove one or more conflicting CLI arguments."); + } + app = className; + } + + protected void computeAndSetApp(BrooklynLauncher launcher, ResourceUtils utils, GroovyClassLoader loader) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + if (app != null) { + // Create the instance of the brooklyn app + log.debug("Loading the user's application: {}", app); + + if (isYamlApp()) { + log.debug("Loading application as YAML spec: {}", app); + String content = utils.getResourceAsString(app); + launcher.application(content); + } else { + Object loadedApp = loadApplicationFromClasspathOrParse(utils, loader, app); + if (loadedApp instanceof ApplicationBuilder) { + launcher.application((ApplicationBuilder)loadedApp); + } else if (loadedApp instanceof Application) { + launcher.application((AbstractApplication)loadedApp); + } else { + throw new FatalConfigurationRuntimeException("Unexpected application type "+(loadedApp==null ? null : loadedApp.getClass())+", for app "+loadedApp); + } + } + } + } + + protected void waitAfterLaunch(ManagementContext ctx, AppShutdownHandler shutdownHandler) throws IOException { + if (stopOnKeyPress) { + // Wait for the user to type a key + log.info("Server started. Press return to stop."); + // Read in another thread so we can use timeout on the wait. + Task readTask = ctx.getExecutionManager().submit(new Callable() { + @Override + public Void call() throws Exception { + stdin.read(); + return null; + } + }); + while (!shutdownHandler.isRequested()) { + try { + readTask.get(Duration.ONE_SECOND); + break; + } catch (TimeoutException e) { + //check if there's a shutdown request + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Exceptions.propagate(e); + } catch (ExecutionException e) { + throw Exceptions.propagate(e); + } + } + log.info("Shutting down applications."); + stopAllApps(ctx.getApplications()); + } else { + // Block forever so that Brooklyn doesn't exit (until someone does cntrl-c or kill) + log.info("Launched Brooklyn; will now block until shutdown command received via GUI/API (recommended) or process interrupt."); + shutdownHandler.waitOnShutdownRequest(); + } + } + + protected void execGroovyScript(ResourceUtils utils, GroovyClassLoader loader, String script) { + log.debug("Running the user provided script: {}", script); + String content = utils.getResourceAsString(script); + GroovyShell shell = new GroovyShell(loader); + shell.evaluate(content); + } + + /** + * Helper method that gets an instance of a brooklyn {@link AbstractApplication} or an {@link ApplicationBuilder}. + * Guaranteed to be non-null result of one of those types (throwing exception if app not appropriate). + */ + @SuppressWarnings("unchecked") + protected Object loadApplicationFromClasspathOrParse(ResourceUtils utils, GroovyClassLoader loader, String app) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + + Class tempclazz; + log.debug("Loading application as class on classpath: {}", app); + try { + tempclazz = loader.loadClass(app, true, false); + } catch (ClassNotFoundException cnfe) { // Not a class on the classpath + log.debug("Loading \"{}\" as class on classpath failed, now trying as .groovy source file", app); + String content = utils.getResourceAsString(app); + tempclazz = loader.parseClass(content); + } + final Class clazz = tempclazz; + + // Instantiate an app builder (wrapping app class in ApplicationBuilder, if necessary) + if (ApplicationBuilder.class.isAssignableFrom(clazz)) { + Constructor constructor = clazz.getConstructor(); + return (ApplicationBuilder) constructor.newInstance(); + } else if (StartableApplication.class.isAssignableFrom(clazz)) { + EntitySpec appSpec; + if (tempclazz.isInterface()) + appSpec = EntitySpec.create((Class) clazz); + else + appSpec = EntitySpec.create(StartableApplication.class, (Class) clazz); + return new ApplicationBuilder(appSpec) { + @Override protected void doBuild() { + }}; + } else if (AbstractApplication.class.isAssignableFrom(clazz)) { + // TODO If this application overrides init() then in trouble, as that won't get called! + // TODO grr; what to do about non-startable applications? + // without this we could return ApplicationBuilder rather than Object + Constructor constructor = clazz.getConstructor(); + return (AbstractApplication) constructor.newInstance(); + } else if (AbstractEntity.class.isAssignableFrom(clazz)) { + // TODO Should we really accept any entity type, and just wrap it in an app? That's not documented! + return new ApplicationBuilder() { + @Override protected void doBuild() { + addChild(EntitySpec.create(Entity.class).impl((Class)clazz).additionalInterfaces(clazz.getInterfaces())); + }}; + } else if (Entity.class.isAssignableFrom(clazz)) { + return new ApplicationBuilder() { + @Override protected void doBuild() { + addChild(EntitySpec.create((Class)clazz)); + }}; + } else { + throw new FatalConfigurationRuntimeException("Application class "+clazz+" must extend one of ApplicationBuilder or AbstractApplication"); + } + } + + @VisibleForTesting + protected void stopAllApps(Collection applications) { + for (Application application : applications) { + try { + if (application instanceof Startable) { + ((Startable)application).stop(); + } + } catch (Exception e) { + log.error("Error stopping "+application+": "+e, e); + } + } + } + + @Override + public ToStringHelper string() { + return super.string() + .add("app", app) + .add("script", script) + .add("location", locations) + .add("port", port) + .add("bindAddress", bindAddress) + .add("noConsole", noConsole) + .add("noConsoleSecurity", noConsoleSecurity) + .add("startupFailOnPersistenceErrors", startupFailOnPersistenceErrors) + .add("startupFailsOnCatalogErrors", startupFailOnCatalogErrors) + .add("startupContinueOnWebErrors", startupContinueOnWebErrors) + .add("startupFailOnManagedAppsErrors", startupFailOnManagedAppsErrors) + .add("catalogInitial", catalogInitial) + .add("catalogAdd", catalogAdd) + .add("catalogReset", catalogReset) + .add("catalogForce", catalogForce) + .add("stopWhichAppsOnShutdown", stopWhichAppsOnShutdown) + .add("stopOnKeyPress", stopOnKeyPress) + .add("localBrooklynProperties", localBrooklynProperties) + .add("persist", persist) + .add("persistenceLocation", persistenceLocation) + .add("persistenceDir", persistenceDir) + .add("highAvailability", highAvailability) + .add("exitAndLeaveAppsRunningAfterStarting", exitAndLeaveAppsRunningAfterStarting); + } + } + + @Command(name = "copy-state", description = "Retrieves persisted state") + public static class CopyStateCommand extends BrooklynCommandCollectingArgs { + + @Option(name = { "--localBrooklynProperties" }, title = "local brooklyn.properties file", + description = "local brooklyn.properties file, specific to this launch (appending to and overriding global properties)") + public String localBrooklynProperties; + + @Option(name = { "--persistenceDir" }, title = "persistence dir", + description = "The directory to read persisted state (or container name if using an object store)") + public String persistenceDir; + + @Option(name = { "--persistenceLocation" }, title = "persistence location", + description = "The location spec for an object store to read persisted state") + public String persistenceLocation; + + @Option(name = { "--destinationDir" }, required = true, title = "destination dir", + description = "The directory to copy persistence data to") + public String destinationDir; + + @Option(name = { "--destinationLocation" }, title = "persistence location", + description = "The location spec for an object store to copy data to") + public String destinationLocation; + + @Option(name = { "--transformations" }, title = "transformations", + description = "local transformations file, to be applied to the copy of the data before uploading it") + public String transformations; + + @Override + public Void call() throws Exception { + checkNotNull(destinationDir, "destinationDir"); // presumably because required=true this will never be null! + + // Configure launcher + BrooklynLauncher launcher; + failIfArguments(); + try { + log.info("Retrieving and copying persisted state to "+destinationDir+(Strings.isBlank(destinationLocation) ? "" : " @ "+destinationLocation)); + + if (!quiet) stdout.println(banner); + + PersistMode persistMode = PersistMode.AUTO; + HighAvailabilityMode highAvailabilityMode = HighAvailabilityMode.DISABLED; + + launcher = BrooklynLauncher.newInstance() + .localBrooklynPropertiesFile(localBrooklynProperties) + .brooklynProperties(OsgiManager.USE_OSGI, false) + .persistMode(persistMode) + .persistenceDir(persistenceDir) + .persistenceLocation(persistenceLocation) + .highAvailabilityMode(highAvailabilityMode); + + } catch (FatalConfigurationRuntimeException e) { + throw e; + } catch (Exception e) { + throw new FatalConfigurationRuntimeException("Fatal error configuring Brooklyn launch: "+e.getMessage(), e); + } + + try { + launcher.copyPersistedState(destinationDir, destinationLocation, loadTransformer(transformations)); + } catch (FatalRuntimeException e) { + // rely on caller logging this propagated exception + throw e; + } catch (Exception e) { + // for other exceptions we log it, possibly redundantly but better too much than too little + Exceptions.propagateIfFatal(e); + log.error("Error retrieving persisted state: "+Exceptions.collapseText(e), e); + Exceptions.propagate(e); + } finally { + try { + launcher.terminate(); + } catch (Exception e2) { + log.warn("Subsequent error during termination: "+e2); + log.debug("Details of subsequent error during termination: "+e2, e2); + } + } + + return null; + } + + protected CompoundTransformer loadTransformer(String transformationsFileUrl) { + return BrooklynPersistenceUtils.loadTransformer(ResourceUtils.create(this), transformationsFileUrl); + } + + @Override + public ToStringHelper string() { + return super.string() + .add("localBrooklynProperties", localBrooklynProperties) + .add("persistenceLocation", persistenceLocation) + .add("persistenceDir", persistenceDir) + .add("destinationDir", destinationDir); + } + } + + /** method intended for overriding when a different {@link Cli} is desired, + * or when the subclass wishes to change any of the arguments */ + @SuppressWarnings("unchecked") + @Override + protected CliBuilder cliBuilder() { + CliBuilder builder = Cli.builder(cliScriptName()) + .withDescription("Brooklyn Management Service") + .withDefaultCommand(cliDefaultInfoCommand()) + .withCommands( + HelpCommand.class, + cliInfoCommand(), + GeneratePasswordCommand.class, + CopyStateCommand.class, + ListAllCommand.class, + cliLaunchCommand() + ); + + builder.withGroup("cloud-compute") + .withDescription("Access compute details of a given cloud") + .withDefaultCommand(HelpCommand.class) + .withCommands( + ComputeListImagesCommand.class, + ComputeListHardwareProfilesCommand.class, + ComputeListInstancesCommand.class, + ComputeGetImageCommand.class, + ComputeDefaultTemplateCommand.class, + ComputeTerminateInstancesCommand.class); + + builder.withGroup("cloud-blobstore") + .withDescription("Access blobstore details of a given cloud") + .withDefaultCommand(HelpCommand.class) + .withCommands( + BlobstoreListContainersCommand.class, + BlobstoreListContainerCommand.class, + BlobstoreGetBlobCommand.class); + + return builder; + } + + /** method intended for overriding when a custom {@link LaunchCommand} is being specified */ + protected Class cliLaunchCommand() { + return LaunchCommand.class; + } + + /** method intended for overriding when a custom {@link InfoCommand} is being specified */ + protected Class cliInfoCommand() { + return InfoCommand.class; + } + + /** method intended for overriding when a custom {@link InfoCommand} is being specified */ + protected Class cliDefaultInfoCommand() { + return DefaultInfoCommand.class; + } + + public static class AppShutdownHandler implements ShutdownHandler { + private CountDownLatch lock = new CountDownLatch(1); + + @Override + public void onShutdownRequest() { + lock.countDown(); + } + + public boolean isRequested() { + return lock.getCount() == 0; + } + + public void waitOnShutdownRequest() { + try { + lock.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; // exit gracefully + } + } + } +}