Bläddra i källkod

initial support for nodemanager

tags/v0.9.18
Jonathan Cobb 4 år sedan
förälder
incheckning
c7e6db9324
17 ändrade filer med 326 tillägg och 8 borttagningar
  1. Binär
     
  2. +1
    -1
      automation/roles/bubble_finalizer/files/bubble_role.json
  3. +5
    -0
      automation/roles/bubble_finalizer/files/supervisor_bubble_nodemanager.conf
  4. +15
    -1
      automation/roles/bubble_finalizer/tasks/main.yml
  5. +1
    -1
      automation/roles/bubble_finalizer/templates/supervisor_bubble.conf.j2
  6. +8
    -0
      automation/roles/nginx/templates/site_node.conf.j2
  7. +8
    -0
      automation/roles/nginx/templates/site_node_alias.conf.j2
  8. +6
    -0
      bubble-server/pom.xml
  9. +2
    -0
      bubble-server/src/main/java/bubble/ApiConstants.java
  10. +7
    -0
      bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java
  11. +12
    -2
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  12. +181
    -0
      bubble-server/src/main/java/bubble/resources/cloud/NodeManagerResource.java
  13. +21
    -1
      bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java
  14. +47
    -0
      bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java
  15. +5
    -0
      bubble-server/src/main/java/bubble/service/boot/SageHelloService.java
  16. +1
    -1
      bubble-server/src/main/resources/ansible/default_roles.json
  17. +6
    -1
      bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties

Binär
Visa fil


+ 1
- 1
automation/roles/bubble_finalizer/files/bubble_role.json Visa fil

@@ -6,7 +6,7 @@
{"name": "server_alias", "value": "[[network.networkDomain]]"},
{"name": "restore_key", "value": "[[restoreKey]]"},
{"name": "install_type", "value": "[[installType]]"},
{"name": "bubble_java_opts", "value": "-Xms[[expr nodeSize.memoryMB '//' 4]]m -Xmx[[expr nodeSize.memoryMB '//' 4]]m"}
{"name": "bubble_java_opts", "value": "-XX:MaxRAM=[[expr nodeSize.memoryMB '//' '2.625']]m"}
],
"optionalConfigNames": ["restore_key"],
"tgzB64": ""

+ 5
- 0
automation/roles/bubble_finalizer/files/supervisor_bubble_nodemanager.conf Visa fil

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

[program:nodemanager]
stdout_logfile = /home/bubble/logs/nodemanager-out.log
stderr_logfile = /home/bubble/logs/nodemanager-err.log
command=/usr/sbin/bubble-nodemanager

+ 15
- 1
automation/roles/bubble_finalizer/tasks/main.yml Visa fil

@@ -29,18 +29,32 @@
shell: /usr/local/bin/copy_certs_to_bubble.sh {{ server_alias }}
when: install_type == 'node'

- name: Install bubble-nodemanager
copy:
src: "bubble-nodemanager"
dest: /usr/sbin/bubble-nodemanager
owner: root
group: root
mode: 0500

- name: Install bubble-nodemanager supervisor conf file
copy:
src: "supervisor_bubble_nodemanager.conf"
dest: /etc/supervisor/conf.d/nodemanager.conf

- name: Install bubble supervisor conf file
template:
src: "supervisor_bubble.conf.j2"
dest: /etc/supervisor/conf.d/bubble.conf

# We cannot receive notifications until nginx is running, so start bubble API as the very last step
- name: Ensure bubble is started
- name: Ensure bubble and bubble_nodemanager are started
supervisorctl:
name: '{{ item }}'
state: restarted
with_items:
- bubble
- nodemanager

- name: Ensure authorized SSH keys are up-to-date
shell: su - bubble bash -c "touch /home/bubble/.refresh_ssh_keys"

+ 1
- 1
automation/roles/bubble_finalizer/templates/supervisor_bubble.conf.j2 Visa fil

@@ -4,7 +4,7 @@ stdout_logfile = /home/bubble/logs/bubble-out.log
stderr_logfile = /home/bubble/logs/bubble-err.log
command=sudo -u bubble bash -c "/usr/bin/java \
-Dfile.encoding=UTF-8 -Djava.net.preferIPv4Stack=true \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 {{ bubble_java_opts }} \
-XX:+UseG1GC -XX:MaxGCPauseMillis=400 {{ bubble_java_opts }} \
-cp /home/bubble/current/bubble.jar \
bubble.server.BubbleServer \
/home/bubble/current/bubble.env"

+ 8
- 0
automation/roles/nginx/templates/site_node.conf.j2 Visa fil

@@ -21,6 +21,14 @@ server {
proxy_set_header X-Forwarded-Proto https;
}

location /nodeman {
proxy_pass http://127.0.0.1:7800/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host {{ server_name }};
proxy_set_header X-Forwarded-Proto https;
}

location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /var/www/html;


+ 8
- 0
automation/roles/nginx/templates/site_node_alias.conf.j2 Visa fil

@@ -21,6 +21,14 @@ server {
proxy_set_header X-Forwarded-Proto https;
}

location /nodeman {
proxy_pass http://127.0.0.1:7800/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host {{ server_name }};
proxy_set_header X-Forwarded-Proto https;
}

location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /var/www/html;


+ 6
- 0
bubble-server/pom.xml Visa fil

@@ -246,6 +246,12 @@
<version>16.2.0</version>
</dependency>

<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.5.1</version>
</dependency>

<dependency>
<groupId>org.cobbzilla</groupId>
<artifactId>restex</artifactId>


+ 2
- 0
bubble-server/src/main/java/bubble/ApiConstants.java Visa fil

@@ -92,6 +92,7 @@ public class ApiConstants {
public static final String EP_APPROVE = "/approve";
public static final String EP_DENY = "/deny";
public static final String EP_AUTHENTICATOR = "/authenticator";
public static final String EP_PATCH = "/patch";

public static final String ACCOUNTS_ENDPOINT = "/users";
public static final String EP_POLICY = "/policy";
@@ -180,6 +181,7 @@ public class ApiConstants {
public static final String EP_STATUS = "/status";
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 DETECT_ENDPOINT = "/detect";
public static final String EP_LOCALE = "/locale";


+ 7
- 0
bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java Visa fil

@@ -27,6 +27,7 @@ import org.cobbzilla.wizard.model.IdentifiableBase;
import org.cobbzilla.wizard.model.entityconfig.EntityFieldType;
import org.cobbzilla.wizard.model.entityconfig.annotations.*;
import org.cobbzilla.wizard.validation.HasValue;
import org.hibernate.annotations.Type;

import javax.persistence.*;
import java.io.File;
@@ -41,6 +42,8 @@ import static org.cobbzilla.util.network.NetworkUtil.isLocalIpv4;
import static org.cobbzilla.util.reflect.ReflectionUtil.copy;
import static org.cobbzilla.util.string.ValidationRegexes.IP4_MAXLEN;
import static org.cobbzilla.util.string.ValidationRegexes.IP6_MAXLEN;
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING;
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD;
import static org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKeySearchDepth.shallow;

@ECType(root=true) @ECTypeCreate(method="DISABLED")
@@ -196,6 +199,10 @@ public class BubbleNode extends IdentifiableBase implements HasNetwork, HasBubbl

@Embedded @Getter @Setter private BubbleTags tags;

@Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(100+ENC_PAD)+")")
@Getter @Setter private String nodeManagerPassword;
public boolean hasNodeManagerPassword () { return !empty(nodeManagerPassword); }

@Transient @JsonIgnore @Getter @Setter private transient Map<String, String> ephemeralTags;

public boolean hasEphemeralTag (String name) { return ephemeralTags != null && ephemeralTags.containsKey(name); }


+ 12
- 2
bubble-server/src/main/java/bubble/resources/account/AuthResource.java Visa fil

@@ -31,6 +31,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.NodeManagerService;
import bubble.service.boot.SageHelloService;
import bubble.service.cloud.DeviceIdService;
import bubble.service.notify.NotificationService;
@@ -64,8 +65,7 @@ import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE;
import static bubble.server.BubbleServer.getRestoreKey;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON;
import static org.cobbzilla.util.http.HttpContentTypes.CONTENT_TYPE_ANY;
import static org.cobbzilla.util.http.HttpContentTypes.*;
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.string.LocaleUtil.currencyForLocale;
@@ -95,6 +95,7 @@ public class AuthResource {
@Autowired private PromotionService promoService;
@Autowired private DeviceIdService deviceIdService;
@Autowired private BubbleNodeKeyDAO nodeKeyDAO;
@Autowired private NodeManagerService nodeManagerService;

public Account updateLastLogin(Account account) { return accountDAO.update(account.setLastLogin()); }

@@ -575,4 +576,13 @@ public class AuthResource {
return ok_empty();
}

@GET @Path(EP_PATCH+"/{token}")
@Produces(APPLICATION_OCTET_STREAM)
public Response getPatchFile(@Context ContainerRequest ctx,
@PathParam("token") String token) {
final File patch = nodeManagerService.findPatch(token);
if (patch == null) return notFound(token);
return send(new FileSendableResource(patch));
}

}

+ 181
- 0
bubble-server/src/main/java/bubble/resources/cloud/NodeManagerResource.java Visa fil

@@ -0,0 +1,181 @@
package bubble.resources.cloud;

import bubble.dao.cloud.BubbleNodeDAO;
import bubble.model.account.Account;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.notify.NotificationReceipt;
import bubble.service.boot.NodeManagerService;
import bubble.service.boot.SelfNodeService;
import bubble.service.notify.NotificationService;
import edu.emory.mathcs.backport.java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import org.cobbzilla.util.http.*;
import org.cobbzilla.util.io.FileUtil;
import org.cobbzilla.util.io.TempDir;
import org.cobbzilla.wizard.auth.LoginRequest;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.glassfish.jersey.server.ContainerRequest;
import org.springframework.beans.factory.annotation.Autowired;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;

import static bubble.model.cloud.notify.NotificationType.hello_to_sage;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;

@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Slf4j
public class NodeManagerResource {

// these constants must match up with those defined in bubble-nodemanager
public static final String BUBBLE_NODE_ADMIN = "bubble_node_admin";
public static final String ROOT_DIR_PREFIX = "root_dir/";
public static final String COMPONENT_ROOT = "root";
public static final Set<String> PATCH_COMPONENTS = new HashSet<>(Arrays.asList(new String[]{COMPONENT_ROOT, "bubble", "mitmproxy"}));

private BubbleNode node;

public NodeManagerResource (BubbleNode node) { this.node = node; }

@Autowired private BubbleNodeDAO nodeDAO;
@Autowired private NodeManagerService nodeManagerService;
@Autowired private SelfNodeService selfNodeService;
@Autowired private NotificationService notificationService;

@POST @Path("/set_password")
public Response setPassword (@Context ContainerRequest ctx,
LoginRequest request) {

final String password = request.getPassword();
if (empty(password)) return invalid("err.password.required");
if (password.length() < 10) return invalid("err.password.tooShort");

nodeManagerService.setPassword(password);
final BubbleNode selfNode = selfNodeService.getThisNode();
if (selfNode != null && selfNode.hasSageNode() && !selfNode.getUuid().equals(selfNode.getSageNode())) {
final BubbleNode sageNode = nodeDAO.findByUuid(selfNode.getSageNode());
if (sageNode == null) {
log.warn("setPassword: error finding sage to notify: "+selfNode.getSageNode());
} else {
selfNode.setNodeManagerPassword(password);
final NotificationReceipt receipt = notificationService.notify(sageNode, hello_to_sage, selfNode);
if (!receipt.isSuccess()) {
log.warn("setPassword: error notifying sage of new nodemanager password: "+receipt);
}
selfNode.setNodeManagerPassword(null); // just in case the object gets sync'd to db
}
}
return ok_empty();
}

@POST @Path("/disable")
public Response disable (@Context ContainerRequest ctx) {
nodeManagerService.disable();
return ok_empty();
}

public HttpRequestBean validateNodeManagerRequest(ContainerRequest ctx, String path) {
final Account caller = userPrincipal(ctx);
if (!caller.admin() && !caller.getUuid().equals(node.getAccount())) throw forbiddenEx();

final BubbleNode n = nodeDAO.findByUuid(node.getUuid());
if (!n.hasNodeManagerPassword()) throw invalidEx("err.nodemanager.noPasswordSet");

final String url = "https://" + node.getFqdn() + ":" + node.getAdminPort() + "/nodeman/" + path;
return new HttpRequestBean(url)
.setAuthType(HttpAuthType.basic)
.setAuthUsername(BUBBLE_NODE_ADMIN)
.setAuthPassword(n.getNodeManagerPassword());
}

public Response callNodeManager(HttpRequestBean request, String prefix) {
prefix = prefix + ": ";
try {
final HttpResponseBean response = HttpUtil.getResponse(request);
if (response.isOk()) return ok(response.getEntityString());
log.error(prefix+response);
} catch (IOException e) {
log.error(prefix+shortError(e));
}
throw invalidEx("err.nodemanager.error");
}

@GET @Path("/stats/{stat}")
public Response getStats (@Context ContainerRequest ctx,
@PathParam("stat") String stat) {
final HttpRequestBean request = validateNodeManagerRequest(ctx, "stats/"+stat);
return callNodeManager(request, "getStats");
}

@POST @Path("/cmd/{command}")
public Response runCommand (@Context ContainerRequest ctx,
@PathParam("command") String command) {
final HttpRequestBean request = validateNodeManagerRequest(ctx, "cmd/"+command)
.setMethod(HttpMethods.POST);
return callNodeManager(request, "runCommand");
}

@POST @Path("/service/{service}/{action}")
public Response service (@Context ContainerRequest ctx,
@PathParam("service") String service,
@PathParam("action") String action) {
final HttpRequestBean request = validateNodeManagerRequest(ctx, "service/"+service+"/"+action)
.setMethod(HttpMethods.POST);
return callNodeManager(request, "service");
}

@POST @Path("/patch/file/{component}/{path : .+}")
@Consumes(MULTIPART_FORM_DATA)
public Response patchFile (@Context ContainerRequest ctx,
@PathParam("component") String component,
@PathParam("path") String path,
@FormDataParam("file") InputStream in,
@FormDataParam("name") String name) {
if (PATCH_COMPONENTS.contains(component)) return notFound(component);
final HttpRequestBean request = validateNodeManagerRequest(ctx, "patch/"+component)
.setMethod(HttpMethods.POST);

// create a zipfile containing the file at the proper path
final File zipFile = buildPatchZip(component, path, in);

// register patch with NodeManagerService, receive URL
final String url = nodeManagerService.registerPatch(zipFile);
request.setEntity(url);

return callNodeManager(request, "patchFile");
}

private File buildPatchZip(String component, String path, InputStream in) {
final TempDir tempDir = new TempDir();
if (component.equals(COMPONENT_ROOT)) {
path = ROOT_DIR_PREFIX + path;
}
if (path.startsWith("/")) path = path.substring(1);
if (empty(path)) throw invalidEx("err.nodemanager.invalidPath");
final String fullPath = abs(tempDir)+"/"+path;
final File parentDir = mkdirOrDie(dirname(fullPath));
FileUtil.toFileOrDie(new File(parentDir, basename(path)), in);
final File zipFile = FileUtil.temp(".zip");
try {
new ZipFile(zipFile).addFolder(tempDir);
} catch (ZipException e) {
return die("buildPatchZip: "+shortError(e));
}
log.info("buildPatchZip: created patch file: "+abs(zipFile));
return zipFile;
}

}

+ 21
- 1
bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java Visa fil

@@ -11,11 +11,16 @@ import bubble.model.cloud.BubbleNetwork;
import bubble.model.cloud.BubbleNode;
import bubble.resources.account.ReadOnlyAccountOwnedResource;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;

import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import java.util.List;

import static org.cobbzilla.wizard.resources.ResourceUtil.forbiddenEx;
import static bubble.ApiConstants.EP_NODE_MANAGER;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;

@Slf4j
public class NodesResource extends ReadOnlyAccountOwnedResource<BubbleNode, BubbleNodeDAO> {
@@ -67,4 +72,19 @@ public class NodesResource extends ReadOnlyAccountOwnedResource<BubbleNode, Bubb
@Override protected BubbleNode setReferences(ContainerRequest ctx, Account caller, BubbleNode node) { throw forbiddenEx(); }
@Override protected Object daoCreate(BubbleNode nodes) { throw forbiddenEx(); }

@Path("/{id}"+EP_NODE_MANAGER)
public NodeManagerResource getNodeManagerResource(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("id") String id) {
final Account caller = userPrincipal(ctx);
final BubbleNode node = super.find(ctx, id);
if (node == null) throw notFoundEx(id);

if (!caller.admin() && !caller.getUuid().equals(node.getAccount())) throw forbiddenEx();

if (!node.hasNodeManagerPassword()) throw invalidEx("err.nodemanager.noPasswordSet");

return configuration.subResource(NodeManagerResource.class, node);
}

}

+ 47
- 0
bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java Visa fil

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

import bubble.server.BubbleConfiguration;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.collection.ExpirationMap;
import org.cobbzilla.util.security.bcrypt.BCryptUtil;
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_PATCH;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
import static org.cobbzilla.util.io.FileUtil.toFileOrDie;

@Service @Slf4j
public class NodeManagerService {

public static final File NODEMANAGER_PASSWORD_FILE = new File("/home/bubble/.nodemanager_pass");

@Autowired private BubbleConfiguration configuration;

public String generatePasswordOrNull() {
if (NODEMANAGER_PASSWORD_FILE.exists()) return null;
final String password = randomAlphanumeric(20);
setPassword(password);
return password;
}

public void setPassword(String password) { toFileOrDie(NODEMANAGER_PASSWORD_FILE, BCryptUtil.hash(password)); }

public void disable() { toFileOrDie(NODEMANAGER_PASSWORD_FILE, "__disabled__"); }

@Getter(lazy=true, value=AccessLevel.PROTECTED) private final ExpirationMap<String, File> patches = new ExpirationMap<>();

public String registerPatch(File zipFile) {
final String token = randomAlphanumeric(20);
getPatches().put(token, zipFile);
return configuration.getApiUriBase() + AUTH_ENDPOINT + EP_PATCH + "/" + token;
}

public File findPatch (String token) { return getPatches().get(token); }

}

+ 5
- 0
bubble-server/src/main/java/bubble/service/boot/SageHelloService.java Visa fil

@@ -39,6 +39,7 @@ public class SageHelloService extends SimpleDaemon {
@Autowired private BubbleConfiguration configuration;
@Autowired private StandardSelfNodeService selfNodeService;
@Autowired private NotificationService notificationService;
@Autowired private NodeManagerService nodeManagerService;

private final AtomicBoolean sageHelloSent = new AtomicBoolean(false);
public boolean sageHelloSuccessful () { return sageHelloSent.get(); }
@@ -62,6 +63,9 @@ public class SageHelloService extends SimpleDaemon {
if (sage == null) {
log.error("hello_to_sage: sage node not found: " + c.getSageNode());
} else {
// If we do not have a nodemanager password, generate one now and include it with our hello
selfNode.setNodeManagerPassword(nodeManagerService.generatePasswordOrNull());

log.info("hello_to_sage: sending hello...");
final NotificationReceipt receipt = notificationService.notify(sage, hello_to_sage, selfNode);
log.info("hello_to_sage: received reply from sage node: " + json(receipt, COMPACT_MAPPER));
@@ -74,6 +78,7 @@ public class SageHelloService extends SimpleDaemon {
}
}
}
selfNode.setNodeManagerPassword(null); // just in case the object gets sync'd to db
}
}
}


+ 1
- 1
bubble-server/src/main/resources/ansible/default_roles.json Visa fil

@@ -100,7 +100,7 @@
{"name": "server_alias", "value": "[[network.networkDomain]]"},
{"name": "restore_key", "value": "[[restoreKey]]"},
{"name": "install_type", "value": "[[installType]]"},
{"name": "bubble_java_opts", "value": "-Xms[[expr nodeSize.memoryMB '//' 2]]m -Xmx[[expr nodeSize.memoryMB '//' 2]]m"}
{"name": "bubble_java_opts", "value": "-XX:MaxRAM=[[expr nodeSize.memoryMB '//' '2.625']]m"}
],
"optionalConfigNames": ["restore_key"],
"tgzB64": ""


+ 6
- 1
bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties Visa fil

@@ -372,7 +372,7 @@ meter_unknown_error=An unknown error occurred
# Help text shown during launch
title_launch_help_html=Next Steps
message_launch_help_html=<p>Your Bubble will take about 20 minutes to launch and configure itself.</p><p>When your Bubble has completed its setup, it will send you an email message with a link to unlock it and start using it.</p><p>While you wait for your Bubble to be ready, please <a href="https://github.com/bubblev/bubble-docs/blob/master/wg_setup/README.md">install WireGuard</a> on each device that you will be connecting to your Bubble.</p><p>If you run into any trouble installing WireGuard or setting up your Bubble, please contact <a href="mailto:support@bubblev.com">support@bubblev.com</a></p>
message_launch_success_help_html=<p>Congratulations! Your Bubble is now running.</p><p>Check your email for message from your Bubble. It should contain a link to unlock it and start using it.</p><p>Follow that link to begin using your Bubble, it will guide you through the process to connect your various devices to it.</p><p>If you run into any trouble setting up your Bubble, please contact <a href="mailto:support@bubblev.com">support@bubblev.com</a></p>
message_launch_success_help_html=<p>Congratulations! Your Bubble is now running.</p><p>Check your email for a message from your Bubble. It should contain a link to unlock it and start using it.</p><p>Follow that link to begin using your Bubble, it will guide you through the process to connect your various devices to it.</p><p>If you run into any trouble setting up your Bubble, please contact <a href="mailto:support@bubblev.com">support@bubblev.com</a></p>

# Network statuses
msg_network_state_created=initialized
@@ -774,3 +774,8 @@ err.addFeed.emptyFqdnList=Feed URL was not found or contained no data

# analytics app errors
err.addFilter.analyticsFilterRequired=Filter pattern is required

# nodemanager errors
err.nodemanager.error=Error calling nodemanager
err.nodemanager.noPasswordSet=No nodemanager password is set
err.nodemanager.invalidPath=Path is invalid

Laddar…
Avbryt
Spara