1
0

hosting-asset-data-migration (#79)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/79
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-07-22 11:30:33 +02:00
parent c191af2ea1
commit 4d27a98c9a
51 changed files with 1761 additions and 602 deletions

View File

@ -18,7 +18,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
@Entity
@Table(name = "hs_booking_debitor_rv")
@Table(name = "hs_booking_debitor_xv")
@Getter
@Builder
@NoArgsConstructor

View File

@ -184,7 +184,9 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Propertie
}
public HsBookingProjectEntity getRelatedProject() {
return project != null ? project : parentItem.getRelatedProject();
return project != null ? project
: parentItem != null ? parentItem.getRelatedProject()
: null; // can be the case for technical assets like IP-numbers
}
public static RbacView rbac() {

View File

@ -22,6 +22,10 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
@Override
public List<String> validateEntity(final HsBookingItemEntity bookingItem) {
// TODO.impl: HsBookingItemType could do this similar to HsHostingAssetType
if ( bookingItem.getParentItem() == null && bookingItem.getProject() == null) {
return List.of(bookingItem + ".'parentItem' or .'project' expected to be set, but both are null");
}
return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem));
}

View File

@ -11,11 +11,12 @@ class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
// @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(),
integerProperty("CPU") .min( 1) .max( 32) .required(),
integerProperty("RAM").unit("GB") .min( 1) .max( 8192) .required(),
integerProperty("SSD").unit("GB") .min( 25) .max( 1000) .step(25).requiresAtLeastOneOf("SDD", "HDD"),
integerProperty("HDD").unit("GB") .min(250) .max( 4000) .step(250).requiresAtLeastOneOf("SSD", "HDD"),
integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).requiresAtMaxOneOf("Bandwidth", "Traffic"),
integerProperty("Bandwidth").unit("GB") .min(250) .max(10000) .step(250).requiresAtMaxOneOf("Bandwidth", "Traffic"), // TODO.spec
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
// @formatter:on

View File

@ -10,11 +10,12 @@ class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator {
HsManagedServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("CPU").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().asTotalLimit().withThreshold(200),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0).asTotalLimit().withThreshold(200),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required().asTotalLimit().withThreshold(200),
integerProperty("SSD").unit("GB").min(25).max(2000).step(25).requiresAtLeastOneOf("SSD", "HDD").asTotalLimit().withThreshold(200),
integerProperty("HDD").unit("GB").min(250).max(10000).step(250).requiresAtLeastOneOf("SSD", "HDD").asTotalLimit().withThreshold(200),
integerProperty("Traffic").unit("GB").min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit().withThreshold(200),
integerProperty("Bandwidth").unit("GB").min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit().withThreshold(200), // TODO.spec
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),

View File

@ -23,16 +23,17 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator
public HsManagedWebspaceBookingItemValidator() {
super(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
integerProperty("SSD").unit("GB").min(1).max(2000).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(10000).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(64000).step(10).requiresAtMaxOneOf("Bandwidth", "Traffic"),
integerProperty("Bandwidth").unit("GB").min(10).max(1000).step(10).requiresAtMaxOneOf("Bandwidth", "Traffic"), // TODO.spec
integerProperty("Multi").min(1).max(100).step(1).withDefault(1)
.eachComprising( 25, unixUsers())
.eachComprising( 5, databaseUsers())
.eachComprising( 5, databases())
.eachComprising(250, eMailAddresses()),
integerProperty("Daemons").min(0).max(10).withDefault(0),
booleanProperty("Online Office Server").optional(),
integerProperty("Daemons").min(0).max(16).withDefault(0),
booleanProperty("Online Office Server").optional(), // TODO.impl: shorten to "Office"
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").withDefault("BASIC")
);
}

View File

@ -7,15 +7,16 @@ class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
HsPrivateCloudBookingItemValidator() {
super(
// @formatter:off
integerProperty("CPUs") .min( 1).max( 128).required().asTotalLimit(),
integerProperty("CPU") .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(),
integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).requiresAtLeastOneOf("SSD", "HDD").asTotalLimit(),
integerProperty("HDD").unit("GB") .min(250).max(16000).step(250).requiresAtLeastOneOf("SSD", "HDD").asTotalLimit(),
integerProperty("Traffic").unit("GB") .min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit(),
integerProperty("Bandwidth").unit("GB") .min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit(), // TODO.spec
// 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("CPU").countsAs(64)
// .each("RAM").countsAs(64)
// .each("SSD").countsAs(18)
// .each("HDD").countsAs(2)

View File

@ -8,6 +8,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
@ -38,6 +39,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static java.util.Collections.emptyMap;
@ -108,7 +110,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti
private HsOfficeContactEntity alarmContact;
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name="parentassetuuid", referencedColumnName="uuid")
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
private List<HsHostingAssetEntity> subHostingAssets;
@Column(name = "identifier")
@ -134,12 +136,20 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti
this.isLoaded = true;
}
public HsBookingProjectEntity getRelatedProject() {
return Optional.ofNullable(bookingItem)
.map(HsBookingItemEntity::getRelatedProject)
.orElseGet(() -> Optional.ofNullable(parentAsset)
.map(HsHostingAssetEntity::getRelatedProject)
.orElse(null));
}
public PatchableMapWrapper<Object> getConfig() {
return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config );
return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config);
}
public void putConfig(Map<String, Object> newConfig) {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig);
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
}
@Override
@ -150,20 +160,19 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti
@Override
public Object getContextValue(final String propName) {
final var v = config.get(propName);
if (v!= null) {
if (v != null) {
return v;
}
if (bookingItem!=null) {
if (bookingItem != null) {
return bookingItem.getResources().get(propName);
}
if (parentAsset!=null && parentAsset.getBookingItem()!=null) {
if (parentAsset != null && parentAsset.getBookingItem() != null) {
return parentAsset.getBookingItem().getResources().get(propName);
}
return emptyMap();
}
@Override
public String toString() {
return stringify.apply(this);
@ -182,9 +191,9 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti
.toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data?
.importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(),
dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(),
dependsOnColumn("parentAssetUuid"),
@ -202,7 +211,8 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti
directlyFetchedByDependsOnColumn(),
NULLABLE)
.switchOnColumn("type",
.switchOnColumn(
"type",
inCaseOf("DOMAIN_SETUP", then -> {
then.toRole(GLOBAL, GUEST).grantPermission(INSERT);
})
@ -231,7 +241,14 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti
with.permission(SELECT);
})
.limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentAsset", "assignedToAsset", "alarmContact", "global");
.limitDiagramTo(
"asset",
"bookingItem",
"bookingItem.debitorRel",
"parentAsset",
"assignedToAsset",
"alarmContact",
"global");
}
public static void main(String[] args) throws IOException {

View File

@ -18,7 +18,7 @@ class HsManagedWebspaceHostingAssetValidator extends HostingAssetEntityValidator
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var prefixPattern =
!assetEntity.isLoaded()
? assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix()
? assetEntity.getRelatedProject().getDebitor().getDefaultPrefix()
: "[a-z][a-z0-9][a-z0-9]";
return Pattern.compile("^" + prefixPattern + "[0-9][0-9]$");
}

View File

@ -68,7 +68,7 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
.withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber())
.withProp(e -> e.getPartner().toShortString())
.withProp(HsOfficeMembershipEntity::getPartner)
.withProp(e -> e.getValidity().asString())
.withProp(HsOfficeMembershipEntity::getStatus)
.quotedValues(false);

View File

@ -13,10 +13,12 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import static java.lang.Boolean.FALSE;
@ -30,7 +32,7 @@ import static org.apache.commons.lang3.ObjectUtils.isArray;
public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T> {
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "requiresAtLeastOneOf", "requiresAtMaxOneOf", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
protected static final String[] KEY_ORDER = Array.join(KEY_ORDER_HEAD, KEY_ORDER_TAIL);
final Class<T> type;
@ -40,6 +42,8 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
private final String[] keyOrder;
private Boolean required;
private Set<String> requiresAtLeastOneOf;
private Set<String> requiresAtMaxOneOf;
private T defaultValue;
@JsonIgnore
@ -100,9 +104,19 @@ protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]>
return self();
}
public ValidatableProperty<P, T> optional() {
public P optional() {
required = FALSE;
return this;
return self();
}
public P requiresAtLeastOneOf(final String... propNames) {
requiresAtLeastOneOf = new LinkedHashSet<>(List.of(propNames));
return self();
}
public P requiresAtMaxOneOf(final String... propNames) {
requiresAtMaxOneOf = new LinkedHashSet<>(List.of(propNames));
return self();
}
public P withDefault(final T value) {
@ -172,28 +186,57 @@ protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]>
final var result = new ArrayList<String>();
final var props = propsProvider.directProps();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
if (required == TRUE) {
result.add(propertyName + "' is required but missing");
}
validateRequiresAtLeastOneOf(result, propsProvider);
}
if (propValue != null){
validateRequiresAtMaxOneOf(result, propsProvider);
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, (T) propValue, propsProvider);
} else {
result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + ", " +
"but is of type " + propValue.getClass().getSimpleName() + "");
"but is of type " + propValue.getClass().getSimpleName());
}
}
return result;
}
private void validateRequiresAtLeastOneOf(final ArrayList<String> result, final PropertiesProvider propsProvider) {
if (requiresAtLeastOneOf != null ) {
final var allPropNames = propsProvider.directProps().keySet();
final var entriesWithValue = allPropNames.stream()
.filter(name -> requiresAtLeastOneOf.contains(name))
.count();
if (entriesWithValue == 0) {
result.add(propertyName + "' is required once in group " + requiresAtLeastOneOf + " but missing");
}
}
}
private void validateRequiresAtMaxOneOf(final ArrayList<String> result, final PropertiesProvider propsProvider) {
if (requiresAtMaxOneOf != null) {
final var allPropNames = propsProvider.directProps().keySet();
final var entriesWithValue = allPropNames.stream()
.filter(name -> requiresAtMaxOneOf.contains(name))
.count();
if (entriesWithValue > 1) {
result.add(propertyName + "' is required at max once in group " + requiresAtMaxOneOf
+ " but multiple properties are set");
}
}
}
protected abstract void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider);
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null ) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
if (required == null && requiresAtLeastOneOf == null && requiresAtMaxOneOf == null) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required(), .optional(), .withDefault(...), .requiresAtLeastOneOf(...) or .requiresAtMaxOneOf(...)" );
}
}