summaryrefslogtreecommitdiff
path: root/simulator/opendc/opendc-compute
diff options
context:
space:
mode:
authorGeorgios Andreadis <info@gandreadis.com>2020-06-29 16:04:57 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2020-08-24 16:18:13 +0200
commit46b06fb446e79c390c01953d31d700b8e73da24d (patch)
treeb2329630ebf2c90d297ba0d3046ccd558d12d042 /simulator/opendc/opendc-compute
parentebcacf96fbc1cd16a91523f95dd01db046fb7f90 (diff)
Prepare simulator repository for monorepo
This change prepares the simulator Git repository for the monorepo residing at https://github.com/atlarge-research.com/opendc. To accomodate for this, we move all files into a simulator subdirectory.
Diffstat (limited to 'simulator/opendc/opendc-compute')
-rw-r--r--simulator/opendc/opendc-compute/build.gradle.kts43
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Flavor.kt41
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/MemoryUnit.kt40
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingNode.kt40
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingUnit.kt38
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt80
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerEvent.kt53
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerState.kt55
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerContext.kt163
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerManagementContext.kt40
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ShutdownException.kt53
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/EmptyImage.kt40
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImage.kt67
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsHistoryFragment.kt3
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/Image.kt46
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/VmImage.kt34
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/PerformanceInterferenceModel.kt125
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/VmWorkload.kt25
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Metadata.kt34
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt74
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeEvent.kt43
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeState.kt55
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt88
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt478
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/power/PowerModels.kt45
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/ProvisioningService.kt66
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningService.kt70
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt58
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorEvent.kt78
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt57
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/InsufficientMemoryOnServerException.kt3
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt625
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt56
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/HypervisorView.kt15
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt351
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningEvent.kt49
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningService.kt42
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AllocationPolicy.kt25
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableCoreMemoryAllocationPolicy.kt40
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableMemoryAllocationPolicy.kt15
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ComparableAllocationPolicyLogic.kt52
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/NumberOfActiveServersAllocationPolicy.kt15
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ProvisionedCoresAllocationPolicy.kt42
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/RandomAllocationPolicy.kt51
-rw-r--r--simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ReplayAllocationPolicy.kt34
-rw-r--r--simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt78
-rw-r--r--simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt88
-rw-r--r--simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt73
-rw-r--r--simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt167
49 files changed, 3953 insertions, 0 deletions
diff --git a/simulator/opendc/opendc-compute/build.gradle.kts b/simulator/opendc/opendc-compute/build.gradle.kts
new file mode 100644
index 00000000..acdcd5a7
--- /dev/null
+++ b/simulator/opendc/opendc-compute/build.gradle.kts
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+description = "Cloud computing fabric simulation model"
+
+/* Build configuration */
+plugins {
+ `kotlin-library-convention`
+}
+
+dependencies {
+ implementation(kotlin("stdlib"))
+ api(project(":odcsim:odcsim-api"))
+ api(project(":opendc:opendc-core"))
+ implementation("io.github.microutils:kotlin-logging:1.7.9")
+
+ testRuntimeOnly(project(":odcsim:odcsim-engine-omega"))
+ testRuntimeOnly("org.slf4j:slf4j-simple:${Library.SLF4J}")
+ testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}")
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}")
+ testImplementation("org.junit.platform:junit-platform-launcher:${Library.JUNIT_PLATFORM}")
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Flavor.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Flavor.kt
new file mode 100644
index 00000000..ff5060de
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Flavor.kt
@@ -0,0 +1,41 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core
+
+/**
+ * Flavors define the compute and memory capacity of [Server] instance. o put it simply, a flavor is an available
+ * hardware configuration for a server. It defines the size of a virtual server that can be launched.
+ */
+public data class Flavor(
+ /**
+ * The number of (virtual) processing cores to use.
+ */
+ public val cpuCount: Int,
+
+ /**
+ * The amount of RAM available to the server (in MB).
+ */
+ public val memorySize: Long
+)
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/MemoryUnit.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/MemoryUnit.kt
new file mode 100644
index 00000000..ce57fc72
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/MemoryUnit.kt
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core
+
+/**
+ * A memory unit of a compute resource, either virtual or physical.
+ *
+ * @property vendor The vendor string of the memory.
+ * @property modelName The name of the memory model.
+ * @property speed The access speed of the memory in MHz.
+ * @property size The size of the memory unit in MBs.
+ */
+public data class MemoryUnit(
+ public val vendor: String,
+ public val modelName: String,
+ public val speed: Double,
+ public val size: Long
+)
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingNode.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingNode.kt
new file mode 100644
index 00000000..91f5dde9
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingNode.kt
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core
+
+/**
+ * A processing node/package/socket containing possibly several CPU cores.
+ *
+ * @property vendor The vendor string of the processor node.
+ * @property modelName The name of the processor node.
+ * @property arch The micro-architecture of the processor node.
+ * @property coreCount The number of logical CPUs in the processor node.
+ */
+data class ProcessingNode(
+ val vendor: String,
+ val arch: String,
+ val modelName: String,
+ val coreCount: Int
+)
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingUnit.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingUnit.kt
new file mode 100644
index 00000000..ba148ee0
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ProcessingUnit.kt
@@ -0,0 +1,38 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core
+
+/**
+ * A single logical compute unit of processor node, either virtual or physical.
+ *
+ * @property node The processing node containing the CPU core.
+ * @property id The identifier of the CPU core within the processing node.
+ * @property frequency The clock rate of the CPU in MHz.
+ */
+public data class ProcessingUnit(
+ public val node: ProcessingNode,
+ public val id: Int,
+ public val frequency: Double
+)
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt
new file mode 100644
index 00000000..01968cd8
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/Server.kt
@@ -0,0 +1,80 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core
+
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.core.resource.Resource
+import com.atlarge.opendc.core.resource.TagContainer
+import com.atlarge.opendc.core.services.ServiceRegistry
+import kotlinx.coroutines.flow.Flow
+import java.util.UUID
+
+/**
+ * A server instance that is running on some physical or virtual machine.
+ */
+public data class Server(
+ /**
+ * The unique identifier of the server.
+ */
+ public override val uid: UUID,
+
+ /**
+ * The optional name of the server.
+ */
+ public override val name: String,
+
+ /**
+ * The tags of this server.
+ */
+ public override val tags: TagContainer,
+
+ /**
+ * The hardware configuration of the server.
+ */
+ public val flavor: Flavor,
+
+ /**
+ * The image running on the server.
+ */
+ public val image: Image,
+
+ /**
+ * The last known state of the server.
+ */
+ public val state: ServerState,
+
+ /**
+ * The services published by this server.
+ */
+ public val services: ServiceRegistry,
+
+ /**
+ * The events that are emitted by the server.
+ */
+ public val events: Flow<ServerEvent>
+) : Resource {
+ override fun hashCode(): Int = uid.hashCode()
+ override fun equals(other: Any?): Boolean = other is Server && uid == other.uid
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerEvent.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerEvent.kt
new file mode 100644
index 00000000..1595937c
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerEvent.kt
@@ -0,0 +1,53 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core
+
+import com.atlarge.opendc.core.services.ServiceKey
+
+/**
+ * An event that is emitted by a [Server].
+ */
+public sealed class ServerEvent {
+ /**
+ * The server that emitted the event.
+ */
+ public abstract val server: Server
+
+ /**
+ * This event is emitted when the state of [server] changes.
+ *
+ * @property server The server of which the state changed.
+ * @property previousState The previous state of the server.
+ */
+ public data class StateChanged(override val server: Server, val previousState: ServerState) : ServerEvent()
+
+ /**
+ * This event is emitted when a server publishes a service.
+ *
+ * @property server The server that published the service.
+ * @property key The service key of the service that was published.
+ */
+ public data class ServicePublished(override val server: Server, val key: ServiceKey<*>) : ServerEvent()
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerState.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerState.kt
new file mode 100644
index 00000000..27372a5e
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/ServerState.kt
@@ -0,0 +1,55 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core
+
+/**
+ * An enumeration describing the possible states of a server.
+ */
+public enum class ServerState {
+ /**
+ * The server has not yet finished the original build process.
+ */
+ BUILD,
+
+ /**
+ * The server was powered down by the user.
+ */
+ SHUTOFF,
+
+ /**
+ * The server is active and running.
+ */
+ ACTIVE,
+
+ /**
+ * The server is in error.
+ */
+ ERROR,
+
+ /**
+ * The state of the server is unknown.
+ */
+ UNKNOWN,
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerContext.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerContext.kt
new file mode 100644
index 00000000..f770fa49
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerContext.kt
@@ -0,0 +1,163 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.execution
+
+import com.atlarge.opendc.compute.core.ProcessingUnit
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.core.services.ServiceKey
+import kotlinx.coroutines.selects.SelectClause0
+import kotlinx.coroutines.selects.select
+
+/**
+ * Represents the execution context in which a bootable [Image] runs on a [Server].
+ */
+public interface ServerContext {
+ /**
+ * The server on which the image runs.
+ */
+ public val server: Server
+
+ /**
+ * A list of processing units available to use.
+ */
+ public val cpus: List<ProcessingUnit>
+
+ /**
+ * Publish the specified [service] at the given [ServiceKey].
+ */
+ public suspend fun <T : Any> publishService(key: ServiceKey<T>, service: T)
+
+ /**
+ * Ask the processor cores to run the specified [slice] and suspend execution until the trigger condition is met as
+ * specified by [triggerMode].
+ *
+ * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which
+ * may be zero). These changes may happen anytime during execution of this method and callers should not rely on
+ * the timing of this change.
+ *
+ * @param slice The representation of work to run on the processors.
+ * @param triggerMode The trigger condition to resume execution.
+ */
+ public suspend fun run(slice: Slice, triggerMode: TriggerMode = TriggerMode.FIRST) =
+ select<Unit> { onRun(slice, triggerMode).invoke {} }
+
+ /**
+ * Ask the processors cores to run the specified [batch] of work slices and suspend execution until the trigger
+ * condition is met as specified by [triggerMode].
+ *
+ * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which
+ * may be zero). These changes may happen anytime during execution of this method and callers should not rely on
+ * the timing of this change.
+ *
+ * In case slices in the batch do not finish processing before their deadline, [merge] is called to merge these
+ * slices with the next slice to be executed.
+ *
+ * @param batch The batch of work to run on the processors.
+ * @param triggerMode The trigger condition to resume execution.
+ * @param merge The merge function for consecutive slices in case the last slice was not completed within its
+ * deadline.
+ */
+ public suspend fun run(
+ batch: Sequence<Slice>,
+ triggerMode: TriggerMode = TriggerMode.FIRST,
+ merge: (Slice, Slice) -> Slice = { _, r -> r }
+ ) = select<Unit> { onRun(batch, triggerMode, merge).invoke {} }
+
+ /**
+ * Ask the processor cores to run the specified [slice] and select when the trigger condition is met as specified
+ * by [triggerMode].
+ *
+ * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which
+ * may be zero). These changes may happen anytime during execution of this method and callers should not rely on
+ * the timing of this change.
+ *
+ * @param slice The representation of work to request from the processors.
+ * @param triggerMode The trigger condition to resume execution.
+ */
+ public fun onRun(slice: Slice, triggerMode: TriggerMode = TriggerMode.FIRST): SelectClause0 =
+ onRun(sequenceOf(slice), triggerMode)
+
+ /**
+ * Ask the processors cores to run the specified [batch] of work slices and select when the trigger condition is met
+ * as specified by [triggerMode].
+ *
+ * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which
+ * may be zero). These changes may happen anytime during execution of this method and callers should not rely on
+ * the timing of this change.
+ *
+ * In case slices in the batch do not finish processing before their deadline, [merge] is called to merge these
+ * slices with the next slice to be executed.
+ *
+ * @param batch The batch of work to run on the processors.
+ * @param triggerMode The trigger condition to resume execution during the **last** slice.
+ * @param merge The merge function for consecutive slices in case the last slice was not completed within its
+ * deadline.
+ */
+ public fun onRun(
+ batch: Sequence<Slice>,
+ triggerMode: TriggerMode = TriggerMode.FIRST,
+ merge: (Slice, Slice) -> Slice = { _, r -> r }
+ ): SelectClause0
+
+ /**
+ * A request to the host machine for a slice of CPU time from the processor cores.
+ *
+ * Both [burst] and [limit] must be of the same size and in any other case the method will throw an
+ * [IllegalArgumentException].
+ *
+ *
+ * @param burst The burst time to request from each of the processor cores.
+ * @param limit The maximum usage in terms of MHz that the processing core may use while running the burst.
+ * @param deadline The instant at which this slice needs to be fulfilled.
+ */
+ public class Slice(val burst: LongArray, val limit: DoubleArray, val deadline: Long) {
+ init {
+ require(burst.size == limit.size) { "Incompatible array dimensions" }
+ }
+ }
+
+ /**
+ * The modes for triggering a machine exit from the machine.
+ */
+ public enum class TriggerMode {
+ /**
+ * A machine exit occurs when either the first processor finishes processing a **non-zero** burst or the
+ * deadline is reached.
+ */
+ FIRST,
+
+ /**
+ * A machine exit occurs when either the last processor finishes processing a **non-zero** burst or the deadline
+ * is reached.
+ */
+ LAST,
+
+ /**
+ * A machine exit occurs only when the deadline is reached.
+ */
+ DEADLINE
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerManagementContext.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerManagementContext.kt
new file mode 100644
index 00000000..5a9b725b
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ServerManagementContext.kt
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.execution
+
+/**
+ * An extended [ServerContext] providing several methods for managing the execution context.
+ */
+public interface ServerManagementContext : ServerContext {
+ /**
+ * Initialize the management context.
+ */
+ public suspend fun init()
+
+ /**
+ * Terminate the execution of the server.
+ */
+ public suspend fun exit(cause: Throwable? = null)
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ShutdownException.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ShutdownException.kt
new file mode 100644
index 00000000..e4da557b
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/execution/ShutdownException.kt
@@ -0,0 +1,53 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.execution
+
+import kotlinx.coroutines.CancellationException
+
+/**
+ * This exception is thrown by the underlying [ServerContext] to indicate that a shutdown flow
+ * has been sent to the server.
+ */
+public class ShutdownException(message: String? = null, override val cause: Throwable? = null) : CancellationException(message)
+
+/**
+ * This method terminates the current active coroutine if the specified [CancellationException] is caused
+ * by a shutdown.
+ */
+public fun CancellationException.assertShutdown() {
+ if (this is ShutdownException) {
+ throw this
+ }
+}
+
+/**
+ * This method terminates the current active coroutine if the specified [CancellationException] is caused
+ * by a failure.
+ */
+public fun CancellationException.assertFailure() {
+ if (this is ShutdownException && cause != null) {
+ throw this
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/EmptyImage.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/EmptyImage.kt
new file mode 100644
index 00000000..8f6c4682
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/EmptyImage.kt
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.image
+
+import com.atlarge.opendc.compute.core.execution.ServerContext
+import com.atlarge.opendc.core.resource.TagContainer
+import java.util.UUID
+
+/**
+ * An empty boot disk [Image] that exits immediately on start.
+ */
+object EmptyImage : Image {
+ override val uid: UUID = UUID.randomUUID()
+ override val name: String = "empty"
+ override val tags: TagContainer = emptyMap()
+
+ override suspend fun invoke(ctx: ServerContext) {}
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImage.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImage.kt
new file mode 100644
index 00000000..d65e7e94
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImage.kt
@@ -0,0 +1,67 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.image
+
+import com.atlarge.opendc.compute.core.execution.ServerContext
+import com.atlarge.opendc.core.resource.TagContainer
+import java.util.UUID
+import kotlin.math.min
+
+/**
+ * An application [Image] that models applications performing a static number of floating point operations ([flops]) on
+ * a compute resource.
+ *
+ * @property uid The unique identifier of this image.
+ * @property name The name of this image.
+ * @property tags The tags attached to the image.
+ * @property flops The number of floating point operations to perform for this task in MFLOPs.
+ * @property cores The number of cores that the image is able to utilize.
+ * @property utilization A model of the CPU utilization of the application.
+ */
+data class FlopsApplicationImage(
+ public override val uid: UUID,
+ public override val name: String,
+ public override val tags: TagContainer,
+ public val flops: Long,
+ public val cores: Int,
+ public val utilization: Double = 0.8
+) : Image {
+ init {
+ require(flops >= 0) { "Negative number of flops" }
+ require(cores > 0) { "Negative number of cores or no cores" }
+ require(utilization > 0.0 && utilization <= 1.0) { "Utilization must be in (0, 1]" }
+ }
+
+ /**
+ * Execute the runtime behavior based on a number of floating point operations to execute.
+ */
+ override suspend fun invoke(ctx: ServerContext) {
+ val cores = min(this.cores, ctx.server.flavor.cpuCount)
+ val burst = LongArray(cores) { flops / cores }
+ val maxUsage = DoubleArray(cores) { i -> ctx.cpus[i].frequency * utilization }
+
+ ctx.run(ServerContext.Slice(burst, maxUsage, Long.MAX_VALUE), triggerMode = ServerContext.TriggerMode.LAST)
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsHistoryFragment.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsHistoryFragment.kt
new file mode 100644
index 00000000..5b0035e3
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/FlopsHistoryFragment.kt
@@ -0,0 +1,3 @@
+package com.atlarge.opendc.compute.core.image
+
+data class FlopsHistoryFragment(val tick: Long, val flops: Long, val duration: Long, val usage: Double, val cores: Int)
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/Image.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/Image.kt
new file mode 100644
index 00000000..52d4d7b5
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/Image.kt
@@ -0,0 +1,46 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.image
+
+import com.atlarge.opendc.compute.core.execution.ServerContext
+import com.atlarge.opendc.core.resource.Resource
+
+/**
+ * An image containing a bootable operating system that can directly be executed by physical or virtual server.
+ *
+ * OpenStack: A collection of files used to create or rebuild a server. Operators provide a number of pre-built OS
+ * images by default. You may also create custom images from cloud servers you have launched. These custom images are
+ * useful for backup purposes or for producing “gold” server images if you plan to deploy a particular server
+ * configuration frequently.
+ */
+public interface Image : Resource {
+ /**
+ * Launch the machine image in the specified [ServerContext].
+ *
+ * This method should encapsulate and characterize the runtime behavior of the instance resulting from launching
+ * the image on some machine, in terms of the resource consumption on the machine.
+ */
+ public suspend operator fun invoke(ctx: ServerContext)
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/VmImage.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/VmImage.kt
new file mode 100644
index 00000000..c615d865
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/image/VmImage.kt
@@ -0,0 +1,34 @@
+package com.atlarge.opendc.compute.core.image
+
+import com.atlarge.odcsim.simulationContext
+import com.atlarge.opendc.compute.core.execution.ServerContext
+import com.atlarge.opendc.core.resource.TagContainer
+import java.util.UUID
+import kotlin.math.min
+
+class VmImage(
+ public override val uid: UUID,
+ public override val name: String,
+ public override val tags: TagContainer,
+ public val flopsHistory: Sequence<FlopsHistoryFragment>,
+ public val maxCores: Int,
+ public val requiredMemory: Long
+) : Image {
+
+ override suspend fun invoke(ctx: ServerContext) {
+ val clock = simulationContext.clock
+ var offset = clock.millis()
+
+ val batch = flopsHistory.map { fragment ->
+ val cores = min(fragment.cores, ctx.server.flavor.cpuCount)
+ val burst = LongArray(cores) { fragment.flops / cores }
+ val usage = DoubleArray(cores) { fragment.usage / cores }
+ offset += fragment.duration
+ ServerContext.Slice(burst, usage, offset)
+ }
+
+ ctx.run(batch)
+ }
+
+ override fun toString(): String = "VmImage(uid=$uid, name=$name, cores=$maxCores, requiredMemory=$requiredMemory)"
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/PerformanceInterferenceModel.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/PerformanceInterferenceModel.kt
new file mode 100644
index 00000000..f458877b
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/PerformanceInterferenceModel.kt
@@ -0,0 +1,125 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.workload
+
+import com.atlarge.opendc.compute.core.Server
+import java.util.SortedSet
+import java.util.TreeSet
+import kotlin.random.Random
+
+/**
+ * Meta-data key for the [PerformanceInterferenceModel] of an image.
+ */
+const val IMAGE_PERF_INTERFERENCE_MODEL = "image:performance-interference"
+
+/**
+ * Performance Interference Model describing the variability incurred by different sets of workloads if colocated.
+ *
+ * @param items The [PerformanceInterferenceModelItem]s that make up this model.
+ */
+class PerformanceInterferenceModel(
+ val items: SortedSet<PerformanceInterferenceModelItem>,
+ val random: Random = Random(0)
+) {
+ private var intersectingItems: List<PerformanceInterferenceModelItem> = emptyList()
+ private val colocatedWorkloads = TreeSet<String>()
+
+ fun vmStarted(server: Server) {
+ colocatedWorkloads.add(server.image.name)
+ intersectingItems = items.filter { item -> doesMatch(item) }
+ }
+
+ fun vmStopped(server: Server) {
+ colocatedWorkloads.remove(server.image.name)
+ intersectingItems = items.filter { item -> doesMatch(item) }
+ }
+
+ private fun doesMatch(item: PerformanceInterferenceModelItem): Boolean {
+ var count = 0
+ for (name in item.workloadNames.subSet(colocatedWorkloads.first(), colocatedWorkloads.last() + "\u0000")) {
+ if (name in colocatedWorkloads)
+ count++
+ if (count > 1)
+ return true
+ }
+ return false
+ }
+
+ fun apply(currentServerLoad: Double): Double {
+ if (intersectingItems.isEmpty()) {
+ return 1.0
+ }
+ val score = intersectingItems
+ .firstOrNull { it.minServerLoad <= currentServerLoad }
+
+ // Apply performance penalty to (on average) only one of the VMs
+ return if (score != null && random.nextInt(score.workloadNames.size) == 0) {
+ score.performanceScore
+ } else {
+ 1.0
+ }
+ }
+}
+
+/**
+ * Model describing how a specific set of workloads causes performance variability for each workload.
+ *
+ * @param workloadNames The names of the workloads that together cause performance variability for each workload in the set.
+ * @param minServerLoad The minimum total server load at which this interference is activated and noticeable.
+ * @param performanceScore The performance score that should be applied to each workload's performance. 1 means no
+ * influence, <1 means that performance degrades, and >1 means that performance improves.
+ */
+data class PerformanceInterferenceModelItem(
+ val workloadNames: SortedSet<String>,
+ val minServerLoad: Double,
+ val performanceScore: Double
+) : Comparable<PerformanceInterferenceModelItem> {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PerformanceInterferenceModelItem
+
+ if (workloadNames != other.workloadNames) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int = workloadNames.hashCode()
+
+ override fun compareTo(other: PerformanceInterferenceModelItem): Int {
+ var cmp = performanceScore.compareTo(other.performanceScore)
+ if (cmp != 0) {
+ return cmp
+ }
+
+ cmp = minServerLoad.compareTo(other.minServerLoad)
+ if (cmp != 0) {
+ return cmp
+ }
+
+ return hashCode().compareTo(other.hashCode())
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/VmWorkload.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/VmWorkload.kt
new file mode 100644
index 00000000..098eb8ca
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/core/workload/VmWorkload.kt
@@ -0,0 +1,25 @@
+package com.atlarge.opendc.compute.core.workload
+
+import com.atlarge.opendc.compute.core.image.VmImage
+import com.atlarge.opendc.core.User
+import com.atlarge.opendc.core.workload.Workload
+import java.util.UUID
+
+/**
+ * A workload that represents a VM.
+ *
+ * @property uid A unique identified of this VM.
+ * @property name The name of this VM.
+ * @property owner The owner of the VM.
+ * @property image The image of the VM.
+ */
+data class VmWorkload(
+ override val uid: UUID,
+ override val name: String,
+ override val owner: User,
+ val image: VmImage
+) : Workload {
+ override fun equals(other: Any?): Boolean = other is VmWorkload && uid == other.uid
+
+ override fun hashCode(): Int = uid.hashCode()
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Metadata.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Metadata.kt
new file mode 100644
index 00000000..a3a851fe
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Metadata.kt
@@ -0,0 +1,34 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal
+
+/*
+ * Common metadata keys for bare-metal nodes.
+ */
+
+/**
+ * The cluster to which the node belongs.
+ */
+const val NODE_CLUSTER = "bare-metal:cluster"
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt
new file mode 100644
index 00000000..7cb4c0c5
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/Node.kt
@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal
+
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.core.Identity
+import kotlinx.coroutines.flow.Flow
+import java.util.UUID
+
+/**
+ * A bare-metal compute node.
+ */
+public data class Node(
+ /**
+ * The unique identifier of the node.
+ */
+ public override val uid: UUID,
+
+ /**
+ * The optional name of the node.
+ */
+ public override val name: String,
+
+ /**
+ * Metadata of the node.
+ */
+ public val metadata: Map<String, Any>,
+
+ /**
+ * The last known state of the compute node.
+ */
+ public val state: NodeState,
+
+ /**
+ * The boot image of the node.
+ */
+ public val image: Image,
+
+ /**
+ * The server instance that is running on the node or `null` if no server is running.
+ */
+ public val server: Server?,
+
+ /**
+ * The events that are emitted by the node.
+ */
+ public val events: Flow<NodeEvent>
+) : Identity {
+ override fun hashCode(): Int = uid.hashCode()
+ override fun equals(other: Any?): Boolean = other is Node && uid == other.uid
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeEvent.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeEvent.kt
new file mode 100644
index 00000000..7719db24
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeEvent.kt
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal
+
+/**
+ * An event that is emitted by a [Node].
+ */
+public sealed class NodeEvent {
+ /**
+ * The node that emitted the event.
+ */
+ public abstract val node: Node
+
+ /**
+ * This event is emitted when the state of [node] changes.
+ *
+ * @property node The node of which the state changed.
+ * @property previousState The previous state of the node.
+ */
+ public data class StateChanged(override val node: Node, val previousState: NodeState) : NodeEvent()
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeState.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeState.kt
new file mode 100644
index 00000000..ca9cf509
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/NodeState.kt
@@ -0,0 +1,55 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal
+
+/**
+ * An enumeration describing the possible states of a bare-metal compute node.
+ */
+public enum class NodeState {
+ /**
+ * The node is booting.
+ */
+ BOOT,
+
+ /**
+ * The node is powered off.
+ */
+ SHUTOFF,
+
+ /**
+ * The node is active and running.
+ */
+ ACTIVE,
+
+ /**
+ * The node is in error.
+ */
+ ERROR,
+
+ /**
+ * The state of the node is unknown.
+ */
+ UNKNOWN,
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt
new file mode 100644
index 00000000..41cec291
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/BareMetalDriver.kt
@@ -0,0 +1,88 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal.driver
+
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.metal.Node
+import com.atlarge.opendc.core.failure.FailureDomain
+import com.atlarge.opendc.core.power.Powerable
+import com.atlarge.opendc.core.services.AbstractServiceKey
+import kotlinx.coroutines.flow.Flow
+import java.util.UUID
+
+/**
+ * A driver interface for the management interface of a bare-metal compute node.
+ */
+public interface BareMetalDriver : Powerable, FailureDomain {
+ /**
+ * The [Node] that is controlled by this driver.
+ */
+ public val node: Flow<Node>
+
+ /**
+ * The amount of work done by the machine in percentage with respect to the total amount of processing power
+ * available.
+ */
+ public val usage: Flow<Double>
+
+ /**
+ * Initialize the driver.
+ */
+ public suspend fun init(): Node
+
+ /**
+ * Start the bare metal node with the specified boot disk image.
+ */
+ public suspend fun start(): Node
+
+ /**
+ * Stop the bare metal node if it is running.
+ */
+ public suspend fun stop(): Node
+
+ /**
+ * Reboot the bare metal node.
+ */
+ public suspend fun reboot(): Node
+
+ /**
+ * Update the boot disk image of the compute node.
+ *
+ * Changing the boot disk image of node does not affect it while the node is running. In order to start the new boot
+ * disk image, the compute node must be restarted.
+ */
+ public suspend fun setImage(image: Image): Node
+
+ /**
+ * Obtain the state of the compute node.
+ */
+ public suspend fun refresh(): Node
+
+ /**
+ * A key that allows access to the [BareMetalDriver] instance from a [Server] that runs on the bare-metal machine.
+ */
+ companion object Key : AbstractServiceKey<BareMetalDriver>(UUID.randomUUID(), "bare-metal:driver")
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt
new file mode 100644
index 00000000..6a77415c
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriver.kt
@@ -0,0 +1,478 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal.driver
+
+import com.atlarge.odcsim.Domain
+import com.atlarge.odcsim.SimulationContext
+import com.atlarge.odcsim.flow.EventFlow
+import com.atlarge.odcsim.flow.StateFlow
+import com.atlarge.opendc.compute.core.ProcessingUnit
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.Flavor
+import com.atlarge.opendc.compute.core.MemoryUnit
+import com.atlarge.opendc.compute.core.ServerEvent
+import com.atlarge.opendc.compute.core.ServerState
+import com.atlarge.opendc.compute.core.execution.ServerContext
+import com.atlarge.opendc.compute.core.execution.ServerManagementContext
+import com.atlarge.opendc.compute.core.execution.ShutdownException
+import com.atlarge.opendc.compute.core.image.EmptyImage
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.metal.Node
+import com.atlarge.opendc.compute.metal.NodeEvent
+import com.atlarge.opendc.compute.metal.NodeState
+import com.atlarge.opendc.compute.metal.power.ConstantPowerModel
+import com.atlarge.opendc.core.power.PowerModel
+import com.atlarge.opendc.core.services.ServiceKey
+import com.atlarge.opendc.core.services.ServiceRegistry
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Delay
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.intrinsics.startCoroutineCancellable
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.selects.SelectClause0
+import kotlinx.coroutines.selects.SelectInstance
+import java.util.UUID
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.math.min
+import kotlinx.coroutines.withContext
+import java.lang.Exception
+import java.time.Clock
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.random.Random
+
+/**
+ * A basic implementation of the [BareMetalDriver] that simulates an [Image] running on a bare-metal machine.
+ *
+ * @param domain The simulation domain the driver runs in.
+ * @param uid The unique identifier of the machine.
+ * @param name An optional name of the machine.
+ * @param metadata The initial metadata of the node.
+ * @param cpus The CPUs available to the bare metal machine.
+ * @param memoryUnits The memory units in this machine.
+ * @param powerModel The power model of this machine.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public class SimpleBareMetalDriver(
+ private val domain: Domain,
+ uid: UUID,
+ name: String,
+ metadata: Map<String, Any>,
+ val cpus: List<ProcessingUnit>,
+ val memoryUnits: List<MemoryUnit>,
+ powerModel: PowerModel<SimpleBareMetalDriver> = ConstantPowerModel(
+ 0.0
+ )
+) : BareMetalDriver {
+ /**
+ * The flavor that corresponds to this machine.
+ */
+ private val flavor = Flavor(cpus.size, memoryUnits.map { it.size }.sum())
+
+ /**
+ * The current active server context.
+ */
+ private var serverContext: BareMetalServerContext? = null
+
+ /**
+ * The events of the machine.
+ */
+ private val events = EventFlow<NodeEvent>()
+
+ /**
+ * The flow containing the load of the server.
+ */
+ private val usageState = MutableStateFlow(0.0)
+
+ /**
+ * The machine state.
+ */
+ private val nodeState = StateFlow(Node(uid, name, metadata + ("driver" to this), NodeState.SHUTOFF, EmptyImage, null, events))
+
+ override val node: Flow<Node> = nodeState
+
+ @OptIn(FlowPreview::class)
+ override val usage: Flow<Double> = usageState
+
+ override val powerDraw: Flow<Double> = powerModel(this)
+
+ /**
+ * The internal random instance.
+ */
+ private val random = Random(uid.leastSignificantBits xor uid.mostSignificantBits)
+
+ override suspend fun init(): Node = withContext(domain.coroutineContext) {
+ nodeState.value
+ }
+
+ override suspend fun start(): Node = withContext(domain.coroutineContext) {
+ val node = nodeState.value
+ if (node.state != NodeState.SHUTOFF) {
+ return@withContext node
+ }
+
+ val events = EventFlow<ServerEvent>()
+ val server = Server(
+ UUID(random.nextLong(), random.nextLong()),
+ node.name,
+ emptyMap(),
+ flavor,
+ node.image,
+ ServerState.BUILD,
+ ServiceRegistry().put(BareMetalDriver, this@SimpleBareMetalDriver),
+ events
+ )
+
+ setNode(node.copy(state = NodeState.BOOT, server = server))
+ serverContext = BareMetalServerContext(events)
+ return@withContext nodeState.value
+ }
+
+ override suspend fun stop(): Node = withContext(domain.coroutineContext) {
+ val node = nodeState.value
+ if (node.state == NodeState.SHUTOFF) {
+ return@withContext node
+ }
+
+ // We terminate the image running on the machine
+ serverContext!!.cancel(fail = false)
+ serverContext = null
+
+ setNode(node.copy(state = NodeState.SHUTOFF, server = null))
+ return@withContext node
+ }
+
+ override suspend fun reboot(): Node = withContext(domain.coroutineContext) {
+ stop()
+ start()
+ }
+
+ override suspend fun setImage(image: Image): Node = withContext(domain.coroutineContext) {
+ setNode(nodeState.value.copy(image = image))
+ return@withContext nodeState.value
+ }
+
+ override suspend fun refresh(): Node = withContext(domain.coroutineContext) { nodeState.value }
+
+ private fun setNode(value: Node) {
+ val field = nodeState.value
+ if (field.state != value.state) {
+ events.emit(NodeEvent.StateChanged(value, field.state))
+ }
+
+ if (field.server != null && value.server != null && field.server.state != value.server.state) {
+ serverContext!!.events.emit(ServerEvent.StateChanged(value.server, field.server.state))
+ }
+
+ nodeState.value = value
+ }
+
+ private inner class BareMetalServerContext(val events: EventFlow<ServerEvent>) : ServerManagementContext {
+ private var finalized: Boolean = false
+
+ // A state in which the machine is still available, but does not run any of the work requested by the
+ // image
+ var unavailable = false
+
+ override val cpus: List<ProcessingUnit> = this@SimpleBareMetalDriver.cpus
+
+ override val server: Server
+ get() = nodeState.value.server!!
+
+ private val job = domain.launch {
+ delay(1) // TODO Introduce boot time
+ init()
+ try {
+ server.image(this@BareMetalServerContext)
+ exit()
+ } catch (cause: Throwable) {
+ exit(cause)
+ }
+ }
+
+ /**
+ * Cancel the image running on the machine.
+ */
+ suspend fun cancel(fail: Boolean) {
+ if (fail)
+ job.cancel(ShutdownException(cause = Exception("Random failure")))
+ else
+ job.cancel(ShutdownException())
+ job.join()
+ }
+
+ override suspend fun <T : Any> publishService(key: ServiceKey<T>, service: T) {
+ val server = server.copy(services = server.services.put(key, service))
+ setNode(nodeState.value.copy(server = server))
+ events.emit(ServerEvent.ServicePublished(server, key))
+ }
+
+ override suspend fun init() {
+ assert(!finalized) { "Machine is already finalized" }
+
+ val server = server.copy(state = ServerState.ACTIVE)
+ setNode(nodeState.value.copy(state = NodeState.ACTIVE, server = server))
+ }
+
+ override suspend fun exit(cause: Throwable?) {
+ finalized = true
+
+ val newServerState =
+ if (cause == null || (cause is ShutdownException && cause.cause == null))
+ ServerState.SHUTOFF
+ else
+ ServerState.ERROR
+ val newNodeState =
+ if (cause == null || (cause is ShutdownException && cause.cause != null))
+ nodeState.value.state
+ else
+ NodeState.ERROR
+ val server = server.copy(state = newServerState)
+ setNode(nodeState.value.copy(state = newNodeState, server = server))
+ }
+
+ /**
+ * A disposable to prevent resetting the usage state for subsequent calls to onRun.
+ */
+ private var usageFlush: DisposableHandle? = null
+
+ /**
+ * Cache the [Clock] for timing.
+ */
+ private val clock = domain.coroutineContext[SimulationContext]!!.clock
+
+ /**
+ * Cache the [Delay] instance for timing.
+ *
+ * XXX We need to cache this before the call to [onRun] since doing this in [onRun] is too heavy.
+ * XXX Note however that this is an ugly hack which may break in the future.
+ */
+ @OptIn(InternalCoroutinesApi::class)
+ private val delay = domain.coroutineContext[ContinuationInterceptor] as Delay
+
+ @OptIn(InternalCoroutinesApi::class)
+ override fun onRun(
+ batch: Sequence<ServerContext.Slice>,
+ triggerMode: ServerContext.TriggerMode,
+ merge: (ServerContext.Slice, ServerContext.Slice) -> ServerContext.Slice
+ ): SelectClause0 {
+ assert(!finalized) { "Server instance is already finalized" }
+
+ return object : SelectClause0 {
+ @InternalCoroutinesApi
+ override fun <R> registerSelectClause0(select: SelectInstance<R>, block: suspend () -> R) {
+ // Do not reset the usage state: we will set it ourselves
+ usageFlush?.dispose()
+ usageFlush = null
+
+ val queue = batch.iterator()
+ var start = Long.MIN_VALUE
+ var currentWork: SliceWork? = null
+ var currentDisposable: DisposableHandle? = null
+
+ fun schedule(slice: ServerContext.Slice) {
+ start = clock.millis()
+
+ val isLastSlice = !queue.hasNext()
+ val work = SliceWork(slice)
+ val candidateDuration = when (triggerMode) {
+ ServerContext.TriggerMode.FIRST -> work.minExit
+ ServerContext.TriggerMode.LAST -> work.maxExit
+ ServerContext.TriggerMode.DEADLINE -> slice.deadline - start
+ }
+
+ // Check whether the deadline is exceeded during the run of the slice.
+ val duration = min(candidateDuration, slice.deadline - start)
+
+ val action = Runnable {
+ currentWork = null
+
+ // Flush all the work that was performed
+ val hasFinished = work.stop(duration)
+
+ if (!isLastSlice) {
+ val candidateSlice = queue.next()
+ val nextSlice =
+ // If our previous slice exceeds its deadline, merge it with the next candidate slice
+ if (hasFinished)
+ candidateSlice
+ else
+ merge(candidateSlice, slice)
+ schedule(nextSlice)
+ } else if (select.trySelect()) {
+ block.startCoroutineCancellable(select.completion)
+ }
+ }
+
+ // Schedule the flush after the entire slice has finished
+ currentDisposable = delay.invokeOnTimeout(duration, action)
+
+ // Start the slice work
+ currentWork = work
+ work.start()
+ }
+
+ // Schedule the first work
+ if (queue.hasNext()) {
+ schedule(queue.next())
+
+ // A DisposableHandle to flush the work in case the call is cancelled
+ val disposable = DisposableHandle {
+ val end = clock.millis()
+ val duration = end - start
+
+ currentWork?.stop(duration)
+ currentDisposable?.dispose()
+
+ // Schedule reset the usage of the machine since the call is returning
+ usageFlush = delay.invokeOnTimeout(1, Runnable {
+ usageState.value = 0.0
+ usageFlush = null
+ })
+ }
+
+ select.disposeOnSelect(disposable)
+ } else if (select.trySelect()) {
+ // No work has been given: select immediately
+ block.startCoroutineCancellable(select.completion)
+ }
+ }
+ }
+ }
+
+ /**
+ * A slice to be processed.
+ */
+ private inner class SliceWork(val slice: ServerContext.Slice) {
+ /**
+ * The duration after which the first processor finishes processing this slice.
+ */
+ public val minExit: Long
+
+ /**
+ * The duration after which the last processor finishes processing this slice.
+ */
+ public val maxExit: Long
+
+ /**
+ * A flag to indicate that the slice will exceed the deadline.
+ */
+ public val exceedsDeadline: Boolean
+ get() = slice.deadline < maxExit
+
+ /**
+ * The total amount of CPU usage.
+ */
+ public val totalUsage: Double
+
+ /**
+ * A flag to indicate that this slice is empty.
+ */
+ public val isEmpty: Boolean
+
+ init {
+ var totalUsage = 0.0
+ var minExit = Long.MAX_VALUE
+ var maxExit = 0L
+ var nonEmpty = false
+
+ // Determine the duration of the first/last CPU to finish
+ for (i in 0 until min(cpus.size, slice.burst.size)) {
+ val cpu = cpus[i]
+ val usage = min(slice.limit[i], cpu.frequency)
+ val cpuDuration = ceil(slice.burst[i] / usage * 1000).toLong() // Convert from seconds to milliseconds
+
+ totalUsage += usage / cpu.frequency
+
+ if (cpuDuration != 0L) { // We only wait for processor cores with a non-zero burst
+ minExit = min(minExit, cpuDuration)
+ maxExit = max(maxExit, cpuDuration)
+ nonEmpty = true
+ }
+ }
+
+ this.isEmpty = !nonEmpty
+ this.totalUsage = totalUsage
+ this.minExit = minExit
+ this.maxExit = maxExit
+ }
+
+ /**
+ * Indicate that the work on the slice has started.
+ */
+ public fun start() {
+ usageState.value = totalUsage / cpus.size
+ }
+
+ /**
+ * Flush the work performed on the slice.
+ */
+ public fun stop(duration: Long): Boolean {
+ var hasFinished = true
+
+ // Only flush the work if the machine is available
+ if (!unavailable) {
+ for (i in 0 until min(cpus.size, slice.burst.size)) {
+ val usage = min(slice.limit[i], cpus[i].frequency)
+ val granted = ceil(duration / 1000.0 * usage).toLong()
+ val res = max(0, slice.burst[i] - granted)
+ slice.burst[i] = res
+
+ if (res != 0L) {
+ hasFinished = false
+ }
+ }
+ }
+
+ return hasFinished
+ }
+ }
+ }
+
+ override val scope: CoroutineScope
+ get() = domain
+
+ override suspend fun fail() {
+ serverContext?.unavailable = true
+
+ val server = nodeState.value.server?.copy(state = ServerState.ERROR)
+ setNode(nodeState.value.copy(state = NodeState.ERROR, server = server))
+ }
+
+ override suspend fun recover() {
+ serverContext?.unavailable = false
+
+ val server = nodeState.value.server?.copy(state = ServerState.ACTIVE)
+ setNode(nodeState.value.copy(state = NodeState.ACTIVE, server = server))
+ }
+
+ override fun toString(): String = "SimpleBareMetalDriver(node = ${nodeState.value.uid})"
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/power/PowerModels.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/power/PowerModels.kt
new file mode 100644
index 00000000..9ddbe08e
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/power/PowerModels.kt
@@ -0,0 +1,45 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal.power
+
+import com.atlarge.opendc.compute.metal.driver.BareMetalDriver
+import com.atlarge.opendc.core.power.PowerModel
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/**
+ * A power model which emits a single value.
+ */
+public fun ConstantPowerModel(value: Double): PowerModel<BareMetalDriver> = { _ -> flowOf(value) }
+
+/**
+ * A power model that assumes a naive linear relation between power usage and host CPU utilization.
+ *
+ * @param idle The power draw in Watts on idle.
+ * @param max The maximum power draw in Watts of the server.
+ */
+public fun LinearLoadPowerModel(idle: Double, max: Double): PowerModel<BareMetalDriver> = { driver ->
+ driver.usage.map { load -> (max - idle) * load + idle }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/ProvisioningService.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/ProvisioningService.kt
new file mode 100644
index 00000000..a54d8df4
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/ProvisioningService.kt
@@ -0,0 +1,66 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal.service
+
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.metal.Node
+import com.atlarge.opendc.compute.metal.driver.BareMetalDriver
+import com.atlarge.opendc.core.services.AbstractServiceKey
+import java.util.UUID
+
+/**
+ * A cloud platform service for provisioning bare-metal compute nodes on the platform.
+ */
+public interface ProvisioningService {
+ /**
+ * Create a new bare-metal compute node.
+ */
+ public suspend fun create(driver: BareMetalDriver): Node
+
+ /**
+ * Obtain the available nodes.
+ */
+ public suspend fun nodes(): Set<Node>
+
+ /**
+ * Refresh the state of a compute node.
+ */
+ public suspend fun refresh(node: Node): Node
+
+ /**
+ * Deploy the specified [Image] on a compute node.
+ */
+ public suspend fun deploy(node: Node, image: Image): Node
+
+ /**
+ * Stop the specified [Node] .
+ */
+ public suspend fun stop(node: Node): Node
+
+ /**
+ * The service key of this service.
+ */
+ companion object Key : AbstractServiceKey<ProvisioningService>(UUID.randomUUID(), "provisioner")
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningService.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningService.kt
new file mode 100644
index 00000000..f6b236ae
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningService.kt
@@ -0,0 +1,70 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal.service
+
+import com.atlarge.odcsim.Domain
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.metal.Node
+import com.atlarge.opendc.compute.metal.driver.BareMetalDriver
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.withContext
+
+/**
+ * A very basic implementation of the [ProvisioningService].
+ */
+public class SimpleProvisioningService(val domain: Domain) : ProvisioningService {
+ /**
+ * The active nodes in this service.
+ */
+ private val nodes: MutableMap<Node, BareMetalDriver> = mutableMapOf()
+
+ override suspend fun create(driver: BareMetalDriver): Node = withContext(domain.coroutineContext) {
+ val node = driver.init()
+ nodes[node] = driver
+ return@withContext node
+ }
+
+ override suspend fun nodes(): Set<Node> = withContext(domain.coroutineContext) { nodes.keys }
+
+ override suspend fun refresh(node: Node): Node = withContext(domain.coroutineContext) {
+ return@withContext nodes[node]!!.refresh()
+ }
+
+ override suspend fun deploy(node: Node, image: Image): Node = withContext(domain.coroutineContext) {
+ val driver = nodes[node]!!
+ driver.setImage(image)
+ val newNode = driver.reboot()
+ return@withContext newNode
+ }
+
+ override suspend fun stop(node: Node): Node = withContext(domain.coroutineContext) {
+ val driver = nodes[node]!!
+ try {
+ driver.stop()
+ } catch (e: CancellationException) {
+ node
+ }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt
new file mode 100644
index 00000000..69b0124d
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/Hypervisor.kt
@@ -0,0 +1,58 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt
+
+import com.atlarge.opendc.core.Identity
+import kotlinx.coroutines.flow.Flow
+import java.util.UUID
+
+/**
+ * A hypervisor (or virtual machine monitor) is software or firmware that virtualizes the host compute environment
+ * into several virtual guest machines.
+ */
+public class Hypervisor(
+ /**
+ * The unique identifier of the hypervisor.
+ */
+ override val uid: UUID,
+
+ /**
+ * The optional name of the hypervisor.
+ */
+ override val name: String,
+
+ /**
+ * Metadata of the hypervisor.
+ */
+ public val metadata: Map<String, Any>,
+
+ /**
+ * The events that are emitted by the hypervisor.
+ */
+ public val events: Flow<HypervisorEvent>
+) : Identity {
+ override fun hashCode(): Int = uid.hashCode()
+ override fun equals(other: Any?): Boolean = other is Hypervisor && uid == other.uid
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorEvent.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorEvent.kt
new file mode 100644
index 00000000..7c088bc8
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorEvent.kt
@@ -0,0 +1,78 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt
+
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.virt.driver.VirtDriver
+
+/**
+ * An event that is emitted by a [VirtDriver].
+ */
+public sealed class HypervisorEvent {
+ /**
+ * The driver that emitted the event.
+ */
+ public abstract val driver: VirtDriver
+
+ /**
+ * This event is emitted when the number of active servers on the server managed by this driver is updated.
+ *
+ * @property driver The driver that emitted the event.
+ * @property numberOfActiveServers The number of active servers.
+ * @property availableMemory The available memory, in MB.
+ */
+ public data class VmsUpdated(
+ override val driver: VirtDriver,
+ public val numberOfActiveServers: Int,
+ public val availableMemory: Long
+ ) : HypervisorEvent()
+
+ /**
+ * This event is emitted when a slice is finished.
+ *
+ * @property driver The driver that emitted the event.
+ * @property requestedBurst The total requested CPU time (can be above capacity).
+ * @property grantedBurst The actual total granted capacity, which might be lower than the requested burst due to
+ * the hypervisor being interrupted during a slice.
+ * @property overcommissionedBurst The CPU time that the hypervisor could not grant to the virtual machine since
+ * it did not have the capacity.
+ * @property interferedBurst The sum of CPU time that virtual machines could not utilize due to performance
+ * interference.
+ * @property cpuUsage CPU use in megahertz.
+ * @property cpuDemand CPU demand in megahertz.
+ * @property numberOfDeployedImages The number of images deployed on this hypervisor.
+ */
+ public data class SliceFinished(
+ override val driver: VirtDriver,
+ public val requestedBurst: Long,
+ public val grantedBurst: Long,
+ public val overcommissionedBurst: Long,
+ public val interferedBurst: Long,
+ public val cpuUsage: Double,
+ public val cpuDemand: Double,
+ public val numberOfDeployedImages: Int,
+ public val hostServer: Server
+ ) : HypervisorEvent()
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt
new file mode 100644
index 00000000..bd395f0d
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/HypervisorImage.kt
@@ -0,0 +1,57 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt
+
+import com.atlarge.opendc.compute.core.execution.ServerContext
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.virt.driver.SimpleVirtDriver
+import com.atlarge.opendc.compute.virt.driver.VirtDriver
+import com.atlarge.opendc.core.resource.TagContainer
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.util.UUID
+
+/**
+ * A hypervisor managing the VMs of a node.
+ */
+object HypervisorImage : Image {
+ override val uid: UUID = UUID.randomUUID()
+ override val name: String = "vmm"
+ override val tags: TagContainer = emptyMap()
+
+ override suspend fun invoke(ctx: ServerContext) {
+ coroutineScope {
+ val driver = SimpleVirtDriver(ctx, this)
+ ctx.publishService(VirtDriver.Key, driver)
+
+ // Suspend image until it is cancelled
+ try {
+ suspendCancellableCoroutine<Unit> {}
+ } finally {
+ driver.cancel()
+ }
+ }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/InsufficientMemoryOnServerException.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/InsufficientMemoryOnServerException.kt
new file mode 100644
index 00000000..0586ae00
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/InsufficientMemoryOnServerException.kt
@@ -0,0 +1,3 @@
+package com.atlarge.opendc.compute.virt.driver
+
+public class InsufficientMemoryOnServerException : IllegalStateException("Insufficient memory left on server.")
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt
new file mode 100644
index 00000000..3c41f52e
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/SimpleVirtDriver.kt
@@ -0,0 +1,625 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt.driver
+
+import com.atlarge.odcsim.flow.EventFlow
+import com.atlarge.odcsim.simulationContext
+import com.atlarge.opendc.compute.core.Flavor
+import com.atlarge.opendc.compute.core.ProcessingUnit
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.ServerEvent
+import com.atlarge.opendc.compute.core.ServerState
+import com.atlarge.opendc.compute.core.execution.ServerContext
+import com.atlarge.opendc.compute.core.execution.ServerManagementContext
+import com.atlarge.opendc.compute.core.execution.ShutdownException
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.virt.HypervisorEvent
+import com.atlarge.opendc.core.services.ServiceKey
+import com.atlarge.opendc.core.services.ServiceRegistry
+import com.atlarge.opendc.compute.core.workload.IMAGE_PERF_INTERFERENCE_MODEL
+import com.atlarge.opendc.compute.core.workload.PerformanceInterferenceModel
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.intrinsics.startCoroutineCancellable
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.selects.SelectClause0
+import kotlinx.coroutines.selects.SelectInstance
+import kotlinx.coroutines.selects.select
+import java.util.UUID
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * A [VirtDriver] that is backed by a simple hypervisor implementation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+class SimpleVirtDriver(
+ private val hostContext: ServerContext,
+ scope: CoroutineScope
+) : VirtDriver, CoroutineScope by scope {
+ /**
+ * The [Server] on which this hypervisor runs.
+ */
+ val server: Server
+ get() = hostContext.server
+
+ /**
+ * A set for tracking the VM context objects.
+ */
+ private val vms: MutableSet<VmServerContext> = mutableSetOf()
+
+ /**
+ * Current total memory use of the images on this hypervisor.
+ */
+ private var availableMemory: Long = hostContext.server.flavor.memorySize
+
+ /**
+ * The [EventFlow] to emit the events.
+ */
+ internal val eventFlow = EventFlow<HypervisorEvent>()
+
+ override val events: Flow<HypervisorEvent> = eventFlow
+
+ init {
+ launch {
+ try {
+ scheduler()
+ } catch (e: Exception) {
+ if (e !is CancellationException) {
+ simulationContext.log.error("Hypervisor scheduler failed", e)
+ }
+ throw e
+ }
+ }
+ }
+
+ override suspend fun spawn(
+ name: String,
+ image: Image,
+ flavor: Flavor
+ ): Server {
+ val requiredMemory = flavor.memorySize
+ if (availableMemory - requiredMemory < 0) {
+ throw InsufficientMemoryOnServerException()
+ }
+ require(flavor.cpuCount <= hostContext.server.flavor.cpuCount) { "Machine does not fit" }
+
+ val events = EventFlow<ServerEvent>()
+ val server = Server(
+ UUID.randomUUID(), name, emptyMap(), flavor, image, ServerState.BUILD,
+ ServiceRegistry(), events
+ )
+ availableMemory -= requiredMemory
+ vms.add(VmServerContext(server, events))
+ vmStarted(server)
+ eventFlow.emit(HypervisorEvent.VmsUpdated(this, vms.size, availableMemory))
+ return server
+ }
+
+ internal fun cancel() {
+ eventFlow.close()
+ }
+
+ private fun vmStarted(server: Server) {
+ vms.forEach {
+ val performanceModel =
+ it.server.image.tags[IMAGE_PERF_INTERFERENCE_MODEL] as? PerformanceInterferenceModel?
+ performanceModel?.vmStarted(server)
+ }
+ }
+
+ private fun vmStopped(server: Server) {
+ vms.forEach {
+ val performanceModel =
+ it.server.image.tags[IMAGE_PERF_INTERFERENCE_MODEL] as? PerformanceInterferenceModel?
+ performanceModel?.vmStopped(server)
+ }
+ }
+
+ /**
+ * A scheduling command processed by the scheduler.
+ */
+ private sealed class SchedulerCommand {
+ /**
+ * Schedule the specified VM on the hypervisor.
+ */
+ data class Schedule(val vm: Vm) : SchedulerCommand()
+
+ /**
+ * De-schedule the specified VM on the hypervisor.
+ */
+ data class Deschedule(val vm: Vm) : SchedulerCommand()
+
+ /**
+ * Interrupt the scheduler.
+ */
+ object Interrupt : SchedulerCommand()
+ }
+
+ /**
+ * A flag to indicate the driver is stopped.
+ */
+ private var stopped: Boolean = false
+
+ /**
+ * The channel for scheduling new CPU requests.
+ */
+ private val schedulingQueue = Channel<SchedulerCommand>(Channel.UNLIMITED)
+
+ /**
+ * The scheduling process of the hypervisor.
+ */
+ private suspend fun scheduler() {
+ val clock = simulationContext.clock
+ val maxUsage = hostContext.cpus.sumByDouble { it.frequency }
+ val pCPUs = hostContext.cpus.indices.sortedBy { hostContext.cpus[it].frequency }
+
+ val vms = mutableSetOf<Vm>()
+ val vcpus = mutableListOf<VCpu>()
+
+ val usage = DoubleArray(hostContext.cpus.size)
+ val burst = LongArray(hostContext.cpus.size)
+
+ fun process(command: SchedulerCommand) {
+ when (command) {
+ is SchedulerCommand.Schedule -> {
+ vms += command.vm
+ vcpus.addAll(command.vm.vcpus)
+ }
+ is SchedulerCommand.Deschedule -> {
+ vms -= command.vm
+ vcpus.removeAll(command.vm.vcpus)
+ }
+ is SchedulerCommand.Interrupt -> {}
+ }
+ }
+
+ fun processRemaining() {
+ var command = schedulingQueue.poll()
+ while (command != null) {
+ process(command)
+ command = schedulingQueue.poll()
+ }
+ }
+
+ while (!stopped) {
+ // Wait for a request to be submitted if we have no work yet.
+ if (vcpus.isEmpty()) {
+ process(schedulingQueue.receive())
+ }
+
+ processRemaining()
+
+ val start = clock.millis()
+
+ val vmCount = vms.size
+ var duration: Double = Double.POSITIVE_INFINITY
+ var deadline: Long = Long.MAX_VALUE
+ var availableUsage = maxUsage
+ var totalRequestedUsage = 0.0
+ var totalRequestedBurst = 0L
+
+ // Sort the vCPUs based on their requested usage
+ // Profiling shows that it is faster to sort every slice instead of maintaining some kind of sorted set
+ vcpus.sort()
+
+ // Divide the available host capacity fairly across the vCPUs using max-min fair sharing
+ for ((i, req) in vcpus.withIndex()) {
+ val remaining = vcpus.size - i
+ val availableShare = availableUsage / remaining
+ val grantedUsage = min(req.limit, availableShare)
+
+ // Take into account the minimum deadline of this slice before we possible continue
+ deadline = min(deadline, req.vm.deadline)
+
+ // Ignore empty CPUs
+ if (grantedUsage <= 0 || req.burst <= 0) {
+ req.allocatedLimit = 0.0
+ continue
+ }
+
+ totalRequestedUsage += req.limit
+ totalRequestedBurst += req.burst
+
+ req.allocatedLimit = grantedUsage
+ availableUsage -= grantedUsage
+
+ // The duration that we want to run is that of the shortest request from a vCPU
+ duration = min(duration, req.burst / grantedUsage)
+ }
+
+ // XXX We set the minimum duration to 5 minutes here to prevent the rounding issues that are occurring with the FLOPs.
+ duration = 300.0
+
+ val totalAllocatedUsage = maxUsage - availableUsage
+ var totalAllocatedBurst = 0L
+ availableUsage = totalAllocatedUsage
+ val serverLoad = totalAllocatedUsage / maxUsage
+
+ // Divide the requests over the available capacity of the pCPUs fairly
+ for (i in pCPUs) {
+ val maxCpuUsage = hostContext.cpus[i].frequency
+ val fraction = maxCpuUsage / maxUsage
+ val grantedUsage = min(maxCpuUsage, totalAllocatedUsage * fraction)
+ val grantedBurst = ceil(duration * grantedUsage).toLong()
+
+ usage[i] = grantedUsage
+ burst[i] = grantedBurst
+ totalAllocatedBurst += grantedBurst
+ availableUsage -= grantedUsage
+ }
+
+ // We run the total burst on the host processor. Note that this call may be cancelled at any moment in
+ // time, so not all of the burst may be executed.
+ select<Boolean> {
+ schedulingQueue.onReceive { schedulingQueue.offer(it); true }
+ hostContext.onRun(ServerContext.Slice(burst, usage, deadline), ServerContext.TriggerMode.DEADLINE).invoke { false }
+ }
+
+ val end = clock.millis()
+
+ // No work was performed
+ if ((end - start) <= 0) {
+ continue
+ }
+
+ // The total requested burst that the VMs wanted to run in the time-frame that we ran.
+ val totalRequestedSubBurst = vcpus.map { ceil((duration * 1000) / (it.vm.deadline - start) * it.burst).toLong() }.sum()
+ val totalRemainder = burst.sum()
+ val totalGrantedBurst = totalAllocatedBurst - totalRemainder
+
+ // The burst that was lost due to overcommissioning of CPU resources
+ var totalOvercommissionedBurst = 0L
+ // The burst that was lost due to interference.
+ var totalInterferedBurst = 0L
+
+ val vmIterator = vms.iterator()
+ while (vmIterator.hasNext()) {
+ val vm = vmIterator.next()
+
+ // Apply performance interference model
+ val performanceModel =
+ vm.ctx.server.image.tags[IMAGE_PERF_INTERFERENCE_MODEL] as? PerformanceInterferenceModel?
+ val performanceScore = performanceModel?.apply(serverLoad) ?: 1.0
+ var hasFinished = false
+
+ for (vcpu in vm.vcpus) {
+ // Compute the fraction of compute time allocated to the VM
+ val fraction = vcpu.allocatedLimit / totalAllocatedUsage
+
+ // Compute the burst time that the VM was actually granted
+ val grantedBurst = ceil(totalGrantedBurst * fraction).toLong()
+
+ // The burst that was actually used by the VM
+ val usedBurst = ceil(grantedBurst * performanceScore).toLong()
+
+ totalInterferedBurst += grantedBurst - usedBurst
+
+ // Compute remaining burst time to be executed for the request
+ if (vcpu.consume(usedBurst)) {
+ hasFinished = true
+ } else if (vm.deadline <= end && hostContext.server.state != ServerState.ERROR) {
+ // Request must have its entire burst consumed or otherwise we have overcommission
+ // Note that we count the overcommissioned burst if the hypervisor has failed.
+ totalOvercommissionedBurst += vcpu.burst
+ }
+ }
+
+ if (hasFinished || vm.deadline <= end) {
+ // Mark the VM as finished and deschedule the VMs if needed
+ if (vm.finish()) {
+ vmIterator.remove()
+ vcpus.removeAll(vm.vcpus)
+ }
+ }
+ }
+
+ eventFlow.emit(
+ HypervisorEvent.SliceFinished(
+ this@SimpleVirtDriver,
+ totalRequestedBurst,
+ min(totalRequestedSubBurst, totalGrantedBurst), // We can run more than requested due to timing
+ totalOvercommissionedBurst,
+ totalInterferedBurst, // Might be smaller than zero due to FP rounding errors,
+ min(totalAllocatedUsage, totalRequestedUsage), // The allocated usage might be slightly higher due to FP rounding
+ totalRequestedUsage,
+ vmCount, // Some VMs might already have finished, so keep initial VM count
+ server
+ )
+ )
+ }
+ }
+
+ /**
+ * A virtual machine running on the hypervisor.
+ *
+ * @param ctx The execution context the vCPU runs in.
+ * @param triggerMode The mode when to trigger the VM exit.
+ * @param merge The function to merge consecutive slices on spillover.
+ * @param select The function to select on finish.
+ */
+ @OptIn(InternalCoroutinesApi::class)
+ private data class Vm(
+ val ctx: VmServerContext,
+ var triggerMode: ServerContext.TriggerMode = ServerContext.TriggerMode.FIRST,
+ var merge: (ServerContext.Slice, ServerContext.Slice) -> ServerContext.Slice = { _, r -> r },
+ var select: () -> Unit = {}
+ ) {
+ /**
+ * The vCPUs of this virtual machine.
+ */
+ val vcpus: List<VCpu>
+
+ /**
+ * The slices that the VM wants to run.
+ */
+ var queue: Iterator<ServerContext.Slice> = emptyList<ServerContext.Slice>().iterator()
+
+ /**
+ * The current active slice.
+ */
+ var activeSlice: ServerContext.Slice? = null
+
+ /**
+ * The current deadline of the VM.
+ */
+ val deadline: Long
+ get() = activeSlice?.deadline ?: Long.MAX_VALUE
+
+ /**
+ * A flag to indicate that the VM is idle.
+ */
+ val isIdle: Boolean
+ get() = activeSlice == null
+
+ init {
+ vcpus = ctx.cpus.mapIndexed { i, model -> VCpu(this, model, i) }
+ }
+
+ /**
+ * Schedule the given slices on this vCPU, replacing the existing slices.
+ */
+ fun schedule(slices: Sequence<ServerContext.Slice>) {
+ queue = slices.iterator()
+
+ if (queue.hasNext()) {
+ activeSlice = queue.next()
+ vcpus.forEach { it.refresh() }
+ }
+ }
+
+ /**
+ * Cancel the existing workload on the VM.
+ */
+ fun cancel() {
+ queue = emptyList<ServerContext.Slice>().iterator()
+ activeSlice = null
+ vcpus.forEach { it.refresh() }
+ }
+
+ /**
+ * Finish the current slice of the VM.
+ *
+ * @return `true` if the vCPUs may be descheduled, `false` otherwise.
+ */
+ fun finish(): Boolean {
+ val activeSlice = activeSlice ?: return true
+
+ return if (queue.hasNext()) {
+ val needsMerge = activeSlice.burst.any { it > 0 }
+ val candidateSlice = queue.next()
+ val slice = if (needsMerge) merge(activeSlice, candidateSlice) else candidateSlice
+
+ this.activeSlice = slice
+
+ // Update the vCPU cache
+ vcpus.forEach { it.refresh() }
+
+ false
+ } else {
+ this.activeSlice = null
+ select()
+ true
+ }
+ }
+ }
+
+ /**
+ * A virtual CPU that can be scheduled on a physical CPU.
+ *
+ * @param vm The VM of which this vCPU is part.
+ * @param model The model of CPU that this vCPU models.
+ * @param id The id of the vCPU with respect to the VM.
+ */
+ private data class VCpu(
+ val vm: Vm,
+ val model: ProcessingUnit,
+ val id: Int
+ ) : Comparable<VCpu> {
+ /**
+ * The current limit on the vCPU.
+ */
+ var limit: Double = 0.0
+
+ /**
+ * The limit allocated by the hypervisor.
+ */
+ var allocatedLimit: Double = 0.0
+
+ /**
+ * The current burst running on the vCPU.
+ */
+ var burst: Long = 0L
+
+ /**
+ * Consume the specified burst on this vCPU.
+ */
+ fun consume(burst: Long): Boolean {
+ this.burst = max(0, this.burst - burst)
+
+ // Flush the result to the slice if it exists
+ vm.activeSlice?.burst?.takeIf { id < it.size }?.set(id, this.burst)
+
+ return allocatedLimit > 0.0 && this.burst == 0L
+ }
+
+ /**
+ * Refresh the information of this vCPU based on the current slice.
+ */
+ fun refresh() {
+ limit = vm.activeSlice?.limit?.takeIf { id < it.size }?.get(id) ?: 0.0
+ burst = vm.activeSlice?.burst?.takeIf { id < it.size }?.get(id) ?: 0
+ }
+
+ /**
+ * Compare to another vCPU based on the current load of the vCPU.
+ */
+ override fun compareTo(other: VCpu): Int {
+ var cmp = limit.compareTo(other.limit)
+
+ if (cmp != 0) {
+ return cmp
+ }
+
+ cmp = vm.ctx.server.uid.compareTo(other.vm.ctx.server.uid)
+
+ if (cmp != 0) {
+ return cmp
+ }
+
+ return id.compareTo(other.id)
+ }
+
+ /**
+ * Create a string representation of the vCPU.
+ */
+ override fun toString(): String =
+ "vCPU(vm=${vm.ctx.server.uid},id=$id,burst=$burst,limit=$limit,allocatedLimit=$allocatedLimit)"
+ }
+
+ /**
+ * The execution context in which a VM runs.
+ *
+ * @param server The details of the VM.
+ * @param events The event stream to publish to.
+ */
+ private inner class VmServerContext(server: Server, val events: EventFlow<ServerEvent>) : ServerManagementContext, DisposableHandle {
+ private var finalized: Boolean = false
+ private var initialized: Boolean = false
+ private val vm: Vm
+
+ internal val job: Job = launch {
+ delay(1) // TODO Introduce boot time
+ init()
+ try {
+ server.image(this@VmServerContext)
+ exit()
+ } catch (cause: Throwable) {
+ exit(cause)
+ }
+ }
+
+ override var server: Server = server
+ set(value) {
+ if (field.state != value.state) {
+ events.emit(ServerEvent.StateChanged(value, field.state))
+ }
+
+ field = value
+ }
+
+ override val cpus: List<ProcessingUnit> = hostContext.cpus.take(server.flavor.cpuCount)
+
+ init {
+ vm = Vm(this)
+ }
+
+ override suspend fun <T : Any> publishService(key: ServiceKey<T>, service: T) {
+ server = server.copy(services = server.services.put(key, service))
+ events.emit(ServerEvent.ServicePublished(server, key))
+ }
+
+ override suspend fun init() {
+ assert(!finalized) { "VM is already finalized" }
+
+ server = server.copy(state = ServerState.ACTIVE)
+ initialized = true
+ }
+
+ override suspend fun exit(cause: Throwable?) {
+ finalized = true
+
+ val serverState =
+ if (cause == null || (cause is ShutdownException && cause.cause == null))
+ ServerState.SHUTOFF
+ else
+ ServerState.ERROR
+ server = server.copy(state = serverState)
+ availableMemory += server.flavor.memorySize
+ vms.remove(this)
+ vmStopped(server)
+ eventFlow.emit(HypervisorEvent.VmsUpdated(this@SimpleVirtDriver, vms.size, availableMemory))
+ events.close()
+ }
+
+ @OptIn(InternalCoroutinesApi::class)
+ override fun onRun(
+ batch: Sequence<ServerContext.Slice>,
+ triggerMode: ServerContext.TriggerMode,
+ merge: (ServerContext.Slice, ServerContext.Slice) -> ServerContext.Slice
+ ): SelectClause0 = object : SelectClause0 {
+ @InternalCoroutinesApi
+ override fun <R> registerSelectClause0(select: SelectInstance<R>, block: suspend () -> R) {
+ vm.triggerMode = triggerMode
+ vm.merge = merge
+ vm.select = {
+ if (select.trySelect()) {
+ block.startCoroutineCancellable(select.completion)
+ }
+ }
+ vm.schedule(batch)
+ // Indicate to the hypervisor that the VM should be re-scheduled
+ schedulingQueue.offer(SchedulerCommand.Schedule(vm))
+ select.disposeOnSelect(this@VmServerContext)
+ }
+ }
+
+ override fun dispose() {
+ if (!vm.isIdle) {
+ vm.cancel()
+ schedulingQueue.offer(SchedulerCommand.Deschedule(vm))
+ }
+ }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt
new file mode 100644
index 00000000..1002d382
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/driver/VirtDriver.kt
@@ -0,0 +1,56 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt.driver
+
+import com.atlarge.opendc.compute.core.Flavor
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.virt.HypervisorEvent
+import com.atlarge.opendc.core.services.AbstractServiceKey
+import kotlinx.coroutines.flow.Flow
+import java.util.UUID
+
+/**
+ * A driver interface for a hypervisor running on some host server and communicating with the central compute service to
+ * provide virtualization for that particular resource.
+ */
+public interface VirtDriver {
+ /**
+ * The events emitted by the driver.
+ */
+ public val events: Flow<HypervisorEvent>
+
+ /**
+ * Spawn the given [Image] on the compute resource of this driver.
+ *
+ * @param name The name of the server to spawn.
+ * @param image The image to deploy.
+ * @param flavor The flavor of the server which this driver is controlling.
+ * @return The virtual server spawned by this method.
+ */
+ public suspend fun spawn(name: String, image: Image, flavor: Flavor): Server
+
+ companion object Key : AbstractServiceKey<VirtDriver>(UUID.randomUUID(), "virtual-driver")
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/HypervisorView.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/HypervisorView.kt
new file mode 100644
index 00000000..e52a1698
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/HypervisorView.kt
@@ -0,0 +1,15 @@
+package com.atlarge.opendc.compute.virt.service
+
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.virt.driver.VirtDriver
+import java.util.UUID
+
+class HypervisorView(
+ val uid: UUID,
+ var server: Server,
+ var numberOfActiveServers: Int,
+ var availableMemory: Long,
+ var provisionedCores: Int
+) {
+ lateinit var driver: VirtDriver
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt
new file mode 100644
index 00000000..ff4aa3d7
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/SimpleVirtProvisioningService.kt
@@ -0,0 +1,351 @@
+package com.atlarge.opendc.compute.virt.service
+
+import com.atlarge.odcsim.SimulationContext
+import com.atlarge.odcsim.flow.EventFlow
+import com.atlarge.odcsim.simulationContext
+import com.atlarge.opendc.compute.core.Flavor
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.ServerEvent
+import com.atlarge.opendc.compute.core.ServerState
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.core.image.VmImage
+import com.atlarge.opendc.compute.metal.service.ProvisioningService
+import com.atlarge.opendc.compute.virt.HypervisorEvent
+import com.atlarge.opendc.compute.virt.driver.VirtDriver
+import com.atlarge.opendc.compute.virt.HypervisorImage
+import com.atlarge.opendc.compute.virt.driver.InsufficientMemoryOnServerException
+import com.atlarge.opendc.compute.virt.service.allocation.AllocationPolicy
+import com.atlarge.opendc.core.services.ServiceKey
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import mu.KotlinLogging
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.math.max
+
+private val logger = KotlinLogging.logger {}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class SimpleVirtProvisioningService(
+ public override val allocationPolicy: AllocationPolicy,
+ private val ctx: SimulationContext,
+ private val provisioningService: ProvisioningService
+) : VirtProvisioningService, CoroutineScope by ctx.domain {
+ /**
+ * The hypervisors that have been launched by the service.
+ */
+ private val hypervisors: MutableMap<Server, HypervisorView> = mutableMapOf()
+
+ /**
+ * The available hypervisors.
+ */
+ private val availableHypervisors: MutableSet<HypervisorView> = mutableSetOf()
+
+ /**
+ * The incoming images to be processed by the provisioner.
+ */
+ private val incomingImages: MutableSet<ImageView> = mutableSetOf()
+
+ /**
+ * The active images in the system.
+ */
+ private val activeImages: MutableSet<ImageView> = mutableSetOf()
+
+ public var submittedVms = 0
+ public var queuedVms = 0
+ public var runningVms = 0
+ public var finishedVms = 0
+ public var unscheduledVms = 0
+
+ private var maxCores = 0
+ private var maxMemory = 0L
+
+ /**
+ * The allocation logic to use.
+ */
+ private val allocationLogic = allocationPolicy()
+
+ /**
+ * The [EventFlow] to emit the events.
+ */
+ internal val eventFlow = EventFlow<VirtProvisioningEvent>()
+
+ override val events: Flow<VirtProvisioningEvent> = eventFlow
+
+ init {
+ launch {
+ val provisionedNodes = provisioningService.nodes()
+ provisionedNodes.forEach { node ->
+ val hypervisorImage = HypervisorImage
+ val node = provisioningService.deploy(node, hypervisorImage)
+ node.server!!.events.onEach { event ->
+ when (event) {
+ is ServerEvent.StateChanged -> stateChanged(event.server)
+ is ServerEvent.ServicePublished -> servicePublished(event.server, event.key)
+ }
+ }.launchIn(this)
+ }
+ }
+ }
+
+ override suspend fun drivers(): Set<VirtDriver> = withContext(coroutineContext) {
+ availableHypervisors.map { it.driver }.toSet()
+ }
+
+ override suspend fun deploy(
+ name: String,
+ image: Image,
+ flavor: Flavor
+ ): Server = withContext(coroutineContext) {
+ eventFlow.emit(VirtProvisioningEvent.MetricsAvailable(
+ this@SimpleVirtProvisioningService,
+ hypervisors.size,
+ availableHypervisors.size,
+ ++submittedVms,
+ runningVms,
+ finishedVms,
+ ++queuedVms,
+ unscheduledVms
+ ))
+
+ suspendCancellableCoroutine<Server> { cont ->
+ val vmInstance = ImageView(name, image, flavor, cont)
+ incomingImages += vmInstance
+ requestCycle()
+ }
+ }
+
+ override suspend fun terminate() {
+ val provisionedNodes = provisioningService.nodes()
+ provisionedNodes.forEach { node -> provisioningService.stop(node) }
+ }
+
+ private var call: Job? = null
+
+ private fun requestCycle() {
+ if (call != null) {
+ return
+ }
+
+ val quantum = 300000 // 5 minutes in milliseconds
+ // We assume that the provisioner runs at a fixed slot every time quantum (e.g t=0, t=60, t=120).
+ // This is important because the slices of the VMs need to be aligned.
+ // We calculate here the delay until the next scheduling slot.
+ val delay = quantum - (ctx.clock.millis() % quantum)
+
+ val call = launch {
+ delay(delay)
+ this@SimpleVirtProvisioningService.call = null
+ schedule()
+ }
+ this.call = call
+ }
+
+ private suspend fun schedule() {
+ val clock = simulationContext.clock
+ val imagesToBeScheduled = incomingImages.toSet()
+
+ for (imageInstance in imagesToBeScheduled) {
+ val requiredMemory = (imageInstance.image as VmImage).requiredMemory
+ val selectedHv = allocationLogic.select(availableHypervisors, imageInstance)
+
+ if (selectedHv == null) {
+ if (requiredMemory > maxMemory || imageInstance.flavor.cpuCount > maxCores) {
+ eventFlow.emit(VirtProvisioningEvent.MetricsAvailable(
+ this@SimpleVirtProvisioningService,
+ hypervisors.size,
+ availableHypervisors.size,
+ submittedVms,
+ runningVms,
+ finishedVms,
+ queuedVms,
+ ++unscheduledVms
+ ))
+
+ incomingImages -= imageInstance
+
+ logger.warn("Failed to spawn ${imageInstance.image}: does not fit [${clock.millis()}]")
+ continue
+ } else {
+ break
+ }
+ }
+
+ try {
+ logger.info { "[${ctx.clock.millis()}] Spawning ${imageInstance.image} on ${selectedHv.server.uid} ${selectedHv.server.name} ${selectedHv.server.flavor}" }
+ incomingImages -= imageInstance
+
+ // Speculatively update the hypervisor view information to prevent other images in the queue from
+ // deciding on stale values.
+ selectedHv.numberOfActiveServers++
+ selectedHv.provisionedCores += imageInstance.flavor.cpuCount
+ selectedHv.availableMemory -= requiredMemory // XXX Temporary hack
+
+ val server = selectedHv.driver.spawn(
+ imageInstance.name,
+ imageInstance.image,
+ imageInstance.flavor
+ )
+ imageInstance.server = server
+ imageInstance.continuation.resume(server)
+
+ eventFlow.emit(VirtProvisioningEvent.MetricsAvailable(
+ this@SimpleVirtProvisioningService,
+ hypervisors.size,
+ availableHypervisors.size,
+ submittedVms,
+ ++runningVms,
+ finishedVms,
+ --queuedVms,
+ unscheduledVms
+ ))
+ activeImages += imageInstance
+
+ server.events
+ .onEach { event ->
+ when (event) {
+ is ServerEvent.StateChanged -> {
+ if (event.server.state == ServerState.SHUTOFF) {
+ logger.info { "[${ctx.clock.millis()}] Server ${event.server.uid} ${event.server.name} ${event.server.flavor} finished." }
+
+ eventFlow.emit(VirtProvisioningEvent.MetricsAvailable(
+ this@SimpleVirtProvisioningService,
+ hypervisors.size,
+ availableHypervisors.size,
+ submittedVms,
+ --runningVms,
+ ++finishedVms,
+ queuedVms,
+ unscheduledVms
+ ))
+
+ activeImages -= imageInstance
+ selectedHv.provisionedCores -= server.flavor.cpuCount
+
+ // Try to reschedule if needed
+ if (incomingImages.isNotEmpty()) {
+ requestCycle()
+ }
+ }
+ }
+ }
+ }
+ .launchIn(this)
+ } catch (e: InsufficientMemoryOnServerException) {
+ logger.error("Failed to deploy VM", e)
+
+ selectedHv.numberOfActiveServers--
+ selectedHv.provisionedCores -= imageInstance.flavor.cpuCount
+ selectedHv.availableMemory += requiredMemory
+ } catch (e: Throwable) {
+ logger.error("Failed to deploy VM", e)
+ }
+ }
+ }
+
+ private fun stateChanged(server: Server) {
+ when (server.state) {
+ ServerState.ACTIVE -> {
+ logger.debug { "[${ctx.clock.millis()}] Server ${server.uid} available: ${server.state}" }
+
+ if (server in hypervisors) {
+ // Corner case for when the hypervisor already exists
+ availableHypervisors += hypervisors.getValue(server)
+ } else {
+ val hv = HypervisorView(
+ server.uid,
+ server,
+ 0,
+ server.flavor.memorySize,
+ 0
+ )
+ maxCores = max(maxCores, server.flavor.cpuCount)
+ maxMemory = max(maxMemory, server.flavor.memorySize)
+ hypervisors[server] = hv
+ }
+
+ eventFlow.emit(VirtProvisioningEvent.MetricsAvailable(
+ this@SimpleVirtProvisioningService,
+ hypervisors.size,
+ availableHypervisors.size,
+ submittedVms,
+ runningVms,
+ finishedVms,
+ queuedVms,
+ unscheduledVms
+ ))
+
+ // Re-schedule on the new machine
+ if (incomingImages.isNotEmpty()) {
+ requestCycle()
+ }
+ }
+ ServerState.SHUTOFF, ServerState.ERROR -> {
+ logger.debug { "[${ctx.clock.millis()}] Server ${server.uid} unavailable: ${server.state}" }
+ val hv = hypervisors[server] ?: return
+ availableHypervisors -= hv
+
+ eventFlow.emit(VirtProvisioningEvent.MetricsAvailable(
+ this@SimpleVirtProvisioningService,
+ hypervisors.size,
+ availableHypervisors.size,
+ submittedVms,
+ runningVms,
+ finishedVms,
+ queuedVms,
+ unscheduledVms
+ ))
+
+ if (incomingImages.isNotEmpty()) {
+ requestCycle()
+ }
+ }
+ else -> throw IllegalStateException()
+ }
+ }
+
+ private fun servicePublished(server: Server, key: ServiceKey<*>) {
+ if (key == VirtDriver.Key) {
+ val hv = hypervisors[server] ?: return
+ hv.driver = server.services[VirtDriver]
+ availableHypervisors += hv
+
+ eventFlow.emit(VirtProvisioningEvent.MetricsAvailable(
+ this@SimpleVirtProvisioningService,
+ hypervisors.size,
+ availableHypervisors.size,
+ submittedVms,
+ runningVms,
+ finishedVms,
+ queuedVms,
+ unscheduledVms
+ ))
+
+ hv.driver.events
+ .onEach { event ->
+ if (event is HypervisorEvent.VmsUpdated) {
+ hv.numberOfActiveServers = event.numberOfActiveServers
+ hv.availableMemory = event.availableMemory
+ }
+ }.launchIn(this)
+
+ requestCycle()
+ }
+ }
+
+ data class ImageView(
+ val name: String,
+ val image: Image,
+ val flavor: Flavor,
+ val continuation: Continuation<Server>,
+ var server: Server? = null
+ )
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningEvent.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningEvent.kt
new file mode 100644
index 00000000..c3fb99f9
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningEvent.kt
@@ -0,0 +1,49 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt.service
+
+/**
+ * An event that is emitted by the [VirtProvisioningService].
+ */
+public sealed class VirtProvisioningEvent {
+ /**
+ * The service that has emitted the event.
+ */
+ public abstract val provisioner: VirtProvisioningService
+
+ /**
+ * An event emitted for writing metrics.
+ */
+ data class MetricsAvailable(
+ override val provisioner: VirtProvisioningService,
+ public val totalHostCount: Int,
+ public val availableHostCount: Int,
+ public val totalVmCount: Int,
+ public val activeVmCount: Int,
+ public val inactiveVmCount: Int,
+ public val waitingVmCount: Int,
+ public val failedVmCount: Int
+ ) : VirtProvisioningEvent()
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningService.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningService.kt
new file mode 100644
index 00000000..c4cbd711
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/VirtProvisioningService.kt
@@ -0,0 +1,42 @@
+package com.atlarge.opendc.compute.virt.service
+
+import com.atlarge.opendc.compute.core.Flavor
+import com.atlarge.opendc.compute.core.Server
+import com.atlarge.opendc.compute.core.image.Image
+import com.atlarge.opendc.compute.virt.driver.VirtDriver
+import com.atlarge.opendc.compute.virt.service.allocation.AllocationPolicy
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A service for VM provisioning on a cloud.
+ */
+interface VirtProvisioningService {
+ /**
+ * The policy used for allocating a VM on the available hypervisors.
+ */
+ val allocationPolicy: AllocationPolicy
+
+ /**
+ * The events emitted by the service.
+ */
+ public val events: Flow<VirtProvisioningEvent>
+
+ /**
+ * Obtain the active hypervisors for this provisioner.
+ */
+ public suspend fun drivers(): Set<VirtDriver>
+
+ /**
+ * Submit the specified [Image] to the provisioning service.
+ *
+ * @param name The name of the server to deploy.
+ * @param image The image to be deployed.
+ * @param flavor The flavor of the machine instance to run this [image] on.
+ */
+ public suspend fun deploy(name: String, image: Image, flavor: Flavor): Server
+
+ /**
+ * Terminate the provisioning service releasing all the leased bare-metal machines.
+ */
+ public suspend fun terminate()
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AllocationPolicy.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AllocationPolicy.kt
new file mode 100644
index 00000000..b7c9388d
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AllocationPolicy.kt
@@ -0,0 +1,25 @@
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.metal.Node
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+import com.atlarge.opendc.compute.virt.service.SimpleVirtProvisioningService
+
+/**
+ * A policy for selecting the [Node] an image should be deployed to,
+ */
+public interface AllocationPolicy {
+ /**
+ * The logic of the allocation policy.
+ */
+ public interface Logic {
+ /**
+ * Select the node on which the server should be scheduled.
+ */
+ public fun select(hypervisors: Set<HypervisorView>, image: SimpleVirtProvisioningService.ImageView): HypervisorView?
+ }
+
+ /**
+ * Builds the logic of the policy.
+ */
+ operator fun invoke(): Logic
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableCoreMemoryAllocationPolicy.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableCoreMemoryAllocationPolicy.kt
new file mode 100644
index 00000000..79b622d2
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableCoreMemoryAllocationPolicy.kt
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+
+/**
+ * An [AllocationPolicy] that selects the machine with the highest/lowest amount of memory per core.
+ *
+ * @param reversed An option to reverse the order of the machines (lower amount of memory scores better).
+ */
+public class AvailableCoreMemoryAllocationPolicy(val reversed: Boolean = false) : AllocationPolicy {
+ override fun invoke(): AllocationPolicy.Logic = object : ComparableAllocationPolicyLogic {
+ override val comparator: Comparator<HypervisorView> =
+ compareBy<HypervisorView> { -it.availableMemory / it.server.flavor.cpuCount }
+ .run { if (reversed) reversed() else this }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableMemoryAllocationPolicy.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableMemoryAllocationPolicy.kt
new file mode 100644
index 00000000..c081244f
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/AvailableMemoryAllocationPolicy.kt
@@ -0,0 +1,15 @@
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+
+/**
+ * Allocation policy that selects the node with the most available memory.
+ *
+ * @param reversed A flag to reverse the order (least amount of memory scores the best).
+ */
+public class AvailableMemoryAllocationPolicy(val reversed: Boolean = false) : AllocationPolicy {
+ override fun invoke(): AllocationPolicy.Logic = object : ComparableAllocationPolicyLogic {
+ override val comparator: Comparator<HypervisorView> = compareBy<HypervisorView> { -it.availableMemory }
+ .run { if (reversed) reversed() else this }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ComparableAllocationPolicyLogic.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ComparableAllocationPolicyLogic.kt
new file mode 100644
index 00000000..79dd95f3
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ComparableAllocationPolicyLogic.kt
@@ -0,0 +1,52 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.core.image.VmImage
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+import com.atlarge.opendc.compute.virt.service.SimpleVirtProvisioningService
+
+/**
+ * The logic for an [AllocationPolicy] that uses a [Comparator] to select the appropriate node.
+ */
+interface ComparableAllocationPolicyLogic : AllocationPolicy.Logic {
+ /**
+ * The comparator to use.
+ */
+ public val comparator: Comparator<HypervisorView>
+
+ override fun select(
+ hypervisors: Set<HypervisorView>,
+ image: SimpleVirtProvisioningService.ImageView
+ ): HypervisorView? {
+ return hypervisors.asSequence()
+ .filter { hv ->
+ val fitsMemory = hv.availableMemory >= (image.image as VmImage).requiredMemory
+ val fitsCpu = hv.server.flavor.cpuCount >= image.flavor.cpuCount
+ fitsMemory && fitsCpu
+ }
+ .minWith(comparator.thenBy { it.server.uid })
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/NumberOfActiveServersAllocationPolicy.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/NumberOfActiveServersAllocationPolicy.kt
new file mode 100644
index 00000000..7e3e5864
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/NumberOfActiveServersAllocationPolicy.kt
@@ -0,0 +1,15 @@
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+
+/**
+ * Allocation policy that selects the node with the least amount of active servers.
+ *
+ * @param reversed A flag to reverse the order, such that the node with the most active servers is selected.
+ */
+public class NumberOfActiveServersAllocationPolicy(val reversed: Boolean = false) : AllocationPolicy {
+ override fun invoke(): AllocationPolicy.Logic = object : ComparableAllocationPolicyLogic {
+ override val comparator: Comparator<HypervisorView> = compareBy<HypervisorView> { it.numberOfActiveServers }
+ .run { if (reversed) reversed() else this }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ProvisionedCoresAllocationPolicy.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ProvisionedCoresAllocationPolicy.kt
new file mode 100644
index 00000000..e1a995a0
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ProvisionedCoresAllocationPolicy.kt
@@ -0,0 +1,42 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+
+/**
+ * An [AllocationPolicy] that takes into account the number of vCPUs that have been provisioned on this machine
+ * relative to its core count.
+ *
+ * @param reversed A flag to reverse the order of the policy, such that the machine with the most provisioned cores
+ * is selected.
+ */
+class ProvisionedCoresAllocationPolicy(val reversed: Boolean = false) : AllocationPolicy {
+ override fun invoke(): AllocationPolicy.Logic = object : ComparableAllocationPolicyLogic {
+ override val comparator: Comparator<HypervisorView> =
+ compareBy<HypervisorView> { it.provisionedCores / it.server.flavor.cpuCount }
+ .run { if (reversed) reversed() else this }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/RandomAllocationPolicy.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/RandomAllocationPolicy.kt
new file mode 100644
index 00000000..07dcf1c5
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/RandomAllocationPolicy.kt
@@ -0,0 +1,51 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.core.image.VmImage
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+import com.atlarge.opendc.compute.virt.service.SimpleVirtProvisioningService
+import kotlin.random.Random
+
+/**
+ * An [AllocationPolicy] that select a random node on which the server fits.
+ */
+public class RandomAllocationPolicy(val random: Random = Random(0)) : AllocationPolicy {
+ @OptIn(ExperimentalStdlibApi::class)
+ override fun invoke(): AllocationPolicy.Logic = object : AllocationPolicy.Logic {
+ override fun select(
+ hypervisors: Set<HypervisorView>,
+ image: SimpleVirtProvisioningService.ImageView
+ ): HypervisorView? {
+ return hypervisors.asIterable()
+ .filter { hv ->
+ val fitsMemory = hv.availableMemory >= (image.image as VmImage).requiredMemory
+ val fitsCpu = hv.server.flavor.cpuCount >= image.flavor.cpuCount
+ fitsMemory && fitsCpu
+ }
+ .randomOrNull(random)
+ }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ReplayAllocationPolicy.kt b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ReplayAllocationPolicy.kt
new file mode 100644
index 00000000..59acfce2
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/main/kotlin/com/atlarge/opendc/compute/virt/service/allocation/ReplayAllocationPolicy.kt
@@ -0,0 +1,34 @@
+package com.atlarge.opendc.compute.virt.service.allocation
+
+import com.atlarge.opendc.compute.virt.service.HypervisorView
+import com.atlarge.opendc.compute.virt.service.SimpleVirtProvisioningService
+import mu.KotlinLogging
+
+private val logger = KotlinLogging.logger {}
+
+/**
+ * Policy replaying VM-cluster assignment.
+ *
+ * Within each cluster, the active servers on each node determine which node gets
+ * assigned the VM image.
+ */
+class ReplayAllocationPolicy(val vmPlacements: Map<String, String>) : AllocationPolicy {
+ override fun invoke(): AllocationPolicy.Logic = object : AllocationPolicy.Logic {
+ override fun select(
+ hypervisors: Set<HypervisorView>,
+ image: SimpleVirtProvisioningService.ImageView
+ ): HypervisorView? {
+ val clusterName = vmPlacements[image.name]
+ ?: throw IllegalStateException("Could not find placement data in VM placement file for VM ${image.name}")
+ val machinesInCluster = hypervisors.filter { it.server.name.contains(clusterName) }
+
+ if (machinesInCluster.isEmpty()) {
+ logger.info { "Could not find any machines belonging to cluster $clusterName for image ${image.name}, assigning randomly." }
+ return hypervisors.maxBy { it.availableMemory }
+ }
+
+ return machinesInCluster.maxBy { it.availableMemory }
+ ?: throw IllegalStateException("Cloud not find any machine and could not randomly assign")
+ }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt
new file mode 100644
index 00000000..417db77d
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/core/image/FlopsApplicationImageTest.kt
@@ -0,0 +1,78 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.core.image
+
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.util.UUID
+
+/**
+ * Test suite for [FlopsApplicationImage]
+ */
+@DisplayName("FlopsApplicationImage")
+internal class FlopsApplicationImageTest {
+ @Test
+ fun `flops must be non-negative`() {
+ assertThrows<IllegalArgumentException>("FLOPs must be non-negative") {
+ FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), -1, 1)
+ }
+ }
+
+ @Test
+ fun `cores cannot be zero`() {
+ assertThrows<IllegalArgumentException>("Cores cannot be zero") {
+ FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1, 0)
+ }
+ }
+
+ @Test
+ fun `cores cannot be negative`() {
+ assertThrows<IllegalArgumentException>("Cores cannot be negative") {
+ FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1, -1)
+ }
+ }
+
+ @Test
+ fun `utilization cannot be zero`() {
+ assertThrows<IllegalArgumentException>("Utilization cannot be zero") {
+ FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1, 1, 0.0)
+ }
+ }
+
+ @Test
+ fun `utilization cannot be negative`() {
+ assertThrows<IllegalArgumentException>("Utilization cannot be negative") {
+ FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1, 1, -1.0)
+ }
+ }
+
+ @Test
+ fun `utilization cannot be larger than one`() {
+ assertThrows<IllegalArgumentException>("Utilization cannot be larger than one") {
+ FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1, 1, 2.0)
+ }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt
new file mode 100644
index 00000000..071c0626
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/driver/SimpleBareMetalDriverTest.kt
@@ -0,0 +1,88 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal.driver
+
+import com.atlarge.odcsim.SimulationEngineProvider
+import com.atlarge.odcsim.simulationContext
+import com.atlarge.opendc.compute.core.ProcessingNode
+import com.atlarge.opendc.compute.core.ProcessingUnit
+import com.atlarge.opendc.compute.core.ServerEvent
+import com.atlarge.opendc.compute.core.ServerState
+import com.atlarge.opendc.compute.core.image.FlopsApplicationImage
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.util.ServiceLoader
+import java.util.UUID
+
+internal class SimpleBareMetalDriverTest {
+ /**
+ * A smoke test for the bare-metal driver.
+ */
+ @Test
+ fun smoke() {
+ var finalState: ServerState = ServerState.BUILD
+ val provider = ServiceLoader.load(SimulationEngineProvider::class.java).first()
+ val system = provider("sim")
+ val root = system.newDomain(name = "root")
+ root.launch {
+ val dom = root.newDomain(name = "driver")
+ val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 4)
+ val cpus = List(4) { ProcessingUnit(cpuNode, it, 2400.0) }
+ val driver = SimpleBareMetalDriver(dom, UUID.randomUUID(), "test", emptyMap(), cpus, emptyList())
+ val image = FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1_000, 2)
+
+ // Batch driver commands
+ withContext(dom.coroutineContext) {
+ driver.init()
+ driver.setImage(image)
+ val server = driver.start().server!!
+ driver.usage
+ .onEach { println("${simulationContext.clock.millis()} $it") }
+ .launchIn(this)
+ server.events.collect { event ->
+ when (event) {
+ is ServerEvent.StateChanged -> {
+ println("${simulationContext.clock.millis()} $event")
+ finalState = event.server.state
+ }
+ }
+ }
+ }
+ }
+
+ runBlocking {
+ system.run()
+ system.terminate()
+ }
+
+ assertEquals(ServerState.SHUTOFF, finalState)
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt
new file mode 100644
index 00000000..f8bd786e
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/metal/service/SimpleProvisioningServiceTest.kt
@@ -0,0 +1,73 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.metal.service
+
+import com.atlarge.odcsim.SimulationEngineProvider
+import com.atlarge.opendc.compute.core.ProcessingNode
+import com.atlarge.opendc.compute.core.ProcessingUnit
+import com.atlarge.opendc.compute.core.image.FlopsApplicationImage
+import com.atlarge.opendc.compute.metal.driver.SimpleBareMetalDriver
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.jupiter.api.Test
+import java.util.ServiceLoader
+import java.util.UUID
+
+/**
+ * Test suite for the [SimpleProvisioningService].
+ */
+internal class SimpleProvisioningServiceTest {
+ /**
+ * A basic smoke test.
+ */
+ @Test
+ fun smoke() {
+ val provider = ServiceLoader.load(SimulationEngineProvider::class.java).first()
+ val system = provider("sim")
+ val root = system.newDomain(name = "root")
+ root.launch {
+ val image = FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1000, 2)
+ val dom = root.newDomain("provisioner")
+
+ val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 4)
+ val cpus = List(4) { ProcessingUnit(cpuNode, it, 2400.0) }
+ val driver = SimpleBareMetalDriver(dom.newDomain(), UUID.randomUUID(), "test", emptyMap(), cpus, emptyList())
+
+ val provisioner = SimpleProvisioningService(dom)
+ provisioner.create(driver)
+ delay(5)
+ val nodes = provisioner.nodes()
+ val node = provisioner.deploy(nodes.first(), image)
+ node.server!!.events.collect { println(it) }
+ }
+
+ runBlocking {
+ system.run()
+ system.terminate()
+ }
+ }
+}
diff --git a/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt
new file mode 100644
index 00000000..ca00fc94
--- /dev/null
+++ b/simulator/opendc/opendc-compute/src/test/kotlin/com/atlarge/opendc/compute/virt/HypervisorTest.kt
@@ -0,0 +1,167 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 atlarge-research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.atlarge.opendc.compute.virt
+
+import com.atlarge.odcsim.SimulationEngineProvider
+import com.atlarge.opendc.compute.core.ProcessingUnit
+import com.atlarge.opendc.compute.core.Flavor
+import com.atlarge.opendc.compute.core.ProcessingNode
+import com.atlarge.opendc.compute.core.image.FlopsApplicationImage
+import com.atlarge.opendc.compute.core.image.FlopsHistoryFragment
+import com.atlarge.opendc.compute.core.image.VmImage
+import com.atlarge.opendc.compute.metal.driver.SimpleBareMetalDriver
+import com.atlarge.opendc.compute.virt.driver.VirtDriver
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertAll
+import java.util.ServiceLoader
+import java.util.UUID
+
+/**
+ * Basic test-suite for the hypervisor.
+ */
+internal class HypervisorTest {
+ /**
+ * A smoke test for the bare-metal driver.
+ */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ @Disabled
+ fun smoke() {
+ val provider = ServiceLoader.load(SimulationEngineProvider::class.java).first()
+ val system = provider("test")
+ val root = system.newDomain("root")
+
+ root.launch {
+ val vmm = HypervisorImage
+ val workloadA = FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 1_000, 1)
+ val workloadB = FlopsApplicationImage(UUID.randomUUID(), "<unnamed>", emptyMap(), 2_000, 1)
+
+ val driverDom = root.newDomain("driver")
+
+ val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 1)
+ val cpus = List(1) { ProcessingUnit(cpuNode, it, 2000.0) }
+ val metalDriver = SimpleBareMetalDriver(driverDom, UUID.randomUUID(), "test", emptyMap(), cpus, emptyList())
+
+ metalDriver.init()
+ metalDriver.setImage(vmm)
+ val node = metalDriver.start()
+ node.server?.events?.onEach { println(it) }?.launchIn(this)
+
+ delay(5)
+
+ val flavor = Flavor(1, 0)
+ val vmDriver = metalDriver.refresh().server!!.services[VirtDriver]
+ vmDriver.events.onEach { println(it) }.launchIn(this)
+ val vmA = vmDriver.spawn("a", workloadA, flavor)
+ vmA.events.onEach { println(it) }.launchIn(this)
+ val vmB = vmDriver.spawn("b", workloadB, flavor)
+ vmB.events.onEach { println(it) }.launchIn(this)
+ }
+
+ runBlocking {
+ system.run()
+ system.terminate()
+ }
+ }
+
+ /**
+ * Test overcommissioning of a hypervisor.
+ */
+ @Test
+ fun overcommission() {
+ val provider = ServiceLoader.load(SimulationEngineProvider::class.java).first()
+ val system = provider("test")
+ val root = system.newDomain("root")
+
+ var requestedBurst = 0L
+ var grantedBurst = 0L
+ var overcommissionedBurst = 0L
+
+ root.launch {
+ val vmm = HypervisorImage
+ val duration = 5 * 60L
+ val vmImageA = VmImage(UUID.randomUUID(), "<unnamed>", emptyMap(), sequenceOf(
+ FlopsHistoryFragment(0, 28L * duration, duration * 1000, 28.0, 2),
+ FlopsHistoryFragment(0, 3500L * duration, duration * 1000, 3500.0, 2),
+ FlopsHistoryFragment(0, 0, duration * 1000, 0.0, 2),
+ FlopsHistoryFragment(0, 183L * duration, duration * 1000, 183.0, 2)
+ ), 2, 0)
+ val vmImageB = VmImage(UUID.randomUUID(), "<unnamed>", emptyMap(), sequenceOf(
+ FlopsHistoryFragment(0, 28L * duration, duration * 1000, 28.0, 2),
+ FlopsHistoryFragment(0, 3100L * duration, duration * 1000, 3100.0, 2),
+ FlopsHistoryFragment(0, 0, duration * 1000, 0.0, 2),
+ FlopsHistoryFragment(0, 73L * duration, duration * 1000, 73.0, 2)
+ ), 2, 0)
+
+ val driverDom = root.newDomain("driver")
+
+ val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2)
+ val cpus = List(2) { ProcessingUnit(cpuNode, it, 3200.0) }
+ val metalDriver = SimpleBareMetalDriver(driverDom, UUID.randomUUID(), "test", emptyMap(), cpus, emptyList())
+
+ metalDriver.init()
+ metalDriver.setImage(vmm)
+ metalDriver.start()
+
+ delay(5)
+
+ val flavor = Flavor(2, 0)
+ val vmDriver = metalDriver.refresh().server!!.services[VirtDriver]
+ vmDriver.events
+ .onEach { event ->
+ when (event) {
+ is HypervisorEvent.SliceFinished -> {
+ requestedBurst += event.requestedBurst
+ grantedBurst += event.grantedBurst
+ overcommissionedBurst += event.overcommissionedBurst
+ }
+ }
+ }
+ .launchIn(this)
+
+ vmDriver.spawn("a", vmImageA, flavor)
+ vmDriver.spawn("b", vmImageB, flavor)
+ }
+
+ runBlocking {
+ system.run()
+ system.terminate()
+ }
+
+ assertAll(
+ { assertEquals(2073600, requestedBurst, "Requested Burst does not match") },
+ { assertEquals(2013600, grantedBurst, "Granted Burst does not match") },
+ { assertEquals(60000, overcommissionedBurst, "Overcommissioned Burst does not match") }
+ )
+ }
+}