Bladeren bron

Merge branch 'master' into kris/fix_restore

# Conflicts:
#	bubble-server/src/main/java/bubble/server/BubbleConfiguration.java
#	bubble-web
pull/26/head
Kristijan Mitrovic 4 jaren geleden
bovenliggende
commit
848ab862ea
34 gewijzigde bestanden met toevoegingen van 615 en 240 verwijderingen
  1. +1
    -0
      bubble-server/src/main/java/bubble/ApiConstants.java
  2. +91
    -73
      bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java
  3. +3
    -2
      bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java
  4. +1
    -1
      bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java
  5. +4
    -0
      bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java
  6. +17
    -0
      bubble-server/src/main/java/bubble/model/cloud/BubbleVersionInfo.java
  7. +4
    -0
      bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java
  8. +1
    -1
      bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_to_sage.java
  9. +2
    -1
      bubble-server/src/main/java/bubble/notify/compute/NotificationHandler_compute_driver_stop.java
  10. +18
    -0
      bubble-server/src/main/java/bubble/notify/upgrade/JarUpgradeNotification.java
  11. +28
    -0
      bubble-server/src/main/java/bubble/notify/upgrade/NotificationHandler_upgrade_request.java
  12. +29
    -0
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  13. +11
    -2
      bubble-server/src/main/java/bubble/resources/account/MeResource.java
  14. +25
    -5
      bubble-server/src/main/java/bubble/server/BubbleConfiguration.java
  15. +93
    -0
      bubble-server/src/main/java/bubble/service/boot/BubbleJarUpgradeService.java
  16. +35
    -26
      bubble-server/src/main/java/bubble/service/cloud/NodeLaunchException.java
  17. +2
    -5
      bubble-server/src/main/java/bubble/service/cloud/NodeLaunchMonitor.java
  18. +27
    -10
      bubble-server/src/main/java/bubble/service/cloud/NodeLauncher.java
  19. +5
    -1
      bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeter.java
  20. +12
    -34
      bubble-server/src/main/java/bubble/service/cloud/NodeService.java
  21. +58
    -41
      bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java
  22. +13
    -9
      bubble-server/src/main/java/bubble/service/cloud/job/NodeStartJob.java
  23. +17
    -3
      bubble-server/src/main/java/bubble/service/notify/NotificationService.java
  24. +0
    -8
      bubble-server/src/main/resources/bubble/host-prefixes.txt
  25. +4
    -0
      bubble-server/src/main/resources/logback.xml
  26. +8
    -0
      bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties
  27. +60
    -0
      bubble-server/src/main/resources/packer/roles/bubble/files/bubble_upgrade.sh
  28. +23
    -0
      bubble-server/src/main/resources/packer/roles/bubble/files/bubble_upgrade_monitor.sh
  29. +5
    -0
      bubble-server/src/main/resources/packer/roles/bubble/files/supervisor_bubble_upgrade_monitor.conf
  30. +13
    -11
      bubble-server/src/main/resources/packer/roles/bubble/tasks/main.yml
  31. +2
    -4
      bubble-server/src/test/java/bubble/mock/MockNetworkService.java
  32. +1
    -1
      bubble-web
  33. +1
    -1
      utils/cobbzilla-utils
  34. +1
    -1
      utils/cobbzilla-wizard

+ 1
- 0
bubble-server/src/main/java/bubble/ApiConstants.java Bestand weergeven

@@ -197,6 +197,7 @@ public class ApiConstants {
public static final String EP_PROMOTIONS = PROMOTIONS_ENDPOINT;
public static final String EP_FORK = "/fork";
public static final String EP_NODE_MANAGER = "/nodeman";
public static final String EP_UPGRADE = "/upgrade";

public static final String DETECT_ENDPOINT = "/detect";
public static final String EP_LOCALE = "/locale";


+ 91
- 73
bubble-server/src/main/java/bubble/cloud/compute/vultr/VultrDriver.java Bestand weergeven

@@ -33,6 +33,7 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpMethods.POST;
import static org.cobbzilla.util.http.HttpStatusCodes.*;
import static org.cobbzilla.util.http.HttpUtil.getResponse;
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;
@@ -54,6 +55,16 @@ public class VultrDriver extends ComputeServiceDriverBase {
public static final String VULTR_SUBID = "SUBID";
public static final String VULTR_V4_IP = "main_ip";
public static final String VULTR_V6_IP = "v6_main_ip";
public static final String VULTR_LABEL = "label";

public static final String VULTR_STATUS = "status";
public static final String VULTR_STATUS_PENDING = "pending";
public static final String VULTR_STATUS_ACTIVE = "active";

public static final String VULTR_SERVER_STATE = "server_state";
public static final String VULTR_STATE_NONE = "none";
public static final String VULTR_STATE_OK = "ok";
public static final String VULTR_STATE_LOCKED = "locked";

public static final String CREATE_SERVER_URL = VULTR_API_BASE + "server/create";
public static final String DESTROY_SERVER_URL = VULTR_API_BASE + "server/destroy";
@@ -129,6 +140,7 @@ public class VultrDriver extends ComputeServiceDriverBase {
final HttpRequestBean serverRequest = auth(new HttpRequestBean(POST, CREATE_SERVER_URL, data));

// create server, check response
if (log.isInfoEnabled()) log.info("start: calling Vultr to start node: "+node.id());
final HttpResponseBean serverResponse = serverRequest.curl(); // fixme: we can do better than shelling to curl
if (serverResponse.getStatus() != 200) return die("start: error creating server: " + serverResponse);
final JsonNode responseJson;
@@ -138,10 +150,11 @@ public class VultrDriver extends ComputeServiceDriverBase {
return die("start: error creating server (error parsing response as JSON): " + serverResponse);
}
final var subId = responseJson.get(VULTR_SUBID).textValue();
if (log.isDebugEnabled()) log.debug("start: Vultr started node: "+node.id()+" SUBID="+subId);

node.setState(BubbleNodeState.booting);
node.setTag(TAG_INSTANCE_ID, subId);
nodeDAO.update(node);
// nodeDAO.update(node);

final long start = now();
boolean startedOk = false;
@@ -151,37 +164,37 @@ public class VultrDriver extends ComputeServiceDriverBase {
sleep(SERVER_START_POLL_INTERVAL);
final HttpResponseBean pollResponse = getResponse(poll);
if (pollResponse.getStatus() != OK) {
return die("start: error polling subid: "+subId+": "+pollResponse);
return die("start: error polling node "+node.id()+" subid: "+subId+": "+pollResponse);
}
// todo: add timeout, if server doesn't come up within X minutes, try to kill it and report an error
final JsonNode serverNode = json(pollResponse.getEntityString(), JsonNode.class);
if (log.isDebugEnabled()) log.debug("start: polled node "+node.id()+" json="+json(serverNode, COMPACT_MAPPER));
if (serverNode != null) {
if (serverNode.has("tag")
&& serverNode.get("tag").textValue().equals(cloud.getUuid())
&& serverNode.has("status")
&& serverNode.has("server_state")
&& serverNode.has(VULTR_STATUS)
&& serverNode.has(VULTR_SERVER_STATE)
&& serverNode.has(VULTR_V4_IP)) {

final String status = serverNode.get("status").textValue();
final String serverState = serverNode.get("server_state").textValue();
final String status = serverNode.get(VULTR_STATUS).textValue();
final String serverState = serverNode.get(VULTR_SERVER_STATE).textValue();
final String ip4 = serverNode.get(VULTR_V4_IP).textValue();
final String ip6 = serverNode.get(VULTR_V6_IP).textValue();
// log.info("start: server_state="+serverState+", status="+status, "ip4="+ip4+", ip6="+ip6);
// if (log.isInfoEnabled()) log.info("start: server_state="+serverState+", status="+status, "ip4="+ip4+", ip6="+ip6);

if (ip4 != null && ip4.length() > 0 && !ip4.equals("0.0.0.0")) {
node.setIp4(ip4);
nodeDAO.update(node);
// nodeDAO.update(node);
}
if (ip6 != null && ip6.length() > 0) {
node.setIp6(ip6);
nodeDAO.update(node);
// nodeDAO.update(node);
}
if (status.equals("active") && (node.hasIp4() || node.hasIp6())) {
if (status.equals(VULTR_STATUS_ACTIVE) && (node.hasIp4() || node.hasIp6())) {
node.setState(BubbleNodeState.booted);
nodeDAO.update(node);
// nodeDAO.update(node);
}
if (serverState.equals("ok")) {
log.info("start: server is ready: "+node.id());
if (serverState.equals(VULTR_STATE_OK)) {
if (log.isInfoEnabled()) log.info("start: server is ready: "+node.id());
startedOk = true;
break;
}
@@ -189,7 +202,7 @@ public class VultrDriver extends ComputeServiceDriverBase {
}
}
if (!startedOk) {
log.error("start: timeout waiting for node to boot and become available, stopping it");
if (log.isErrorEnabled()) log.error("start: timeout waiting for node "+node.id()+" to boot and become available, stopping it");
stop(node);
}
return node;
@@ -210,45 +223,35 @@ public class VultrDriver extends ComputeServiceDriverBase {
try {
_stop(node);
} catch (EntityNotFoundException e) {
log.info("stop: node stopped");
if (log.isInfoEnabled()) log.info("stop: node stopped");
return node;

} catch (Exception e) {
if (log.isInfoEnabled()) log.info("stop: _stop failed with: "+shortError(e));
lastEx = e;
}
sleep(SERVER_STOP_CHECK_INTERVAL, "stop: waiting to try stopping again until node is not found");
log.warn("stop: node still running: "+node.id());
if (log.isWarnEnabled()) log.warn("stop: node still running: "+node.id());
}
log.error("stop: error stopping node: "+node.id());
if (log.isErrorEnabled()) log.error("stop: error stopping node: "+node.id());
if (lastEx != null) throw lastEx;
return die("stop: timeout stopping node: "+node.id());
}

public BubbleNode _stop(BubbleNode node) throws IOException {
BubbleNode vultrNode;
final String ip4 = node.getIp4();
if (!node.hasTag(TAG_INSTANCE_ID)) {
if (ip4 == null) {
throw notFoundEx(node.id());
}
log.warn("stop: no "+TAG_INSTANCE_ID+" tag found on node ("+node.getFqdn()+"/"+ ip4 +"), searching based in ip4...");
vultrNode = findByIp4(node, ip4);
} else {
// does the node still exist?
vultrNode = listNode(node);
if (vultrNode == null) {
vultrNode = findByIp4(node, ip4);
}
}
// does the node still exist?
BubbleNode vultrNode = listNode(node);
if (vultrNode == null) {
throw notFoundEx(node.id());
}

final String subId = vultrNode.getTag(TAG_INSTANCE_ID);
if (subId == null) {
if (log.isErrorEnabled()) log.error("_stop: node "+node.id()+" is missing tag "+TAG_INSTANCE_ID+", cannot stop, throwing invalidEx");
throw invalidEx("err.node.stop.error", "stop: no " + VULTR_SUBID + " on node, returning");
}

if (log.isInfoEnabled()) log.info("_stop: calling stopServer("+subId+") for node "+node.id());
stopServer(subId);
return node;
}
@@ -267,11 +270,11 @@ public class VultrDriver extends ComputeServiceDriverBase {
.findFirst()
.orElse(null);
if (found == null) {
log.warn("stop: no subid tag found on node ("+node.getFqdn()+"/"+ ip4 +") and no server had this ip4");
if (log.isWarnEnabled()) log.warn("stop: no subid tag found on node ("+node.getFqdn()+"/"+ ip4 +") and no server had this ip4");
return null;
}
if (!found.hasTag(TAG_INSTANCE_ID)) {
log.warn("stop: no subid tag found on node ("+node.getFqdn()+"/"+ ip4 +"), cannot stop");
if (log.isWarnEnabled()) log.warn("stop: no subid tag found on node ("+node.getFqdn()+"/"+ ip4 +"), cannot stop");
return null;
}
return found;
@@ -288,16 +291,19 @@ public class VultrDriver extends ComputeServiceDriverBase {
final JsonNode subId = jsonNode.get(VULTR_SUBID);
final JsonNode ip4 = jsonNode.get(VULTR_V4_IP);
final JsonNode ip6 = jsonNode.get(VULTR_V6_IP);
return (subId != null && node.hasTag(TAG_INSTANCE_ID) && subId.textValue().equals(node.getTag(TAG_INSTANCE_ID)))
|| (ip4 != null && node.hasIp4() && ip4.textValue().equals(node.getIp4()))
|| (ip6 != null && node.hasIp6() && ip6.textValue().equals(node.getIp6())) ? node : null;
if (log.isTraceEnabled()) log.trace("listNode("+node.id()+") found node: "+json(jsonNode, COMPACT_MAPPER));
if (subId != null) node.setTag(TAG_INSTANCE_ID, subId.textValue());
return node.setIp4(ip4 == null ? null : ip4.textValue()).setIp6(ip6 == null ? null : ip6.textValue());

} catch (Exception e) {
log.error("listNode: error finding node "+node.id()+", status="+listResponse.getStatus()+": "+listResponse+": exception="+shortError(e));
if (log.isErrorEnabled()) log.error("listNode: error finding node "+node.id()+", status="+listResponse.getStatus()+": "+listResponse+": exception="+shortError(e));
return null;
}
case NOT_FOUND: return null;
case NOT_FOUND:
if (log.isDebugEnabled()) log.debug("listNode("+node.id()+") returned 404 Not Found");
return null;
default:
log.error("listNode: error finding node "+node.id()+", status="+listResponse.getStatus()+": "+listResponse);
if (log.isErrorEnabled()) log.error("listNode: error finding node "+node.id()+", status="+listResponse.getStatus()+": "+listResponse);
return null;
}
}
@@ -320,7 +326,7 @@ public class VultrDriver extends ComputeServiceDriverBase {
final String subid = iter.next();
final ObjectNode server = (ObjectNode) entity.get(subid);
if (!filter.apply(server)) {
log.debug("Skipping node without cloud tag "+cloud.getUuid()+": "+subid);
if (log.isTraceEnabled()) log.trace("Skipping node without cloud tag "+cloud.getUuid()+": "+subid);
continue;
}
final String subId = server.has(VULTR_SUBID) ? server.get(VULTR_SUBID).textValue() : null;
@@ -336,41 +342,53 @@ public class VultrDriver extends ComputeServiceDriverBase {
}

@Override public BubbleNode status(BubbleNode node) throws Exception {
if (node.hasTag(TAG_INSTANCE_ID)) {
final BubbleNode found = listNode(node);
if (found == null) return node.setState(BubbleNodeState.stopped);
return node;

} else if (node.hasIp4()) {
// find by IPv4
final HttpRequestBean listServerRequest = auth(new HttpRequestBean(LIST_SERVERS_URL));
final HttpResponseBean listResponse = getResponse(listServerRequest);
switch (listResponse.getStatus()) {
case OK:
final JsonNode entity = json(listResponse.getEntityString(), JsonNode.class);
for (Iterator<String> iter = entity.fieldNames(); iter.hasNext(); ) {
final String subid = iter.next();
final ObjectNode server = (ObjectNode) entity.get(subid);
final String ip4 = server.has(VULTR_V4_IP) ? server.get(VULTR_V4_IP).textValue() : "";
if (ip4.equals(node.getIp4())) {
if (server.has("power_status") && server.get("power_status").textValue().equals("running")
&& server.has("server_state") && server.get("server_state").textValue().equals("ok")) {
final String ip6 = server.has(VULTR_V6_IP) ? server.get(VULTR_V6_IP).textValue() : null;
return node.setIp4(ip4).setIp6(ip6).setState(BubbleNodeState.running);
// find by label
final HttpRequestBean listServerRequest = auth(new HttpRequestBean(LIST_SERVERS_URL+"?"+VULTR_LABEL+"="+node.getFqdn()));
final HttpResponseBean listResponse = getResponse(listServerRequest);
switch (listResponse.getStatus()) {
case OK:
final JsonNode entity = json(listResponse.getEntityString(), JsonNode.class);
for (Iterator<String> iter = entity.fieldNames(); iter.hasNext(); ) {
final String subid = iter.next();
final ObjectNode server = (ObjectNode) entity.get(subid);
final String label = server.has(VULTR_LABEL) ? server.get(VULTR_LABEL).textValue() : "";
if (label.equals(node.getFqdn())) {
if (log.isDebugEnabled()) log.debug("status("+node.id()+"): found json: "+json(server, COMPACT_MAPPER));
if (server.has(VULTR_SERVER_STATE) && server.has(VULTR_STATUS)) {
final String status = server.get(VULTR_STATUS).textValue();
final String serverState = server.get(VULTR_SERVER_STATE).textValue();
final String ip4 = server.has(VULTR_V4_IP) ? server.get(VULTR_V4_IP).textValue() : null;
final String ip6 = server.has(VULTR_V6_IP) ? server.get(VULTR_V6_IP).textValue() : null;
node.setIp4(ip4).setIp6(ip6);
if (status.equals(VULTR_STATUS_PENDING) || serverState.equals(VULTR_STATE_NONE)) {
if (log.isDebugEnabled()) log.debug("status("+node.id()+"): pending/none: returning node status==starting");
return node.setState(BubbleNodeState.starting);
}
if (status.equals(VULTR_STATUS_ACTIVE)) {
if (serverState.equals(VULTR_STATE_OK)) {
if (log.isDebugEnabled()) log.debug("status(" + node.id() + "): active/ok: returning node status==running");
return node.setState(BubbleNodeState.running);

} else if (serverState.equals(VULTR_STATE_LOCKED)) {
if (log.isDebugEnabled()) log.debug("status(" + node.id() + "): active/locked: returning node status==starting");
return node.setState(BubbleNodeState.starting);
}
}
if (log.isDebugEnabled()) log.debug("status("+node.id()+"): status/state = "+status+"/"+serverState+": returning node status==unknown_error");
return node.setState(BubbleNodeState.unknown_error);
}
}
case NOT_FOUND: case PRECONDITION_FAILED:
log.error("status: error response from API, returning unknown");
return node.setState(BubbleNodeState.unknown_error);
}
log.error("status: error finding node "+node.id()+", status="+listResponse.getStatus()+": "+listResponse);
return node.setState(BubbleNodeState.unknown_error);

default:
log.error("status: error finding node "+node.id()+", status="+listResponse.getStatus()+": "+listResponse);
return node.setState(BubbleNodeState.unknown_error);
}
} else {
// Node has no IP4
return node.setState(BubbleNodeState.unknown_error);
case NOT_FOUND: case PRECONDITION_FAILED:
log.error("status: error response from API, returning unknown");
return node.setState(BubbleNodeState.unknown_error);
default:
log.error("status: error finding node "+node.id()+", status="+listResponse.getStatus()+": "+listResponse);
return node.setState(BubbleNodeState.unknown_error);
}
}



+ 3
- 2
bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java Bestand weergeven

@@ -34,7 +34,8 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import static bubble.ApiConstants.*;
import static bubble.ApiConstants.EP_NETWORKS;
import static bubble.ApiConstants.ROOT_NETWORK_UUID;
import static bubble.model.cloud.BubbleDomain.DOMAIN_NAME_MAXLEN;
import static bubble.model.cloud.BubbleNetworkState.created;
import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE;
@@ -52,7 +53,7 @@ import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD;
@ECTypeChild(type=BubbleNode.class, backref="network")
})
@Entity @NoArgsConstructor @Accessors(chain=true)
@Slf4j @ToString(of={"name", "domainName", "installType"})
@Slf4j @ToString(of={"name", "domainName", "installType", "state"})
@ECIndexes({
@ECIndex(unique=true, of={"account", "name"}),
@ECIndex(unique=true, of={"name", "domainName"})


+ 1
- 1
bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java Bestand weergeven

@@ -16,6 +16,6 @@ public enum BubbleNetworkState {

public boolean canStart() { return this == created || this == stopped; }

public boolean canStop() { return this != stopping && this != stopped && this != error_stopping; }
public boolean canStop() { return this != stopped && this != error_stopping; }

}

+ 4
- 0
bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java Bestand weergeven

@@ -254,4 +254,8 @@ public class BubbleNode extends IdentifiableBase implements HasNetwork, HasBubbl
}
if (!hasIp4() || !hasIp6()) throw new TimeoutException("waitForIpAddresses: timeout");
}

@Transient @Getter @Setter private BubbleVersionInfo sageVersion;
public boolean hasSageVersion () { return sageVersion != null && sageVersion.valid(); }

}

+ 17
- 0
bubble-server/src/main/java/bubble/model/cloud/BubbleVersionInfo.java Bestand weergeven

@@ -0,0 +1,17 @@
package bubble.model.cloud;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

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

@Accessors(chain=true)
public class BubbleVersionInfo {

@Getter @Setter private String version;
@Getter @Setter private String sha256;

public boolean valid() { return !empty(version) && !empty(sha256); }

}

+ 4
- 0
bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java Bestand weergeven

@@ -37,6 +37,10 @@ public enum NotificationType {
health_check, hello_to_sage, hello_from_sage, peer_hello, sync_password,
register_backup, retrieve_backup, backup_response, restore_complete, fork,

// upgrade notifications
upgrade_request (String.class),
upgrade_response (true),

// driver-level notifications

// delegated dns driver notifications


+ 1
- 1
bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_to_sage.java Bestand weergeven

@@ -32,7 +32,7 @@ public class NotificationHandler_hello_to_sage extends ReceivedNotificationHandl
log.info("hello_to_sage: returning peers: "+peers.stream().map(BubbleNode::getFqdn).collect(joining(", ")));
node.setPeers(peers);

notificationService.notify(node, hello_from_sage, node);
notificationService.notify(node, hello_from_sage, node.setSageVersion(configuration.getVersionInfo()));
}
}
}

+ 2
- 1
bubble-server/src/main/java/bubble/notify/compute/NotificationHandler_compute_driver_stop.java Bestand weergeven

@@ -19,7 +19,8 @@ public class NotificationHandler_compute_driver_stop extends NotificationHandler
@Override protected BubbleNode handle(ReceivedNotification n,
ComputeDriverNotification notification,
ComputeServiceDriver compute) throws Exception {
return nodeService.stopNode(compute, notification.getNode());
nodeService.stopNode(compute, notification.getNode());
return notification.getNode();
}

}

+ 18
- 0
bubble-server/src/main/java/bubble/notify/upgrade/JarUpgradeNotification.java Bestand weergeven

@@ -0,0 +1,18 @@
package bubble.notify.upgrade;

import bubble.model.cloud.BubbleVersionInfo;
import bubble.notify.SynchronousNotification;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

@NoArgsConstructor @AllArgsConstructor @Accessors(chain=true)
public class JarUpgradeNotification extends SynchronousNotification {

@Getter @Setter private BubbleVersionInfo versionInfo;

@Override protected String getCacheKey() { return versionInfo.getSha256(); }

}

+ 28
- 0
bubble-server/src/main/java/bubble/notify/upgrade/NotificationHandler_upgrade_request.java Bestand weergeven

@@ -0,0 +1,28 @@
package bubble.notify.upgrade;

import bubble.dao.cloud.BubbleNodeDAO;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.notify.ReceivedNotification;
import bubble.notify.DelegatedNotificationHandlerBase;
import bubble.service.boot.BubbleJarUpgradeService;
import org.springframework.beans.factory.annotation.Autowired;

import static bubble.model.cloud.notify.NotificationType.upgrade_response;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;

public class NotificationHandler_upgrade_request extends DelegatedNotificationHandlerBase {

@Autowired private BubbleNodeDAO nodeDAO;
@Autowired private BubbleJarUpgradeService upgradeService;

@Override public void handleNotification(ReceivedNotification n) {
final BubbleNode sender = nodeDAO.findByUuid(n.getFromNode());
if (sender == null) {
die("sender not found: "+n.getFromNode());
} else {
final String key = upgradeService.registerNodeUpgrade(sender.getUuid());
notifySender(upgrade_response, n.getNotificationId(), sender, key);
}
}

}

+ 29
- 0
bubble-server/src/main/java/bubble/resources/account/AuthResource.java Bestand weergeven

@@ -30,6 +30,7 @@ import bubble.service.account.StandardAuthenticatorService;
import bubble.service.backup.RestoreService;
import bubble.service.bill.PromotionService;
import bubble.service.boot.ActivationService;
import bubble.service.boot.BubbleJarUpgradeService;
import bubble.service.boot.NodeManagerService;
import bubble.service.boot.SageHelloService;
import bubble.service.cloud.DeviceIdService;
@@ -675,4 +676,32 @@ public class AuthResource {
return send(new FileSendableResource(patch));
}

@Autowired private BubbleJarUpgradeService upgradeService;

@GET @Path(EP_UPGRADE+"/{key}")
@Produces(APPLICATION_OCTET_STREAM)
public Response getUpgrade(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("key") String key) {
final String nodeUuid = upgradeService.getNodeForKey(key);
if (nodeUuid == null) {
log.warn("getUpgrade: key not found: "+key);
return unauthorized();
}

final BubbleNode node = nodeDAO.findByUuid(nodeUuid);
if (node == null) {
log.warn("getUpgrade: node not found: "+nodeUuid);
return unauthorized();
}

final String remoteAddr = req.getRemoteAddr();
if (!node.hasSameIp(remoteAddr)) {
log.warn("getUpgrade: node has wrong IP (request came from "+remoteAddr+"): "+node.id());
return unauthorized();
}

return send(new FileSendableResource(configuration.getBubbleJar()));
}

}

+ 11
- 2
bubble-server/src/main/java/bubble/resources/account/MeResource.java Bestand weergeven

@@ -26,6 +26,7 @@ import bubble.server.BubbleConfiguration;
import bubble.service.account.StandardAccountMessageService;
import bubble.service.account.StandardAuthenticatorService;
import bubble.service.account.download.AccountDownloadService;
import bubble.service.boot.BubbleJarUpgradeService;
import bubble.service.boot.BubbleModelSetupService;
import bubble.service.cloud.NodeLaunchMonitor;
import com.fasterxml.jackson.databind.JsonNode;
@@ -65,8 +66,7 @@ import java.util.Map;
import static bubble.ApiConstants.*;
import static bubble.model.account.Account.validatePassword;
import static bubble.resources.account.AuthResource.forgotPasswordMessage;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.daemon.ZillaRuntime.errorString;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpContentTypes.*;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
@@ -406,4 +406,13 @@ public class MeResource {
return ok(modelSetupService.setupModel(api, caller, modelFile));
}

@Autowired private BubbleJarUpgradeService jarUpgradeService;

@POST @Path(EP_UPGRADE)
public Response uploadModel(@Context Request req,
@Context ContainerRequest ctx) {
background(() -> jarUpgradeService.upgrade());
return ok(configuration.getPublicSystemConfigs());
}

}

+ 25
- 5
bubble-server/src/main/java/bubble/server/BubbleConfiguration.java Bestand weergeven

@@ -11,10 +11,7 @@ import bubble.client.BubbleApiClient;
import bubble.cloud.CloudServiceDriver;
import bubble.dao.account.AccountDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.cloud.AnsibleInstallType;
import bubble.model.cloud.BubbleNetwork;
import bubble.model.cloud.BubbleNetworkState;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.*;
import bubble.model.device.DeviceSecurityLevel;
import bubble.server.listener.BubbleFirstTimeListener;
import bubble.service.backup.RestoreService;
@@ -69,6 +66,7 @@ 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;
import static org.cobbzilla.wizard.model.SemanticVersion.isNewerVersion;

@Configuration @NoArgsConstructor @Slf4j
public class BubbleConfiguration extends PgRestServerConfiguration
@@ -95,6 +93,8 @@ public class BubbleConfiguration extends PgRestServerConfiguration
public static final String TAG_SECURITY_LEVELS = "securityLevels";
public static final String TAG_RESTORE_MODE = "awaitingRestore";
public static final String TAG_RESTORING_IN_PROGRESS = "restoringInProgress";
public static final String TAG_JAR_VERSION = "jarVersion";
public static final String TAG_JAR_UPGRADE_AVAILABLE = "jarUpgradeAvailable";

public static final String DEFAULT_LOCAL_STORAGE_DIR = HOME_DIR + "/.bubble_local_storage";

@@ -245,6 +245,24 @@ public class BubbleConfiguration extends PgRestServerConfiguration
}
return properties.getProperty(META_PROP_BUBBLE_VERSION);
}
@Getter(lazy=true) private final BubbleVersionInfo versionInfo = initBubbleVersionInfo();
private BubbleVersionInfo initBubbleVersionInfo() {
return new BubbleVersionInfo()
.setVersion(getVersion())
.setSha256(getJarSha());
}

@Getter private BubbleVersionInfo sageVersionInfo;
public void setSageVersionInfo(BubbleVersionInfo version) {
sageVersionInfo = version;
final boolean isNewer = isNewerVersion(getVersionInfo().getVersion(), sageVersionInfo.getVersion());
if (!jarUpgradeAvailable && isNewer) {
jarUpgradeAvailable = true;
refreshPublicSystemConfigs();
}
}
public boolean hasSageVersionInfo () { return sageVersionInfo != null; }
@Getter private Boolean jarUpgradeAvailable = false;

@JsonIgnore public String getUnlockKey () { return BubbleFirstTimeListener.getUnlockKey(); }

@@ -320,7 +338,9 @@ public class BubbleConfiguration extends PgRestServerConfiguration
&& getBean(RestoreService.class).isSelfRestoreStarted()},
{TAG_SSL_PORT, getDefaultSslPort()},
{TAG_SUPPORT, getSupport()},
{TAG_SECURITY_LEVELS, DeviceSecurityLevel.values()}
{TAG_SECURITY_LEVELS, DeviceSecurityLevel.values()},
{TAG_JAR_VERSION, getVersion()},
{TAG_JAR_UPGRADE_AVAILABLE, getJarUpgradeAvailable() ? getSageVersionInfo() : null}
}));
} else {
// some things has to be refreshed all the time in some cases:


+ 93
- 0
bubble-server/src/main/java/bubble/service/boot/BubbleJarUpgradeService.java Bestand weergeven

@@ -0,0 +1,93 @@
package bubble.service.boot;

import bubble.dao.cloud.BubbleBackupDAO;
import bubble.model.cloud.BackupStatus;
import bubble.model.cloud.BubbleBackup;
import bubble.model.cloud.BubbleVersionInfo;
import bubble.notify.upgrade.JarUpgradeNotification;
import bubble.server.BubbleConfiguration;
import bubble.service.backup.BackupService;
import bubble.service.notify.NotificationService;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.http.HttpRequestBean;
import org.cobbzilla.wizard.cache.redis.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.File;

import static bubble.ApiConstants.AUTH_ENDPOINT;
import static bubble.ApiConstants.EP_UPGRADE;
import static bubble.client.BubbleNodeClient.nodeBaseUri;
import static bubble.model.cloud.notify.NotificationType.upgrade_request;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
import static org.cobbzilla.util.daemon.ZillaRuntime.now;
import static org.cobbzilla.util.http.HttpMethods.GET;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.util.time.TimeUtil.DATE_FORMAT_YYYY_MM_DD_HH_mm_ss_SSS;
import static org.cobbzilla.wizard.cache.redis.RedisService.EX;

@Service @Slf4j
public class BubbleJarUpgradeService {

private static final long PRE_UPGRADE_BACKUP_TIMEOUT = MINUTES.toMillis(20);

@Autowired private BubbleConfiguration configuration;
@Autowired private BackupService backupService;
@Autowired private BubbleBackupDAO backupDAO;
@Autowired private NotificationService notificationService;
@Autowired private RedisService redis;

@Getter(lazy=true) private final RedisService nodeUpgradeRequests = redis.prefixNamespace(getClass().getName());

public String registerNodeUpgrade(String nodeUuid) {
final String key = randomAlphanumeric(10) + "." + now();
getNodeUpgradeRequests().set(key, nodeUuid, EX, MINUTES.toMillis(1));
return key;
}

public String getNodeForKey(String key) { return getNodeUpgradeRequests().get(key); }

public void upgrade() {
if (!configuration.getJarUpgradeAvailable()) {
log.warn("upgrade: No upgrade available, returning");
return;
}
final String currentVersion = configuration.getVersion();
final BubbleVersionInfo sageVersion = configuration.getSageVersionInfo();
final String newVersion = sageVersion.getVersion();
BubbleBackup bubbleBackup = backupService.queueBackup("before_upgrade_" + currentVersion + "_to_" + newVersion + "_on_" + DATE_FORMAT_YYYY_MM_DD_HH_mm_ss_SSS.print(now()));

// monitor backup, ensure it completes
final long start = now();
while (bubbleBackup.getStatus() != BackupStatus.backup_completed && now() - start < PRE_UPGRADE_BACKUP_TIMEOUT) {
sleep(SECONDS.toMillis(5), "waiting for backup to complete before upgrading");
bubbleBackup = backupDAO.findByUuid(bubbleBackup.getUuid());
}
if (bubbleBackup.getStatus() != BackupStatus.backup_completed) {
log.warn("upgrade: timeout waiting for backup to complete, status="+bubbleBackup.getStatus());
return;
}

final File upgradeJar = new File(configuration.getBubbleJar().getParentFile(), ".upgrade.jar");
if (upgradeJar.exists()) {
log.error("upgrade: jar already exists, not upgrading: "+abs(upgradeJar));
return;
}

// ask the sage to allow us to download the upgrade
final String key = notificationService.notifySync(configuration.getSageNode(), upgrade_request, new JarUpgradeNotification(sageVersion));

// request the jar from the sage
final String uri = nodeBaseUri(configuration.getSageNode(), configuration) + AUTH_ENDPOINT + EP_UPGRADE + "/" + key;
final HttpRequestBean requestBean = new HttpRequestBean(GET, uri);
final File newJar = temp(".jar");

// move to upgrade location
renameOrDie(newJar, upgradeJar);
}
}

+ 35
- 26
bubble-server/src/main/java/bubble/service/cloud/NodeLaunchException.java Bestand weergeven

@@ -9,50 +9,59 @@ import lombok.Getter;

public class NodeLaunchException extends RuntimeException {

public enum NodeLaunchExceptionType { fatal, canRetry, interrupted }

@Getter private final BubbleNode node;
public boolean hasNode () { return node != null; }
public String nodeSummary () { return node == null ? "null" : node.id()+"/"+node.getState(); }

@Getter private final boolean fatal;
@Getter private final NodeLaunchExceptionType type;

private NodeLaunchException (BubbleNode node, Exception e, String message, boolean fatal) {
private NodeLaunchException (BubbleNode node, Exception e, String message, NodeLaunchExceptionType type) {
super(message, e);
this.node = node;
this.fatal = fatal;
this.type = type;
}

private NodeLaunchException (BubbleNode node, Exception e, boolean fatal) {
this(node, e, e.getMessage(), fatal);
private NodeLaunchException (BubbleNode node, Exception e, NodeLaunchExceptionType type) {
this(node, e, e.getMessage(), type);
}

private NodeLaunchException (BubbleNode node, String message, boolean fatal) {
this(node, null, message, fatal);
private NodeLaunchException (BubbleNode node, String message, NodeLaunchExceptionType type) {
this(node, null, message, type);
}

private NodeLaunchException (Exception e, String message, boolean fatal) {
this(null, e, message, fatal);
private NodeLaunchException (Exception e, String message, NodeLaunchExceptionType type) {
this(null, e, message, type);
}

private NodeLaunchException (String message, boolean fatal) {
this(null, null, message, fatal);
private NodeLaunchException (String message, NodeLaunchExceptionType type) {
this(null, null, message, type);
}

private NodeLaunchException (Exception e, boolean fatal) {
this(null, e, e.getMessage(), fatal);
private NodeLaunchException (Exception e, NodeLaunchExceptionType type) {
this(null, e, e.getMessage(), type);
}

public static <T> T fatalLaunchFailure (String message) { throw new NodeLaunchException(message, true); }
public static <T> T fatalLaunchFailure (Exception e, String message) { throw new NodeLaunchException(e, message, true); }
public static <T> T fatalLaunchFailure (Exception e) { throw new NodeLaunchException(e, true); }
public static <T> T fatalLaunchFailure (BubbleNode node, String message) { throw new NodeLaunchException(node, message, true); }
public static <T> T fatalLaunchFailure (BubbleNode node, Exception e) { throw new NodeLaunchException(node, e, true); }
public static <T> T fatalLaunchFailure (BubbleNode node, Exception e, String message) { throw new NodeLaunchException(node, e, message, true); }

public static <T> T launchFailureCanRetry (String message) { throw new NodeLaunchException(message, false); }
public static <T> T launchFailureCanRetry (Exception e, String message) { throw new NodeLaunchException(e, message, false); }
public static <T> T launchFailureCanRetry (Exception e) { throw new NodeLaunchException(e, false); }
public static <T> T launchFailureCanRetry (BubbleNode node, String message) { throw new NodeLaunchException(node, message, false); }
public static <T> T launchFailureCanRetry (BubbleNode node, Exception e) { throw new NodeLaunchException(node, e, false); }
public static <T> T launchFailureCanRetry (BubbleNode node, Exception e, String message) { throw new NodeLaunchException(node, e, message, false); }
public static <T> T fatalLaunchFailure (String message) { throw new NodeLaunchException(message, NodeLaunchExceptionType.fatal); }
public static <T> T fatalLaunchFailure (Exception e, String message) { throw new NodeLaunchException(e, message, NodeLaunchExceptionType.fatal); }
public static <T> T fatalLaunchFailure (Exception e) { throw new NodeLaunchException(e, NodeLaunchExceptionType.fatal); }
public static <T> T fatalLaunchFailure (BubbleNode node, String message) { throw new NodeLaunchException(node, message, NodeLaunchExceptionType.fatal); }
public static <T> T fatalLaunchFailure (BubbleNode node, Exception e) { throw new NodeLaunchException(node, e, NodeLaunchExceptionType.fatal); }
public static <T> T fatalLaunchFailure (BubbleNode node, Exception e, String message) { throw new NodeLaunchException(node, e, message, NodeLaunchExceptionType.fatal); }

public static <T> T launchFailureCanRetry (String message) { throw new NodeLaunchException(message, NodeLaunchExceptionType.canRetry); }
public static <T> T launchFailureCanRetry (Exception e, String message) { throw new NodeLaunchException(e, message, NodeLaunchExceptionType.canRetry); }
public static <T> T launchFailureCanRetry (Exception e) { throw new NodeLaunchException(e, NodeLaunchExceptionType.canRetry); }
public static <T> T launchFailureCanRetry (BubbleNode node, String message) { throw new NodeLaunchException(node, message, NodeLaunchExceptionType.canRetry); }
public static <T> T launchFailureCanRetry (BubbleNode node, Exception e) { throw new NodeLaunchException(node, e, NodeLaunchExceptionType.canRetry); }
public static <T> T launchFailureCanRetry (BubbleNode node, Exception e, String message) { throw new NodeLaunchException(node, e, message, NodeLaunchExceptionType.canRetry); }

public static <T> T launchInterrupted (String message) { throw new NodeLaunchException(message, NodeLaunchExceptionType.interrupted); }
public static <T> T launchInterrupted (Exception e, String message) { throw new NodeLaunchException(e, message, NodeLaunchExceptionType.interrupted); }
public static <T> T launchInterrupted (Exception e) { throw new NodeLaunchException(e, NodeLaunchExceptionType.interrupted); }
public static <T> T launchInterrupted (BubbleNode node, String message) { throw new NodeLaunchException(node, message, NodeLaunchExceptionType.interrupted); }
public static <T> T launchInterrupted (BubbleNode node, Exception e) { throw new NodeLaunchException(node, e, NodeLaunchExceptionType.interrupted); }
public static <T> T launchInterrupted (BubbleNode node, Exception e, String message) { throw new NodeLaunchException(node, e, message, NodeLaunchExceptionType.interrupted); }

}

+ 2
- 5
bubble-server/src/main/java/bubble/service/cloud/NodeLaunchMonitor.java Bestand weergeven

@@ -28,8 +28,7 @@ import java.util.concurrent.ConcurrentHashMap;

import static bubble.service.cloud.NodeProgressMeter.getProgressMeterKey;
import static bubble.service.cloud.NodeProgressMeter.getProgressMeterPrefix;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.concurrent.TimeUnit.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.json.JsonUtil.json;

@@ -37,7 +36,7 @@ import static org.cobbzilla.util.json.JsonUtil.json;
public class NodeLaunchMonitor extends SimpleDaemon {

private static final long LAUNCH_ACTIVITY_TIMEOUT = SECONDS.toMillis(180);
private static final long LAUNCH_TERMINATE_TIMEOUT = SECONDS.toMillis(5);
private static final long LAUNCH_TERMINATE_TIMEOUT = MINUTES.toMillis(6);

@Getter private final long sleepTime = SECONDS.toMillis(15);

@@ -50,7 +49,6 @@ public class NodeLaunchMonitor extends SimpleDaemon {
@Getter(lazy=true) private final RedisService networkSetupStatus = redis.prefixNamespace(getClass().getSimpleName()+"_status_");

private final Map<String, LauncherEntry> launcherThreads = new ConcurrentHashMap<>();
private final Map<String, String> canceledNetworks = new ExpirationMap<>(50, HOURS.toMillis(2));

public void register(String nnUuid, String networkUuid, Thread t) {
startIfNotRunning();
@@ -63,7 +61,6 @@ public class NodeLaunchMonitor extends SimpleDaemon {
}

public void cancel(String networkUuid) {
canceledNetworks.put(networkUuid, networkUuid);
final LauncherEntry previousLaunch = launcherThreads.get(networkUuid);
if (previousLaunch == null || !previousLaunch.isAlive()) {
log.warn("cancel("+networkUuid+"): entry does not thread exist, or is not alive: "+previousLaunch);


+ 27
- 10
bubble-server/src/main/java/bubble/service/cloud/NodeLauncher.java Bestand weergeven

@@ -13,8 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.system.Sleep.sleep;

@AllArgsConstructor @Slf4j
@@ -33,12 +32,13 @@ public class NodeLauncher implements Runnable {
try {
for (int i=0; i<LAUNCH_MAX_RETRIES; i++) {
if (i > 0 && !launchMonitor.isRegistered(networkUuid)) {
throw new IllegalStateException("NodeLauncher.run: no longer registered: "+networkUuid);
log.warn("NodeLauncher.run: no longer registered: "+networkUuid);
return;
}
if (!lock.get().equals(newNodeRequest.getLock())) {
die("NodeLauncher.run: existingLock (" + lock.get() + ") is different than lock in NewNodeNotification: " + newNodeRequest.getLock());
}
if (!networkService.confirmLock(networkUuid, lock.get())) {
if (!networkService.confirmNetLock(networkUuid, lock.get())) {
die("NodeLauncher.run: error confirming lock (" + lock.get() + ") for network: " + networkUuid);
}

@@ -51,20 +51,37 @@ public class NodeLauncher implements Runnable {

log.info("NodeLauncher.run: launching node..."+newNodeRequest.getFqdn());
launchThread.start();
launchThread.join();
do {
launchThread.join(SECONDS.toMillis(5));
if (log.isTraceEnabled()) log.trace("NodeLauncher.run: still waiting for thread join: "+newNodeRequest.getFqdn()+" stack="+stacktrace(launchThread));
} while (launchThread.isAlive() && !launchThread.isInterrupted());

if (launchThread.isInterrupted()) {
log.warn("NodeLauncher.run: launch interrupted while waiting for join, exiting early");
return;
}

final Exception exception = exceptionRef.get();
final BubbleNode node = nodeRef.get();
log.debug("NodeLauncher.run: node="+(node == null ? "null" : node.id())+", exception="+shortError(exception));
if (exception != null) {
if (exception instanceof NodeLaunchException) {
final NodeLaunchException launchException = (NodeLaunchException) exception;
if (launchException.isFatal()) {
die("NodeLauncher.run: fatal launch exception: " + shortError(launchException));
} else {
log.warn("NodeLauncher.run: nonfatal launch exception for node " + launchException.nodeSummary() + " : " + shortError(launchException));
switch (launchException.getType()) {
case fatal:
die("NodeLauncher.run: fatal launch exception: " + shortError(launchException));
break;
case interrupted:
log.warn("NodeLauncher.run: launch interrupted, exiting early");
return;
case canRetry:
log.warn("NodeLauncher.run: nonfatal launch exception for node " + launchException.nodeSummary() + " : " + shortError(launchException));
break;
default:
die("NodeLauncher.run: unknown launch exception (type="+launchException.getType()+"): "+shortError(launchException));
}
} else {
die("NodeLauncher.run: fatal launch exception: " + shortError(exception));
die("NodeLauncher.run: fatal launch exception: " + shortError(exception), exception);
}
}
if (node != null && node.isRunning()) {


+ 5
- 1
bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeter.java Bestand weergeven

@@ -59,7 +59,7 @@ public class NodeProgressMeter extends PipedOutputStream implements Runnable {
public void touch() {
if (now() > lastTouch + MAX_TOUCH_INTERVAL) {
launchMonitor.touch(nn.getNetwork());
networkService.confirmLock(nn.getNetwork(), nn.getLock());
networkService.confirmNetLock(nn.getNetwork(), nn.getLock());
lastTouch = now();
}
}
@@ -99,6 +99,10 @@ public class NodeProgressMeter extends PipedOutputStream implements Runnable {

public void write(String line) throws IOException {
touch();
if (closed.get()) {
log.warn("write("+line+"): stream closed, not writing");
return;
}
writer.write(line.endsWith("\n") ? line : line+"\n");
writer.flush();
}


+ 12
- 34
bubble-server/src/main/java/bubble/service/cloud/NodeService.java Bestand weergeven

@@ -6,10 +6,10 @@ package bubble.service.cloud;

import bubble.cloud.compute.ComputeServiceDriver;
import bubble.dao.cloud.BubbleDomainDAO;
import bubble.dao.cloud.BubbleNetworkDAO;
import bubble.dao.cloud.BubbleNodeDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.cloud.*;
import bubble.model.cloud.BubbleDomain;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.wizard.validation.SimpleViolationException;
@@ -18,58 +18,36 @@ import org.springframework.stereotype.Service;

import javax.persistence.EntityNotFoundException;

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

@Service @Slf4j
public class NodeService {

@Autowired private BubbleNodeDAO nodeDAO;
@Autowired private BubbleNetworkDAO networkDAO;
@Autowired private BubbleDomainDAO domainDAO;
@Autowired private CloudServiceDAO cloudDAO;
@Autowired private BubbleConfiguration configuration;

public BubbleNode stopNode(ComputeServiceDriver compute, BubbleNode node) {
public void stopNode(ComputeServiceDriver compute, BubbleNode node) {
log.info("stopNode: starting for node "+node.id());

final BubbleDomain domain = domainDAO.findByUuid(node.getDomain());
final CloudService dns = cloudDAO.findByUuid(domain.getPublicDns());

node.setState(BubbleNodeState.stopping);
if (node.hasUuid()) nodeDAO.update(node);
try {
log.debug("stopNode: deleting dns entries for node: "+node.id());
dns.getDnsDriver(configuration).deleteNode(node);
log.debug("stopNode: stopping instance for node: "+node.id());
node = compute.stop(node);
return safeUpdateNodeState(node, BubbleNodeState.stopped);
log.debug("stopNode: node stopped: "+node.id());

} catch (EntityNotFoundException e) {
log.info("stopNode: node not found by compute service: "+node.id()+": "+e);
return safeUpdateNodeState(node, BubbleNodeState.unreachable);
log.warn("stopNode: node not found by compute service: "+node.id()+": "+e);

} catch (SimpleViolationException e) {
log.info("stopNode: error stopping "+node.id()+": "+e);
log.warn("stopNode: error stopping "+node.id()+": "+e);
throw e;

} catch (Exception e) {
log.info("stopNode: error stopping "+node.id());
return die("stopNode: "+e, e);
}
}

public BubbleNode safeUpdateNodeState(BubbleNode node, BubbleNodeState newState) {
// ensure node still exists
final BubbleNode existingNode = nodeDAO.findByUuid(node.getUuid());
if (existingNode == null) {
log.warn("stopNode: node not found, not updating: " + node.id());
return node;
} else {
// ensure network still exists
final BubbleNetwork network = networkDAO.findByUuid(node.getNetwork());
if (network == null) {
log.warn("stopNode: node exists (" + node.id() + ") but network (" + node.getNetwork() + ") does not, deleting node");
nodeDAO.delete(node.getUuid());
return node;
}
return nodeDAO.update(node.setState(newState));
log.warn("stopNode: error stopping "+node.id()+": "+shortError(e));
}
}



+ 58
- 41
bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java Bestand weergeven

@@ -49,6 +49,7 @@ 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.cobbzilla.util.system.SleepInterruptedException;
import org.cobbzilla.wizard.api.ApiException;
import org.cobbzilla.wizard.cache.redis.RedisService;
import org.cobbzilla.wizard.validation.MultiViolationException;
@@ -76,8 +77,7 @@ import static bubble.model.cloud.BubbleNode.TAG_ERROR;
import static bubble.server.BubbleConfiguration.DEBUG_NODE_INSTALL_FILE;
import static bubble.server.BubbleConfiguration.ENV_DEBUG_NODE_INSTALL;
import static bubble.service.boot.StandardSelfNodeService.*;
import static bubble.service.cloud.NodeLaunchException.fatalLaunchFailure;
import static bubble.service.cloud.NodeLaunchException.launchFailureCanRetry;
import static bubble.service.cloud.NodeLaunchException.*;
import static bubble.service.cloud.NodeProgressMeterConstants.*;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -136,7 +136,8 @@ public class StandardNetworkService implements NetworkService {
@Autowired private NodeLaunchMonitor launchMonitor;

@Autowired private RedisService redisService;
@Getter(lazy=true) private final RedisService networkLocks = redisService.prefixNamespace(getClass().getSimpleName()+"_lock_");
@Getter(lazy=true) private final RedisService networkLocks = redisService.prefixNamespace(getClass().getSimpleName()+"_net_lock_");
@Getter(lazy=true) private final RedisService nodeKillLocks = redisService.prefixNamespace(getClass().getSimpleName()+"_node_kill_lock_");

@NonNull public BubbleNode newNode(@NonNull final NewNodeNotification nn,
NodeLaunchMonitor launchMonitor) {
@@ -148,11 +149,12 @@ public class StandardNetworkService implements NetworkService {
NodeProgressMeter progressMeter = null;
final BubbleNetwork network = nn.getNetworkObject();
final ExecutorService backgroundJobs = DaemonThreadFactory.fixedPool(3);
boolean killNode = false;
try {
progressMeter = launchMonitor.getProgressMeter(nn);
progressMeter.write(METER_TICK_CONFIRMING_NETWORK_LOCK);

if (!confirmLock(nn.getNetwork(), lock)) {
if (!confirmNetLock(nn.getNetwork(), lock)) {
progressMeter.error(METER_ERROR_CONFIRMING_NETWORK_LOCK);
return launchFailureCanRetry("newNode: Error confirming network lock");
}
@@ -245,7 +247,7 @@ public class StandardNetworkService implements NetworkService {
final List<Future<?>> jobFutures = new ArrayList<>();

// Start the cloud compute instance
final NodeStartJob startJob = new NodeStartJob(node, nodeDAO, computeDriver);
final NodeStartJob startJob = new NodeStartJob(node, computeDriver);
jobFutures.add(backgroundJobs.submit(startJob));

// Create DNS records for node
@@ -410,18 +412,14 @@ public class StandardNetworkService implements NetworkService {
log.info("newNode: ready in "+formatDuration(now() - start));

} catch (Exception e) {
log.error("newNode: "+e, e);
if (node != null) {
node.setState(BubbleNodeState.unknown_error);
nodeDAO.update(node);
if (!progressMeter.hasError()) progressMeter.error(METER_UNKNOWN_ERROR);
killNode(node, "error: "+e);
}
if (noNodesActive(network)) {
// if no nodes are running, then the network is stopped
networkDAO.update(network.setState(BubbleNetworkState.stopped));
if (e instanceof SleepInterruptedException) {
log.warn("newNode: interrupted!");
} else {
log.error("newNode: " + e, e);
}
killNode = node != null;
if (e instanceof NodeLaunchException) throw (NodeLaunchException) e;
if (e instanceof SleepInterruptedException) launchInterrupted("newNode: interrupted: "+shortError(e));
return die("newNode: "+e, e);

} finally {
@@ -434,15 +432,11 @@ public class StandardNetworkService implements NetworkService {
}
}

if (node != null && !node.isRunning()) {
if (node != null && (killNode || !node.isRunning())) {
node.setState(BubbleNodeState.unknown_error);
nodeDAO.update(node);
if (!progressMeter.hasError()) progressMeter.error(METER_UNKNOWN_ERROR);
killNode(node, "error: node not running: "+node.id()+": "+node.getState());
if (noNodesActive(network)) {
// if no nodes are running, then the network is stopped
networkDAO.update(network.setState(BubbleNetworkState.stopped));
}
}

if (progressMeter != null) {
@@ -501,25 +495,30 @@ public class StandardNetworkService implements NetworkService {
}

public BubbleNode killNode(BubbleNode node, String message) {
if (node == null) return die("(but node was null?): "+message);
node.setState(BubbleNodeState.error_stopping);
node.setTag(TAG_ERROR, message);
if (node.hasUuid()) nodeDAO.update(node);
if (node == null) return die("killNode: node was null (message=" + message + ")");
String lock = null;
try {
stopNode(node); // kill it
} catch (Exception e) {
log.warn("killNode("+node.id()+"): error stopping: "+e);
}
node.setState(BubbleNodeState.error_stopped);
if (node.hasUuid()) nodeDAO.update(node);
lock = lockNode(node.getUuid());
if (nodeDAO.findByUuid(node.getUuid()) == null) {
log.warn("killNode: node already deleted");
return node;
}

final BubbleNetwork network = networkDAO.findByUuid(node.getNetwork());
if (noNodesActive(network)) {
// if no nodes are running, then the network is stopped
networkDAO.update(network.setState(BubbleNetworkState.stopped));
}
node.setState(BubbleNodeState.error_stopping);
node.setTag(TAG_ERROR, message);
if (node.hasUuid()) nodeDAO.update(node);
try {
stopNode(node); // kill it
} catch (Exception e) {
log.warn("killNode(" + node.id() + "): error stopping: " + e);
}
node.setState(BubbleNodeState.error_stopped);
nodeDAO.update(node);
return node;

return node;
} finally {
if (lock != null) unlockNode(node.getUuid(), lock);
}
}

protected String lockNetwork(String network) {
@@ -529,7 +528,7 @@ public class StandardNetworkService implements NetworkService {
return lock;
}

protected boolean confirmLock(String network, String lock) {
protected boolean confirmNetLock(String network, String lock) {
return getNetworkLocks().confirmLock(network, lock);
}

@@ -539,10 +538,27 @@ public class StandardNetworkService implements NetworkService {
log.info("unlockNetwork: unlocked "+network);
}

public BubbleNode stopNode(BubbleNode node) {
protected String lockNode(String node) {
log.info("lockNode: locking "+node);
final String lock = getNodeKillLocks().lock(node, NET_LOCK_TIMEOUT, NET_DEADLOCK_TIMEOUT);
log.info("lockNode: locked "+node);
return lock;
}

protected boolean confirmNodeLock(String node, String lock) {
return getNodeKillLocks().confirmLock(node, lock);
}

protected void unlockNode(String node, String lock) {
log.info("unlockNode: unlocking "+node);
getNodeKillLocks().unlock(node, lock);
log.info("unlockNode: unlocked "+node);
}

public void stopNode(BubbleNode node) {
log.info("stopNode: stopping "+node.id());
final CloudService cloud = cloudDAO.findByUuid(node.getCloud());
return nodeService.stopNode(cloud.getComputeDriver(configuration), node);
nodeService.stopNode(cloud.getComputeDriver(configuration), node);
}

public boolean isReachable(BubbleNode node) {
@@ -550,6 +566,7 @@ public class StandardNetworkService implements NetworkService {
try {
log.info(prefix+"starting");
final NotificationReceipt receipt = notificationService.notify(node, NotificationType.health_check, null);
BubbleNodeState state = null;
if (receipt == null) {
log.info(prefix+"health_check failed, checking via cloud");
final CloudService cloud = cloudDAO.findByUuid(node.getCloud());
@@ -559,14 +576,14 @@ public class StandardNetworkService implements NetworkService {
}
final BubbleNode status = cloud.getComputeDriver(configuration).status(node);
if (status != null) {
final BubbleNodeState state = status.getState();
state = status.getState();
if (state != null && state.active()) {
log.info(prefix + "cloud status was: " + state + ", returning true");
return true;
}
}
}
log.warn(prefix+"no way of reaching node, returning false");
log.warn(prefix+"no way of reaching node "+node.id()+" (state="+state+"), returning false");
return false;

} catch (Exception e) {


+ 13
- 9
bubble-server/src/main/java/bubble/service/cloud/job/NodeStartJob.java Bestand weergeven

@@ -5,35 +5,39 @@
package bubble.service.cloud.job;

import bubble.cloud.compute.ComputeServiceDriver;
import bubble.dao.cloud.BubbleNodeDAO;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.BubbleNodeState;
import lombok.extern.slf4j.Slf4j;

import static bubble.service.cloud.NodeProgressMeterConstants.METER_ERROR_NO_IP;
import static bubble.service.cloud.NodeProgressMeterConstants.METER_ERROR_STARTING_NODE;
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError;

@Slf4j
public class NodeStartJob implements Runnable {

private BubbleNode node;
private final BubbleNodeDAO nodeDAO;
// private final BubbleNodeDAO nodeDAO;
private final ComputeServiceDriver computeDriver;

public NodeStartJob(BubbleNode node,
BubbleNodeDAO nodeDAO,
// BubbleNodeDAO nodeDAO,
ComputeServiceDriver computeDriver) {
this.node = node;
this.nodeDAO = nodeDAO;
// this.nodeDAO = nodeDAO;
this.computeDriver = computeDriver;
}

@Override public void run() {
try {
node.setState(BubbleNodeState.booting);
nodeDAO.update(node);
// node.setState(BubbleNodeState.booting);
// nodeDAO.update(node);

log.debug("run: calling computeDriver.start("+node.id()+")");
node = computeDriver.start(node);
node.setState(BubbleNodeState.booted);
nodeDAO.update(node);
log.debug("run: computeDriver.start("+node.id()+") returned successfully");

// node.setState(BubbleNodeState.booted);
// nodeDAO.update(node);

if (!node.hasIp4()) {
throw new NodeJobException(METER_ERROR_NO_IP, "node booted but has no IP");


+ 17
- 3
bubble-server/src/main/java/bubble/service/notify/NotificationService.java Bestand weergeven

@@ -22,6 +22,7 @@ import org.cobbzilla.wizard.util.RestResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.net.ssl.SSLException;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.List;
@@ -123,17 +124,30 @@ public class NotificationService {
log.debug("_notify: <<<<< RECEIPT <<<<<< " + json(receipt, COMPACT_MAPPER) + " <<<<<<<<<<<<<<<<<<");
return receipt;

} catch (ConnectException | ConnectTimeoutException | UnknownHostException | ApiException e) {
} catch (ConnectException | ConnectTimeoutException | UnknownHostException | SSLException | ApiException e) {
notification.setStatus(NotificationSendStatus.error);
notification.setException(e);
sentNotificationDAO.update(notification);
throw new IllegalStateException("_notify: "+shortError(e), e);
return handleNotifyException(notification, e, true);

} catch (Exception e) {
notification.setStatus(NotificationSendStatus.error);
notification.setException(e);
sentNotificationDAO.update(notification);
return die("_notify: "+shortError(e), e);
return handleNotifyException(notification, e, true);
}
}
}

public NotificationReceipt handleNotifyException(SentNotification notification, Exception e, boolean die) {
if (notification.getType() == NotificationType.health_check) {
log.error("_notify: health check failed for node "+notification.getToNode()+": "+shortError(e));
return null;
} else {
if (die) {
return die("_notify: " + shortError(e), e);
} else {
throw new IllegalStateException("_notify: "+shortError(e), e);
}
}
}


+ 0
- 8
bubble-server/src/main/resources/bubble/host-prefixes.txt Bestand weergeven

@@ -2139,7 +2139,6 @@ comix
comma
commo
comms
commy
compo
comps
compt
@@ -2165,7 +2164,6 @@ conte
conto
conus
convo
cooch
cooed
cooee
cooer
@@ -2182,7 +2180,6 @@ coops
coopt
coost
coots
cooze
copal
copay
coped
@@ -2307,7 +2304,6 @@ creds
creed
creek
creel
creep
crees
creme
crems
@@ -2346,7 +2342,6 @@ crome
crone
cronk
crony
crook
crool
croon
crops
@@ -2403,8 +2398,6 @@ cully
culms
culpa
culti
cults
culty
cumec
cumin
cundy
@@ -2433,7 +2426,6 @@ curns
curny
currs
curry
curse
cursi
curst
curve


+ 4
- 0
bubble-server/src/main/resources/logback.xml Bestand weergeven

@@ -52,6 +52,10 @@
<logger name="bubble.service.stream" level="INFO" />
<!-- <logger name="bubble.service.account.StandardAccountMessageService" level="DEBUG" />-->
<!-- <logger name="bubble.dao.account.message.AccountMessageDAO" level="DEBUG" />-->
<logger name="bubble.service.cloud.job" level="DEBUG" />
<logger name="bubble.service.cloud.NodeLauncher" level="DEBUG" />
<logger name="bubble.service.cloud.NodeService" level="DEBUG" />
<logger name="bubble.cloud.compute.vultr" level="DEBUG" />
<logger name="bubble.resources.message" level="INFO" />
<logger name="bubble.app.analytics" level="DEBUG" />
<logger name="bubble.app.passthru" level="DEBUG" />


+ 8
- 0
bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties Bestand weergeven

@@ -41,6 +41,14 @@ message_profile_update_success=Profile update was successful
downloading_notice=File download will start promptly...
downloading_failed=File download failed. Please retry from the start

# Jar upgrade
message_jar_upgrade_available=A new version of Bubble is available
message_jar_upgrade_version=The new Bubble version is
message_jar_current_version=Your current Bubble version is
button_label_jar_upgrade=Upgrade Your Bubble
button_label_jar_upgrading=Upgrading...
message_jar_upgrading=Your Bubble may be unresponsive for a minute or two while the upgrade occurs

# Account SSH key fields
form_title_ssh_keys=Account SSH Keys
form_title_add_ssh_key=Add SSH Key


+ 60
- 0
bubble-server/src/main/resources/packer/roles/bubble/files/bubble_upgrade.sh Bestand weergeven

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

BUBBLE_HOME="/home/bubble"
UPGRADE_JAR="${BUBBLE_HOME}/api/.upgrade.jar"
BUBBLE_JAR="${BUBBLE_HOME}/api/bubble.jar"

LOG=/tmp/bubble.upgrade.log

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

function log {
echo "$(date): ${1}" >> ${LOG}
}

function verify_api_ok {
log "Restarting API..."
supervisorctl restart bubble || die "Error restarting bubble"

OK=255
START_VERIFY=$(date +%s)
VERIFY_TIMEOUT=180
VERIFY_URL="https://$(hostname):1443/api/auth/ready"
if [[ ${OK} -ne 0 && $(expr $(date +%s) - ${START_VERIFY} -le ${VERIFY_TIMEOUT}) ]] ; then
sleep 10s
log "Verifying ${VERIFY_URL} is OK...."
curl "${VERIFY_URL}" 2>&1 | tee -a ${LOG}
OK=$?
fi

if [[ ${OK} -eq 0 ]] ; then
echo "ok"
else
echo "error"
fi
}

BACKUP_JAR=$(mktemp /tmp/bubble.jar.XXXXXXX)
log "Backing up to ${BACKUP_JAR} ..."
cp ${BUBBLE_JAR} ${BACKUP_JAR} || die "Error backing up existing jar before upgrade ${BUBBLE_JAR} ${BACKUP_JAR}"

log "Upgrading..."
mv ${UPGRADE_JAR} ${BUBBLE_JAR} || die "Error moving ${UPGRADE_JAR} -> ${BUBBLE_JAR}"

log "Verifying upgrade..."
API_OK=$(verify_api_ok)
if [[ -z "${API_OK}" || "${API_OK}" != "ok" ]] ; then
log "Error starting upgraded API, reverting...."
cp ${BACKUP_JAR} ${BUBBLE_JAR} || die "Error restoring API jar from backup!"
API_OK=$(verify_api_ok)
if [[ -z "${API_OK}" || "${API_OK}" != "ok" ]] ; then
log "Error starting API from backup!"
fi
else
log "Upgrading web site files..."
cd ~bubble && jar xf ${BUBBLE_JAR} site && chown -R bubble:bubble site || die "Error updating web files..."
fi

+ 23
- 0
bubble-server/src/main/resources/packer/roles/bubble/files/bubble_upgrade_monitor.sh Bestand weergeven

@@ -0,0 +1,23 @@
#!/bin/bash
#
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/
#
THIS_DIR="$(cd "$(dirname "${0}")" && pwd)"
LOG=/tmp/bubble.upgrade.log

function log {
echo "$(date): ${1}" >> ${LOG}
}

while : ; do
sleep 5
if [[ -f "${UPGRADE_JAR}" ]] ; then
"${THIS_DIR}/bubble_upgrade.sh"
if [[ $? -eq 0 ]] ; then
log "Upgrade completed successfully"
else
log "Upgrade failed"
fi
rm -f "${UPGRADE_JAR}"
fi
done

+ 5
- 0
bubble-server/src/main/resources/packer/roles/bubble/files/supervisor_bubble_upgrade_monitor.conf Bestand weergeven

@@ -0,0 +1,5 @@

[program:supervisor_bubble_upgrade_monitor]
stdout_logfile = /dev/null
stderr_logfile = /dev/null
command=/usr/local/sbin/supervisor_bubble_upgrade_monitor.sh

+ 13
- 11
bubble-server/src/main/resources/packer/roles/bubble/tasks/main.yml Bestand weergeven

@@ -101,27 +101,29 @@
with_items:
- init_bubble_db.sh

- name: Install refresh_bubble_ssh_keys monitor
- name: Install sbin monitors and scripts
copy:
src: "refresh_bubble_ssh_keys_monitor.sh"
dest: "/usr/local/sbin/refresh_bubble_ssh_keys_monitor.sh"
owner: root
group: root
mode: 0500

- name: Install refresh_bubble_ssh_keys script
copy:
src: refresh_bubble_ssh_keys.sh
dest: /usr/local/sbin/refresh_bubble_ssh_keys.sh
src: "{{ item }}"
dest: "/usr/local/sbin/{{ item }}"
owner: root
group: root
mode: 0500
with_items:
- refresh_bubble_ssh_keys_monitor.sh
- refresh_bubble_ssh_keys.sh
- bubble_upgrade_monitor.sh
- bubble_upgrade.sh

- name: Install refresh_bubble_ssh_keys_monitor supervisor conf file
copy:
src: supervisor_refresh_bubble_ssh_keys_monitor.conf
dest: /etc/supervisor/conf.d/refresh_bubble_ssh_keys_monitor.conf

- name: Install bubble_upgrade_monitor supervisor conf file
copy:
src: supervisor_bubble_upgrade_monitor.conf
dest: /etc/supervisor/conf.d/bubble_upgrade_monitor.conf

- name: Install packer for sage node
shell: su - bubble bash -c install_packer.sh
when: install_type == 'sage'

+ 2
- 4
bubble-server/src/test/java/bubble/mock/MockNetworkService.java Bestand weergeven

@@ -28,7 +28,7 @@ public class MockNetworkService extends StandardNetworkService {
@Autowired private BubbleConfiguration configuration;

@Override protected String lockNetwork(String network) { return "lock"; }
@Override protected boolean confirmLock(String network, String lock) { return true; }
@Override protected boolean confirmNetLock(String network, String lock) { return true; }
@Override protected void unlockNetwork(String network, String lock) {}

@Override public BubbleNode newNode(NewNodeNotification nn, NodeLaunchMonitor launchMonitor) {
@@ -70,9 +70,7 @@ public class MockNetworkService extends StandardNetworkService {
return true;
}

@Override public BubbleNode stopNode(BubbleNode node) {
return node.setState(BubbleNodeState.stopped);
}
@Override public void stopNode(BubbleNode node) { node.setState(BubbleNodeState.stopped); }

@Override public BubbleNode killNode(BubbleNode node, String message) {
return node.setState(BubbleNodeState.stopped);


+ 1
- 1
bubble-web

@@ -1 +1 @@
Subproject commit 38af7d4ed0f733e35f8a743a65331984c377d758
Subproject commit 9dc962290999b5105549ed0d495152288f0d8281

+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit 009a52edb53315fcb7d90c9feed382970aa9a4b8
Subproject commit 549884d63dc1d46f15c33cdcf5d9604deb821992

+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit 773a75f6cc2659bc330a5b4bdbb61d31affeec10
Subproject commit 035690f052f72841bfedc5b25e3798fa22f7b2dd

Laden…
Annuleren
Opslaan