From 0037e24177fe9fc28809b8afd67a881af94037c0 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sun, 24 Jan 2021 21:43:30 +0100 Subject: WIP: Optaplanner. --- acme.yaml | 5 +++ module/acme/classpath.txt | 33 ++++++++++++++ module/acme/pom.xml | 14 ++++++ .../acme/planning/machine/CloudBalance.java | 48 ++++++++++++++++++++ .../machine/CloudBalancingEasyScoreCalculator.java | 45 +++++++++++++++++++ .../acme/planning/machine/CloudComputer.java | 16 +++++++ .../acme/planning/machine/CloudPlaningMain.java | 52 ++++++++++++++++++++++ .../acme/planning/machine/CloudPlanner.java | 22 +++++++++ .../acme/planning/machine/CloudProcess.java | 48 ++++++++++++++++++++ .../machine/CloudProcessDifficultyComparator.java | 15 +++++++ .../acme/planning/machine/ScalewayInstance.java | 25 +++++++++++ .../main/resources/io/trygvis/acme/apps/apps.drl | 22 +++++---- .../acme/planning/machine/solver-config.xml | 18 ++++++++ module/ri-engine/pom.xml | 2 + .../main/java/io/trygvis/rules/dba/Container.java | 6 ++- .../rules/machine/MachineSpecification.java | 19 ++++++++ 16 files changed, 380 insertions(+), 10 deletions(-) create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java create mode 100644 module/acme/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java create mode 100644 module/acme/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml create mode 100644 module/ri-engine/src/main/java/io/trygvis/rules/machine/MachineSpecification.java diff --git a/acme.yaml b/acme.yaml index e82200a..711720e 100644 --- a/acme.yaml +++ b/acme.yaml @@ -28,6 +28,11 @@ data: - ci-app --- +type: io.trygvis.rules.machine.MachineSpecificaiton +data: + mCpu: 2000 + memory: 2000 +--- type: io.trygvis.rules.machine.Machine data: name: acme-2 diff --git a/module/acme/classpath.txt b/module/acme/classpath.txt index ed8e5f6..8777318 100644 --- a/module/acme/classpath.txt +++ b/module/acme/classpath.txt @@ -6,6 +6,7 @@ com.fasterxml.jackson.core:jackson-annotations:2.12.0:jar com.fasterxml.jackson.core:jackson-core:2.12.0:jar com.fasterxml.jackson.core:jackson-databind:2.12.0:jar com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.0:jar +com.github.javaparser:javaparser-core:3.13.10:jar com.github.virtuald:curvesapi:1.06:jar com.google.code.findbugs:annotations:3.0.1:jar com.google.errorprone:error_prone_annotations:2.1.3:jar @@ -14,6 +15,8 @@ com.google.j2objc:j2objc-annotations:1.1:jar com.google.re2j:re2j:1.2:jar com.googlecode.java-ipv6:java-ipv6:0.17:jar com.hubspot.jinjava:jinjava:2.5.6:jar +com.sun.istack:istack-commons-runtime:3.0.8:jar +com.sun.xml.fastinfoset:FastInfoset:1.2.16:jar com.thoughtworks.xstream:xstream:1.4.14:jar com.zaxxer:SparseBitSet:1.2:jar commons-codec:commons-codec:1.11:jar @@ -21,6 +24,8 @@ commons-io:commons-io:2.8.0:jar commons-net:commons-net:2.2:jar io.trygvis.rules-sandbox:ri-engine:1.0-SNAPSHOT:jar io.trygvis.rules-sandbox:ri-wireguard:1.0-SNAPSHOT:jar +jakarta.activation:jakarta.activation-api:1.2.1:jar +jakarta.xml.bind:jakarta.xml.bind-api:2.3.2:jar org.antlr:antlr-runtime:3.5.2:jar org.apache.commons:commons-collections4:4.4:jar org.apache.commons:commons-compress:1.19:jar @@ -32,26 +37,54 @@ org.apache.poi:poi-ooxml-schemas:4.1.2:jar org.apache.xmlbeans:xmlbeans:3.1.0:jar org.checkerframework:checker-compat-qual:2.0.0:jar org.codehaus.mojo:animal-sniffer-annotations:1.14:jar +org.drools:drools-canonical-model:7.47.0.Final:jar org.drools:drools-compiler:7.47.0.Final:jar org.drools:drools-core:7.47.0.Final:jar org.drools:drools-core-dynamic:7.47.0.Final:jar org.drools:drools-core-reflective:7.47.0.Final:jar org.drools:drools-decisiontables:7.47.0.Final:jar org.drools:drools-ecj:7.47.0.Final:jar +org.drools:drools-model-compiler:7.47.0.Final:jar org.drools:drools-mvel:7.47.0.Final:jar +org.drools:drools-mvel-compiler:7.47.0.Final:jar +org.drools:drools-mvel-parser:7.47.0.Final:jar org.drools:drools-templates:7.47.0.Final:jar +org.glassfish.jaxb:jaxb-runtime:2.3.2:jar +org.glassfish.jaxb:txw2:2.3.2:jar org.javassist:javassist:3.26.0-GA:jar org.jsoup:jsoup:1.8.3:jar +org.jvnet.staxex:stax-ex:1.8.1:jar org.kie:kie-api:7.47.0.Final:jar org.kie:kie-internal:7.47.0.Final:jar org.kie:kie-memory-compiler:7.47.0.Final:jar +org.kie.kogito:drools-compiler:1.1.0.Final:jar +org.kie.kogito:drools-core:1.1.0.Final:jar +org.kie.kogito:drools-core-dynamic:1.1.0.Final:jar +org.kie.kogito:drools-core-static:1.1.0.Final:jar +org.kie.kogito:kogito-api:1.1.0.Final:jar +org.kie.kogito:kogito-drools-model:1.1.0.Final:jar +org.kie.kogito:kogito-internal:1.1.0.Final:jar +org.kie.kogito:kogito-services:1.1.0.Final:jar org.kie.soup:kie-soup-commons:7.47.0.Final:jar org.kie.soup:kie-soup-maven-support:7.47.0.Final:jar org.kie.soup:kie-soup-project-datamodel-api:7.47.0.Final:jar org.kie.soup:kie-soup-project-datamodel-commons:7.47.0.Final:jar org.kie.soup:kie-soup-xstream:7.47.0.Final:jar org.mvel:mvel2:2.4.10.Final:jar +org.optaplanner:optaplanner-core:8.1.0.Final:jar +org.optaplanner:optaplanner-persistence-common:8.1.0.Final:jar +org.optaplanner:optaplanner-persistence-jackson:8.1.0.Final:jar +org.optaplanner:optaplanner-spring-boot-autoconfigure:8.1.0.Final:jar +org.optaplanner:optaplanner-spring-boot-starter:8.1.0.Final:jar org.slf4j:slf4j-api:1.7.26:jar +org.springframework:spring-aop:5.2.5.RELEASE:jar +org.springframework:spring-beans:5.2.5.RELEASE:jar +org.springframework:spring-context:5.2.5.RELEASE:jar +org.springframework:spring-core:5.2.5.RELEASE:jar +org.springframework:spring-expression:5.2.5.RELEASE:jar +org.springframework:spring-jcl:5.2.5.RELEASE:jar +org.springframework.boot:spring-boot:2.2.6.RELEASE:jar +org.springframework.boot:spring-boot-autoconfigure:2.2.6.RELEASE:jar org.yaml:snakeyaml:1.26:jar xmlpull:xmlpull:1.2.0:jar xpp3:xpp3_min:1.2.0:jar diff --git a/module/acme/pom.xml b/module/acme/pom.xml index b3fc43c..ceb01b9 100644 --- a/module/acme/pom.xml +++ b/module/acme/pom.xml @@ -14,6 +14,10 @@ acme + + org.optaplanner + optaplanner-spring-boot-starter + ${project.groupId} ri-engine @@ -21,4 +25,14 @@ + + + + org.optaplanner + optaplanner-spring-boot-starter + 8.1.0.Final + + + + diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java new file mode 100644 index 0000000..7311918 --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java @@ -0,0 +1,48 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty; +import org.optaplanner.core.api.domain.solution.PlanningScore; +import org.optaplanner.core.api.domain.solution.PlanningSolution; +import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty; +import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider; +import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; + +import java.util.List; + +@PlanningSolution +public class CloudBalance { + + private List computerList; + + private List processList; + + private HardSoftScore score; + + public CloudBalance(List computerList, List processList) { + this.computerList = computerList; + this.processList = processList; + } + + public CloudBalance() { + } + + @ValueRangeProvider(id = "computerRange") + @ProblemFactCollectionProperty + public List getComputerList() { + return computerList; + } + + @PlanningEntityCollectionProperty + public List getProcessList() { + return processList; + } + + @PlanningScore + public HardSoftScore getScore() { + return score; + } + + public void setScore(HardSoftScore score) { + this.score = score; + } +} diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java new file mode 100644 index 0000000..6a14372 --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java @@ -0,0 +1,45 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; +import org.optaplanner.core.api.score.calculator.EasyScoreCalculator; + +public class CloudBalancingEasyScoreCalculator implements EasyScoreCalculator { + + @Override + public HardSoftScore calculateScore(CloudBalance cloudBalance) { + int hardScore = 0; + int softScore = 0; + for (CloudComputer computer : cloudBalance.getComputerList()) { + int cpu = 0; + int memory = 0; + boolean used = false; + + // Calculate usage + for (CloudProcess process : cloudBalance.getProcessList()) { + if (computer.equals(process.computer)) { + cpu += process.requiredCpu; + memory += process.requiredMemory; + used = true; + } + } + + var instance = computer.instance; + + // Hard constraints + int cpuPowerAvailable = instance.cpu - cpu; + if (cpuPowerAvailable < 0) { + hardScore += cpuPowerAvailable; + } + int memoryAvailable = instance.memory - memory; + if (memoryAvailable < 0) { + hardScore += memoryAvailable; + } + + // Soft constraints + if (used) { + softScore -= instance.cost; + } + } + return HardSoftScore.of(hardScore, softScore); + } +} diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java new file mode 100644 index 0000000..aa2087d --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java @@ -0,0 +1,16 @@ +package io.trygvis.acme.planning.machine; + +public class CloudComputer { + public final String host; + public final ScalewayInstance instance; + + public CloudComputer(String host, ScalewayInstance instance) { + this.host = host; + this.instance = instance; + } + + @Override + public String toString() { + return host; + } +} diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java new file mode 100644 index 0000000..60b3fd3 --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java @@ -0,0 +1,52 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.solver.SolverFactory; + +import java.util.ArrayList; + +public class CloudPlaningMain { + public static final ScalewayInstance Stardust1_s = new ScalewayInstance("Stardust1-s", 0.0025, 1000, 1000); + public static final ScalewayInstance DEV1_S = new ScalewayInstance("DEV1-S", 0.01, 2000, 2000); + public static final ScalewayInstance DEV1_M = new ScalewayInstance("DEV1-M", 0.02, 3000, 4000); + public static final ScalewayInstance DEV1_L = new ScalewayInstance("DEV1-L", 0.04, 4000, 8000); + public static final ScalewayInstance DEV1_XL = new ScalewayInstance("DEV1-XL", 0.06, 4000, 12000); + + public static void main(String[] args) { + var solverFactory = SolverFactory.createFromXmlResource("io/trygvis/acme/planning/machine/solver-config.xml"); + var solver = solverFactory.buildSolver(); + + var computers = new ArrayList(); + + computers.add(new CloudComputer("acme-1", DEV1_S)); + computers.add(new CloudComputer("acme-2", DEV1_M)); + computers.add(new CloudComputer("acme-3", DEV1_S)); + + var processes = new ArrayList(); + + processes.add(new CloudProcess("statera", 200, 1000)); + processes.add(new CloudProcess("statera-console", 100, 50)); + processes.add(new CloudProcess("4tune-web", 100, 50)); + processes.add(new CloudProcess("4tune-api", 200, 200)); + processes.add(new CloudProcess("pdb", 500, 500)); + processes.add(new CloudProcess("mdb", 500, 200)); + + var unsolvedCloudBalance = new CloudBalance(computers, processes); + + var solvedCloudBalance = solver.solve(unsolvedCloudBalance); + + System.out.println("solvedCloudBalance.getScore() = " + solvedCloudBalance.getScore()); + for (CloudProcess process : solvedCloudBalance.getProcessList()) { + System.out.println("process.id = " + process.id); + if (process.computer == null) { + System.out.println("COMPUTER IS NULL"); + } else { + System.out.println("process.computer.host = " + process.computer.host); + } + } + + System.out.println("------------------------------------------------------------------------"); + +// System.out.println("\nSolved cloudBalance with 400 computers and 1200 processes:\n" +// + toDisplayString(solvedCloudBalance)); + } +} diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java new file mode 100644 index 0000000..c629656 --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java @@ -0,0 +1,22 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.variable.PlanningVariable; + +@PlanningEntity() +public class CloudPlanner { + private int requiredCpuPower; + private int requiredMemory; + private int requiredNetworkBandwidth; + + private CloudComputer computer; + + @PlanningVariable(valueRangeProviderRefs = {"computerRange"}) + public CloudComputer getComputer() { + return computer; + } + + public void setComputer(CloudComputer computer) { + this.computer = computer; + } +} diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java new file mode 100644 index 0000000..2002903 --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java @@ -0,0 +1,48 @@ +package io.trygvis.acme.planning.machine; + +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.variable.PlanningVariable; + +import java.util.Comparator; + +@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class) +public class CloudProcess { + public String id; + public int requiredCpu; + public int requiredMemory; + public int requiredMultiplicand; + + @PlanningVariable( + valueRangeProviderRefs = "computerRange", + strengthComparatorClass = CloudComputerStrengthComparator.class) + public CloudComputer computer; + + public CloudProcess(String id, int requiredCpu, int requiredMemory) { + this.id = id; + this.requiredCpu = requiredCpu; + this.requiredMemory = requiredMemory; + + this.requiredMultiplicand = requiredCpu * requiredMemory; + } + + @SuppressWarnings("unused") + public CloudProcess() { + } + + public String toString() { + return id; + } + + public static class CloudComputerStrengthComparator implements Comparator { + public int compare(CloudComputer a, CloudComputer b) { + var x = a.instance; + var y = b.instance; + return new CompareToBuilder() + .append(x.multiplicand, y.multiplicand) + .append(y.cost, x.cost) // Descending (but this is debatable) + .append(x.kind, y.kind) + .toComparison(); + } + } +} diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java new file mode 100644 index 0000000..658e826 --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java @@ -0,0 +1,15 @@ +package io.trygvis.acme.planning.machine; + +import org.apache.commons.lang3.builder.CompareToBuilder; + +import java.util.Comparator; + +public class CloudProcessDifficultyComparator implements Comparator { + + public int compare(CloudProcess a, CloudProcess b) { + return new CompareToBuilder() + .append(a.requiredMultiplicand, b.requiredMultiplicand) + .append(a.id, b.id) + .toComparison(); + } +} diff --git a/module/acme/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java b/module/acme/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java new file mode 100644 index 0000000..dfdc0e0 --- /dev/null +++ b/module/acme/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java @@ -0,0 +1,25 @@ +package io.trygvis.acme.planning.machine; + +public class ScalewayInstance { + public final String kind; + // Euros / hour + public final double cost; + public final int cpu; + public final int memory; + + public int multiplicand; + + public ScalewayInstance(String kind, double cost, int cpu, int memory) { + this.kind = kind; + this.cost = cost; + this.cpu = cpu; + this.memory = memory; + + this.multiplicand = cpu * memory; + } + + @Override + public String toString() { + return kind; + } +} diff --git a/module/acme/src/main/resources/io/trygvis/acme/apps/apps.drl b/module/acme/src/main/resources/io/trygvis/acme/apps/apps.drl index e7bdfe3..670079f 100644 --- a/module/acme/src/main/resources/io/trygvis/acme/apps/apps.drl +++ b/module/acme/src/main/resources/io/trygvis/acme/apps/apps.drl @@ -1,9 +1,13 @@ -package io.trygvis.acme.apps; +package io.trygvis.acme.apps import io.trygvis.rules.machine.Machine; import io.trygvis.rules.dba.Cluster; import io.trygvis.rules.dba.Container import io.trygvis.rules.dns.DnsZone; +import io.trygvis.rules.machine.Machine +import io.trygvis.rules.machine.MachineSpecification +import io.trygvis.rules.dba.Cluster +import io.trygvis.rules.dba.Container dialect "mvel" @@ -13,8 +17,8 @@ when then var cluster = new Cluster("acme-ops"); insert(cluster); - insert(new Container(cluster, "pdb", "ops", "postgresql", "11")); - insert(new Container(cluster, "n8n", "ops", "n8n", "0.84.1")); + insert(new Container(cluster, "pdb", "ops", "postgresql", "11", null)); + insert(new Container(cluster, "n8n", "ops", "n8n", "0.84.1", null)); end rule "MyApp" @@ -31,10 +35,10 @@ then var db = $app.environment + "-db"; var tag = $app.dockerTag; - insert(new Container(cluster, "statera", app, "statera", tag)); - insert(new Container(cluster, "statera-console", app, "statera-console", tag)); - insert(new Container(cluster, "4tune-web", app, "4tune-web", tag)); - insert(new Container(cluster, "4tune-api", app, "4tune-api", tag)); - insert(new Container(cluster, "pdb", db, "postgresql", "13")); - insert(new Container(cluster, "mdb", db, "mongodb", "3.2")); + insert(new Container(cluster, "statera", app, "statera", tag, new MachineSpecification(200, 1000))); + insert(new Container(cluster, "statera-console", app, "statera-console", tag, new MachineSpecification(100, 50))); + insert(new Container(cluster, "4tune-web", app, "4tune-web", tag, new MachineSpecification(100, 50))); + insert(new Container(cluster, "4tune-api", app, "4tune-api", tag, new MachineSpecification(200, 200))); + insert(new Container(cluster, "pdb", db, "postgresql", "13", new MachineSpecification(500, 500))); + insert(new Container(cluster, "mdb", db, "mongodb", "3.2", new MachineSpecification(500, 200))); end diff --git a/module/acme/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml b/module/acme/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml new file mode 100644 index 0000000..81ae8ed --- /dev/null +++ b/module/acme/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml @@ -0,0 +1,18 @@ + + + + io.trygvis.acme.planning.machine.CloudBalance + io.trygvis.acme.planning.machine.CloudProcess + + + + io.trygvis.acme.planning.machine.CloudBalancingEasyScoreCalculator + + + + + + 3 + + diff --git a/module/ri-engine/pom.xml b/module/ri-engine/pom.xml index fd9ba7c..80ef901 100644 --- a/module/ri-engine/pom.xml +++ b/module/ri-engine/pom.xml @@ -41,10 +41,12 @@ org.drools drools-templates + org.mvel diff --git a/module/ri-engine/src/main/java/io/trygvis/rules/dba/Container.java b/module/ri-engine/src/main/java/io/trygvis/rules/dba/Container.java index f6d2ba4..3888661 100644 --- a/module/ri-engine/src/main/java/io/trygvis/rules/dba/Container.java +++ b/module/ri-engine/src/main/java/io/trygvis/rules/dba/Container.java @@ -2,6 +2,7 @@ package io.trygvis.rules.dba; import com.fasterxml.jackson.annotation.JsonIdentityReference; import io.trygvis.rules.machine.Machine; +import io.trygvis.rules.machine.MachineSpecification; //@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Container { @@ -15,14 +16,17 @@ public class Container { public final String tag; private Machine machine; + public final MachineSpecification machineSpecification; - public Container(Cluster cluster, String name, String machineRole, String image, String tag) { + public Container(Cluster cluster, String name, String machineRole, String image, String tag, + MachineSpecification machineSpecification) { this.id = cluster.name + "-" + name; this.cluster = cluster; this.name = name; this.machineRole = machineRole; this.image = image; this.tag = tag; + this.machineSpecification = machineSpecification; } public Cluster getCluster() { diff --git a/module/ri-engine/src/main/java/io/trygvis/rules/machine/MachineSpecification.java b/module/ri-engine/src/main/java/io/trygvis/rules/machine/MachineSpecification.java new file mode 100644 index 0000000..2e17ae5 --- /dev/null +++ b/module/ri-engine/src/main/java/io/trygvis/rules/machine/MachineSpecification.java @@ -0,0 +1,19 @@ +package io.trygvis.rules.machine; + +public class MachineSpecification { + public final int cpu; + public final int memory; + + public MachineSpecification(int cpu, int memory) { + this.cpu = cpu; + this.memory = memory; + } + + public int getCpu() { + return cpu; + } + + public int getMemory() { + return memory; + } +} -- cgit v1.2.3