@@ -164,7 +164,7 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||||
// create an uninitialized device for the account, but only if this is a regular node network | // create an uninitialized device for the account, but only if this is a regular node network | ||||
// sage networks do not allow devices, they launch and manage other regular node networks | // sage networks do not allow devices, they launch and manage other regular node networks | ||||
if (!account.isRoot() && !configuration.isSage()) { | |||||
if (!isFirstAdmin(account) && !configuration.isSage()) { | |||||
deviceDAO.ensureAllSpareDevices(account.getUuid(), configuration.getThisNetwork().getUuid()); | deviceDAO.ensureAllSpareDevices(account.getUuid(), configuration.getThisNetwork().getUuid()); | ||||
deviceDAO.refreshVpnUsers(); | deviceDAO.refreshVpnUsers(); | ||||
} | } | ||||
@@ -475,7 +475,8 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||||
private final Refreshable<Account> firstAdmin = new Refreshable<>("firstAdmin", FIRST_ADMIN_CACHE_MILLIS, this::findFirstAdmin); | private final Refreshable<Account> firstAdmin = new Refreshable<>("firstAdmin", FIRST_ADMIN_CACHE_MILLIS, this::findFirstAdmin); | ||||
public Account getFirstAdmin() { return firstAdmin.get(); } | public Account getFirstAdmin() { return firstAdmin.get(); } | ||||
public boolean isFirstAdmin(Account account) { return getFirstAdmin().getUuid().equals(account.getUuid()); } | |||||
public boolean isFirstAdmin(Account account) { return isFirstAdmin(account.getUuid()); } | |||||
public boolean isFirstAdmin(String accountUuid) { return getFirstAdmin().getUuid().equals(accountUuid); } | |||||
public Account findFirstAdmin() { | public Account findFirstAdmin() { | ||||
final List<Account> admins = findByField("admin", true); | final List<Account> admins = findByField("admin", true); | ||||
@@ -81,7 +81,7 @@ public class DeviceDAO extends AccountOwnedEntityDAO<Device> { | |||||
var uninitializedDevices = findByAccountAndUninitialized(accountUuid); | var uninitializedDevices = findByAccountAndUninitialized(accountUuid); | ||||
if (uninitializedDevices.size() <= SPARE_DEVICES_PER_ACCOUNT_THRESHOLD | if (uninitializedDevices.size() <= SPARE_DEVICES_PER_ACCOUNT_THRESHOLD | ||||
&& !configuration.getBean(AccountDAO.class).findByUuid(accountUuid).isRoot()) { | |||||
&& !configuration.getBean(AccountDAO.class).isFirstAdmin(accountUuid)) { | |||||
if (ensureAllSpareDevices(accountUuid, device.getNetwork())) refreshVpnUsers(); | if (ensureAllSpareDevices(accountUuid, device.getNetwork())) refreshVpnUsers(); | ||||
} | } | ||||
@@ -112,7 +112,6 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||||
@Override @Transient public String getName() { return getEmail(); } | @Override @Transient public String getName() { return getEmail(); } | ||||
public Account setName(String n) { return setEmail(n); } | public Account setName(String n) { return setEmail(n); } | ||||
@Transient @JsonIgnore public boolean isRoot() { return getName().equals(ROOT_USERNAME); } | |||||
// make this updatable if we ever want accounts to be able to change parents | // make this updatable if we ever want accounts to be able to change parents | ||||
// there might be a lot more involved in that action though (read-only parent objects that will no longer be visible, must be copied in?) | // there might be a lot more involved in that action though (read-only parent objects that will no longer be visible, must be copied in?) | ||||
@@ -166,6 +166,9 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||||
@Transient @Getter @Setter private transient String forkHost = null; | @Transient @Getter @Setter private transient String forkHost = null; | ||||
public boolean hasForkHost () { return !empty(forkHost); } | public boolean hasForkHost () { return !empty(forkHost); } | ||||
@Transient @Getter @Setter private transient String adminEmail = null; | |||||
public boolean hasAdminEmail() { return !empty(adminEmail); } | |||||
@Transient @Getter @Setter private transient Boolean syncAccount = null; | @Transient @Getter @Setter private transient Boolean syncAccount = null; | ||||
public boolean syncAccount() { return syncAccount == null || syncAccount; } | public boolean syncAccount() { return syncAccount == null || syncAccount; } | ||||
@@ -200,7 +203,8 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||||
.setComputeSizeType(plan.getComputeSizeType()) | .setComputeSizeType(plan.getComputeSizeType()) | ||||
.setStorage(storage.getUuid()) | .setStorage(storage.getUuid()) | ||||
.setLaunchType(hasForkHost() && hasLaunchType() ? getLaunchType() : LaunchType.node) | .setLaunchType(hasForkHost() && hasLaunchType() ? getLaunchType() : LaunchType.node) | ||||
.setForkHost(hasForkHost() ? getForkHost() : null); | |||||
.setForkHost(hasForkHost() ? getForkHost() : null) | |||||
.setAdminEmail(hasAdminEmail() ? getAdminEmail() : null); | |||||
} | } | ||||
} | } |
@@ -23,7 +23,8 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||||
public class ActivationRequest { | public class ActivationRequest { | ||||
@HasValue(message="err.email.required") | @HasValue(message="err.email.required") | ||||
@Getter @Setter private String email; | |||||
@Getter private String email; | |||||
public ActivationRequest setEmail(String e) { this.email = empty(e) ? e : e.trim(); return this; } | |||||
public boolean hasEmail() { return !empty(email); } | public boolean hasEmail() { return !empty(email); } | ||||
public String getName() { return getEmail(); } | public String getName() { return getEmail(); } | ||||
@@ -55,6 +56,9 @@ public class ActivationRequest { | |||||
@Getter @Setter private Boolean skipTests = false; | @Getter @Setter private Boolean skipTests = false; | ||||
public boolean skipTests () { return bool(skipTests); }; | public boolean skipTests () { return bool(skipTests); }; | ||||
@Getter @Setter private Boolean skipPacker = false; | |||||
public boolean skipPacker () { return bool(skipPacker); }; | |||||
@Getter @Setter private AccountSshKey sshKey; | @Getter @Setter private AccountSshKey sshKey; | ||||
public boolean hasSshKey () { return sshKey != null; } | public boolean hasSshKey () { return sshKey != null; } | ||||
@@ -208,6 +208,9 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||||
public boolean hasForkHost () { return !empty(forkHost); } | public boolean hasForkHost () { return !empty(forkHost); } | ||||
public boolean fork() { return hasForkHost(); } | public boolean fork() { return hasForkHost(); } | ||||
@Transient @Getter @Setter private transient String adminEmail; | |||||
public boolean hasAdminEmail () { return !empty(adminEmail); } | |||||
@ECField(index=190) @Column(length=20, updatable=false) | @ECField(index=190) @Column(length=20, updatable=false) | ||||
@Enumerated(EnumType.STRING) @Getter @Setter private LaunchType launchType = null; | @Enumerated(EnumType.STRING) @Getter @Setter private LaunchType launchType = null; | ||||
public boolean hasLaunchType () { return launchType != null; } | public boolean hasLaunchType () { return launchType != null; } | ||||
@@ -5,13 +5,20 @@ | |||||
package bubble.model.cloud; | package bubble.model.cloud; | ||||
import com.fasterxml.jackson.annotation.JsonCreator; | import com.fasterxml.jackson.annotation.JsonCreator; | ||||
import lombok.AllArgsConstructor; | |||||
import static bubble.ApiConstants.enumFromString; | import static bubble.ApiConstants.enumFromString; | ||||
@AllArgsConstructor | |||||
public enum LaunchType { | public enum LaunchType { | ||||
node, fork_node, fork_sage; | |||||
node (false), | |||||
fork_node (true), | |||||
fork_sage (true); | |||||
@JsonCreator public static LaunchType fromString(String v) { return enumFromString(LaunchType.class, v); } | @JsonCreator public static LaunchType fromString(String v) { return enumFromString(LaunchType.class, v); } | ||||
private final boolean fork; | |||||
public boolean fork () { return fork; } | |||||
} | } |
@@ -166,6 +166,11 @@ public class AuthResource { | |||||
if (accountDAO.activated()) { | if (accountDAO.activated()) { | ||||
return invalid("err.activation.alreadyDone", "activation has already been done"); | return invalid("err.activation.alreadyDone", "activation has already been done"); | ||||
} | } | ||||
if (!request.hasEmail()) return invalid("err.email.required", "email is required"); | |||||
if (request.getEmail().contains("{{") && request.getEmail().contains("}}")) { | |||||
request.setEmail(configuration.applyHandlebars(request.getEmail()).trim()); | |||||
} | |||||
if (!request.hasEmail()) return invalid("err.email.required", "email is required"); | if (!request.hasEmail()) return invalid("err.email.required", "email is required"); | ||||
if (!request.hasPassword()) return invalid("err.password.required", "password is required"); | if (!request.hasPassword()) return invalid("err.password.required", "password is required"); | ||||
@@ -41,10 +41,10 @@ import java.util.List; | |||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
import static bubble.ApiConstants.*; | import static bubble.ApiConstants.*; | ||||
import static bubble.model.account.Account.ROOT_EMAIL; | |||||
import static bubble.model.cloud.BubbleNetwork.validateHostname; | import static bubble.model.cloud.BubbleNetwork.validateHostname; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | import static org.cobbzilla.util.daemon.ZillaRuntime.*; | ||||
import static org.cobbzilla.util.string.ValidationRegexes.HOST_PATTERN; | |||||
import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; | |||||
import static org.cobbzilla.util.string.ValidationRegexes.*; | |||||
import static org.cobbzilla.wizard.model.NamedEntity.NAME_MAXLEN; | import static org.cobbzilla.wizard.model.NamedEntity.NAME_MAXLEN; | ||||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | import static org.cobbzilla.wizard.resources.ResourceUtil.*; | ||||
@@ -167,6 +167,18 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||||
validateName(request, errors); | validateName(request, errors); | ||||
} | } | ||||
} | } | ||||
final String adminEmail = request.getAdminEmail(); | |||||
if (request.hasLaunchType() && request.getLaunchType().fork() && caller.admin()) { | |||||
if (empty(adminEmail) && caller.getEmail().equals(ROOT_EMAIL)) { | |||||
errors.addViolation("err.adminEmail.required"); | |||||
} else if (!empty(adminEmail) && !validateRegexMatches(EMAIL_PATTERN, adminEmail)) { | |||||
errors.addViolation("err.adminEmail.invalid"); | |||||
} | |||||
} else { | |||||
if (!empty(adminEmail)) { | |||||
errors.addViolation("err.adminEmail.cannotSet"); | |||||
} | |||||
} | |||||
} else { | } else { | ||||
if (!request.hasNickname()) { | if (!request.hasNickname()) { | ||||
if (!request.hasName()) request.setName(newNetworkName()); | if (!request.hasName()) request.setName(newNetworkName()); | ||||
@@ -28,8 +28,8 @@ public class ComputePackerResource { | |||||
@Autowired private BubbleConfiguration configuration; | @Autowired private BubbleConfiguration configuration; | ||||
@Autowired private PackerService packer; | @Autowired private PackerService packer; | ||||
private Account account; | |||||
private CloudService cloud; | |||||
private final Account account; | |||||
private final CloudService cloud; | |||||
public ComputePackerResource (Account account, CloudService cloud) { | public ComputePackerResource (Account account, CloudService cloud) { | ||||
this.account = account; | this.account = account; | ||||
@@ -18,6 +18,7 @@ import bubble.model.boot.ActivationRequest; | |||||
import bubble.model.boot.CloudServiceConfig; | import bubble.model.boot.CloudServiceConfig; | ||||
import bubble.model.cloud.*; | import bubble.model.cloud.*; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import bubble.service.packer.PackerService; | |||||
import com.fasterxml.jackson.databind.JsonNode; | import com.fasterxml.jackson.databind.JsonNode; | ||||
import lombok.Cleanup; | import lombok.Cleanup; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
@@ -64,6 +65,7 @@ public class ActivationService { | |||||
@Autowired private StandardSelfNodeService selfNodeService; | @Autowired private StandardSelfNodeService selfNodeService; | ||||
@Autowired private BubbleConfiguration configuration; | @Autowired private BubbleConfiguration configuration; | ||||
@Autowired private ModelSetupService modelSetupService; | @Autowired private ModelSetupService modelSetupService; | ||||
@Autowired private PackerService packerService; | |||||
public BubbleNode bootstrapThisNode(Account account, ActivationRequest request) { | public BubbleNode bootstrapThisNode(Account account, ActivationRequest request) { | ||||
String ip = getFirstPublicIpv4(); | String ip = getFirstPublicIpv4(); | ||||
@@ -228,12 +230,26 @@ public class ActivationService { | |||||
final Map<CrudOperation, Collection<Identifiable>> objects | final Map<CrudOperation, Collection<Identifiable>> objects | ||||
= modelSetupService.setupModel(api, account, "manifest-defaults"); | = modelSetupService.setupModel(api, account, "manifest-defaults"); | ||||
log.info("bootstrapThisNode: created default objects\n"+json(objects)); | log.info("bootstrapThisNode: created default objects\n"+json(objects)); | ||||
if (!request.skipPacker()) initialPacker(account); | |||||
}, "ActivationService.bootstrapThisNode.createDefaultObjects"); | }, "ActivationService.bootstrapThisNode.createDefaultObjects"); | ||||
} else if (!request.skipPacker()) { | |||||
initialPacker(account); | |||||
} | } | ||||
return node; | return node; | ||||
} | } | ||||
private void initialPacker(Account account) { | |||||
for (CloudService cloud : cloudDAO.findByAccountAndType(account.getUuid(), CloudServiceType.compute)) { | |||||
log.info("initialPacker: creating images for compute cloud: "+cloud.getName()); | |||||
packerService.writePackerImages(cloud, AnsibleInstallType.sage, null); | |||||
packerService.writePackerImages(cloud, AnsibleInstallType.node, null); | |||||
} | |||||
} | |||||
public BubbleNetwork createRootNetwork(BubbleNetwork network) { | public BubbleNetwork createRootNetwork(BubbleNetwork network) { | ||||
network.setUuid(ROOT_NETWORK_UUID); | network.setUuid(ROOT_NETWORK_UUID); | ||||
return networkDAO.create(network); | return networkDAO.create(network); | ||||
@@ -15,10 +15,12 @@ import bubble.dao.cloud.BubbleNodeDAO; | |||||
import bubble.dao.cloud.BubbleNodeKeyDAO; | import bubble.dao.cloud.BubbleNodeKeyDAO; | ||||
import bubble.dao.cloud.CloudServiceDAO; | import bubble.dao.cloud.CloudServiceDAO; | ||||
import bubble.dao.device.DeviceDAO; | import bubble.dao.device.DeviceDAO; | ||||
import bubble.model.account.Account; | |||||
import bubble.model.bill.AccountPlan; | import bubble.model.bill.AccountPlan; | ||||
import bubble.model.bill.BubblePlan; | import bubble.model.bill.BubblePlan; | ||||
import bubble.model.cloud.*; | |||||
import bubble.model.cloud.BubbleNetwork; | |||||
import bubble.model.cloud.BubbleNode; | |||||
import bubble.model.cloud.BubbleNodeKey; | |||||
import bubble.model.cloud.BubbleNodeState; | |||||
import bubble.model.cloud.notify.NotificationReceipt; | import bubble.model.cloud.notify.NotificationReceipt; | ||||
import bubble.model.cloud.notify.NotificationType; | import bubble.model.cloud.notify.NotificationType; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
@@ -29,7 +31,6 @@ import lombok.Getter; | |||||
import lombok.NonNull; | import lombok.NonNull; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.cache.Refreshable; | import org.cobbzilla.util.cache.Refreshable; | ||||
import org.cobbzilla.util.daemon.SimpleDaemon; | |||||
import org.cobbzilla.util.http.HttpSchemes; | import org.cobbzilla.util.http.HttpSchemes; | ||||
import org.cobbzilla.util.http.HttpUtil; | import org.cobbzilla.util.http.HttpUtil; | ||||
import org.cobbzilla.util.io.FileUtil; | import org.cobbzilla.util.io.FileUtil; | ||||
@@ -53,7 +54,6 @@ import static bubble.server.BubbleServer.disableRestoreMode; | |||||
import static bubble.server.BubbleServer.isRestoreMode; | import static bubble.server.BubbleServer.isRestoreMode; | ||||
import static java.util.concurrent.TimeUnit.DAYS; | import static java.util.concurrent.TimeUnit.DAYS; | ||||
import static java.util.concurrent.TimeUnit.MINUTES; | import static java.util.concurrent.TimeUnit.MINUTES; | ||||
import static java.util.function.Predicate.not; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | import static org.cobbzilla.util.daemon.ZillaRuntime.*; | ||||
import static org.cobbzilla.util.io.FileUtil.abs; | import static org.cobbzilla.util.io.FileUtil.abs; | ||||
import static org.cobbzilla.util.io.FileUtil.toFileOrDie; | import static org.cobbzilla.util.io.FileUtil.toFileOrDie; | ||||
@@ -162,7 +162,7 @@ public class StandardSelfNodeService implements SelfNodeService { | |||||
background(() -> { | background(() -> { | ||||
if (accountDAO.findAll() | if (accountDAO.findAll() | ||||
.stream() | .stream() | ||||
.filter(not(Account::isRoot)) | |||||
.filter(a -> !accountDAO.isFirstAdmin(a)) | |||||
.map(a -> deviceDAO.ensureAllSpareDevices(a.getUuid(), thisNetworkUuid)) | .map(a -> deviceDAO.ensureAllSpareDevices(a.getUuid(), thisNetworkUuid)) | ||||
.reduce(false, Boolean::logicalOr) | .reduce(false, Boolean::logicalOr) | ||||
.booleanValue()) { | .booleanValue()) { | ||||
@@ -7,9 +7,7 @@ package bubble.service.dbfilter; | |||||
import bubble.cloud.CloudServiceType; | import bubble.cloud.CloudServiceType; | ||||
import bubble.cloud.storage.local.LocalStorageConfig; | import bubble.cloud.storage.local.LocalStorageConfig; | ||||
import bubble.cloud.storage.local.LocalStorageDriver; | import bubble.cloud.storage.local.LocalStorageDriver; | ||||
import bubble.model.account.Account; | |||||
import bubble.model.account.AccountSshKey; | |||||
import bubble.model.account.AccountTemplate; | |||||
import bubble.model.account.*; | |||||
import bubble.model.app.AppTemplateEntity; | import bubble.model.app.AppTemplateEntity; | ||||
import bubble.model.app.BubbleApp; | import bubble.model.app.BubbleApp; | ||||
import bubble.model.bill.AccountPaymentMethod; | import bubble.model.bill.AccountPaymentMethod; | ||||
@@ -135,7 +133,29 @@ public abstract class EntityIterator implements Iterator<Identifiable> { | |||||
.forEach(this::add); | .forEach(this::add); | ||||
} else if (Account.class.isAssignableFrom(c)) { | } else if (Account.class.isAssignableFrom(c)) { | ||||
entities.forEach(e -> add(((Account) e).setPreferredPlan(null))); | |||||
entities.forEach(e -> { | |||||
if (network.hasAdminEmail() && network.getAccount().equals(e.getUuid())) { | |||||
final Account a = (Account) e; | |||||
a.setEmail(network.getAdminEmail()); | |||||
} | |||||
add(((Account) e).setPreferredPlan(null)); | |||||
}); | |||||
} else if (AccountPolicy.class.isAssignableFrom(c) && network.hasAdminEmail()) { | |||||
entities.forEach(e -> { | |||||
if (network.hasAdminEmail()) { | |||||
final AccountPolicy p = (AccountPolicy) e; | |||||
if (p.getAccount().equals(network.getAccount())) { | |||||
final AccountContact adminContact = new AccountContact() | |||||
.setType(CloudServiceType.email) | |||||
.setInfo(network.getAdminEmail()) | |||||
.setVerified(true) | |||||
.setRemovable(false); | |||||
p.setAccountContacts(new AccountContact[]{adminContact}); | |||||
} | |||||
} | |||||
add(e); | |||||
}); | |||||
} else if (AccountSshKey.class.isAssignableFrom(c)) { | } else if (AccountSshKey.class.isAssignableFrom(c)) { | ||||
entities.forEach(e -> add(setInstallKey((AccountSshKey) e, network))); | entities.forEach(e -> add(setInstallKey((AccountSshKey) e, network))); | ||||
@@ -1 +1 @@ | |||||
Subproject commit 6557ae47aae789ce6dc6ff1aca189787a89103bb | |||||
Subproject commit c0070a05666df680157c6740879ad5a3c98c3bba |
@@ -1 +1 @@ | |||||
Subproject commit b7120e4099a382278c43813644f99e928c9ae828 | |||||
Subproject commit 290017992084da8d799aba24409099d54c1a9305 |
@@ -54,9 +54,9 @@ | |||||
} | } | ||||
}, | }, | ||||
// You must configure the an SMTP service, it is required to send emails | |||||
// SendGrid, MailGun, or any other SMTP service should work fine | |||||
"SmtpServer": { | |||||
// You must configure an email service, it is required to send emails | |||||
// Comment out the email clouds that you don't use | |||||
"SmtpEmail": { | |||||
"config": { | "config": { | ||||
"tlsEnabled": true | "tlsEnabled": true | ||||
}, | }, | ||||
@@ -68,6 +68,21 @@ | |||||
} | } | ||||
}, | }, | ||||
"SendGridEmail": { | |||||
"config": {}, | |||||
"credentials": { | |||||
"apiKey": "your_sendgrid_api_key" | |||||
} | |||||
}, | |||||
"MailgunEmail": { | |||||
"config": {}, | |||||
"credentials": { | |||||
"domain": "your_mailgun_domain", | |||||
"apiKey": "your_mailgun_api_key" | |||||
} | |||||
}, | |||||
// Required for TOTP-based authentication. Nothing to configure, just leave this as-is | // Required for TOTP-based authentication. Nothing to configure, just leave this as-is | ||||
"TOTPAuthenticator": {}, | "TOTPAuthenticator": {}, | ||||
@@ -1 +1 @@ | |||||
Subproject commit d7ea904f35401302cbfb4bde5d70475d2721df07 | |||||
Subproject commit d3b5e5254c2eca16290dc5746d0ab489160a8ff1 |