Cloud, Server and Webspace BillingItems and HostingAssets

This commit is contained in:
Michael Hoennig 2024-06-17 06:26:30 +02:00
parent 62867a4cac
commit 0d0c7d74fb
22 changed files with 453 additions and 131 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

@ -17,8 +17,6 @@ import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.annotations.Type;
import jakarta.persistence.CascadeType;

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

@ -2,6 +2,7 @@ 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 +10,17 @@ 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( 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(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
// @formatter:on
);
}
}

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 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;
}

View File

@ -55,6 +55,7 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}",
"{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}",
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}",
"{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=false}",
@ -109,10 +110,10 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then
assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs 5",
"'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
"'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
"'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
"'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
"'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
);
}
}

View File

@ -120,10 +120,10 @@ class HsManagedServerBookingItemValidatorUnitTest {
// then
assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
);
}

View File

@ -28,29 +28,38 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
// given
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
.type(PRIVATE_CLOUD)
.caption("myPC")
.resources(ofEntries(
entry("CPUs", 4),
entry("RAM", 20),
entry("SSD", 100),
entry("Traffic", 5000)
entry("Traffic", 5000),
entry("SLA-Platform EXT4H", 2),
entry("SLA-EMail", 2)
))
.subBookingItems(of(
HsBookingItemEntity.builder()
.type(MANAGED_SERVER)
.caption("myMS-1")
.resources(ofEntries(
entry("CPUs", 2),
entry("RAM", 10),
entry("SSD", 50),
entry("Traffic", 2500)
entry("Traffic", 2500),
entry("SLA-Platform", "EXT4H"),
entry("SLA-EMail", true)
))
.build(),
HsBookingItemEntity.builder()
.type(CLOUD_SERVER)
.caption("myMS-2")
.resources(ofEntries(
entry("CPUs", 2),
entry("RAM", 10),
entry("SSD", 50),
entry("Traffic", 2500)
entry("Traffic", 2500),
entry("SLA-Platform", "EXT4H"),
entry("SLA-EMail", true)
))
.build()
))
@ -69,29 +78,42 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
.project(project)
.type(PRIVATE_CLOUD)
.caption("myPC")
.resources(ofEntries(
entry("CPUs", 4),
entry("RAM", 20),
entry("SSD", 100),
entry("Traffic", 5000)
entry("Traffic", 5000),
entry("SLA-Platform EXT2H", 1),
entry("SLA-EMail", 1)
))
.subBookingItems(of(
HsBookingItemEntity.builder()
.type(MANAGED_SERVER)
.caption("myMS-1")
.resources(ofEntries(
entry("CPUs", 3),
entry("RAM", 20),
entry("SSD", 100),
entry("Traffic", 3000)
entry("Traffic", 3000),
entry("SLA-Platform", "EXT2H"),
entry("SLA-EMail", true)
))
.build(),
HsBookingItemEntity.builder()
.type(CLOUD_SERVER)
.caption("myMS-2")
.resources(ofEntries(
entry("CPUs", 2),
entry("RAM", 10),
entry("SSD", 50),
entry("Traffic", 2500)
entry("Traffic", 2500),
entry("SLA-Platform", "EXT2H"),
entry("SLA-EMail", true),
entry("SLA-Maria", true),
entry("SLA-PgSQL", true),
entry("SLA-Office", true),
entry("SLA-Web", true)
))
.build()
))
@ -102,11 +124,16 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
// then
assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
);
"'D-12345:Test-Project:myPC.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
"'D-12345:Test-Project:myPC.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:myPC.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:myPC.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB",
"'D-12345:Test-Project:myPC.resources.SLA-Platform EXT2H maximum total is 1, but actual total for SLA-Platform=EXT2H is 2",
"'D-12345:Test-Project:myPC.resources.SLA-EMail' maximum total is 1, but actual total SLA-EMail is 2",
"'D-12345:Test-Project:myPC.resources.SLA-Maria' maximum total is 0, but actual total SLA-Maria is 1",
"'D-12345:Test-Project:myPC.resources.SLA-PgSQL' maximum total is 0, but actual total SLA-PgSQL is 1",
"'D-12345:Test-Project:myPC.resources.SLA-Office' maximum total is 0, but actual total SLA-Office is 1",
"'D-12345:Test-Project:myPC.resources.SLA-Web' maximum total is 0, but actual total SLA-Web is 1"
);
}
}

View File

@ -10,8 +10,11 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
@ -28,11 +31,12 @@ import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.matchesRegex;
@Transactional
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class }
)
@Transactional
@TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems
class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup {
@LocalServerPort
@ -54,6 +58,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
JpaAttempt jpaAttempt;
@Nested
@Order(2)
class ListAssets {
@Test
@ -152,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
@Nested
@Order(3)
class AddAsset {
@Test
@ -231,17 +237,17 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.when()
.post("http://localhost/api/hs/hosting/assets")
.then().log().all().assertThat()
.statusCode(201)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"type": "MANAGED_WEBSPACE",
"identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer",
"config": {}
}
"""))
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
.statusCode(201)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"type": "MANAGED_WEBSPACE",
"identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer",
"config": {}
}
"""))
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
.extract().header("Location"); // @formatter:on
// finally, the new asset can be accessed under the generated UUID
@ -258,34 +264,33 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"bookingItemUuid": "%s",
"type": "MANAGED_SERVER",
"identifier": "vm1400",
"caption": "some new ManagedServer",
"config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 }
}
""".formatted(givenBookingItem.getUuid()))
.port(port)
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"bookingItemUuid": "%s",
"type": "MANAGED_SERVER",
"identifier": "vm1400",
"caption": "some new ManagedServer",
"config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 }
}
""".formatted(givenBookingItem.getUuid()))
.port(port)
.when()
.post("http://localhost/api/hs/hosting/assets")
.post("http://localhost/api/hs/hosting/assets")
.then().log().all().assertThat()
.statusCode(400)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"statusPhrase": "Bad Request",
"message": "[
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0,
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing
<<<]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on
.statusCode(400)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"statusPhrase": "Bad Request",
"message": "[
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0
<<<]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on
}
@ -333,15 +338,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("", lenientlyEquals("""
{
"statusPhrase": "Bad Request",
"message": "[
<<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found
<<<]"
"message": "['D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on
}
}
@Nested
@Order(1)
class GetAsset {
@Test
@ -413,6 +417,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
@Nested
@Order(4)
class PatchAsset {
@Test
@ -466,6 +471,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
@Nested
@Order(5)
class DeleteAsset {
@Test

View File

@ -55,18 +55,22 @@ class HsHostingAssetPropsControllerAcceptanceTest {
[
{
"type": "integer",
"propertyName": "monit_min_free_ssd",
"min": 1,
"max": 1000,
"propertyName": "monit_max_cpu_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 92,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_min_free_hdd",
"min": 1,
"max": 4000,
"propertyName": "monit_max_ram_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 92,
"isTotalsValidator": false
},
{
@ -75,7 +79,17 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"unit": "%",
"min": 10,
"max": 100,
"required": true,
"required": false,
"defaultValue": 98,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_min_free_ssd",
"min": 1,
"max": 1000,
"required": false,
"defaultValue": 5,
"isTotalsValidator": false
},
{
@ -85,29 +99,157 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"min": 10,
"max": 100,
"required": false,
"defaultValue": 95,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_max_cpu_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": true,
"propertyName": "monit_min_free_hdd",
"min": 1,
"max": 4000,
"required": false,
"defaultValue": 10,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_max_ram_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": true,
"type": "boolean",
"propertyName": "software-pgsql",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-mariadb",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "enumeration",
"propertyName": "php-default",
"values": [
"5.6",
"7.0",
"7.1",
"7.2",
"7.3",
"7.4",
"8.0",
"8.1",
"8.2"
],
"required": false,
"defaultValue": "8.2",
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-5.6",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.1",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.2",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.3",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.4",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-8.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-8.1",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-8.2",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-postfix-tls-1.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-dovecot-tls-1.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-clamav",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-collabora",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-libreoffice",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-imagemagick-ghostscript",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
}
]
"""));
// @formatter:on
}
}

View File

@ -24,8 +24,6 @@ import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;

View File

@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
import jakarta.validation.ValidationException;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static org.assertj.core.api.Assertions.assertThat;
@ -23,10 +22,6 @@ class HsHostingAssetEntityValidatorUnitTest {
final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
// then
assertThat(result).isInstanceOf(ValidationException.class)
.hasMessageContaining(
"'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is required but missing",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is required but missing");
assertThat(result).isNull(); // all required properties have defaults
}
}

View File

@ -32,7 +32,6 @@ class HsManagedServerHostingAssetValidatorUnitTest {
assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
}
}

View File

@ -109,9 +109,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
assertThat(debitorRepo.count()).isEqualTo(count + 1);
}
@Transactional
@ParameterizedTest
@ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"})
@Transactional
public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) {
// given
context("superuser-alex@hostsharing.net");

View File

@ -14,9 +14,12 @@ import org.junit.jupiter.api.TestInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import jakarta.persistence.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import static java.lang.System.out;
import static java.util.Comparator.comparing;
@ -28,9 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat;
public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
private static final boolean DETAILED_BUT_SLOW_CHECK = true;
@PersistenceContext
protected EntityManager em;
@Autowired
private PlatformTransactionManager tm;
@Autowired
RbacGrantRepository rbacGrantRepo;
@ -166,12 +173,16 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
@AfterEach
void cleanupAndCheckCleanup(final TestInfo testInfo) {
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup");
cleanupTemporaryTestData();
deleteLeakedRbacObjects();
long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked();
// If the whole test method has its own transaction, cleanup makes no sense.
// If that transaction even failed, cleaunup would cause an exception.
if (!tm.getTransaction(null).isRollbackOnly()) {
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup");
cleanupTemporaryTestData();
repeatUntilTrue(3, this::deleteLeakedRbacObjects);
out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount);
long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked();
out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount);
}
}
private void cleanupTemporaryTestData() {
@ -218,7 +229,8 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
}).assertSuccessful().returnedValue();
}
private void deleteLeakedRbacObjects() {
private boolean deleteLeakedRbacObjects() {
final var deletionSuccessful = new AtomicBoolean(true);
rbacObjectRepo.findAll().stream()
.filter(o -> o.serialId > latestIntialTestDataSerialId)
.sorted(comparing(o -> o.serialId))
@ -235,8 +247,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
if (exception != null) {
out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception);
deletionSuccessful.set(false);
}
});
return deletionSuccessful.get();
}
private void assertEqual(final Set<String> before, final Set<String> after) {
@ -297,6 +311,15 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
"doc/temp/" + name + ".md"
);
}
public static boolean repeatUntilTrue(int maxAttempts, Supplier<Boolean> method) {
for (int attempts = 0; attempts < maxAttempts; attempts++) {
if (method.get()) {
return true;
}
}
return false;
}
}
interface RbacObjectRepository extends Repository<RbacObjectEntity, UUID> {