@@ -8,18 +8,18 @@ import static bubble.service.packer.PackerJob.PACKER_IMAGE_PREFIX; | |||||
public abstract class PackerImageParserBase extends ListResourceParser<PackerImage> { | public abstract class PackerImageParserBase extends ListResourceParser<PackerImage> { | ||||
private String bubbleVersion; | |||||
private String keyHash; | |||||
private final String bubbleVersion; | |||||
private final String versionHash; | |||||
public PackerImageParserBase(String bubbleVersion, String keyHash) { | |||||
public PackerImageParserBase(String bubbleVersion, String versionHash) { | |||||
this.bubbleVersion = bubbleVersion; | this.bubbleVersion = bubbleVersion; | ||||
this.keyHash = keyHash; | |||||
this.versionHash = versionHash; | |||||
} | } | ||||
public boolean isValidPackerImage(String name) { | public boolean isValidPackerImage(String name) { | ||||
if (!name.startsWith(PACKER_IMAGE_PREFIX)) return false; | if (!name.startsWith(PACKER_IMAGE_PREFIX)) return false; | ||||
if (!name.contains("_"+bubbleVersion+"_")) return false; | if (!name.contains("_"+bubbleVersion+"_")) return false; | ||||
if (!name.contains("_"+keyHash+"_")) return false; | |||||
if (!name.contains("_"+versionHash+"_")) return false; | |||||
return true; | return true; | ||||
} | } | ||||
@@ -235,7 +235,7 @@ public class DigitalOceanDriver extends ComputeServiceDriverBase { | |||||
@Override public List<PackerImage> getPackerImagesForRegion(String region) { return getPackerImages(); } | @Override public List<PackerImage> getPackerImagesForRegion(String region) { return getPackerImages(); } | ||||
public List<PackerImage> getPackerImages () { | public List<PackerImage> getPackerImages () { | ||||
final List<PackerImage> images = getResources(PACKER_IMAGES_URI, new DigitalOceanPackerImageParser(configuration.getShortVersion(), packerService.getPackerPublicKeyHash())); | |||||
final List<PackerImage> images = getResources(PACKER_IMAGES_URI, new DigitalOceanPackerImageParser(configuration.getShortVersion(), packerService.getPackerVersionHash())); | |||||
return images == null ? Collections.emptyList() : images; | return images == null ? Collections.emptyList() : images; | ||||
} | } | ||||
@@ -183,7 +183,7 @@ public class AmazonEC2Driver extends ComputeServiceDriverBase { | |||||
final ArrayList<Filter> filters = new ArrayList<>(); | final ArrayList<Filter> filters = new ArrayList<>(); | ||||
filters.add(new Filter("root-device-type", new SingletonList<>("ebs"))); | filters.add(new Filter("root-device-type", new SingletonList<>("ebs"))); | ||||
filters.add(new Filter("state", new SingletonList<>("available"))); | filters.add(new Filter("state", new SingletonList<>("available"))); | ||||
filters.add(new Filter("name", new SingletonList<>("packer_*_"+packerService.getPackerPublicKeyHash()+"_"+configuration.getShortVersion()+"_*"))); | |||||
filters.add(new Filter("name", new SingletonList<>("packer_*_"+packerService.getPackerVersionHash()+"_"+configuration.getShortVersion()+"_*"))); | |||||
final AmazonEC2 ec2 = getEc2Client(region); | final AmazonEC2 ec2 = getEc2Client(region); | ||||
final DescribeImagesRequest imageRequest = new DescribeImagesRequest().withFilters(filters); | final DescribeImagesRequest imageRequest = new DescribeImagesRequest().withFilters(filters); | ||||
final DescribeImagesResult imagesResult = ec2.describeImages(imageRequest); | final DescribeImagesResult imagesResult = ec2.describeImages(imageRequest); | ||||
@@ -410,7 +410,7 @@ public class VultrDriver extends ComputeServiceDriverBase { | |||||
@Override public List<PackerImage> getPackerImagesForRegion(String region) { return getPackerImages(); } | @Override public List<PackerImage> getPackerImagesForRegion(String region) { return getPackerImages(); } | ||||
public List<PackerImage> getPackerImages () { | public List<PackerImage> getPackerImages () { | ||||
final List<PackerImage> images = loadCloudResources(SNAPSHOT_URL, new VultrPackerImageParser(configuration.getShortVersion(), packerService.getPackerPublicKeyHash())); | |||||
final List<PackerImage> images = loadCloudResources(SNAPSHOT_URL, new VultrPackerImageParser(configuration.getShortVersion(), packerService.getPackerVersionHash())); | |||||
return images == null ? Collections.emptyList() : images; | return images == null ? Collections.emptyList() : images; | ||||
} | } | ||||
@@ -424,16 +424,16 @@ public class VultrDriver extends ComputeServiceDriverBase { | |||||
} | } | ||||
// wait longer for the snapshot... | // wait longer for the snapshot... | ||||
final String keyHash = packerService.getPackerPublicKeyHash(); | |||||
final String versionHash = packerService.getPackerVersionHash(); | |||||
final long start = now(); | final long start = now(); | ||||
PackerImage snapshot = null; | PackerImage snapshot = null; | ||||
while (now() - start < SNAPSHOT_TIMEOUT) { | while (now() - start < SNAPSHOT_TIMEOUT) { | ||||
snapshot = getPackerImages().stream() | snapshot = getPackerImages().stream() | ||||
.filter(i -> i.getName().contains("_"+installType.name()+"_") && i.getName().contains(keyHash)) | |||||
.filter(i -> i.getName().contains("_"+installType.name()+"_") && i.getName().contains(versionHash)) | |||||
.findFirst() | .findFirst() | ||||
.orElse(null); | .orElse(null); | ||||
if (snapshot != null) break; | if (snapshot != null) break; | ||||
sleep(SECONDS.toMillis(20), "finalizeIncompletePackerRun: waiting for snapshot: "+keyHash); | |||||
sleep(SECONDS.toMillis(20), "finalizeIncompletePackerRun: waiting for snapshot: "+versionHash); | |||||
} | } | ||||
if (snapshot == null) { | if (snapshot == null) { | ||||
log.error("finalizeIncompletePackerRun: timeout waiting for snapshot"); | log.error("finalizeIncompletePackerRun: timeout waiting for snapshot"); | ||||
@@ -447,14 +447,14 @@ public class VultrDriver extends ComputeServiceDriverBase { | |||||
public boolean stopImageServer(AnsibleInstallType installType) { | public boolean stopImageServer(AnsibleInstallType installType) { | ||||
final String keyHash = packerService.getPackerPublicKeyHash(); | |||||
final String versionHash = packerService.getPackerVersionHash(); | |||||
final List<BubbleNode> servers; | final List<BubbleNode> servers; | ||||
// find the server(s) | // find the server(s) | ||||
try { | try { | ||||
servers = listNodes(server -> { | servers = listNodes(server -> { | ||||
final String tag = server.has(VULTR_TAG) ? server.get(VULTR_TAG).textValue() : null; | final String tag = server.has(VULTR_TAG) ? server.get(VULTR_TAG).textValue() : null; | ||||
return tag != null && tag.contains("_"+installType.name()+"_") && tag.contains(keyHash); | |||||
return tag != null && tag.contains("_"+installType.name()+"_") && tag.contains(versionHash); | |||||
}); | }); | ||||
} catch (IOException e) { | } catch (IOException e) { | ||||
log.error("stopImageServer: error listing servers: "+shortError(e), e); | log.error("stopImageServer: error listing servers: "+shortError(e), e); | ||||
@@ -33,6 +33,7 @@ import org.cobbzilla.util.time.TimeUtil; | |||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
import java.io.File; | import java.io.File; | ||||
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.AtomicReference; | import java.util.concurrent.atomic.AtomicReference; | ||||
@@ -62,12 +63,12 @@ public class PackerJob implements Callable<List<PackerImage>> { | |||||
public static final String INSTALL_TYPE_VAR = "@@TYPE@@"; | public static final String INSTALL_TYPE_VAR = "@@TYPE@@"; | ||||
public static final String SAGE_NET_VAR = "@@SAGE_NET@@"; | public static final String SAGE_NET_VAR = "@@SAGE_NET@@"; | ||||
public static final String PACKER_KEY_VAR = "@@PACKER_KEY_HASH@@"; | |||||
public static final String PACKER_VERSION_HASH_VAR = "@@PACKER_VERSION_HASH@@"; | |||||
public static final String BUBBLE_VERSION_VAR = "@@BUBBLE_VERSION@@"; | public static final String BUBBLE_VERSION_VAR = "@@BUBBLE_VERSION@@"; | ||||
public static final String TIMESTAMP_VAR = "@@TIMESTAMP@@"; | public static final String TIMESTAMP_VAR = "@@TIMESTAMP@@"; | ||||
public static final String PACKER_IMAGE_NAME_TEMPLATE = PACKER_IMAGE_PREFIX + INSTALL_TYPE_VAR | public static final String PACKER_IMAGE_NAME_TEMPLATE = PACKER_IMAGE_PREFIX + INSTALL_TYPE_VAR | ||||
+ "_" + SAGE_NET_VAR | + "_" + SAGE_NET_VAR | ||||
+ "_" + PACKER_KEY_VAR | |||||
+ "_" + PACKER_VERSION_HASH_VAR | |||||
+ "_" + BUBBLE_VERSION_VAR | + "_" + BUBBLE_VERSION_VAR | ||||
+ "_" + TIMESTAMP_VAR; | + "_" + TIMESTAMP_VAR; | ||||
@@ -158,18 +159,16 @@ public class PackerJob implements Callable<List<PackerImage>> { | |||||
// copy ansible and other packer files to temp dir | // copy ansible and other packer files to temp dir | ||||
@Cleanup final TempDir tempDir = copyClasspathDirectory("packer"); | @Cleanup final TempDir tempDir = copyClasspathDirectory("packer"); | ||||
final String releaseUrlBase = configuration.getReleaseUrlBase(); | |||||
// create var for algo_sha256 | |||||
final String algoVarsDir = abs(tempDir) + "/roles/algo/vars"; | |||||
mkdirOrDie(algoVarsDir); | |||||
final String algoHash = url2string(releaseUrlBase+"/algo/latest/algo.zip.sha256"); | |||||
FileUtil.toFileOrDie(new File(algoVarsDir, "main.yml"), "algo_sha256 : '"+algoHash+"'"); | |||||
// create var for mitmproxy_sha256 | |||||
final String mitmproxyVarsDir = abs(tempDir) + "/roles/mitmproxy/vars"; | |||||
mkdirOrDie(mitmproxyVarsDir); | |||||
final String mitmproxyHash = url2string(releaseUrlBase+"/mitmproxy/latest/mitmproxy.zip.sha256"); | |||||
FileUtil.toFileOrDie(new File(mitmproxyVarsDir, "main.yml"), "mitmproxy_sha256 : '"+mitmproxyHash+"'"); | |||||
// for nodes, record versions of algo, mitmproxy and dnscrypt_proxy | |||||
if (installType == AnsibleInstallType.node) { | |||||
// ensure we use the latest algo and mitmproxy versions | |||||
final Map<String, String> versions = new HashMap<>(); | |||||
versions.putAll(useLatestVersion(ROLE_ALGO, tempDir)); | |||||
versions.putAll(useLatestVersion(ROLE_MITMPROXY, tempDir)); | |||||
// write versions to bubble vars | |||||
writeBubbleVersions(tempDir, versions); | |||||
} | |||||
// copy packer ssh key | // copy packer ssh key | ||||
copyFile(packerService.getPackerPublicKey(), new File(abs(tempDir)+"/roles/common/files/"+PACKER_KEY_NAME)); | copyFile(packerService.getPackerPublicKey(), new File(abs(tempDir)+"/roles/common/files/"+PACKER_KEY_NAME)); | ||||
@@ -226,7 +225,7 @@ public class PackerJob implements Callable<List<PackerImage>> { | |||||
final String imageName = PACKER_IMAGE_NAME_TEMPLATE | final String imageName = PACKER_IMAGE_NAME_TEMPLATE | ||||
.replace(INSTALL_TYPE_VAR, installType.name()) | .replace(INSTALL_TYPE_VAR, installType.name()) | ||||
.replace(SAGE_NET_VAR, truncate(domainname(), 19)) | .replace(SAGE_NET_VAR, truncate(domainname(), 19)) | ||||
.replace(PACKER_KEY_VAR, packerService.getPackerPublicKeyHash()) | |||||
.replace(PACKER_VERSION_HASH_VAR, packerService.getPackerVersionHash()) | |||||
.replace(BUBBLE_VERSION_VAR, configuration.getShortVersion()) | .replace(BUBBLE_VERSION_VAR, configuration.getShortVersion()) | ||||
.replace(TIMESTAMP_VAR, TimeUtil.format(now(), DATE_FORMAT_YYYYMMDDHHMMSS)); | .replace(TIMESTAMP_VAR, TimeUtil.format(now(), DATE_FORMAT_YYYYMMDDHHMMSS)); | ||||
if (imageName.length() > 128) return die("imageName.length > 128: "+imageName); // sanity check | if (imageName.length() > 128) return die("imageName.length > 128: "+imageName); // sanity check | ||||
@@ -296,6 +295,38 @@ public class PackerJob implements Callable<List<PackerImage>> { | |||||
return images; | return images; | ||||
} | } | ||||
private void writeBubbleVersions(TempDir tempDir, Map<String, String> versions) { | |||||
final File varsDir = mkdirOrDie(abs(tempDir) + "/roles/"+ROLE_BUBBLE+"/vars"); | |||||
final StringBuilder b = new StringBuilder(); | |||||
for (Map.Entry<String, String> var : versions.entrySet()) { | |||||
b.append(var.getKey()).append(" : '").append(var.getValue()).append("'\n"); | |||||
} | |||||
FileUtil.toFileOrDie(new File(varsDir, "main.yml"), b.toString()); | |||||
} | |||||
private Map<String, String> useLatestVersion(String roleName, TempDir tempDir) throws IOException { | |||||
final Map<String, String> vars = new HashMap<>(); | |||||
final String releaseUrlBase = configuration.getReleaseUrlBase(); | |||||
final File varsDir = mkdirOrDie(abs(tempDir) + "/roles/"+roleName+"/vars"); | |||||
// determine latest version | |||||
final String version = packerService.getSoftwareVersion(roleName); | |||||
vars.put(roleName, version); | |||||
final String hash = url2string(releaseUrlBase+"/"+version+"/"+roleName+".zip.sha256"); | |||||
String varsData = roleName+"_sha256 : '"+hash+"'\n" | |||||
+ roleName+"_version : '" + version + "'\n"; | |||||
if (roleName.equals(ROLE_ALGO)) { | |||||
// capture dnscrypt_proxy version for algo | |||||
final String dnscryptVersion = url2string(releaseUrlBase+"/"+version+"/dnscrypt-proxy_version.txt"); | |||||
varsData += "dnscrypt_version : '"+dnscryptVersion+"'"; | |||||
vars.put(ROLE_DNSCRYPT, dnscryptVersion); | |||||
} | |||||
FileUtil.toFileOrDie(new File(varsDir, "main.yml"), varsData); | |||||
return vars; | |||||
} | |||||
private List<String> getRolesForInstallType(AnsibleInstallType installType) { | private List<String> getRolesForInstallType(AnsibleInstallType installType) { | ||||
switch (installType) { | switch (installType) { | ||||
case sage: return SAGE_ROLES; | case sage: return SAGE_ROLES; | ||||
@@ -10,11 +10,12 @@ import bubble.model.cloud.CloudService; | |||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.daemon.DaemonThreadFactory; | import org.cobbzilla.util.daemon.DaemonThreadFactory; | ||||
import org.cobbzilla.util.security.ShaUtil; | |||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||
import java.io.File; | import java.io.File; | ||||
import java.io.IOException; | |||||
import java.util.HashMap; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.concurrent.ConcurrentHashMap; | import java.util.concurrent.ConcurrentHashMap; | ||||
@@ -23,9 +24,11 @@ import java.util.concurrent.atomic.AtomicReference; | |||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | import static org.cobbzilla.util.daemon.ZillaRuntime.*; | ||||
import static org.cobbzilla.util.http.HttpUtil.url2string; | |||||
import static org.cobbzilla.util.io.FileUtil.abs; | import static org.cobbzilla.util.io.FileUtil.abs; | ||||
import static org.cobbzilla.util.io.FileUtil.mkdirOrDie; | import static org.cobbzilla.util.io.FileUtil.mkdirOrDie; | ||||
import static org.cobbzilla.util.io.StreamUtil.stream2string; | import static org.cobbzilla.util.io.StreamUtil.stream2string; | ||||
import static org.cobbzilla.util.security.ShaUtil.*; | |||||
import static org.cobbzilla.util.string.StringUtil.splitAndTrim; | import static org.cobbzilla.util.string.StringUtil.splitAndTrim; | ||||
import static org.cobbzilla.util.system.CommandShell.chmod; | import static org.cobbzilla.util.system.CommandShell.chmod; | ||||
import static org.cobbzilla.util.system.CommandShell.execScript; | import static org.cobbzilla.util.system.CommandShell.execScript; | ||||
@@ -40,6 +43,12 @@ public class PackerService { | |||||
public static final List<String> NODE_ROLES = splitAndTrim(stream2string(PACKER_DIR + "/node-roles.txt"), "\n") | public static final List<String> NODE_ROLES = splitAndTrim(stream2string(PACKER_DIR + "/node-roles.txt"), "\n") | ||||
.stream().filter(s -> !empty(s)).collect(Collectors.toList()); | .stream().filter(s -> !empty(s)).collect(Collectors.toList()); | ||||
public static final String ROLE_ALGO = "algo"; | |||||
public static final String ROLE_MITMPROXY = "mitmproxy"; | |||||
public static final String ROLE_DNSCRYPT = "dnscrypt"; | |||||
public static final String ROLE_BUBBLE = "bubble"; | |||||
public static final String PACKER_KEY_NAME = "packer_rsa"; | public static final String PACKER_KEY_NAME = "packer_rsa"; | ||||
private final Map<String, PackerJob> activeJobs = new ConcurrentHashMap<>(16); | private final Map<String, PackerJob> activeJobs = new ConcurrentHashMap<>(16); | ||||
@@ -82,7 +91,17 @@ public class PackerService { | |||||
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 ShaUtil.sha256_file(getPackerPublicKey()); } | |||||
public String getPackerPublicKeyHash () { return sha256_file(getPackerPublicKey()); } | |||||
public String getPackerVersionHash () { | |||||
final String keyHash = getPackerPublicKeyHash(); | |||||
final String versions = "" | |||||
+"_d"+getSoftwareVersion(ROLE_DNSCRYPT) | |||||
+"_a"+getSoftwareVersion(ROLE_ALGO) | |||||
+"_m"+getSoftwareVersion(ROLE_MITMPROXY); | |||||
if (versions.length() > 48) return die("getPackerVersionHash: software versions are too long (versions.length == "+versions.length()+" > 48): "+versions); | |||||
return keyHash.substring(64 - versions.length())+versions; | |||||
} | |||||
public synchronized File initPackerKey(boolean pub) { | public synchronized File initPackerKey(boolean pub) { | ||||
final File keyDir = new File(System.getProperty("user.home"),".ssh"); | final File keyDir = new File(System.getProperty("user.home"),".ssh"); | ||||
@@ -98,4 +117,16 @@ public class PackerService { | |||||
return pub ? pubKeyFile : privateKeyFile; | return pub ? pubKeyFile : privateKeyFile; | ||||
} | } | ||||
private final Map<String, String> softwareVersions = new HashMap<>(); | |||||
public String getSoftwareVersion(String roleName) { | |||||
final String releaseUrlBase = configuration.getReleaseUrlBase(); | |||||
return softwareVersions.computeIfAbsent(roleName, r -> { | |||||
try { | |||||
return url2string(releaseUrlBase+"/"+r+"/latest.txt"); | |||||
} catch (IOException e) { | |||||
return die("getSoftwareVersion("+r+"): "+shortError(e), e); | |||||
} | |||||
}); | |||||
} | |||||
} | } |
@@ -1 +1 @@ | |||||
bubble.version=Adventure 1.1.3 | |||||
bubble.version=Adventure 1.1.4 |
@@ -132,3 +132,12 @@ | |||||
special_time: "hourly" | special_time: "hourly" | ||||
user: "root" | user: "root" | ||||
job: "find /tmp ~bubble/tmp -mtime +1 -type f -delete && find /tmp ~bubble/tmp -mtime +1 -type d -empty -delete" | job: "find /tmp ~bubble/tmp -mtime +1 -type f -delete && find /tmp ~bubble/tmp -mtime +1 -type d -empty -delete" | ||||
- name: Record software versions | |||||
template: | |||||
src: bubble_versions.properties.j2 | |||||
dest: /home/bubble/bubble_versions.properties | |||||
owner: bubble | |||||
group: bubble | |||||
mode: 0400 | |||||
when: install_type == 'node' |
@@ -0,0 +1,3 @@ | |||||
algo_version={{ algo_version }} | |||||
dnscrypt_version={{ dnscrypt_version }} | |||||
mitmproxy_version={{ mitmproxy_version }} |