@@ -4,11 +4,13 @@ | |||||
# | # | ||||
# Create packer images for sage and/or node | # Create packer images for sage and/or node | ||||
# | # | ||||
# Usage: pack_bubble [-node|-sage] [-cloud CloudName] | |||||
# Usage: | |||||
# | # | ||||
# -node : only pack the node image, do not pack the sage | |||||
# -sage : only pack the sage image, do not pack the node | |||||
# -cloud CloudName : only pack for CloudName compute cloud, do not pack for all clouds | |||||
# pack_bubble [node|sage] [cloud CloudName] | |||||
# | |||||
# node : only pack the node image, do not pack the sage | |||||
# sage : only pack the sage image, do not pack the node | |||||
# cloud CloudName : only pack for CloudName compute cloud, do not pack for all clouds | |||||
# | # | ||||
SCRIPT="${0}" | SCRIPT="${0}" | ||||
SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | ||||
@@ -16,10 +18,10 @@ SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | |||||
if [[ -z "${1}" ]] ; then | if [[ -z "${1}" ]] ; then | ||||
IMAGES="node sage" | IMAGES="node sage" | ||||
elif [[ "${1}" == "-node" ]] ; then | |||||
elif [[ "${1}" == "node" ]] ; then | |||||
IMAGES="node" | IMAGES="node" | ||||
shift | shift | ||||
elif [[ "${1}" == "-sage" ]] ; then | |||||
elif [[ "${1}" == "sage" ]] ; then | |||||
IMAGES="sage" | IMAGES="sage" | ||||
shift | shift | ||||
fi | fi | ||||
@@ -31,7 +33,7 @@ if [[ -z "${1}" ]] ; then | |||||
die "Error reading compute cloud names from ${CLOUDS_URL}" | die "Error reading compute cloud names from ${CLOUDS_URL}" | ||||
fi | fi | ||||
elif [[ "${1}" == "-cloud" ]] ; then | |||||
elif [[ "${1}" == "cloud" ]] ; then | |||||
CLOUDS="${2}" | CLOUDS="${2}" | ||||
if [[ -z "${CLOUDS}" ]] ; then | if [[ -z "${CLOUDS}" ]] ; then | ||||
die "No cloud name specified after -cloud" | die "No cloud name specified after -cloud" | ||||
@@ -0,0 +1,51 @@ | |||||
#!/bin/bash | |||||
# | |||||
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||||
# | |||||
# Display active and completed packer jobs | |||||
# | |||||
# Usage: | |||||
# | |||||
# pack_status [running|completed] | |||||
# | |||||
# If the first argument is 'running' then only the status of running jobs will be shown | |||||
# If the first argument is 'completed' then only the status of completed jobs will be shown | |||||
# | |||||
# Based on your BUBBLE_USER, BUBBLE_PASS and BUBBLE_API environment variables, this command will | |||||
# use the bubble API to display the current status of the PackerService | |||||
# | |||||
# It returns a JSON object in the form: | |||||
# { | |||||
# "running": [ | |||||
# {...job1...}, | |||||
# {...job2...}, | |||||
# ... | |||||
# ], | |||||
# "completed": { | |||||
# "cloud_key1": [ {...image1...}, {...image2...}, ... ], | |||||
# "cloud_key2": [ {...image1...}, {...image2...}, ... ], | |||||
# ... | |||||
# } | |||||
# } | |||||
# | |||||
# In the above, "running" is an array of job summary objects | |||||
# and "completed" is a key/value map where they key indicates a cloud, | |||||
# and the value is an array of packer images that have completed | |||||
# | |||||
# If you pass the 'running' argument, only the array of running jobs will be printed | |||||
# If you pass the 'completed' argument, only the map of cloud->image[] will be printed | |||||
# | |||||
SCRIPT="${0}" | |||||
SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | |||||
. ${SCRIPT_DIR}/bubble_common | |||||
if [[ -z "${1}" ]] ; then | |||||
bget me/packer | |||||
elif [[ "${1}" == "running" ]] ; then | |||||
bget me/packer/running | |||||
elif [[ "${1}" == "completed" ]] ; then | |||||
bget me/packer/completed | |||||
else | |||||
echo "Unrecognized argument ${1}, expected 'running' or 'completed' (or nothing)" | |||||
exit 1 | |||||
fi |
@@ -24,7 +24,7 @@ public class AccountPromotionsResource { | |||||
@Autowired private PromotionService promoService; | @Autowired private PromotionService promoService; | ||||
private Account account; | |||||
private final Account account; | |||||
public AccountPromotionsResource (Account account) { this.account = account; } | public AccountPromotionsResource (Account account) { this.account = account; } | ||||
@@ -29,7 +29,6 @@ import bubble.service.account.StandardAccountMessageService; | |||||
import bubble.service.account.StandardAuthenticatorService; | import bubble.service.account.StandardAuthenticatorService; | ||||
import bubble.service.account.download.AccountDownloadService; | import bubble.service.account.download.AccountDownloadService; | ||||
import bubble.service.boot.BubbleModelSetupService; | import bubble.service.boot.BubbleModelSetupService; | ||||
import bubble.service.boot.SageHelloService; | |||||
import bubble.service.boot.StandardSelfNodeService; | import bubble.service.boot.StandardSelfNodeService; | ||||
import bubble.service.cloud.NodeLaunchMonitor; | import bubble.service.cloud.NodeLaunchMonitor; | ||||
import bubble.service.upgrade.BubbleJarUpgradeService; | import bubble.service.upgrade.BubbleJarUpgradeService; | ||||
@@ -398,6 +397,13 @@ public class MeResource { | |||||
return ok(launchMonitor.listLaunchStatuses(caller.getUuid())); | return ok(launchMonitor.listLaunchStatuses(caller.getUuid())); | ||||
} | } | ||||
@Path(EP_PACKER) | |||||
public PackerResource getPackerResource(@Context Request req, | |||||
@Context ContainerRequest ctx) { | |||||
final Account caller = userPrincipal(ctx); | |||||
return configuration.subResource(PackerResource.class, caller); | |||||
} | |||||
@Path(EP_PROMOTIONS) | @Path(EP_PROMOTIONS) | ||||
public AccountPromotionsResource getPromotionsResource(@Context Request req, | public AccountPromotionsResource getPromotionsResource(@Context Request req, | ||||
@Context ContainerRequest ctx) { | @Context ContainerRequest ctx) { | ||||
@@ -18,8 +18,9 @@ import javax.ws.rs.*; | |||||
import javax.ws.rs.core.Context; | import javax.ws.rs.core.Context; | ||||
import javax.ws.rs.core.Response; | import javax.ws.rs.core.Response; | ||||
import static bubble.resources.cloud.PackerResource.packerNotAllowedForUser; | |||||
import static javax.ws.rs.core.MediaType.APPLICATION_JSON; | import static javax.ws.rs.core.MediaType.APPLICATION_JSON; | ||||
import static org.cobbzilla.wizard.resources.ResourceUtil.ok; | |||||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||||
@Consumes(APPLICATION_JSON) | @Consumes(APPLICATION_JSON) | ||||
@Produces(APPLICATION_JSON) | @Produces(APPLICATION_JSON) | ||||
@@ -39,6 +40,7 @@ public class ComputePackerResource { | |||||
@GET | @GET | ||||
public Response listImages(@Context Request req, | public Response listImages(@Context Request req, | ||||
@Context ContainerRequest ctx) { | @Context ContainerRequest ctx) { | ||||
if (packerNotAllowedForUser(ctx)) return forbidden(); | |||||
final ComputeServiceDriver driver = cloud.getComputeDriver(configuration); | final ComputeServiceDriver driver = cloud.getComputeDriver(configuration); | ||||
return ok(driver.getAllPackerImages()); | return ok(driver.getAllPackerImages()); | ||||
} | } | ||||
@@ -47,6 +49,7 @@ public class ComputePackerResource { | |||||
public Response writeImages(@Context Request req, | public Response writeImages(@Context Request req, | ||||
@Context ContainerRequest ctx, | @Context ContainerRequest ctx, | ||||
@PathParam("type") AnsibleInstallType installType) { | @PathParam("type") AnsibleInstallType installType) { | ||||
if (packerNotAllowedForUser(ctx)) return forbidden(); | |||||
packer.writePackerImages(cloud, installType, null); | packer.writePackerImages(cloud, installType, null); | ||||
return ok(); | return ok(); | ||||
} | } | ||||
@@ -0,0 +1,65 @@ | |||||
package bubble.resources.cloud; | |||||
import bubble.model.account.Account; | |||||
import bubble.service.packer.PackerService; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.cobbzilla.util.collection.MapBuilder; | |||||
import org.glassfish.grizzly.http.server.Request; | |||||
import org.glassfish.jersey.server.ContainerRequest; | |||||
import org.springframework.beans.factory.annotation.Autowired; | |||||
import javax.ws.rs.Consumes; | |||||
import javax.ws.rs.GET; | |||||
import javax.ws.rs.Path; | |||||
import javax.ws.rs.Produces; | |||||
import javax.ws.rs.core.Context; | |||||
import javax.ws.rs.core.Response; | |||||
import static javax.ws.rs.core.MediaType.APPLICATION_JSON; | |||||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||||
@Produces(APPLICATION_JSON) | |||||
@Consumes(APPLICATION_JSON) | |||||
@Slf4j | |||||
public class PackerResource { | |||||
public static final String STATUS_RUNNING = "running"; | |||||
public static final String STATUS_COMPLETED = "completed"; | |||||
private final Account account; | |||||
public PackerResource(Account account) { this.account = account; } | |||||
@Autowired private PackerService packerService; | |||||
@GET | |||||
public Response listAllStatus(@Context Request req, | |||||
@Context ContainerRequest ctx) { | |||||
if (packerNotAllowedForUser(ctx)) return forbidden(); | |||||
return ok(MapBuilder.build(new Object[][] { | |||||
{STATUS_RUNNING, packerService.getActiveSummary(account.getUuid()) }, | |||||
{STATUS_COMPLETED, packerService.getCompletedSummary(account.getUuid()) } | |||||
})); | |||||
} | |||||
@GET @Path(STATUS_RUNNING) | |||||
public Response listRunningBuilds(@Context Request req, | |||||
@Context ContainerRequest ctx) { | |||||
if (packerNotAllowedForUser(ctx)) return forbidden(); | |||||
return ok(packerService.getActiveSummary(account.getUuid())); | |||||
} | |||||
@GET @Path(STATUS_COMPLETED) | |||||
public Response listCompletedBuilds(@Context Request req, | |||||
@Context ContainerRequest ctx) { | |||||
if (packerNotAllowedForUser(ctx)) return forbidden(); | |||||
return ok(packerService.getCompletedSummary(account.getUuid())); | |||||
} | |||||
public static boolean packerNotAllowedForUser(@Context ContainerRequest ctx) { | |||||
final Account caller = userPrincipal(ctx); | |||||
if (!caller.admin()) return true; | |||||
return false; | |||||
} | |||||
} |
@@ -4,7 +4,6 @@ | |||||
*/ | */ | ||||
package bubble.service.packer; | package bubble.service.packer; | ||||
import bubble.ApiConstants; | |||||
import bubble.cloud.CloudRegion; | import bubble.cloud.CloudRegion; | ||||
import bubble.cloud.CloudRegionRelative; | import bubble.cloud.CloudRegionRelative; | ||||
import bubble.cloud.compute.ComputeConfig; | import bubble.cloud.compute.ComputeConfig; | ||||
@@ -38,6 +37,7 @@ import java.io.File; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.util.*; | import java.util.*; | ||||
import java.util.concurrent.Callable; | import java.util.concurrent.Callable; | ||||
import java.util.concurrent.atomic.AtomicLong; | |||||
import java.util.concurrent.atomic.AtomicReference; | import java.util.concurrent.atomic.AtomicReference; | ||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
@@ -92,6 +92,13 @@ public class PackerJob implements Callable<List<PackerImage>> { | |||||
@Getter private final AnsibleInstallType installType; | @Getter private final AnsibleInstallType installType; | ||||
@Getter private final List<AtomicReference<List<PackerImage>>> imagesRefList = new ArrayList<>(); | @Getter private final List<AtomicReference<List<PackerImage>>> imagesRefList = new ArrayList<>(); | ||||
@Getter private List<PackerImage> images = new ArrayList<>(); | @Getter private List<PackerImage> images = new ArrayList<>(); | ||||
@Getter private final long ctime = now(); | |||||
private final AtomicLong completedAt = new AtomicLong(0); | |||||
public Long getCompletedAt() { | |||||
long t = completedAt.get(); | |||||
return t == 0 ? null : t; | |||||
} | |||||
public PackerJob(CloudService cloud, AnsibleInstallType installType) { | public PackerJob(CloudService cloud, AnsibleInstallType installType) { | ||||
this.cloud = cloud; | this.cloud = cloud; | ||||
@@ -121,10 +128,12 @@ public class PackerJob implements Callable<List<PackerImage>> { | |||||
@Override public List<PackerImage> call() throws Exception { | @Override public List<PackerImage> call() throws Exception { | ||||
try { | try { | ||||
final List<PackerImage> images = _call(); | final List<PackerImage> images = _call(); | ||||
completedAt.set(now()); | |||||
packerService.recordJobCompleted(this); | packerService.recordJobCompleted(this); | ||||
return images; | return images; | ||||
} catch (Exception e) { | } catch (Exception e) { | ||||
completedAt.set(now()); | |||||
packerService.recordJobError(this, e); | packerService.recordJobError(this, e); | ||||
throw e; | throw e; | ||||
} | } | ||||
@@ -0,0 +1,35 @@ | |||||
package bubble.service.packer; | |||||
import bubble.model.cloud.AnsibleInstallType; | |||||
import bubble.model.cloud.CloudService; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.experimental.Accessors; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||||
import static org.cobbzilla.util.time.TimeUtil.formatDuration; | |||||
@NoArgsConstructor @Accessors(chain=true) | |||||
public class PackerJobSummary { | |||||
@Getter private CloudService cloud; | |||||
@Getter private AnsibleInstallType installType; | |||||
@Getter private long ctime; | |||||
private static final String[] CLOUD_SUMMARY_FIELDS = {"uuid", "name", "account"}; | |||||
public PackerJobSummary (PackerJob job) { | |||||
this.cloud = new CloudService(); | |||||
copy(this.cloud, job.getCloud(), CLOUD_SUMMARY_FIELDS); | |||||
this.installType = job.getInstallType(); | |||||
this.ctime = job.getCtime(); | |||||
} | |||||
// derived properties, useful when displaying PackerJobSummary as JSON via `pack_status` | |||||
public String getDuration () { return formatDuration(now() - getCtime()); } | |||||
public PackerJobSummary setDuration (String d) { return this; } // noop | |||||
} |
@@ -68,7 +68,10 @@ public class PackerService { | |||||
} | } | ||||
public static String cacheKey(CloudService cloud, AnsibleInstallType installType) { | public static String cacheKey(CloudService cloud, AnsibleInstallType installType) { | ||||
return cloud.getUuid()+"_"+installType; | |||||
// note: only cloud.uuid and installType are needed for uniqueness in the cache key | |||||
// we add the account uuid so we can filter completed jobs based on account | |||||
// we add the cloud name so to make the key human-readable | |||||
return cloud.getAccount()+"_"+cloud.getUuid()+"_"+cloud.getName()+"_"+installType; | |||||
} | } | ||||
public void recordJobCompleted(PackerJob job) { | public void recordJobCompleted(PackerJob job) { | ||||
@@ -83,6 +86,22 @@ public class PackerService { | |||||
activeJobs.remove(job.cacheKey()); | activeJobs.remove(job.cacheKey()); | ||||
} | } | ||||
public List<PackerJobSummary> getActiveSummary(String accountUuid) { | |||||
synchronized (activeJobs) { | |||||
return activeJobs.values().stream() | |||||
.filter(j -> j.getCloud().getAccount().equals(accountUuid)) | |||||
.map(PackerJobSummary::new) | |||||
.collect(Collectors.toList()); | |||||
} | |||||
} | |||||
public Map<String, List<PackerImage>> getCompletedSummary(String accountUuid) { | |||||
return completedJobs.entrySet() | |||||
.stream() | |||||
.filter(entry -> entry.getKey().contains(accountUuid)) | |||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); | |||||
} | |||||
public File getPackerPublicKey () { return initPackerKey(true); } | public File getPackerPublicKey () { return initPackerKey(true); } | ||||
public File getPackerPrivateKey () { return initPackerKey(false); } | public File getPackerPrivateKey () { return initPackerKey(false); } | ||||
public String getPackerPublicKeyHash () { return sha256_file(getPackerPublicKey()); } | public String getPackerPublicKeyHash () { return sha256_file(getPackerPublicKey()); } | ||||
@@ -1 +1 @@ | |||||
bubble.version=Adventure 1.4.18 | |||||
bubble.version=Adventure 1.4.19 |
@@ -20,4 +20,6 @@ rkeys | |||||
rmembers | rmembers | ||||
rdelkeys | rdelkeys | ||||
mitm_pid | mitm_pid | ||||
reset_bubble_logs | |||||
reset_bubble_logs | |||||
pack_bubble | |||||
pack_status |