Selaa lähdekoodia

WIP: packer basics working for digitalocean

cobbzilla/introduce_packer
Jonathan Cobb 4 vuotta sitten
vanhempi
commit
a2ea0bceaf
20 muutettua tiedostoa jossa 435 lisäystä ja 56 poistoa
  1. +5
    -1
      bin/first_time_ubuntu.sh
  2. +26
    -0
      bin/install_packer.sh
  3. +23
    -5
      bubble-server/src/main/java/bubble/ApiConstants.java
  4. +14
    -6
      bubble-server/src/main/java/bubble/cloud/compute/digitalocean/DigitalOceanDriver.java
  5. +14
    -1
      bubble-server/src/main/java/bubble/cloud/compute/digitalocean/ImageParser.java
  6. +2
    -0
      bubble-server/src/main/java/bubble/cloud/compute/digitalocean/ResourceParser.java
  7. +5
    -4
      bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java
  8. +10
    -4
      bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java
  9. +3
    -0
      bubble-server/src/main/java/bubble/server/BubbleConfiguration.java
  10. +6
    -17
      bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java
  11. +29
    -0
      bubble-server/src/main/java/bubble/service/packer/PackerBuild.java
  12. +187
    -0
      bubble-server/src/main/java/bubble/service/packer/PackerJob.java
  13. +11
    -0
      bubble-server/src/main/java/bubble/service/packer/PackerManifest.java
  14. +53
    -0
      bubble-server/src/main/java/bubble/service/packer/PackerService.java
  15. +1
    -0
      bubble-server/src/main/resources/ansible/bubble_scripts.txt
  16. +26
    -10
      bubble-server/src/main/resources/models/defaults/cloudService.json
  17. +4
    -5
      bubble-server/src/main/resources/packer/packer-sage.json.hbs
  18. +14
    -1
      bubble-server/src/test/resources/models/system/cloudService.json
  19. +1
    -1
      utils/cobbzilla-utils
  20. +1
    -1
      utils/cobbzilla-wizard

+ 5
- 1
bin/first_time_ubuntu.sh Näytä tiedosto

@@ -12,9 +12,13 @@ sudo apt update -y || die "Error running apt update"
sudo apt upgrade -y || die "Error running apt upgrade"

# Install packages
sudo apt install openjdk-11-jdk maven postgresql-10 redis-server jq python3 python3-pip npm webpack curl -y || die "Error installing apt packages"
sudo apt install openjdk-11-jdk maven postgresql-10 redis-server jq python3 python3-pip npm webpack curl unzip -y || die "Error installing apt packages"
sudo pip3 install setuptools psycopg2-binary || die "Error installing pip packages"

# Install packer
BUBBLE_BIN="$(cd "$(dirname "${0}")" && pwd)"
"${BUBBLE_BIN}/install_packer.sh" || die "Error installing packer"

# Create DB user for current user, as superuser
CURRENT_USER="$(whoami)"
sudo su - postgres bash -c 'createuser -U postgres --createdb --createrole --superuser '"${CURRENT_USER}"'' || die "Error creating ${CURRENT_USER} DB user"


+ 26
- 0
bin/install_packer.sh Näytä tiedosto

@@ -0,0 +1,26 @@
#!/bin/bash

function die {
echo 1>&2 "${1}"
exit 1
}

# Install packer
if [[ ! -f ${HOME}/packer/packer ]] ; then
PACKER_VERSION=1.5.6
PACKER_FILE=packer_${PACKER_VERSION}_linux_amd64.zip
PACKER_URL=https://releases.hashicorp.com/packer/${PACKER_VERSION}/${PACKER_FILE}
mkdir -p ${HOME}/packer && cd ${HOME}/packer && wget ${PACKER_URL} && unzip ${PACKER_FILE} || die "Error installing packer"
else
echo "Packer already installed"
fi

# Install packer Vultr plugin
if [[ ! -f ${HOME}/.packer.d/plugins/packer-builder-vultr ]] ; then
PACKER_VULTR_VERSION=1.0.8
PACKER_VULTR_FILE=packer-builder-vultr_${PACKER_VULTR_VERSION}_linux_64-bit.tar.gz
PACKER_VULTR_URL=https://github.com/vultr/packer-builder-vultr/releases/download/v${PACKER_VULTR_VERSION}/${PACKER_VULTR_FILE}
mkdir -p ${HOME}/.packer.d/plugins && cd ${HOME}/.packer.d/plugins && wget ${PACKER_VULTR_URL} && tar xzf ${PACKER_VULTR_FILE} || die "Error installing packer vultr plugin"
else
echo "Packer vultr plugin already installed"
fi

+ 23
- 5
bubble-server/src/main/java/bubble/ApiConstants.java Näytä tiedosto

@@ -17,21 +17,24 @@ import org.glassfish.jersey.server.ContainerRequest;

import javax.ws.rs.core.Context;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
import static org.apache.http.HttpHeaders.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.io.StreamUtil.stream2string;
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.network.NetworkUtil.*;
import static org.cobbzilla.util.string.StringUtil.splitAndTrim;
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;

@Slf4j
@@ -44,9 +47,9 @@ public class ApiConstants {
public static final String[] ROLES_SAGE = {"common", "nginx", "bubble", "bubble_finalizer"};
public static final String[] ROLES_NODE = {"common", "nginx", "algo", "mitmproxy", "bubble", "bubble_finalizer"};

public static final String PACKER_IMAGE_TAG = "packer_bubble";
public static final String PACKER_IMAGE_TAG_SAGE = "packer_bubble_sage";
public static final String PACKER_IMAGE_TAG_NODE = "packer_bubble_node";
public static final String ANSIBLE_DIR = "ansible";
public static final List<String> BUBBLE_SCRIPTS = splitAndTrim(stream2string(ANSIBLE_DIR + "/bubble_scripts.txt"), "\n")
.stream().filter(s -> !empty(s)).collect(Collectors.toList());

private static final AtomicReference<String> bubbleDefaultDomain = new AtomicReference<>();

@@ -291,4 +294,19 @@ public class ApiConstants {
invalidEx("err."+e.getSimpleName()+".invalid", "Invalid "+e.getSimpleName()+": "+v, v));
}

public static void copyScriptsOrDie(File scriptsParentDir) {
try {
copyScripts(scriptsParentDir);
} catch (IOException e) {
die("copyScriptsOrDie: "+shortError(e), e);
}
}

public static void copyScripts(File scriptsParentDir) throws IOException {
// write scripts
final File scriptsDir = mkdirOrDie(new File(scriptsParentDir, "scripts"));
for (String script : BUBBLE_SCRIPTS) {
toFile(new File(scriptsDir, script), stream2string("scripts/"+script));
}
}
}

+ 14
- 6
bubble-server/src/main/java/bubble/cloud/compute/digitalocean/DigitalOceanDriver.java Näytä tiedosto

@@ -4,7 +4,6 @@
*/
package bubble.cloud.compute.digitalocean;

import bubble.ApiConstants;
import bubble.cloud.CloudRegion;
import bubble.cloud.compute.ComputeNodeSize;
import bubble.cloud.compute.ComputeServiceDriverBase;
@@ -40,13 +39,13 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;
@Slf4j
public class DigitalOceanDriver extends ComputeServiceDriverBase {

private static final String PARAM_API_KEY = "apiKey";
public static final String PARAM_API_KEY = "apiKey";

public static final String DO_API_BASE = "https://api.digitalocean.com/v2/";

public static final String TAG_PREFIX_CLOUD = "cloud_";
public static final String TAG_PREFIX_NODE = "node_";
public static final String PACKER_IMAGES_URI = "images?tag=" + ApiConstants.PACKER_IMAGE_TAG;
public static final String PACKER_IMAGES_URI = "images?private=true";

@Getter(lazy=true) private final Set<String> regionSlugs = getResourceSlugs("regions");
@Getter(lazy=true) private final Set<String> sizeSlugs = getResourceSlugs("sizes");
@@ -63,9 +62,16 @@ public class DigitalOceanDriver extends ComputeServiceDriverBase {
JsonNode page = found;
do {
final JsonNode items = page.has(type) ? page.get(type) : null;
if (empty(items) || !items.isArray()) return die("getResourceSlugs("+uri+"): expected "+type+" property to contain a (non-empty) array");
if (empty(items)) {
if (!parser.allowEmpty()) return die("getResources("+uri+"): expected "+type+" property to contain a (non-empty) array");
return null;
}
if (!items.isArray()) return die("getResources("+uri+"): expected "+type+" property to contain a (non-empty) array");

for (int i=0; i<items.size(); i++) results.add(parser.parse(items.get(i)));
for (int i=0; i<items.size(); i++) {
final E item = parser.parse(items.get(i));
if (item != null) results.add(item);
}

final String next = getNext(page);
page = next == null ? null : doGet(next, JsonNode.class, false);
@@ -171,6 +177,7 @@ public class DigitalOceanDriver extends ComputeServiceDriverBase {

final CloudRegion region = config.getRegion(node.getRegion());
final ComputeNodeSize size = config.getSize(node.getSize());
// todo: lookup image based on node installType
final String os = config.getConfig("os");

if (!getRegionSlugs().contains(region.getInternalName())) {
@@ -260,7 +267,8 @@ public class DigitalOceanDriver extends ComputeServiceDriverBase {
}

@Override public List<PackerImage> getPackerImages() {
return getResources(PACKER_IMAGES_URI, new ImageParser());
final List<PackerImage> images = getResources(PACKER_IMAGES_URI, new ImageParser());
return images == null ? Collections.emptyList() : images;
}

@Override public List<PackerImage> writePackerImages() {


+ 14
- 1
bubble-server/src/main/java/bubble/cloud/compute/digitalocean/ImageParser.java Näytä tiedosto

@@ -7,14 +7,27 @@ import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.List;

import static bubble.service.packer.PackerJob.PACKER_IMAGE_PREFIX;

public class ImageParser implements ResourceParser<PackerImage, List<PackerImage>> {

@Override public List<PackerImage> newResults() { return new ArrayList<>(); }

@Override public boolean allowEmpty() { return true; }

@Override public PackerImage parse(JsonNode item) {

final PackerImage image = new PackerImage();

final String name;
if (item.has("name")) {
name = item.get("name").textValue();
if (!name.startsWith(PACKER_IMAGE_PREFIX)) return null;
image.setName(name);
}

if (item.has("id")) image.setId(item.get("id").textValue());
if (item.has("name")) image.setName(item.get("name").textValue());
if (item.has("regions")) {
final JsonNode regionsNode = item.get("regions");
if (regionsNode.isArray()) {


+ 2
- 0
bubble-server/src/main/java/bubble/cloud/compute/digitalocean/ResourceParser.java Näytä tiedosto

@@ -10,4 +10,6 @@ public interface ResourceParser<E, C extends Collection<E>> {

E parse(JsonNode item);

default boolean allowEmpty () { return false; }

}

+ 5
- 4
bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java Näytä tiedosto

@@ -39,6 +39,7 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.notFoundEx;
@Slf4j
public class VultrDriver extends ComputeServiceDriverBase {

public static final String PARAM_API_KEY = "apiKey";
public static final String API_KEY_HEADER = "API-Key";

public static final String VULTR_API_BASE = "https://api.vultr.com/v1/";
@@ -84,9 +85,9 @@ public class VultrDriver extends ComputeServiceDriverBase {
}

@Override public void postSetup() {
if (credentials != null && credentials.hasParam(API_KEY_HEADER) && credentials.getParam(API_KEY_HEADER).contains("{{")) {
final String apiKey = configuration.applyHandlebars(credentials.getParam(API_KEY_HEADER));
credentials.setParam(API_KEY_HEADER, apiKey);
if (credentials != null && credentials.hasParam(PARAM_API_KEY) && credentials.getParam(PARAM_API_KEY).contains("{{")) {
final String apiKey = configuration.applyHandlebars(credentials.getParam(PARAM_API_KEY));
credentials.setParam(PARAM_API_KEY, apiKey);
}
super.postSetup();
}
@@ -187,7 +188,7 @@ public class VultrDriver extends ComputeServiceDriverBase {
}

private HttpRequestBean auth(HttpRequestBean req) {
return req.setHeader(API_KEY_HEADER, credentials.getParam(API_KEY_HEADER));
return req.setHeader(API_KEY_HEADER, credentials.getParam(PARAM_API_KEY));
}

@Override public BubbleNode stop(BubbleNode node) throws Exception {


+ 10
- 4
bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java Näytä tiedosto

@@ -1,9 +1,11 @@
package bubble.resources.cloud;

import bubble.cloud.compute.ComputeServiceDriver;
import bubble.cloud.compute.PackerImage;
import bubble.model.account.Account;
import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
import bubble.service.packer.PackerService;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;
import org.springframework.beans.factory.annotation.Autowired;
@@ -14,6 +16,8 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.cobbzilla.wizard.resources.ResourceUtil.ok;
@@ -23,6 +27,7 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.ok;
public class ComputePackerResource {

@Autowired private BubbleConfiguration configuration;
@Autowired private PackerService packer;

private Account account;
private CloudService cloud;
@@ -40,10 +45,11 @@ public class ComputePackerResource {
}

@PUT
public Response ensureImagesExist(@Context Request req,
@Context ContainerRequest ctx) {
final ComputeServiceDriver driver = cloud.getComputeDriver(configuration);
return ok(driver.writePackerImages());
public Response writeImages(@Context Request req,
@Context ContainerRequest ctx) {
final List<PackerImage> images = packer.writePackerImages(account, cloud);
if (images != null) return ok(images);
return ok(Collections.emptyList());
}

}

+ 3
- 0
bubble-server/src/main/java/bubble/server/BubbleConfiguration.java Näytä tiedosto

@@ -64,6 +64,7 @@ import static org.cobbzilla.util.handlebars.HandlebarsUtil.registerUtilityHelper
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.StreamUtil.loadResourceAsStream;
import static org.cobbzilla.util.reflect.ReflectionUtil.copy;
import static org.cobbzilla.util.security.ShaUtil.sha256_file;

@Configuration @NoArgsConstructor @Slf4j
public class BubbleConfiguration extends PgRestServerConfiguration
@@ -160,6 +161,8 @@ public class BubbleConfiguration extends PgRestServerConfiguration
return bubbleJar;
}

@Getter(lazy=true) private final String jarSha = sha256_file(getBubbleJar());

private static final AtomicReference<String> _DEFAULT_LOCALE = new AtomicReference<>();
public static String getDEFAULT_LOCALE() {
final String locale = _DEFAULT_LOCALE.get();


+ 6
- 17
bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java Näytä tiedosto

@@ -4,6 +4,7 @@
*/
package bubble.service.cloud;

import bubble.ApiConstants;
import bubble.cloud.CloudAndRegion;
import bubble.cloud.compute.ComputeServiceDriver;
import bubble.cloud.dns.DnsServiceDriver;
@@ -57,7 +58,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static bubble.ApiConstants.getRemoteHost;
import static bubble.ApiConstants.newNodeHostname;
@@ -77,7 +77,6 @@ import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.io.StreamUtil.stream2string;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.reflect.ReflectionUtil.closeQuietly;
import static org.cobbzilla.util.string.StringUtil.splitAndTrim;
import static org.cobbzilla.util.system.CommandShell.chmod;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;
@@ -86,16 +85,11 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.notFoundEx;
@Service @Slf4j
public class StandardNetworkService implements NetworkService {

public static final String ANSIBLE_DIR = "ansible";

public static final String PLAYBOOK_YML = "playbook.yml";
public static final String PLAYBOOK_TEMPLATE = stream2string(ANSIBLE_DIR + "/" + PLAYBOOK_YML + ".hbs");
public static final String PLAYBOOK_TEMPLATE = stream2string(ApiConstants.ANSIBLE_DIR + "/" + PLAYBOOK_YML + ".hbs");

public static final String INSTALL_LOCAL_SH = "install_local.sh";
public static final String INSTALL_LOCAL_TEMPLATE = stream2string(ANSIBLE_DIR + "/" + INSTALL_LOCAL_SH + ".hbs");

public static final List<String> BUBBLE_SCRIPTS = splitAndTrim(stream2string(ANSIBLE_DIR + "/bubble_scripts.txt"), "\n")
.stream().filter(s -> !empty(s)).collect(Collectors.toList());
public static final String INSTALL_LOCAL_TEMPLATE = stream2string(ApiConstants.ANSIBLE_DIR + "/" + INSTALL_LOCAL_SH + ".hbs");

public static final int MAX_ANSIBLE_TRIES = 1;
public static final int RESTORE_KEY_LEN = 6;
@@ -288,12 +282,7 @@ public class StandardNetworkService implements NetworkService {
// add a newline after so keys appended later will be OK
toFile(sshPubKeyFile, "\n"+sshKey.getSshPublicKey()+"\n");
}

// write scripts
final File scriptsDir = mkdirOrDie(new File(bubbleFilesDir, "scripts"));
for (String script : BUBBLE_SCRIPTS) {
toFile(new File(scriptsDir, script), stream2string("scripts/"+script));
}
ApiConstants.copyScripts(bubbleFilesDir);

// write self_node.json file
writeFile(bubbleFilesDir, null, SELF_NODE_JSON, json(node
@@ -428,11 +417,11 @@ public class StandardNetworkService implements NetworkService {

// rsync ansible dir to remote host
"echo '" + METER_TICK_COPYING_ANSIBLE + "' && " +
"rsync -az -e \"ssh " + sshArgs + "\" . "+sshTarget+ ":" + ANSIBLE_DIR + " && " +
"rsync -az -e \"ssh " + sshArgs + "\" . "+sshTarget+ ":" + ApiConstants.ANSIBLE_DIR + " && " +

// run install_local.sh on remote host, installs ansible locally
"echo '" + METER_TICK_RUNNING_ANSIBLE + "' && " +
"ssh "+sshArgs+" "+sshTarget+" ~"+nodeUser+ "/" + ANSIBLE_DIR + "/" + INSTALL_LOCAL_SH;
"ssh "+sshArgs+" "+sshTarget+" ~"+nodeUser+ "/" + ApiConstants.ANSIBLE_DIR + "/" + INSTALL_LOCAL_SH;
}

private File writeFile(File dir, Map<String, Object> ctx, String filename, String templateOrData) throws IOException {


+ 29
- 0
bubble-server/src/main/java/bubble/service/packer/PackerBuild.java Näytä tiedosto

@@ -0,0 +1,29 @@
package bubble.service.packer;

import bubble.cloud.CloudRegion;
import bubble.cloud.compute.PackerImage;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.Getter;
import lombok.Setter;

public class PackerBuild {

@Getter @Setter private String name;
@Getter @Setter private String builder_type;
@Getter @Setter private Long build_time;
@Getter @Setter private ArrayNode files;
@Getter @Setter private String artifact_id;
@Getter @Setter private String packer_run_uuid;
@Getter @Setter private JsonNode custom_data;

public PackerImage toPackerImage(String name) {
final String[] parts = artifact_id.split(":");
final String regionName = parts[0];
final String id = parts[1];
return new PackerImage()
.setId(id)
.setName(name)
.setRegions(new CloudRegion[]{new CloudRegion().setInternalName(regionName)});
}
}

+ 187
- 0
bubble-server/src/main/java/bubble/service/packer/PackerJob.java Näytä tiedosto

@@ -0,0 +1,187 @@
package bubble.service.packer;

import bubble.cloud.CloudRegion;
import bubble.cloud.CloudRegionRelative;
import bubble.cloud.compute.ComputeConfig;
import bubble.cloud.compute.ComputeServiceDriver;
import bubble.cloud.compute.PackerConfig;
import bubble.cloud.compute.PackerImage;
import bubble.cloud.geoLocation.GeoLocation;
import bubble.model.account.Account;
import bubble.model.cloud.AnsibleInstallType;
import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
import bubble.service.cloud.GeoService;
import lombok.Cleanup;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.CommandLine;
import org.cobbzilla.util.collection.NameAndValue;
import org.cobbzilla.util.collection.SingletonList;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.cobbzilla.util.io.FileUtil;
import org.cobbzilla.util.io.TempDir;
import org.cobbzilla.util.system.Command;
import org.cobbzilla.util.system.CommandResult;
import org.cobbzilla.util.system.CommandShell;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.File;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import static bubble.ApiConstants.copyScripts;
import static bubble.model.cloud.RegionalServiceDriver.findClosestRegions;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.FileUtil.toFileOrDie;
import static org.cobbzilla.util.io.StreamUtil.copyClasspathDirectory;
import static org.cobbzilla.util.io.StreamUtil.stream2string;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.network.NetworkUtil.getExternalIp;

@Slf4j
public class PackerJob implements Callable<List<PackerImage>> {

public static final AnsibleInstallType[] PACKER_TYPES = {AnsibleInstallType.sage, AnsibleInstallType.node};
public static final String INSTALL_TYPE_VAR = "@@TYPE@@";
public static final String BUBBLE_VERSION_VAR = "@@BUBBLE_VERSION@@";
public static final String JAR_SHA_VAR = "@@JAR_SHA256@@";
public static final String PACKER_TEMPLATE = "packer/packer-"+INSTALL_TYPE_VAR+".json.hbs";
public static final String PACKER_IMAGE_NAME_VAR = "packerImageName";
public static final String PACKER_IMAGE_PREFIX = "packer_bubble_";
public static final String PACKER_IMAGE_NAME_TEMPLATE = PACKER_IMAGE_PREFIX+INSTALL_TYPE_VAR+"_"+BUBBLE_VERSION_VAR+"_"+JAR_SHA_VAR;
public static final String VARIABLES_VAR = "packerVariables";
public static final String BUILD_REGION_VAR = "buildRegion";
public static final String IMAGE_REGIONS_VAR = "imageRegions";
public static final String BUILDER_VAR = "builder";

@Autowired private BubbleConfiguration configuration;
@Autowired private GeoService geoService;
@Autowired private PackerService packerService;

@Getter private Account account;
@Getter private CloudService cloud;
@Getter private List<PackerImage> images = new ArrayList<>();
private CloudRegionRelative buildRegion;

public PackerJob(Account account, CloudService cloud) {
this.account = account;
this.cloud = cloud;
}

@Override public List<PackerImage> call() throws Exception {
try {
final List<PackerImage> images = _call();
packerService.recordJobCompleted(this);
return images;

} catch (Exception e) {
packerService.recordJobError(this, e);
throw e;
}
}

public List<PackerImage> _call() throws Exception {
final String jarSha = configuration.getJarSha();

final ComputeConfig computeConfig = json(cloud.getDriverConfigJson(), ComputeConfig.class);
final ComputeServiceDriver computeDriver = cloud.getComputeDriver(configuration);
final PackerConfig packerConfig = computeConfig.getPacker();

// create handlebars context
final Map<String, Object> ctx = new HashMap<>();
ctx.put("credentials", NameAndValue.toMap(cloud.getCredentials().getParams()));
ctx.put("compute", computeDriver);

// determine lat/lon to find closest cloud region to perform build in
final GeoLocation here = geoService.locate(account.getUuid(), getExternalIp());
final List<CloudRegionRelative> closestRegions = findClosestRegions(new SingletonList<>(cloud), null, here.getLatitude(), here.getLongitude());
if (empty(closestRegions)) return die("writePackerImages: no closest region could be determined");
buildRegion = closestRegions.get(0);
ctx.put(BUILD_REGION_VAR, buildRegion);

// create list of all regions, without leading/trailing double-quote, which should already be in the template
ctx.put(IMAGE_REGIONS_VAR, toInnerStringList(computeDriver.getRegions().stream()
.map(CloudRegion::getInternalName)
.collect(Collectors.toList())));

// set environment variables
final Map<String, String> env = new HashMap<>();
for (NameAndValue variable : packerConfig.getVars()) {
env.put(variable.getName(), HandlebarsUtil.apply(configuration.getHandlebars(), variable.getValue(), ctx, '[', ']'));
}
ctx.put(VARIABLES_VAR, packerConfig.getVars());

// copy ansible and other packer files to temp dir
@Cleanup final TempDir tempDir = copyClasspathDirectory("packer");

// copy bubble jar and scripts to role dir, calculate shasum for packer image name
final File jar = configuration.getBubbleJar();
final File bubbleFilesDir = new File(abs(tempDir)+"/roles/bubble/files");
FileUtil.copyFile(jar, new File(abs(bubbleFilesDir)+"/bubble.jar"));
copyScripts(bubbleFilesDir);

// write packer config file
for (AnsibleInstallType installType : PACKER_TYPES) {
final String imageName = PACKER_IMAGE_NAME_TEMPLATE
.replace(INSTALL_TYPE_VAR, installType.name())
.replace(BUBBLE_VERSION_VAR, configuration.getVersion())
.replace(JAR_SHA_VAR, jarSha);
ctx.put(PACKER_IMAGE_NAME_VAR, imageName);

final String packerTemplatePath = PACKER_TEMPLATE.replace(INSTALL_TYPE_VAR, installType.name());
final String packerConfigTemplate = stream2string(packerTemplatePath);
ctx.put(BUILDER_VAR, generateBuilder(packerConfig, ctx));

// write packer file
final String packerJson = HandlebarsUtil.apply(configuration.getHandlebars(), packerConfigTemplate, ctx, '[', ']');
toFileOrDie(abs(tempDir)+"/packer.json", packerJson);

// run packer, return handle to running packer
log.info("running packer for "+installType+"...");
final CommandResult commandResult = CommandShell.exec(new Command(new CommandLine("packer")
.addArgument("build").addArgument("packer.json"))
.setDir(tempDir)
.setEnv(env)
.setCopyToStandard(true));

if (!commandResult.isZeroExitStatus()) {
return die("Error executing packer: exit status "+commandResult.getExitStatus());
}

// read manifest, populate images
final File packerManifestFile = new File(tempDir, "manifest.json");
if (!packerManifestFile.exists()) {
return die("Error executing packer: manifest file not found: "+abs(packerManifestFile));
}
final PackerManifest packerManifest = json(FileUtil.toString(packerManifestFile), PackerManifest.class);
final PackerBuild[] builds = packerManifest.getBuilds();
if (empty(builds)) {
return die("Error executing packer: no builds found");
}
images.addAll(Arrays.stream(builds).map(b -> b.toPackerImage(imageName)).collect(Collectors.toList()));
}

return images;
}

private String toInnerStringList(List<String> list) {
if (empty(list)) return die("toInnerStringList: empty list");
final StringBuilder b = new StringBuilder();
for (String val : list) {
if (b.length() > 0) b.append("\", \"");
b.append(val);
}
return b.toString();
}

public String generateBuilder(PackerConfig packerConfig, Map<String, Object> ctx) {
return HandlebarsUtil.apply(configuration.getHandlebars(), json(packerConfig.getBuilder()), ctx, '<', '>')
.replace("[[", "{{")
.replace("]]", "}}");
}

}

+ 11
- 0
bubble-server/src/main/java/bubble/service/packer/PackerManifest.java Näytä tiedosto

@@ -0,0 +1,11 @@
package bubble.service.packer;

import lombok.Getter;
import lombok.Setter;

public class PackerManifest {

@Getter @Setter private PackerBuild[] builds;
@Getter @Setter private String last_run_uuid;

}

+ 53
- 0
bubble-server/src/main/java/bubble/service/packer/PackerService.java Näytä tiedosto

@@ -0,0 +1,53 @@
package bubble.service.packer;

import bubble.cloud.compute.PackerImage;
import bubble.model.account.Account;
import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.daemon.DaemonThreadFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import static org.cobbzilla.util.daemon.ZillaRuntime.shortError;

@Service @Slf4j
public class PackerService {

private final Map<String, Future<List<PackerImage>>> activeJobs = new ConcurrentHashMap<>(16);
private final Map<String, List<PackerImage>> completedJobs = new ConcurrentHashMap<>(16);
private final ExecutorService pool = DaemonThreadFactory.fixedPool(5);

@Autowired private BubbleConfiguration configuration;

public List<PackerImage> writePackerImages(Account account, CloudService cloud) {
synchronized (activeJobs) {
final List<PackerImage> images = completedJobs.get(cloud.getUuid());
if (images != null) return images;
activeJobs.computeIfAbsent(cloud.getUuid(), k -> {
final PackerJob packerJob = configuration.autowire(new PackerJob(account, cloud));
return pool.submit(packerJob);
});
return null;
}
}

public void recordJobCompleted(PackerJob job) {
synchronized (activeJobs) {
activeJobs.remove(job.getCloud().getUuid());
completedJobs.put(job.getCloud().getUuid(), job.getImages());
}
}

public void recordJobError(PackerJob job, Exception e) {
log.error("recordJobError: "+shortError(e), e);
activeJobs.remove(job.getCloud().getUuid());
}

}

+ 1
- 0
bubble-server/src/main/resources/ansible/bubble_scripts.txt Näytä tiedosto

@@ -15,3 +15,4 @@ bencrypt
bdecrypt
list_bubble_databases
cleanup_bubble_databases
install_packer.sh

+ 26
- 10
bubble-server/src/main/resources/models/defaults/cloudService.json Näytä tiedosto

@@ -199,11 +199,26 @@
{"name": "medium", "type": "medium", "internalName": "2048 MB RAM,55 GB SSD,2.00 TB BW", "vcpu": 1, "memoryMB": 2048, "ssdGB": 55},
{"name": "large", "type": "large", "internalName": "4096 MB RAM,80 GB SSD,3.00 TB BW", "vcpu": 2, "memoryMB": 4096, "ssdGB": 80}
],
"config": [{"name": "os", "value": "Ubuntu 18.04 x64"}]
"config": [{"name": "os", "value": "Ubuntu 18.04 x64"}],
"packer": {
"vars": [{"name": "VULTR_API_KEY", "value": "[[credentials.apiKey]]"}],
"iterateRegions": true,
"builder": {
"type": "vultr",
"ssh_username": "root",
"api_key": "[[user `VULTR_API_KEY`]]",
"os_id": "<<os.id>>",
"region_id": "<<region.id>>",
"plan_id": "<<sizes.small.id>>",
"instance_label": "<<packerImageName>>",
"snapshot_description": "<<packerImageName>>",
"tag": "<<packerImageName>>"
}
}
},
"credentials": {
"params": [
{"name": "API-Key", "value": "{{VULTR_API_KEY}}"}
{"name": "apiKey", "value": "{{VULTR_API_KEY}}"}
]
},
"template": true
@@ -256,20 +271,21 @@
{"name": "medium", "type": "medium", "internalName": "s-1vcpu-2gb", "vcpu": 1, "memoryMB": 2048, "ssdGB": 50},
{"name": "large", "type": "large", "internalName": "s-2vcpu-4gb", "vcpu": 2, "memoryMB": 4096, "ssdGB": 80}
],
"config": [{"name": "os", "value": "ubuntu-18-04-x64"}],
"packer": {
"vars": [{"name": "DIGITALOCEAN_API_KEY", "value": "credentials.apiKey"}],
"vars": [{"name": "DIGITALOCEAN_API_KEY", "value": "[[credentials.apiKey]]"}],
"builder": {
"type": "digitalocean",
"ssh_username": "root",
"api_token": "<<user `DIGITALOCEAN_API_KEY`>>",
"image": "ubuntu-18-04-x64",
"region": "[[region]]",
"size": "s-1vcpu-1gb",
"api_token": "[[user `DIGITALOCEAN_API_KEY`]]",
"image": "<<os.name>>",
"region": "<<buildRegion.internalName>>",
"size": "<<sizes.small.name>>",
"ipv6": true,
"tags": ["packer-bubble"]
"snapshot_name": "<<packerImageName>>",
"snapshot_regions": ["<<<imageRegions>>>"]
}
},
"config": [{"name": "os", "value": "ubuntu-18-04-x64"}]
}
},
"credentials": {
"params": [


+ 4
- 5
bubble-server/src/main/resources/packer/packer-sage.json.hbs Näytä tiedosto

@@ -1,12 +1,11 @@
{
"variables": {
[[#each packer.vars]]"[[name]]": "{{env `[[name]]`}}"[[#unless @last]],
[[#each packerVariables]]"[[name]]": "{{env `[[name]]`}}"[[#unless @last]],
[[/unless]][[/each]]
},
"builders": [
[[#each packer.builders]][[json this]][[#unless @last]],
[[/unless]][[/each]]
],
[[[builder]]]
],
"provisioners": [
{
"type": "shell",
@@ -21,7 +20,7 @@
{
"type": "ansible-local",
"playbook_file": "packer-sage-playbook.yml",
"role_paths": ["."],
"role_paths": ["roles/common", "roles/firewall", "roles/nginx", "roles/bubble", "roles/bubble_finalizer"],
"inventory_file": "hosts"
}
],


+ 14
- 1
bubble-server/src/test/resources/models/system/cloudService.json Näytä tiedosto

@@ -223,7 +223,20 @@
{"name": "medium", "type": "medium", "internalName": "s-1vcpu-2gb", "vcpu": 1, "memoryMB": 2048, "ssdGB": 50},
{"name": "large", "type": "large", "internalName": "s-2vcpu-4gb", "vcpu": 2, "memoryMB": 4096, "ssdGB": 80}
],
"config": [{"name": "os", "value": "ubuntu-18-04-x64"}]
"packer": {
"vars": [{"name": "DIGITALOCEAN_API_KEY", "value": "[[credentials.apiKey]]"}],
"builder": {
"type": "digitalocean",
"ssh_username": "root",
"api_token": "[[user `DIGITALOCEAN_API_KEY`]]",
"image": "ubuntu-18-04-x64",
"region": "<<buildRegion.internalName>>",
"size": "s-1vcpu-1gb",
"ipv6": true,
"snapshot_name": "<<packerImageName>>",
"snapshot_regions": ["<<<imageRegions>>>"]
}
}
},
"credentials": {
"params": [


+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit 5e49b6e3e4281c59abd127279e90299f633db53f
Subproject commit b1273943835c8002c7aae24b880f2038fa71e73c

+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit 2a086ae6d7887b42b647db33e241cfada5574a2e
Subproject commit a07578cf1fde1cdaee0abc626fa4761fe8421446

Ladataan…
Peruuta
Tallenna