@@ -168,30 +168,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||||
@Getter @Setter private String letsencryptEmail; | @Getter @Setter private String letsencryptEmail; | ||||
@Getter @Setter private String releaseUrlBase; | @Getter @Setter private String releaseUrlBase; | ||||
public static final File SOFTWARE_VERSIONS_FILE = new File(HOME_DIR+"/bubble_versions.properties"); | |||||
@Getter(lazy=true) private final Properties defaultSoftwareVersions = initDefaultSoftwareVersions(); | |||||
private Properties initDefaultSoftwareVersions() { | |||||
if (!SOFTWARE_VERSIONS_FILE.exists()) return null; | |||||
final Properties props = new Properties(); | |||||
try (InputStream in = new FileInputStream(SOFTWARE_VERSIONS_FILE)) { | |||||
props.load(in); | |||||
return props; | |||||
} catch (Exception e) { | |||||
log.error("initDefaultSoftwareVersions: "+shortError(e)); | |||||
return null; | |||||
} | |||||
} | |||||
public void saveSoftwareVersions (Properties softwareVersions) { | |||||
if (!SOFTWARE_VERSIONS_FILE.exists()) { | |||||
try (OutputStream out = new FileOutputStream(SOFTWARE_VERSIONS_FILE)) { | |||||
softwareVersions.store(out, null); | |||||
} catch (Exception e) { | |||||
log.error("saveSoftwareVersions: "+shortError(e)); | |||||
} | |||||
} | |||||
} | |||||
@Getter(lazy=true) private final SoftwareVersions softwareVersions = new SoftwareVersions(getReleaseUrlBase()); | |||||
@Setter private String localStorageDir = DEFAULT_LOCAL_STORAGE_DIR; | @Setter private String localStorageDir = DEFAULT_LOCAL_STORAGE_DIR; | ||||
public String getLocalStorageDir () { return empty(localStorageDir) ? DEFAULT_LOCAL_STORAGE_DIR : localStorageDir; } | public String getLocalStorageDir () { return empty(localStorageDir) ? DEFAULT_LOCAL_STORAGE_DIR : localStorageDir; } | ||||
@@ -292,7 +269,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||||
.setVersion(version) | .setVersion(version) | ||||
.setShortVersion(shortVersion) | .setShortVersion(shortVersion) | ||||
.setSha256(getJarSha()) | .setSha256(getJarSha()) | ||||
.setSoftware(getDefaultSoftwareVersions()); | |||||
.setSoftware(getSoftwareVersions().getDefaultSoftwareVersions()); | |||||
} | } | ||||
public String getShortVersion () { return getVersionInfo().getShortVersion(); } | public String getShortVersion () { return getVersionInfo().getShortVersion(); } | ||||
@@ -0,0 +1,141 @@ | |||||
/** | |||||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | |||||
* For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||||
*/ | |||||
package bubble.server; | |||||
import lombok.Getter; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.cobbzilla.util.io.FileUtil; | |||||
import java.io.*; | |||||
import java.util.HashMap; | |||||
import java.util.Map; | |||||
import java.util.Properties; | |||||
import static bubble.ApiConstants.HOME_DIR; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||||
import static org.cobbzilla.util.http.HttpUtil.url2string; | |||||
@Slf4j | |||||
public class SoftwareVersions { | |||||
public static final String ROLE_ALGO = "algo"; | |||||
public static final String ROLE_MITMPROXY = "mitmproxy"; | |||||
public static final String ROLE_DNSCRYPT = "dnscrypt-proxy"; | |||||
public static final String ROLE_BUBBLE = "bubble"; | |||||
public static final String[] VERSIONED_SOFTWARE = {ROLE_DNSCRYPT, ROLE_ALGO, ROLE_MITMPROXY}; | |||||
public static final File SOFTWARE_VERSIONS_FILE = new File(HOME_DIR+"/bubble_versions.properties"); | |||||
public static final String SUFFIX_VERSION = "_version"; | |||||
public static final String SUFFIX_SHA = "_sha"; | |||||
private final String releaseUrlBase; | |||||
public SoftwareVersions (String releaseUrlBase) { this.releaseUrlBase = releaseUrlBase; } | |||||
public String getRolePropBase(String roleName) { return roleName.replace("-", "_"); } | |||||
public String getLatestVersion(String r) { | |||||
try { | |||||
return url2string(releaseUrlBase+"/"+ r +"/latest.txt").trim(); | |||||
} catch (IOException e) { | |||||
return die("getLatestVersion("+ r +"): "+shortError(e), e); | |||||
} | |||||
} | |||||
public String downloadHash(String roleName, String version) { | |||||
try { | |||||
return url2string(releaseUrlBase+"/"+ roleName +"/"+ version +"/"+ roleName +getSoftwareSuffix(roleName)+".sha256").trim(); | |||||
} catch (IOException e) { | |||||
return die("getSoftwareHash("+ roleName +"): "+shortError(e), e); | |||||
} | |||||
} | |||||
@Getter(lazy=true) private final Properties defaultSoftwareVersions = initDefaultSoftwareVersions(); | |||||
private Properties initDefaultSoftwareVersions() { | |||||
if (empty(releaseUrlBase)) { | |||||
log.warn("initDefaultSoftwareVersions: releaseUrlBase not defined"); | |||||
return null; | |||||
} | |||||
final Properties props = new Properties(); | |||||
if (!SOFTWARE_VERSIONS_FILE.exists()) { | |||||
// write latest versions | |||||
for (String roleName : VERSIONED_SOFTWARE) { | |||||
final String latestVersion = getLatestVersion(roleName); | |||||
props.setProperty(getRolePropBase(roleName)+SUFFIX_VERSION, latestVersion); | |||||
props.setProperty(getRolePropBase(roleName)+SUFFIX_SHA, downloadHash(roleName, latestVersion)); | |||||
} | |||||
writeVersions(props, SOFTWARE_VERSIONS_FILE); | |||||
} | |||||
try (InputStream in = new FileInputStream(SOFTWARE_VERSIONS_FILE)) { | |||||
props.load(in); | |||||
return props; | |||||
} catch (Exception e) { | |||||
log.error("initDefaultSoftwareVersions: "+shortError(e)); | |||||
return null; | |||||
} | |||||
} | |||||
public void writeVersions(File file) { writeVersions(getDefaultSoftwareVersions(), file); } | |||||
public void writeVersions(Properties props, File file) { | |||||
try (OutputStream out = new FileOutputStream(file)) { | |||||
props.store(out, null); | |||||
} catch (Exception e) { | |||||
log.error("saveSoftwareVersions: "+shortError(e)); | |||||
} | |||||
} | |||||
public void writeAnsibleVars(File file) { writeAnsibleVars(getDefaultSoftwareVersions(), file); } | |||||
public void writeAnsibleVars(Properties props, File file) { | |||||
try (OutputStream out = new FileOutputStream(file)) { | |||||
final StringBuilder b = new StringBuilder(); | |||||
for (String name : props.stringPropertyNames()) { | |||||
b.append(name).append(" : '").append(props.getProperty(name)).append("'\n"); | |||||
} | |||||
FileUtil.toFile(file, b.toString()); | |||||
} catch (Exception e) { | |||||
die("writeAnsibleVars: "+shortError(e)); | |||||
} | |||||
} | |||||
private final Map<String, String> softwareVersions = new HashMap<>(); | |||||
public String getSoftwareVersion(String roleName) { | |||||
final Properties defaults = getDefaultSoftwareVersions(); | |||||
if (defaults != null) { | |||||
final String propName = getRolePropBase(roleName) + SUFFIX_VERSION; | |||||
final String version = defaults.getProperty(propName); | |||||
if (version != null) return version; | |||||
} | |||||
return softwareVersions.computeIfAbsent(roleName, this::getLatestVersion); | |||||
} | |||||
private final Map<String, String> softwareHashes = new HashMap<>(); | |||||
public String getSoftwareHash(String roleName, String version) { | |||||
final Properties defaults = getDefaultSoftwareVersions(); | |||||
if (defaults != null) { | |||||
final String roleBase = getRolePropBase(roleName); | |||||
final String foundVersion = defaults.getProperty(roleBase + SUFFIX_VERSION); | |||||
if (foundVersion != null && foundVersion.equals(version)) { | |||||
final String hash = defaults.getProperty(roleBase + SUFFIX_SHA); | |||||
if (hash != null) return hash; | |||||
} | |||||
} | |||||
return softwareHashes.computeIfAbsent(roleName, r -> downloadHash(r, version)); | |||||
} | |||||
private String getSoftwareSuffix(String roleName) { | |||||
switch (roleName) { | |||||
case ROLE_ALGO: case ROLE_MITMPROXY: return ".zip"; | |||||
case ROLE_DNSCRYPT: return ""; | |||||
default: return die("getSoftwareSuffix: unrecognized roleName: "+roleName); | |||||
} | |||||
} | |||||
} |
@@ -16,6 +16,7 @@ import bubble.model.account.Account; | |||||
import bubble.model.cloud.AnsibleInstallType; | import bubble.model.cloud.AnsibleInstallType; | ||||
import bubble.model.cloud.CloudService; | import bubble.model.cloud.CloudService; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import bubble.server.SoftwareVersions; | |||||
import bubble.service.cloud.GeoService; | import bubble.service.cloud.GeoService; | ||||
import lombok.Cleanup; | import lombok.Cleanup; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
@@ -41,9 +42,9 @@ import java.util.stream.Collectors; | |||||
import static bubble.ApiConstants.copyScripts; | import static bubble.ApiConstants.copyScripts; | ||||
import static bubble.model.cloud.RegionalServiceDriver.findClosestRegions; | import static bubble.model.cloud.RegionalServiceDriver.findClosestRegions; | ||||
import static bubble.server.SoftwareVersions.*; | |||||
import static bubble.service.packer.PackerService.*; | import static bubble.service.packer.PackerService.*; | ||||
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.*; | import static org.cobbzilla.util.io.FileUtil.*; | ||||
import static org.cobbzilla.util.io.StreamUtil.copyClasspathDirectory; | import static org.cobbzilla.util.io.StreamUtil.copyClasspathDirectory; | ||||
import static org.cobbzilla.util.io.StreamUtil.stream2string; | import static org.cobbzilla.util.io.StreamUtil.stream2string; | ||||
@@ -160,12 +161,9 @@ public class PackerJob implements Callable<List<PackerImage>> { | |||||
@Cleanup final TempDir tempDir = copyClasspathDirectory("packer"); | @Cleanup final TempDir tempDir = copyClasspathDirectory("packer"); | ||||
// record versions of algo, mitmproxy and dnscrypt_proxy | // record versions of algo, mitmproxy and dnscrypt_proxy | ||||
final Map<String, String> versions = new HashMap<>(); | |||||
versions.putAll(getSoftwareVersion(ROLE_ALGO, tempDir)); | |||||
versions.putAll(getSoftwareVersion(ROLE_MITMPROXY, tempDir)); | |||||
// write versions to bubble vars | |||||
writeBubbleVersions(tempDir, versions); | |||||
writeSoftwareVars(ROLE_ALGO, tempDir); | |||||
writeSoftwareVars(ROLE_MITMPROXY, tempDir); | |||||
writeSoftwareVars(ROLE_BUBBLE, tempDir); | |||||
// 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)); | ||||
@@ -292,46 +290,10 @@ 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(); | |||||
final Properties softwareProps = new Properties(); | |||||
for (Map.Entry<String, String> var : versions.entrySet()) { | |||||
final String roleName = var.getKey(); | |||||
final String version = var.getValue().trim(); | |||||
final String roleBase = roleName.replace("-", "_"); | |||||
final String hash = packerService.getSoftwareHash(roleName, version); | |||||
b.append(roleBase).append("_version : '").append(version).append("'\n") | |||||
.append(roleBase).append("_sha : '").append(hash).append("'\n"); | |||||
softwareProps.setProperty(roleBase+"_version", version); | |||||
softwareProps.setProperty(roleBase+"_sha", hash); | |||||
} | |||||
FileUtil.toFileOrDie(new File(varsDir, "main.yml"), b.toString()); | |||||
configuration.saveSoftwareVersions(softwareProps); | |||||
} | |||||
private Map<String, String> getSoftwareVersion(String roleName, TempDir tempDir) throws IOException { | |||||
final Map<String, String> vars = new HashMap<>(); | |||||
final String releaseUrlBase = configuration.getReleaseUrlBase(); | |||||
private void writeSoftwareVars(String roleName, TempDir tempDir) throws IOException { | |||||
final SoftwareVersions softwareVersions = configuration.getSoftwareVersions(); | |||||
final File varsDir = mkdirOrDie(abs(tempDir) + "/roles/"+roleName+"/vars"); | 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 = packerService.getSoftwareHash(roleName, version); | |||||
String varsData = roleName+"_sha : '"+hash+"'\n" | |||||
+ roleName+"_version : '" + version + "'\n"; | |||||
if (roleName.equals(ROLE_ALGO)) { | |||||
// capture dnscrypt_proxy version for algo | |||||
final String dnscryptVersion = url2string(releaseUrlBase+"/"+roleName+"/"+version+"/dnscrypt-proxy_version.txt").trim(); | |||||
varsData += "dnscrypt_proxy_version : '"+dnscryptVersion+"'\n" | |||||
+ "dnscrypt_proxy_sha : '"+packerService.getSoftwareHash(ROLE_DNSCRYPT, dnscryptVersion)+"'"; | |||||
vars.put(ROLE_DNSCRYPT, dnscryptVersion); | |||||
} | |||||
FileUtil.toFileOrDie(new File(varsDir, "main.yml"), varsData); | |||||
return vars; | |||||
softwareVersions.writeAnsibleVars(new File(varsDir, "main.yml")); | |||||
} | } | ||||
private List<String> getRolesForInstallType(AnsibleInstallType installType) { | private List<String> getRolesForInstallType(AnsibleInstallType installType) { | ||||
@@ -8,24 +8,22 @@ import bubble.cloud.compute.PackerImage; | |||||
import bubble.model.cloud.AnsibleInstallType; | import bubble.model.cloud.AnsibleInstallType; | ||||
import bubble.model.cloud.CloudService; | import bubble.model.cloud.CloudService; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import bubble.server.SoftwareVersions; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.daemon.DaemonThreadFactory; | import org.cobbzilla.util.daemon.DaemonThreadFactory; | ||||
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.Properties; | |||||
import java.util.concurrent.ConcurrentHashMap; | import java.util.concurrent.ConcurrentHashMap; | ||||
import java.util.concurrent.ExecutorService; | import java.util.concurrent.ExecutorService; | ||||
import java.util.concurrent.atomic.AtomicReference; | import java.util.concurrent.atomic.AtomicReference; | ||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
import static bubble.server.SoftwareVersions.*; | |||||
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; | ||||
@@ -45,11 +43,6 @@ 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-proxy"; | |||||
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); | ||||
@@ -95,11 +88,12 @@ public class PackerService { | |||||
public String getPackerPublicKeyHash () { return sha256_file(getPackerPublicKey()); } | public String getPackerPublicKeyHash () { return sha256_file(getPackerPublicKey()); } | ||||
public String getPackerVersionHash () { | public String getPackerVersionHash () { | ||||
final SoftwareVersions softwareVersions = configuration.getSoftwareVersions(); | |||||
final String keyHash = getPackerPublicKeyHash(); | final String keyHash = getPackerPublicKeyHash(); | ||||
final String versions = "" | final String versions = "" | ||||
+"_d"+getSoftwareVersion(ROLE_DNSCRYPT) | |||||
+"_a"+getSoftwareVersion(ROLE_ALGO) | |||||
+"_m"+getSoftwareVersion(ROLE_MITMPROXY); | |||||
+"_d"+softwareVersions.getSoftwareVersion(ROLE_DNSCRYPT) | |||||
+"_a"+softwareVersions.getSoftwareVersion(ROLE_ALGO) | |||||
+"_m"+softwareVersions.getSoftwareVersion(ROLE_MITMPROXY); | |||||
if (versions.length() > 48) return die("getPackerVersionHash: software versions are too long (versions.length == "+versions.length()+" > 48): "+versions); | 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; | return keyHash.substring(64 - versions.length())+versions; | ||||
} | } | ||||
@@ -118,53 +112,4 @@ 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 Properties defaults = configuration.getDefaultSoftwareVersions(); | |||||
if (defaults != null) { | |||||
final String propName = roleName.replace("-", "_")+"_version"; | |||||
final String version = defaults.getProperty(propName); | |||||
if (version != null) return version; | |||||
} | |||||
final String releaseUrlBase = configuration.getReleaseUrlBase(); | |||||
return softwareVersions.computeIfAbsent(roleName, r -> { | |||||
try { | |||||
return url2string(releaseUrlBase+"/"+r+"/latest.txt").trim(); | |||||
} catch (IOException e) { | |||||
return die("getSoftwareVersion("+r+"): "+shortError(e), e); | |||||
} | |||||
}); | |||||
} | |||||
private final Map<String, String> softwareHashes = new HashMap<>(); | |||||
public String getSoftwareHash(String roleName, String version) { | |||||
final Properties defaults = configuration.getDefaultSoftwareVersions(); | |||||
if (defaults != null) { | |||||
final String roleBase = roleName.replace("-", "_"); | |||||
final String foundVersion = defaults.getProperty(roleBase +"_version"); | |||||
if (foundVersion != null && foundVersion.equals(version)) { | |||||
final String hash = defaults.getProperty(roleBase +"_sha"); | |||||
if (hash != null) return hash; | |||||
} | |||||
} | |||||
final String releaseUrlBase = configuration.getReleaseUrlBase(); | |||||
return softwareHashes.computeIfAbsent(roleName, r -> { | |||||
try { | |||||
return url2string(releaseUrlBase+"/"+roleName+"/"+version+"/"+roleName+getSoftwareSuffix(roleName)+".sha256").trim(); | |||||
} catch (IOException e) { | |||||
return die("getSoftwareHash("+r+"): "+shortError(e), e); | |||||
} | |||||
}); | |||||
} | |||||
private String getSoftwareSuffix(String roleName) { | |||||
switch (roleName) { | |||||
case ROLE_ALGO: case ROLE_MITMPROXY: return ".zip"; | |||||
case ROLE_DNSCRYPT: return ""; | |||||
default: return die("getSoftwareSuffix: unrecognized roleName: "+roleName); | |||||
} | |||||
} | |||||
} | } |