@@ -168,30 +168,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||
@Getter @Setter private String letsencryptEmail; | |||
@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; | |||
public String getLocalStorageDir () { return empty(localStorageDir) ? DEFAULT_LOCAL_STORAGE_DIR : localStorageDir; } | |||
@@ -292,7 +269,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||
.setVersion(version) | |||
.setShortVersion(shortVersion) | |||
.setSha256(getJarSha()) | |||
.setSoftware(getDefaultSoftwareVersions()); | |||
.setSoftware(getSoftwareVersions().getDefaultSoftwareVersions()); | |||
} | |||
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.CloudService; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.server.SoftwareVersions; | |||
import bubble.service.cloud.GeoService; | |||
import lombok.Cleanup; | |||
import lombok.Getter; | |||
@@ -41,9 +42,9 @@ import java.util.stream.Collectors; | |||
import static bubble.ApiConstants.copyScripts; | |||
import static bubble.model.cloud.RegionalServiceDriver.findClosestRegions; | |||
import static bubble.server.SoftwareVersions.*; | |||
import static bubble.service.packer.PackerService.*; | |||
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.StreamUtil.copyClasspathDirectory; | |||
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"); | |||
// 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 | |||
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; | |||
} | |||
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"); | |||
// 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) { | |||
@@ -8,24 +8,22 @@ import bubble.cloud.compute.PackerImage; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.CloudService; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.server.SoftwareVersions; | |||
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.io.File; | |||
import java.io.IOException; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Properties; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.atomic.AtomicReference; | |||
import java.util.stream.Collectors; | |||
import static bubble.server.SoftwareVersions.*; | |||
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.mkdirOrDie; | |||
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") | |||
.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"; | |||
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 getPackerVersionHash () { | |||
final SoftwareVersions softwareVersions = configuration.getSoftwareVersions(); | |||
final String keyHash = getPackerPublicKeyHash(); | |||
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); | |||
return keyHash.substring(64 - versions.length())+versions; | |||
} | |||
@@ -118,53 +112,4 @@ public class PackerService { | |||
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); | |||
} | |||
} | |||
} |