1
0

finalize PrivateCloud, Cloud- and ManagedServer and ManagedWebspace Billingtems and HostingAssets (#63)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/63
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-06-20 11:03:59 +02:00
parent 04d9b43301
commit d157730de7
20 changed files with 458 additions and 130 deletions

View File

@ -8,7 +8,11 @@ import static java.lang.String.join;
public class MultiValidationException extends ValidationException {
private MultiValidationException(final List<String> violations) {
super("[\n" + join(",\n", violations) + "\n]");
super(
violations.size() > 1
? "[\n" + join(",\n", violations) + "\n]"
: "[" + join(",\n", violations) + "]"
);
}
public static void throwInvalid(final List<String> violations) {

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Collection;
import java.util.List;
@ -59,19 +60,24 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
.stream()
.map(subItem -> propDef.getValue(subItem.getResources()))
.map(HsBookingItemEntityValidator::toNonNullInteger)
.map(HsBookingItemEntityValidator::convertBooleanToInteger)
.map(HsBookingItemEntityValidator::toIntegerWithDefault0)
.reduce(0, Integer::sum);
final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources());
final var maxValue = getIntegerValueWithDefault0(propDef, bookingItem.getResources());
if (propDef.thresholdPercentage() != null ) {
return totalValue > (maxValue * propDef.thresholdPercentage() / 100)
? "%s' maximum total is %d%s, but actual total %s %d%s, which exceeds threshold of %d%%"
? "%s' maximum total is %d%s, but actual total %s is %d%s, which exceeds threshold of %d%%"
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage())
: null;
} else {
return totalValue > maxValue
? "%s' maximum total is %d%s, but actual total %s %d%s"
? "%s' maximum total is %d%s, but actual total %s is %d%s"
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit)
: null;
}
}
private static Object convertBooleanToInteger(final Object value) {
return value instanceof Boolean ? BooleanUtils.toInteger((Boolean)value) : value;
}
}

View File

@ -1,7 +1,6 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
@ -9,12 +8,21 @@ class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
HsCloudServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
// @formatter:off
booleanProperty("active") .withDefault(true),
integerProperty("CPUs") .min( 1) .max( 32) .required(),
integerProperty("RAM").unit("GB") .min( 1) .max( 128) .required(),
integerProperty("SSD").unit("GB") .min( 0) .max( 1000) .step(25).required(), // (1)
integerProperty("HDD").unit("GB") .min( 0) .max( 4000) .step(250).withDefault(0),
integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).required(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
// @formatter:on
);
// (q) We do have pre-existing CloudServers without SSD, just HDD, thus SSD starts with min=0.
// TODO.impl: Validation that SSD+HDD is at minimum 25 GB is missing.
// e.g. validationGroup("SSD", "HDD").min(0);
}
}

View File

@ -1,18 +1,40 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
HsPrivateCloudBookingItemValidator() {
super(
integerProperty("CPUs").min(4).max(128).required().asTotalLimit(),
integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(),
integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(),
integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(),
integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC")
// @formatter:off
integerProperty("CPUs") .min( 1).max( 128).required().asTotalLimit(),
integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(),
integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).required().asTotalLimit(),
integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).withDefault(0).asTotalLimit(),
integerProperty("Traffic").unit("GB") .min(250).max(40000).step(250).required().asTotalLimit(),
// Alternatively we could specify it similarly to "Multi" option but exclusively counting:
// integerProperty("Resource-Points") .min(4).max(100).required()
// .each("CPUs").countsAs(64)
// .each("RAM").countsAs(64)
// .each("SSD").countsAs(18)
// .each("HDD").countsAs(2)
// .each("Traffic").countsAs(1),
integerProperty("SLA-Infrastructure EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT8H"),
integerProperty("SLA-Infrastructure EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT4H"),
integerProperty("SLA-Infrastructure EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT2H"),
integerProperty("SLA-Platform EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT8H"),
integerProperty("SLA-Platform EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT4H"),
integerProperty("SLA-Platform EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT2H"),
integerProperty("SLA-EMail") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-Maria") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-PgSQL") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-Office") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-Web") .min( 0).max( 20).withDefault(0).asTotalLimit()
// @formatter:on
);
}
}

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
import net.hostsharing.hsadminng.context.Context;
@ -34,6 +35,9 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired
private HsHostingAssetRepository assetRepo;
@Autowired
private HsBookingItemRepository bookingItemRepo;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsHostingAssetResource>> listAssets(
@ -124,6 +128,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putConfig(KeyValueMap.from(resource.getConfig()));
if (resource.getBookingItemUuid() != null) {
entity.setBookingItem(bookingItemRepo.findByUuid(resource.getBookingItemUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] bookingItemUuid %s not found".formatted(
resource.getBookingItemUuid()))));
}
if (resource.getParentAssetUuid() != null) {
entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted(

View File

@ -27,6 +27,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
@ -78,7 +79,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
@Version
private int version;
@ManyToOne(fetch = FetchType.LAZY)
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bookingitemuuid")
private HsBookingItemEntity bookingItem;
@ -142,7 +143,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.toRole("bookingItem", AGENT).grantPermission(INSERT)
.importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(),
dependsOnColumn("parentAssetUuid"),

View File

@ -65,11 +65,11 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
.stream()
.map(subItem -> propDef.getValue(subItem.getConfig()))
.map(HsEntityValidator::toNonNullInteger)
.map(HsEntityValidator::toIntegerWithDefault0)
.reduce(0, Integer::sum);
final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig());
final var maxValue = getIntegerValueWithDefault0(propDef, hostingAsset.getConfig());
return totalValue > maxValue
? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted(
? "%s' maximum total is %d%s, but actual total %s is %d%s".formatted(
propName, maxValue, propUnit, propName, totalValue, propUnit)
: null;
}

View File

@ -1,18 +1,48 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator {
public HsManagedServerHostingAssetValidator() {
super(
integerProperty("monit_min_free_ssd").min(1).max(1000).optional(),
integerProperty("monit_min_free_hdd").min(1).max(4000).optional(),
integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).required(),
integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).optional(),
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).required(),
integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).required()
// TODO: stringProperty("monit_alarm_email").unit("GB").optional()
// monitoring
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92),
integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92),
integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).withDefault(98),
integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5),
integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95),
integerProperty("monit_min_free_hdd").min(1).max(4000).withDefault(10),
// stringProperty("monit_alarm_email").unit("GB").optional() TODO.impl: via Contact?
// other settings
// booleanProperty("fastcgi_small").withDefault(false), TODO.spec: clarify Salt-Grains
// database software
booleanProperty("software-pgsql").withDefault(true),
booleanProperty("software-mariadb").withDefault(true),
// PHP
enumerationProperty("php-default").valuesFromProperties("software-php-").withDefault("8.2"),
booleanProperty("software-php-5.6").withDefault(false),
booleanProperty("software-php-7.0").withDefault(false),
booleanProperty("software-php-7.1").withDefault(false),
booleanProperty("software-php-7.2").withDefault(false),
booleanProperty("software-php-7.3").withDefault(false),
booleanProperty("software-php-7.4").withDefault(true),
booleanProperty("software-php-8.0").withDefault(false),
booleanProperty("software-php-8.1").withDefault(false),
booleanProperty("software-php-8.2").withDefault(true),
// other software
booleanProperty("software-postfix-tls-1.0").withDefault(false),
booleanProperty("software-dovecot-tls-1.0").withDefault(false),
booleanProperty("software-clamav").withDefault(true),
booleanProperty("software-collabora").withDefault(false),
booleanProperty("software-libreoffice").withDefault(false),
booleanProperty("software-imagemagick-ghostscript").withDefault(false)
);
}
}

View File

@ -7,6 +7,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import static java.util.Arrays.stream;
@Setter
public class EnumerationProperty extends ValidatableProperty<String> {
@ -30,9 +32,27 @@ public class EnumerationProperty extends ValidatableProperty<String> {
return this;
}
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
if (deferredInit != null) {
if (this.values != null) {
throw new IllegalStateException("property " + toString() + " already has values");
}
this.values = deferredInit.apply(allProperties);
}
}
public ValidatableProperty<String> valuesFromProperties(final String propertyNamePrefix) {
this.deferredInit = (ValidatableProperty<?>[] allProperties) -> stream(allProperties)
.map(ValidatableProperty::propertyName)
.filter(name -> name.startsWith(propertyNamePrefix))
.map(name -> name.substring(propertyNamePrefix.length()))
.toArray(String[]::new);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
if (stream(values).noneMatch(v -> v.equals(propValue))) {
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}
}

View File

@ -16,6 +16,7 @@ public abstract class HsEntityValidator<E> {
public HsEntityValidator(final ValidatableProperty<?>... validators) {
propertyValidators = validators;
stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators));
}
protected static List<String> enrich(final String prefix, final List<String> messages) {
@ -59,18 +60,24 @@ public abstract class HsEntityValidator<E> {
.orElse(emptyList()));
}
protected static Integer getNonNullIntegerValue(final ValidatableProperty<?> prop, final Map<String, Object> propValues) {
protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?> prop, final Map<String, Object> propValues) {
final var value = prop.getValue(propValues);
if (value instanceof Integer) {
return (Integer) value;
}
if (value == null) {
return 0;
}
throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value);
}
protected static Integer toNonNullInteger(final Object value) {
protected static Integer toIntegerWithDefault0(final Object value) {
if (value instanceof Integer) {
return (Integer) value;
}
throw new IllegalArgumentException("Integer value expected, but got " + value);
if (value == null) {
return 0;
}
throw new IllegalArgumentException("Integer value (or null) expected, but got " + value);
}
}

View File

@ -19,6 +19,7 @@ import java.util.function.Function;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
@RequiredArgsConstructor
public abstract class ValidatableProperty<T> {
@ -31,6 +32,7 @@ public abstract class ValidatableProperty<T> {
private final String[] keyOrder;
private Boolean required;
private T defaultValue;
protected Function<ValidatableProperty<?>[], T[]> deferredInit;
private boolean isTotalsValidator = false;
@JsonIgnore
private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
@ -57,11 +59,38 @@ public abstract class ValidatableProperty<T> {
return this;
}
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
}
public ValidatableProperty<T> asTotalLimit() {
isTotalsValidator = true;
return this;
}
public ValidatableProperty<T> asTotalLimitFor(final String propertyName, final String propertyValue) {
if (asTotalLimitValidators == null) {
asTotalLimitValidators = new ArrayList<>();
}
final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator =
(final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var total = entity.getSubBookingItems().stream()
.map(server -> server.getResources().get(propertyName))
.filter(propertyValue::equals)
.count();
final long limitingValue = ofNullable(prop.getValue(entity.getResources())).orElse(0);
if (total > factor*limitingValue) {
return List.of(
prop.propertyName() + " maximum total is " + (factor*limitingValue) + ", but actual total for " + propertyName + "=" + propertyValue + " is " + total
);
}
return emptyList();
};
asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1));
return this;
}
public String propertyName() {
return propertyName;
}