@@ -12,6 +12,7 @@ | |||
{"name": "default_locale", "value": "[[network.locale]]"}, | |||
{"name": "bubble_version", "value": "[[configuration.version]]"}, | |||
{"name": "bubble_host", "value": "[[node.fqdn]]"}, | |||
{"name": "bubble_cname", "value": "[[node.networkDomain]]"}, | |||
{"name": "admin_user", "value": "[[node.user]]"}, | |||
{"name": "db_encoding", "value": "UTF-8"}, | |||
{"name": "db_locale", "value": "en_US"}, | |||
@@ -1,4 +1,4 @@ | |||
export PUBLIC_BASE_URI=https://{{ bubble_host }}:{{ ssl_port }} | |||
export PUBLIC_BASE_URI=https://{{ bubble_cname }}:{{ ssl_port }} | |||
export SELF_NODE={{ node_uuid }} | |||
export SAGE_NODE={{ sage_node }} | |||
export LETSENCRYPT_EMAIL={{ letsencrypt_email }} | |||
@@ -4,6 +4,7 @@ | |||
"template": true, | |||
"config": [ | |||
{"name": "server_name", "value": "[[node.fqdn]]"}, | |||
{"name": "server_alias", "value": "[[node.networkDomain]]"}, | |||
{"name": "letsencrypt_email", "value": "[[configuration.letsencryptEmail]]"}, | |||
{"name": "ssl_port", "value": "[[configuration.nginxPort]]"}, | |||
{"name": "admin_port", "value": "[[node.adminPort]]"} | |||
@@ -0,0 +1,5 @@ | |||
#!/bin/bash | |||
service mitmproxy stop && service nginx stop && certbot renew --standalone --non-interactive || echo "Error updating SSL certificates" | |||
service mitmproxy restart | |||
service nginx restart |
@@ -2,12 +2,13 @@ | |||
LE_EMAIL="${1}" | |||
SERVER_NAME="${2}" | |||
SERVER_ALIAS="${3}" | |||
if [[ $(find /etc/letsencrypt/accounts -type f -name regr.json | xargs grep -l \"${LE_EMAIL}\" | wc -l | tr -d ' ') -eq 0 ]] ; then | |||
certbot register --agree-tos -m ${LE_EMAIL} --non-interactive | |||
fi | |||
if [[ ! -f /etc/letsencrypt/live/${SERVER_NAME}/fullchain.pem ]] ; then | |||
certbot certonly --standalone --non-interactive -d ${SERVER_NAME} | |||
certbot certonly --standalone --non-interactive -d ${SERVER_NAME} -d ${SERVER_ALIAS} | |||
else | |||
certbot renew --standalone --non-interactive | |||
fi |
@@ -48,7 +48,15 @@ | |||
mode: 0555 | |||
- name: Init certbot | |||
shell: init_certbot.sh {{ letsencrypt_email }} {{ server_name }} | |||
shell: init_certbot.sh {{ letsencrypt_email }} {{ server_name }} {{ server_alias }} | |||
- name: Install certbot_renew.sh weekly cron job | |||
copy: | |||
src: "certbot_renew.sh" | |||
dest: /etc/cron.weekly/certbot_renew.sh | |||
owner: root | |||
group: root | |||
mode: 0755 | |||
# see https://weakdh.org/sysadmin.html | |||
- name: Create a strong dhparam.pem | |||
@@ -1,6 +1,6 @@ | |||
server { | |||
server_name {{ server_name }}; | |||
server_name {{ server_name }} {{ server_alias }}; | |||
listen {{ ssl_port }} ssl http2; | |||
ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem; | |||
@@ -15,11 +15,13 @@ import bubble.server.BubbleConfiguration; | |||
import bubble.service.bill.RefundService; | |||
import bubble.service.cloud.NetworkService; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.validation.ValidationResult; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Repository; | |||
import java.util.List; | |||
import static bubble.model.cloud.BubbleNetwork.validateHostname; | |||
import static java.util.concurrent.TimeUnit.SECONDS; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.background; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||
@@ -69,6 +71,9 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||
} | |||
@Override public Object preCreate(AccountPlan accountPlan) { | |||
final ValidationResult errors = validateHostname(accountPlan); | |||
if (errors.isInvalid()) throw invalidEx(errors); | |||
if (configuration.paymentsEnabled()) { | |||
if (!accountPlan.hasPaymentMethodObject()) throw invalidEx("err.paymentMethod.required"); | |||
if (!accountPlan.getPaymentMethodObject().hasUuid()) throw invalidEx("err.paymentMethod.required"); | |||
@@ -4,6 +4,7 @@ import bubble.dao.account.AccountOwnedEntityDAO; | |||
import bubble.model.bill.BubblePlan; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.server.BubbleConfiguration; | |||
import org.hibernate.criterion.Order; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Repository; | |||
@@ -14,6 +15,8 @@ public class BubblePlanDAO extends AccountOwnedEntityDAO<BubblePlan> { | |||
@Override public boolean dbFilterIncludeAll() { return true; } | |||
@Override public Order getDefaultSortOrder() { return Order.asc("priority"); } | |||
@Override public BubblePlan findByUuid(String uuid) { | |||
final BubblePlan plan = super.findByUuid(uuid); | |||
if (plan != null) return plan; | |||
@@ -12,6 +12,7 @@ import bubble.service.boot.SelfNodeService; | |||
import bubble.service.cloud.NetworkService; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.model.IdentifiableBase; | |||
import org.cobbzilla.wizard.validation.ValidationResult; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Repository; | |||
@@ -19,7 +20,9 @@ import java.io.IOException; | |||
import java.util.List; | |||
import java.util.stream.Collectors; | |||
import static bubble.model.cloud.BubbleNetwork.validateHostname; | |||
import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
@Repository @Slf4j | |||
public class BubbleNetworkDAO extends AccountOwnedEntityDAO<BubbleNetwork> { | |||
@@ -34,6 +37,9 @@ public class BubbleNetworkDAO extends AccountOwnedEntityDAO<BubbleNetwork> { | |||
@Autowired private BubbleConfiguration configuration; | |||
@Override public Object preCreate(BubbleNetwork network) { | |||
final ValidationResult errors = validateHostname(network); | |||
if (errors.isInvalid()) throw invalidEx(errors); | |||
if (!network.hasLocale()) network.setLocale(getDEFAULT_LOCALE()); | |||
return super.preCreate(network); | |||
} | |||
@@ -42,7 +42,6 @@ public class AccountSshKey extends IdentifiableBase implements HasAccount { | |||
@HasValue(message="err.name.required") | |||
@ECIndex(unique=true) @Column(nullable=false, updatable=false, length=100) | |||
@Getter @Setter private String name; | |||
public boolean hasName () { return !empty(name); } | |||
@ECSearchable @ECField(index=20) | |||
@ECForeignKey(entity=Account.class) | |||
@@ -10,6 +10,5 @@ public interface HasAccount extends Identifiable, NamedEntity, SqlViewSearchResu | |||
<E> E setAccount (String account); | |||
default boolean hasAccount () { return getAccount() != null; } | |||
String getName(); | |||
default boolean hasName() { return getName() != null; } | |||
} |
@@ -22,6 +22,7 @@ import javax.persistence.Transient; | |||
import javax.validation.constraints.Size; | |||
import static bubble.model.bill.BillPeriod.BILL_START_END_FORMAT; | |||
import static bubble.model.cloud.BubbleNetwork.NETWORK_NAME_MAXLEN; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.bool; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
@@ -50,8 +51,8 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { | |||
// mirrors network name | |||
@ECSearchable(filter=true) @ECField(index=10) | |||
@Size(max=100, message="err.name.length") | |||
@Column(length=100, nullable=false) | |||
@Size(max=NETWORK_NAME_MAXLEN, message="err.name.length") | |||
@Column(length=NETWORK_NAME_MAXLEN, nullable=false) | |||
@Getter @Setter private String name; | |||
@ECSearchable @ECField(index=20) | |||
@@ -138,6 +139,9 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { | |||
@Transient @Getter @Setter private transient AccountPaymentMethod paymentMethodObject = null; | |||
public boolean hasPaymentMethodObject () { return paymentMethodObject != null; } | |||
@Transient @Getter @Setter private transient String forkHost = null; | |||
public boolean hasForkHost () { return !empty(forkHost); } | |||
public BubbleNetwork bubbleNetwork(Account account, | |||
BubbleDomain domain, | |||
BubblePlan plan, | |||
@@ -153,7 +157,8 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { | |||
.setDomainName(domain.getName()) | |||
.setFootprint(getFootprint()) | |||
.setComputeSizeType(plan.getComputeSizeType()) | |||
.setStorage(storage.getUuid()); | |||
.setStorage(storage.getUuid()) | |||
.setForkHost(hasForkHost() ? getForkHost() : null); | |||
} | |||
} |
@@ -8,6 +8,7 @@ import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import org.cobbzilla.util.collection.HasPriority; | |||
import org.cobbzilla.wizard.model.IdentifiableBase; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.*; | |||
import org.cobbzilla.wizard.validation.HasValue; | |||
@@ -32,13 +33,13 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
}) | |||
@Entity @NoArgsConstructor @Accessors(chain=true) | |||
@ECIndexes({ @ECIndex(unique=true, of={"account", "name"}) }) | |||
public class BubblePlan extends IdentifiableBase implements HasAccount { | |||
public class BubblePlan extends IdentifiableBase implements HasAccount, HasPriority { | |||
public static final int MAX_CHARGENAME_LEN = 12; | |||
public static final String[] CREATE_FIELDS = { | |||
"name", "chargeName", "enabled", "price", "period", "computeSizeType", | |||
"nodesIncluded", "additionalPerNodePrice", | |||
"name", "chargeName", "enabled", "priority", "price", "period", | |||
"computeSizeType", "nodesIncluded", "additionalPerNodePrice", | |||
"storageGbIncluded", "additionalStoragePerGbPrice", | |||
"bandwidthGbIncluded", "additionalBandwidthPerGbPrice" | |||
}; | |||
@@ -77,27 +78,30 @@ public class BubblePlan extends IdentifiableBase implements HasAccount { | |||
@Getter @Setter private Boolean enabled = true; | |||
public boolean enabled () { return enabled == null || enabled; } | |||
@ECSearchable @ECField(index=50) | |||
@ECSearchable @ECField(index=50) @Column(nullable=false) | |||
@ECIndex @Getter @Setter private Integer priority = 1; | |||
@ECSearchable @ECField(index=60) | |||
@ECIndex @Column(nullable=false) | |||
@Getter @Setter private Long price; | |||
@ECSearchable @ECField(index=60) | |||
@ECSearchable @ECField(index=70) | |||
@ECIndex @Column(nullable=false, length=10) | |||
@Getter @Setter private String currency = "USD"; | |||
@ECSearchable @ECField(index=70) | |||
@ECSearchable @ECField(index=80) | |||
@Enumerated(EnumType.STRING) @Column(nullable=false, updatable=false, length=20) | |||
@Getter @Setter private BillPeriod period = BillPeriod.monthly; | |||
@ECSearchable @ECField(index=80) | |||
@ECSearchable @ECField(index=90) | |||
@ECIndex @Column(nullable=false, updatable=false, length=20) | |||
@Enumerated(EnumType.STRING) @Getter @Setter private ComputeNodeSizeType computeSizeType; | |||
@ECSearchable @ECField(index=90) | |||
@ECSearchable @ECField(index=100) | |||
@Column(nullable=false, updatable=false) | |||
@Getter @Setter private Integer nodesIncluded; | |||
@ECSearchable @ECField(index=100) | |||
@ECSearchable @ECField(index=110) | |||
@Column(nullable=false, updatable=false) | |||
@Getter @Setter private Integer additionalPerNodePrice; | |||
@@ -14,9 +14,11 @@ import lombok.experimental.Accessors; | |||
import org.cobbzilla.util.collection.ArrayUtil; | |||
import org.cobbzilla.wizard.model.Identifiable; | |||
import org.cobbzilla.wizard.model.IdentifiableBase; | |||
import org.cobbzilla.wizard.model.NamedEntity; | |||
import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.*; | |||
import org.cobbzilla.wizard.validation.HasValue; | |||
import org.cobbzilla.wizard.validation.ValidationResult; | |||
import org.hibernate.annotations.Type; | |||
import javax.persistence.*; | |||
@@ -33,6 +35,8 @@ import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
import static org.cobbzilla.util.string.ValidationRegexes.HOST_PART_PATTERN; | |||
import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; | |||
@@ -67,9 +71,13 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
@Transient @JsonIgnore public String getNetwork () { return getUuid(); } | |||
public static final int NETWORK_NAME_MAXLEN = 100; | |||
public static final int NETWORK_NAME_MINLEN = 4; | |||
@ECSearchable @ECField(index=10) | |||
@HasValue(message="err.name.required") | |||
@ECIndex @Column(nullable=false, updatable=false, length=200) | |||
@Size(min=NETWORK_NAME_MINLEN, max=NETWORK_NAME_MAXLEN, message="err.name.length") | |||
@ECIndex @Column(nullable=false, updatable=false, length=NETWORK_NAME_MAXLEN) | |||
@Getter @Setter private String name; | |||
@ECSearchable @ECField(index=20) | |||
@@ -143,4 +151,22 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
} | |||
return die("hostFromFqdn("+fqdn+"): expected suffix ."+getNetworkDomain()); | |||
} | |||
public static ValidationResult validateHostname(NamedEntity request) { | |||
return validateHostname(request, new ValidationResult()); | |||
} | |||
public static ValidationResult validateHostname(NamedEntity request, ValidationResult errors) { | |||
if (!request.hasName()) { | |||
errors.addViolation("err.name.required"); | |||
} else if (!validateRegexMatches(HOST_PART_PATTERN, request.getName())) { | |||
errors.addViolation("err.name.invalid"); | |||
} else if (request.getName().length() > NETWORK_NAME_MAXLEN) { | |||
errors.addViolation("err.name.length"); | |||
} else if (request.getName().length() < NETWORK_NAME_MINLEN) { | |||
errors.addViolation("err.name.tooShort"); | |||
} | |||
return errors; | |||
} | |||
} |
@@ -102,7 +102,7 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun | |||
@ECIndex @Column(nullable=false, updatable=false, length=20) | |||
@Getter @Setter private CloudServiceType type; | |||
@ECSearchable @ECField(index=50) | |||
@ECSearchable @ECField(index=50) @Column(nullable=false) | |||
@ECIndex @Getter @Setter private Integer priority = 1; | |||
@ECSearchable @ECField(index=60) | |||
@@ -38,6 +38,8 @@ import java.util.List; | |||
import java.util.stream.Collectors; | |||
import static bubble.ApiConstants.*; | |||
import static org.cobbzilla.util.string.ValidationRegexes.HOST_PART_PATTERN; | |||
import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Slf4j | |||
@@ -115,6 +117,16 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
request.setSshKey(null); // if it's an empty string, make it null (see simple_network test) | |||
} | |||
if (request.hasForkHost()) { | |||
if (!configuration.isSageLauncher()) { | |||
errors.addViolation("err.forkHost.notAllowed"); | |||
} else if (!validateRegexMatches(HOST_PART_PATTERN, request.getForkHost())) { | |||
errors.addViolation("err.forkHost.invalid"); | |||
} | |||
} else { | |||
BubbleNetwork.validateHostname(request, errors); | |||
} | |||
final BubbleDomain domain = domainDAO.findByAccountAndId(caller.getUuid(), request.getDomain()); | |||
if (domain == null) { | |||
log.info("setReferences: domain not found: "+request.getDomain()+" for caller: "+caller.getUuid()); | |||
@@ -186,6 +186,11 @@ message_no_verified_contacts_subtext=Before creating your first Bubble, please v | |||
form_title_new_network=New Bubble | |||
field_label_network_name=Name | |||
field_label_network_type=Bubble Type | |||
field_label_network_type_regular=Regular | |||
field_label_network_type_fork=Fork | |||
field_label_network_fork_host=Fork Host | |||
field_description_network_fork_host=The current Sage Launcher will be forked onto this host. | |||
field_label_network_domain=Domain | |||
field_label_plan=Plan | |||
field_label_show_advanced_plan_options=Show All Options | |||
@@ -222,11 +227,13 @@ link_network_action_delete_description=Delete this Bubble and all backups. You w | |||
# Bubble Plans | |||
plan_name_bubble=Bubble Standard | |||
plan_description_bubble=Try this one first. Most users probably don't need anything more. | |||
plan_description_bubble=Try this one first. Most users probably don't need anything more | |||
plan_name_bubble_plus=Bubble Plus | |||
plan_description_bubble_plus=Enhanced for faster performance. | |||
plan_description_bubble_plus=Enhanced for faster performance | |||
plan_name_bubble_big=Bubble Big | |||
plan_description_bubble_big=A truly powerful bubble node | |||
plan_name_bubble_pro=Bubble Pro | |||
plan_description_bubble_pro=Two bubble nodes operate in concert to improve overall reliability. | |||
plan_description_bubble_pro=Two bubble nodes operate in concert to improve overall reliability | |||
# Footprints | |||
footprint_name_US=United States | |||
@@ -418,6 +425,8 @@ err.email.invalid=Email address is invalid | |||
err.email.required=Email address is required | |||
err.expiration.cannotCreateSshKeyAlreadyExpired=Expiration date has already passed | |||
err.footprint.required=Footprint is required | |||
err.forkHost.notAllowed=Fork Host is not allowed | |||
err.forkHost.invalid=Fork Host is not a valid hostname | |||
err.fqdn.domain.invalid=Domain not found for FQDN | |||
err.fqdn.length=FQDN is too long | |||
err.fqdn.network.invalid=network not found for FQDN | |||
@@ -442,6 +451,7 @@ err.nodeOperationTimeout.required=Node operation timeout is required | |||
err.nodeOperationTimeout.tooLong=Node operation timeout cannot be longer than 3 days | |||
err.nodeOperationTimeout.tooShort=Node operation timeout cannot be shorter than 1 minute | |||
err.name.length=Name is too long | |||
err.name.tooShort=Name is too short | |||
err.name.mismatch=Name mismatch | |||
err.name.reserved=Name is reserved | |||
err.netlocation.invalid=Must specify both cloud and region, or neither | |||
@@ -1,5 +1,6 @@ | |||
[{ | |||
"name": "bubble", | |||
"priority": 1000, | |||
"chargeName": "BubbleVPN", | |||
"computeSizeType": "small", | |||
"nodesIncluded": 1, | |||
@@ -11,6 +12,7 @@ | |||
"additionalBandwidthPerGbPrice": 0 | |||
}, { | |||
"name": "bubble_plus", | |||
"priority": 2000, | |||
"chargeName": "BubblePlus", | |||
"computeSizeType": "medium", | |||
"nodesIncluded": 1, | |||
@@ -20,39 +20,4 @@ | |||
"additionalStoragePerGbPrice": 2, | |||
"bandwidthGbIncluded": 1000, | |||
"additionalBandwidthPerGbPrice": 2 | |||
}, { | |||
"name": "bubble_big", | |||
"chargeName": "BubbleBig", | |||
"computeSizeType": "large", | |||
"nodesIncluded": 1, | |||
"additionalPerNodePrice": 2800, | |||
"price": 2800, | |||
"storageGbIncluded": 70, | |||
"additionalStoragePerGbPrice": 2, | |||
"bandwidthGbIncluded": 4000, | |||
"additionalBandwidthPerGbPrice": 2 | |||
}//, | |||
// { | |||
// "name": "bubble_pro", | |||
// "chargeName": "BubblePro", | |||
// "computeSizeType": "small", | |||
// "nodesIncluded": 2, | |||
// "additionalPerNodePrice": 1200, | |||
// "price": 2500, | |||
// "storageGbIncluded": 25, | |||
// "additionalStoragePerGbPrice": 2, | |||
// "bandwidthGbIncluded": 1000, | |||
// "additionalBandwidthPerGbPrice": 2 | |||
// }, | |||
// { | |||
// "name": "bubble_power_user", | |||
// "computeSizeType": "medium", | |||
// "nodesIncluded": 3, | |||
// "additionalPerNodePrice": 2500, | |||
// "price": 7500, | |||
// "storageGbIncluded": 50, | |||
// "additionalStoragePerGbPrice": 2, | |||
// "bandwidthGbIncluded": 2000, | |||
// "additionalBandwidthPerGbPrice": 2 | |||
// } | |||
] | |||
}] |
@@ -1 +1 @@ | |||
Subproject commit 10a000cac0bda587ddd16c402672cc464689c372 | |||
Subproject commit a45c053235fc63154b27f6833958a3be7f73fac0 |
@@ -1 +1 @@ | |||
Subproject commit 7e349b52203f4f8a49b6539f83804bcb1a9bc905 | |||
Subproject commit 31dde8a290f6339c34866b1e15e17ef0041034e9 |
@@ -1 +1 @@ | |||
Subproject commit b59b6ec756aa38bfeb358c3431c5dc5b1bf594b8 | |||
Subproject commit b171791b38e47716ff299b99b8941ce60e9ee4d9 |