summaryrefslogtreecommitdiff
path: root/opendc-compute
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-compute')
-rw-r--r--opendc-compute/build.gradle.kts23
-rw-r--r--opendc-compute/opendc-compute-api/build.gradle.kts32
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt120
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt44
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt33
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/InsufficientServerCapacityException.kt29
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt55
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt81
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerState.kt53
-rw-r--r--opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt39
-rw-r--r--opendc-compute/opendc-compute-service/build.gradle.kts41
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt85
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt103
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt41
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt31
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt38
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt68
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt61
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt113
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt500
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt44
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt66
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt56
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt153
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt50
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt66
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt64
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt40
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt38
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt38
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt39
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.kt36
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt391
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt80
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt81
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt285
-rw-r--r--opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml38
-rw-r--r--opendc-compute/opendc-compute-simulator/build.gradle.kts43
-rw-r--r--opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt423
-rw-r--r--opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimMetaWorkloadMapper.kt35
-rw-r--r--opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt36
-rw-r--r--opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt227
47 files changed, 4104 insertions, 0 deletions
diff --git a/opendc-compute/build.gradle.kts b/opendc-compute/build.gradle.kts
new file mode 100644
index 00000000..bf920306
--- /dev/null
+++ b/opendc-compute/build.gradle.kts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+description = "Cloud computing fabric controller of OpenDC"
diff --git a/opendc-compute/opendc-compute-api/build.gradle.kts b/opendc-compute/opendc-compute-api/build.gradle.kts
new file mode 100644
index 00000000..835dbbb8
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/build.gradle.kts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 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 = "API interface for the OpenDC Compute service"
+
+/* Build configuration */
+plugins {
+ `kotlin-library-conventions`
+}
+
+dependencies {
+ api(platform(project(":opendc-platform")))
+}
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt
new file mode 100644
index 00000000..baa1ba2f
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+import java.util.UUID
+
+/**
+ * A client interface for the OpenDC Compute service.
+ */
+public interface ComputeClient : AutoCloseable {
+ /**
+ * Obtain the list of [Flavor]s accessible by the requesting user.
+ */
+ public suspend fun queryFlavors(): List<Flavor>
+
+ /**
+ * Obtain a [Flavor] by its unique identifier.
+ *
+ * @param id The identifier of the flavor.
+ */
+ public suspend fun findFlavor(id: UUID): Flavor?
+
+ /**
+ * Create a new [Flavor] instance at this compute service.
+ *
+ * @param name The name of the flavor.
+ * @param cpuCount The amount of CPU cores for this flavor.
+ * @param memorySize The size of the memory.
+ * @param labels The identifying labels of the image.
+ * @param meta The non-identifying meta-data of the image.
+ */
+ public suspend fun newFlavor(
+ name: String,
+ cpuCount: Int,
+ memorySize: Long,
+ labels: Map<String, String> = emptyMap(),
+ meta: Map<String, Any> = emptyMap()
+ ): Flavor
+
+ /**
+ * Obtain the list of [Image]s accessible by the requesting user.
+ */
+ public suspend fun queryImages(): List<Image>
+
+ /**
+ * Obtain a [Image] by its unique identifier.
+ *
+ * @param id The identifier of the image.
+ */
+ public suspend fun findImage(id: UUID): Image?
+
+ /**
+ * Create a new [Image] instance at this compute service.
+ *
+ * @param name The name of the image.
+ * @param labels The identifying labels of the image.
+ * @param meta The non-identifying meta-data of the image.
+ */
+ public suspend fun newImage(
+ name: String,
+ labels: Map<String, String> = emptyMap(),
+ meta: Map<String, Any> = emptyMap()
+ ): Image
+
+ /**
+ * Obtain the list of [Server]s accessible by the requesting user.
+ */
+ public suspend fun queryServers(): List<Server>
+
+ /**
+ * Obtain a [Server] by its unique identifier.
+ *
+ * @param id The identifier of the server.
+ */
+ public suspend fun findServer(id: UUID): Server?
+
+ /**
+ * Create a new [Server] instance at this compute 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.
+ * @param labels The identifying labels of the server.
+ * @param meta The non-identifying meta-data of the server.
+ * @param start A flag to indicate that the server should be started immediately.
+ */
+ public suspend fun newServer(
+ name: String,
+ image: Image,
+ flavor: Flavor,
+ labels: Map<String, String> = emptyMap(),
+ meta: Map<String, Any> = emptyMap(),
+ start: Boolean = true
+ ): Server
+
+ /**
+ * Release the resources associated with this client, preventing any further API calls.
+ */
+ public override fun close()
+}
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt
new file mode 100644
index 00000000..5f511f91
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+/**
+ * Flavors define the compute and memory capacity of [Server] instance. To 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 interface Flavor : Resource {
+ /**
+ * 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
+
+ /**
+ * Delete the flavor instance.
+ */
+ public suspend fun delete()
+}
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt
new file mode 100644
index 00000000..83e63b81
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+/**
+ * An image containing a bootable operating system that can directly be executed by physical or virtual server.
+ */
+public interface Image : Resource {
+ /**
+ * Delete the image instance.
+ */
+ public suspend fun delete()
+}
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/InsufficientServerCapacityException.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/InsufficientServerCapacityException.kt
new file mode 100644
index 00000000..8fbb7308
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/InsufficientServerCapacityException.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+/**
+ * This exception is thrown to indicate that the compute service does not have enough capacity at the moment to
+ * fulfill a launch request.
+ */
+public class InsufficientServerCapacityException(override val cause: Throwable? = null) : Exception("There was insufficient capacity available to satisfy the launch request")
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt
new file mode 100644
index 00000000..08120848
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+import java.util.UUID
+
+/**
+ * A generic resource provided by the OpenDC Compute service.
+ */
+public interface Resource {
+ /**
+ * The unique identifier of the resource.
+ */
+ public val uid: UUID
+
+ /**
+ * The name of the resource.
+ */
+ public val name: String
+
+ /**
+ * The identifying labels attached to the resource.
+ */
+ public val labels: Map<String, String>
+
+ /**
+ * The non-identifying metadata attached to the resource.
+ */
+ public val meta: Map<String, Any>
+
+ /**
+ * Refresh the local state of the resource.
+ */
+ public suspend fun refresh()
+}
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt
new file mode 100644
index 00000000..b508a9f8
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+/**
+ * A stateful object representing a server instance that is running on some physical or virtual machine.
+ */
+public interface Server : Resource {
+ /**
+ * The flavor of the server.
+ */
+ public val flavor: Flavor
+
+ /**
+ * The image of the server.
+ */
+ public val image: Image
+
+ /**
+ * The last known state of the server.
+ */
+ public val state: ServerState
+
+ /**
+ * Request the server to be started.
+ *
+ * This method is guaranteed to return after the request was acknowledged, but might return before the server was
+ * started.
+ */
+ public suspend fun start()
+
+ /**
+ * Request the server to be stopped.
+ *
+ * This method is guaranteed to return after the request was acknowledged, but might return before the server was
+ * stopped.
+ */
+ public suspend fun stop()
+
+ /**
+ * Request the server to be deleted.
+ *
+ * This method is guaranteed to return after the request was acknowledged, but might return before the server was
+ * deleted.
+ */
+ public suspend fun delete()
+
+ /**
+ * Register the specified [ServerWatcher] to watch the state of the server.
+ *
+ * @param watcher The watcher to register for the server.
+ */
+ public fun watch(watcher: ServerWatcher)
+
+ /**
+ * De-register the specified [ServerWatcher] from the server to stop it from receiving events.
+ *
+ * @param watcher The watcher to de-register from the server.
+ */
+ public fun unwatch(watcher: ServerWatcher)
+}
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerState.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerState.kt
new file mode 100644
index 00000000..a4d7d7d7
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerState.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+/**
+ * An enumeration describing the possible states of a server.
+ */
+public enum class ServerState {
+ /**
+ * Resources are being allocated for the instance. The instance is not running yet.
+ */
+ PROVISIONING,
+
+ /**
+ * A user shut down the instance.
+ */
+ TERMINATED,
+
+ /**
+ * The server instance is booting up or running.
+ */
+ RUNNING,
+
+ /**
+ * The server is in an error state.
+ */
+ ERROR,
+
+ /**
+ * The server has been deleted and cannot be started later on.
+ */
+ DELETED,
+}
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt
new file mode 100644
index 00000000..48a17b30
--- /dev/null
+++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.api
+
+/**
+ * An interface used to watch the state of [Server] instances.
+ */
+public interface ServerWatcher {
+ /**
+ * This method is invoked when the state of a [Server] changes.
+ *
+ * Note that the state of [server] might not reflect the state as reported by the invocation, as a call to
+ * [Server.refresh] is required to update its state.
+ *
+ * @param server The server whose state has changed.
+ * @param newState The new state of the server.
+ */
+ public fun onStateChanged(server: Server, newState: ServerState) {}
+}
diff --git a/opendc-compute/opendc-compute-service/build.gradle.kts b/opendc-compute/opendc-compute-service/build.gradle.kts
new file mode 100644
index 00000000..909e2dcd
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/build.gradle.kts
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 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 = "OpenDC Compute Service implementation"
+
+/* Build configuration */
+plugins {
+ `kotlin-library-conventions`
+ `testing-conventions`
+ `jacoco-conventions`
+}
+
+dependencies {
+ api(platform(project(":opendc-platform")))
+ api(project(":opendc-compute:opendc-compute-api"))
+ api(project(":opendc-telemetry:opendc-telemetry-api"))
+ implementation(project(":opendc-utils"))
+ implementation("io.github.microutils:kotlin-logging")
+
+ testImplementation(project(":opendc-simulator:opendc-simulator-core"))
+ testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl")
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt
new file mode 100644
index 00000000..1873eb99
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service
+
+import io.opentelemetry.api.metrics.Meter
+import org.opendc.compute.api.ComputeClient
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.scheduler.ComputeScheduler
+import java.time.Clock
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * The [ComputeService] hosts the API implementation of the OpenDC Compute service.
+ */
+public interface ComputeService : AutoCloseable {
+ /**
+ * The hosts that are used by the compute service.
+ */
+ public val hosts: Set<Host>
+
+ /**
+ * The number of hosts available in the system.
+ */
+ public val hostCount: Int
+
+ /**
+ * Create a new [ComputeClient] to control the compute service.
+ */
+ public fun newClient(): ComputeClient
+
+ /**
+ * Add a [host] to the scheduling pool of the compute service.
+ */
+ public fun addHost(host: Host)
+
+ /**
+ * Remove a [host] from the scheduling pool of the compute service.
+ */
+ public fun removeHost(host: Host)
+
+ /**
+ * Terminate the lifecycle of the compute service, stopping all running instances.
+ */
+ public override fun close()
+
+ public companion object {
+ /**
+ * Construct a new [ComputeService] implementation.
+ *
+ * @param context The [CoroutineContext] to use in the service.
+ * @param clock The clock instance to use.
+ * @param scheduler The scheduler implementation to use.
+ */
+ public operator fun invoke(
+ context: CoroutineContext,
+ clock: Clock,
+ meter: Meter,
+ scheduler: ComputeScheduler,
+ schedulingQuantum: Long = 300000,
+ ): ComputeService {
+ return ComputeServiceImpl(context, clock, meter, scheduler, schedulingQuantum)
+ }
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt
new file mode 100644
index 00000000..bed15dfd
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.driver
+
+import org.opendc.compute.api.Server
+import java.util.*
+
+/**
+ * Base interface for representing compute resources that host virtualized [Server] instances.
+ */
+public interface Host {
+ /**
+ * A unique identifier representing the host.
+ */
+ public val uid: UUID
+
+ /**
+ * The name of this host.
+ */
+ public val name: String
+
+ /**
+ * The machine model of the host.
+ */
+ public val model: HostModel
+
+ /**
+ * The state of the host.
+ */
+ public val state: HostState
+
+ /**
+ * Meta-data associated with the host.
+ */
+ public val meta: Map<String, Any>
+
+ /**
+ * Determine whether the specified [instance][server] can still fit on this host.
+ */
+ public fun canFit(server: Server): Boolean
+
+ /**
+ * Register the specified [instance][server] on the host.
+ *
+ * Once the method returns, the instance should be running if [start] is true or else the instance should be
+ * stopped.
+ */
+ public suspend fun spawn(server: Server, start: Boolean = true)
+
+ /**
+ * Determine whether the specified [instance][server] exists on the host.
+ */
+ public operator fun contains(server: Server): Boolean
+
+ /**
+ * Start the server [instance][server] if it is currently not running on this host.
+ *
+ * @throws IllegalArgumentException if the server is not present on the host.
+ */
+ public suspend fun start(server: Server)
+
+ /**
+ * Stop the server [instance][server] if it is currently running on this host.
+ *
+ * @throws IllegalArgumentException if the server is not present on the host.
+ */
+ public suspend fun stop(server: Server)
+
+ /**
+ * Delete the specified [instance][server] on this host and cleanup all resources associated with it.
+ */
+ public suspend fun delete(server: Server)
+
+ /**
+ * Add a [HostListener] to this host.
+ */
+ public fun addListener(listener: HostListener)
+
+ /**
+ * Remove a [HostListener] from this host.
+ */
+ public fun removeListener(listener: HostListener)
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt
new file mode 100644
index 00000000..f076cae3
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.driver
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+
+/**
+ * Listener interface for events originating from a [Host].
+ */
+public interface HostListener {
+ /**
+ * This method is invoked when the state of an [instance][server] on [host] changes.
+ */
+ public fun onStateChanged(host: Host, server: Server, newState: ServerState) {}
+
+ /**
+ * This method is invoked when the state of a [Host] has changed.
+ */
+ public fun onStateChanged(host: Host, newState: HostState) {}
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt
new file mode 100644
index 00000000..5632a55e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.driver
+
+/**
+ * Describes the static machine properties of the host.
+ *
+ * @property vcpuCount The number of logical processing cores available for this host.
+ * @property memorySize The amount of memory available for this host in MB.
+ */
+public data class HostModel(public val cpuCount: Int, public val memorySize: Long)
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt
new file mode 100644
index 00000000..6d85ee2d
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.driver
+
+/**
+ * The state of a host.
+ */
+public enum class HostState {
+ /**
+ * The host is up.
+ */
+ UP,
+
+ /**
+ * The host is down.
+ */
+ DOWN
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt
new file mode 100644
index 00000000..4a8d3046
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.internal
+
+import org.opendc.compute.api.Flavor
+import java.util.UUID
+
+/**
+ * A [Flavor] implementation that is passed to clients but delegates its implementation to another class.
+ */
+internal class ClientFlavor(private val delegate: Flavor) : Flavor {
+ override val uid: UUID = delegate.uid
+
+ override var name: String = delegate.name
+ private set
+
+ override var cpuCount: Int = delegate.cpuCount
+ private set
+
+ override var memorySize: Long = delegate.memorySize
+ private set
+
+ override var labels: Map<String, String> = delegate.labels.toMap()
+ private set
+
+ override var meta: Map<String, Any> = delegate.meta.toMap()
+ private set
+
+ override suspend fun delete() {
+ delegate.delete()
+ }
+
+ override suspend fun refresh() {
+ delegate.refresh()
+
+ name = delegate.name
+ cpuCount = delegate.cpuCount
+ memorySize = delegate.memorySize
+ labels = delegate.labels
+ meta = delegate.meta
+ }
+
+ override fun equals(other: Any?): Boolean = other is Flavor && other.uid == uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Flavor[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt
new file mode 100644
index 00000000..e0b5c171
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.internal
+
+import org.opendc.compute.api.Image
+import java.util.*
+
+/**
+ * An [Image] implementation that is passed to clients but delegates its implementation to another class.
+ */
+internal class ClientImage(private val delegate: Image) : Image {
+ override val uid: UUID = delegate.uid
+
+ override var name: String = delegate.name
+ private set
+
+ override var labels: Map<String, String> = delegate.labels.toMap()
+ private set
+
+ override var meta: Map<String, Any> = delegate.meta.toMap()
+ private set
+
+ override suspend fun delete() {
+ delegate.delete()
+ refresh()
+ }
+
+ override suspend fun refresh() {
+ delegate.refresh()
+
+ name = delegate.name
+ labels = delegate.labels
+ meta = delegate.meta
+ }
+
+ override fun equals(other: Any?): Boolean = other is Image && other.uid == uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Image[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt
new file mode 100644
index 00000000..f2929bf3
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.internal
+
+import org.opendc.compute.api.Flavor
+import org.opendc.compute.api.Image
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+import org.opendc.compute.api.ServerWatcher
+import java.util.*
+
+/**
+ * A [Server] implementation that is passed to clients but delegates its implementation to another class.
+ */
+internal class ClientServer(private val delegate: Server) : Server, ServerWatcher {
+ private val watchers = mutableListOf<ServerWatcher>()
+
+ override val uid: UUID = delegate.uid
+
+ override var name: String = delegate.name
+ private set
+
+ override var flavor: Flavor = delegate.flavor
+ private set
+
+ override var image: Image = delegate.image
+ private set
+
+ override var labels: Map<String, String> = delegate.labels.toMap()
+ private set
+
+ override var meta: Map<String, Any> = delegate.meta.toMap()
+ private set
+
+ override var state: ServerState = delegate.state
+ private set
+
+ override suspend fun start() {
+ delegate.start()
+ refresh()
+ }
+
+ override suspend fun stop() {
+ delegate.stop()
+ refresh()
+ }
+
+ override suspend fun delete() {
+ delegate.delete()
+ refresh()
+ }
+
+ override fun watch(watcher: ServerWatcher) {
+ if (watchers.isEmpty()) {
+ delegate.watch(this)
+ }
+
+ watchers += watcher
+ }
+
+ override fun unwatch(watcher: ServerWatcher) {
+ watchers += watcher
+
+ if (watchers.isEmpty()) {
+ delegate.unwatch(this)
+ }
+ }
+
+ override suspend fun refresh() {
+ delegate.refresh()
+
+ name = delegate.name
+ flavor = delegate.flavor
+ image = delegate.image
+ labels = delegate.labels
+ meta = delegate.meta
+ state = delegate.state
+ }
+
+ override fun onStateChanged(server: Server, newState: ServerState) {
+ val watchers = watchers
+
+ for (watcher in watchers) {
+ watcher.onStateChanged(this, newState)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean = other is Server && other.uid == uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Server[uid=$uid,name=$name,state=$state]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt
new file mode 100644
index 00000000..8af5f86e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt
@@ -0,0 +1,500 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.internal
+
+import io.opentelemetry.api.metrics.Meter
+import kotlinx.coroutines.*
+import mu.KotlinLogging
+import org.opendc.compute.api.*
+import org.opendc.compute.service.ComputeService
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.driver.HostListener
+import org.opendc.compute.service.driver.HostState
+import org.opendc.compute.service.scheduler.ComputeScheduler
+import org.opendc.utils.TimerScheduler
+import java.time.Clock
+import java.util.*
+import kotlin.coroutines.CoroutineContext
+import kotlin.math.max
+
+/**
+ * Internal implementation of the OpenDC Compute service.
+ *
+ * @param context The [CoroutineContext] to use.
+ * @param clock The clock instance to keep track of time.
+ */
+internal class ComputeServiceImpl(
+ private val context: CoroutineContext,
+ private val clock: Clock,
+ private val meter: Meter,
+ private val scheduler: ComputeScheduler,
+ private val schedulingQuantum: Long
+) : ComputeService, HostListener {
+ /**
+ * The [CoroutineScope] of the service bounded by the lifecycle of the service.
+ */
+ private val scope = CoroutineScope(context + Job())
+
+ /**
+ * The logger instance of this server.
+ */
+ private val logger = KotlinLogging.logger {}
+
+ /**
+ * The [Random] instance used to generate unique identifiers for the objects.
+ */
+ private val random = Random(0)
+
+ /**
+ * A mapping from host to host view.
+ */
+ private val hostToView = mutableMapOf<Host, HostView>()
+
+ /**
+ * The available hypervisors.
+ */
+ private val availableHosts: MutableSet<HostView> = mutableSetOf()
+
+ /**
+ * The servers that should be launched by the service.
+ */
+ private val queue: Deque<SchedulingRequest> = ArrayDeque()
+
+ /**
+ * The active servers in the system.
+ */
+ private val activeServers: MutableMap<Server, Host> = mutableMapOf()
+
+ /**
+ * The registered flavors for this compute service.
+ */
+ internal val flavors = mutableMapOf<UUID, InternalFlavor>()
+
+ /**
+ * The registered images for this compute service.
+ */
+ internal val images = mutableMapOf<UUID, InternalImage>()
+
+ /**
+ * The registered servers for this compute service.
+ */
+ private val servers = mutableMapOf<UUID, InternalServer>()
+
+ private var maxCores = 0
+ private var maxMemory = 0L
+
+ /**
+ * The number of servers that have been submitted to the service for provisioning.
+ */
+ private val _submittedServers = meter.longCounterBuilder("servers.submitted")
+ .setDescription("Number of start requests")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that failed to be scheduled.
+ */
+ private val _unscheduledServers = meter.longCounterBuilder("servers.unscheduled")
+ .setDescription("Number of unscheduled servers")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that are waiting to be provisioned.
+ */
+ private val _waitingServers = meter.longUpDownCounterBuilder("servers.waiting")
+ .setDescription("Number of servers waiting to be provisioned")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that are waiting to be provisioned.
+ */
+ private val _runningServers = meter.longUpDownCounterBuilder("servers.active")
+ .setDescription("Number of servers currently running")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that have finished running.
+ */
+ private val _finishedServers = meter.longCounterBuilder("servers.finished")
+ .setDescription("Number of servers that finished running")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of hosts registered at the compute service.
+ */
+ private val _hostCount = meter.longUpDownCounterBuilder("hosts.total")
+ .setDescription("Number of hosts")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of available hosts registered at the compute service.
+ */
+ private val _availableHostCount = meter.longUpDownCounterBuilder("hosts.available")
+ .setDescription("Number of available hosts")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The [TimerScheduler] to use for scheduling the scheduler cycles.
+ */
+ private var timerScheduler: TimerScheduler<Unit> = TimerScheduler(scope.coroutineContext, clock)
+
+ override val hosts: Set<Host>
+ get() = hostToView.keys
+
+ override val hostCount: Int
+ get() = hostToView.size
+
+ override fun newClient(): ComputeClient {
+ check(scope.isActive) { "Service is already closed" }
+ return object : ComputeClient {
+ private var isClosed: Boolean = false
+
+ override suspend fun queryFlavors(): List<Flavor> {
+ check(!isClosed) { "Client is already closed" }
+
+ return flavors.values.map { ClientFlavor(it) }
+ }
+
+ override suspend fun findFlavor(id: UUID): Flavor? {
+ check(!isClosed) { "Client is already closed" }
+
+ return flavors[id]?.let { ClientFlavor(it) }
+ }
+
+ override suspend fun newFlavor(
+ name: String,
+ cpuCount: Int,
+ memorySize: Long,
+ labels: Map<String, String>,
+ meta: Map<String, Any>
+ ): Flavor {
+ check(!isClosed) { "Client is already closed" }
+
+ val uid = UUID(clock.millis(), random.nextLong())
+ val flavor = InternalFlavor(
+ this@ComputeServiceImpl,
+ uid,
+ name,
+ cpuCount,
+ memorySize,
+ labels,
+ meta
+ )
+
+ flavors[uid] = flavor
+
+ return ClientFlavor(flavor)
+ }
+
+ override suspend fun queryImages(): List<Image> {
+ check(!isClosed) { "Client is already closed" }
+
+ return images.values.map { ClientImage(it) }
+ }
+
+ override suspend fun findImage(id: UUID): Image? {
+ check(!isClosed) { "Client is already closed" }
+
+ return images[id]?.let { ClientImage(it) }
+ }
+
+ override suspend fun newImage(name: String, labels: Map<String, String>, meta: Map<String, Any>): Image {
+ check(!isClosed) { "Client is already closed" }
+
+ val uid = UUID(clock.millis(), random.nextLong())
+ val image = InternalImage(this@ComputeServiceImpl, uid, name, labels, meta)
+
+ images[uid] = image
+
+ return ClientImage(image)
+ }
+
+ override suspend fun newServer(
+ name: String,
+ image: Image,
+ flavor: Flavor,
+ labels: Map<String, String>,
+ meta: Map<String, Any>,
+ start: Boolean
+ ): Server {
+ check(!isClosed) { "Client is closed" }
+
+ val uid = UUID(clock.millis(), random.nextLong())
+ val server = InternalServer(
+ this@ComputeServiceImpl,
+ uid,
+ name,
+ requireNotNull(flavors[flavor.uid]) { "Unknown flavor" },
+ requireNotNull(images[image.uid]) { "Unknown image" },
+ labels.toMutableMap(),
+ meta.toMutableMap()
+ )
+
+ servers[uid] = server
+
+ if (start) {
+ server.start()
+ }
+
+ return ClientServer(server)
+ }
+
+ override suspend fun findServer(id: UUID): Server? {
+ check(!isClosed) { "Client is already closed" }
+
+ return servers[id]?.let { ClientServer(it) }
+ }
+
+ override suspend fun queryServers(): List<Server> {
+ check(!isClosed) { "Client is already closed" }
+
+ return servers.values.map { ClientServer(it) }
+ }
+
+ override fun close() {
+ isClosed = true
+ }
+
+ override fun toString(): String = "ComputeClient"
+ }
+ }
+
+ override fun addHost(host: Host) {
+ // Check if host is already known
+ if (host in hostToView) {
+ return
+ }
+
+ val hv = HostView(host)
+ maxCores = max(maxCores, host.model.cpuCount)
+ maxMemory = max(maxMemory, host.model.memorySize)
+ hostToView[host] = hv
+
+ if (host.state == HostState.UP) {
+ _availableHostCount.add(1)
+ availableHosts += hv
+ }
+
+ scheduler.addHost(hv)
+ _hostCount.add(1)
+ host.addListener(this)
+ }
+
+ override fun removeHost(host: Host) {
+ val view = hostToView.remove(host)
+ if (view != null) {
+ if (availableHosts.remove(view)) {
+ _availableHostCount.add(-1)
+ }
+ scheduler.removeHost(view)
+ host.removeListener(this)
+ _hostCount.add(-1)
+ }
+ }
+
+ override fun close() {
+ scope.cancel()
+ }
+
+ internal fun schedule(server: InternalServer): SchedulingRequest {
+ logger.debug { "Enqueueing server ${server.uid} to be assigned to host." }
+
+ val request = SchedulingRequest(server)
+ queue.add(request)
+ _submittedServers.add(1)
+ _waitingServers.add(1)
+ requestSchedulingCycle()
+ return request
+ }
+
+ internal fun delete(flavor: InternalFlavor) {
+ flavors.remove(flavor.uid)
+ }
+
+ internal fun delete(image: InternalImage) {
+ images.remove(image.uid)
+ }
+
+ internal fun delete(server: InternalServer) {
+ servers.remove(server.uid)
+ }
+
+ /**
+ * Indicate that a new scheduling cycle is needed due to a change to the service's state.
+ */
+ private fun requestSchedulingCycle() {
+ // Bail out in case we have already requested a new cycle or the queue is empty.
+ if (timerScheduler.isTimerActive(Unit) || queue.isEmpty()) {
+ return
+ }
+
+ // 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 = schedulingQuantum - (clock.millis() % schedulingQuantum)
+
+ timerScheduler.startSingleTimer(Unit, delay) {
+ doSchedule()
+ }
+ }
+
+ /**
+ * Run a single scheduling iteration.
+ */
+ private fun doSchedule() {
+ while (queue.isNotEmpty()) {
+ val request = queue.peek()
+
+ if (request.isCancelled) {
+ queue.poll()
+ _waitingServers.add(-1)
+ continue
+ }
+
+ val server = request.server
+ val hv = scheduler.select(request.server)
+ if (hv == null || !hv.host.canFit(server)) {
+ logger.trace { "Server $server selected for scheduling but no capacity available for it at the moment" }
+
+ if (server.flavor.memorySize > maxMemory || server.flavor.cpuCount > maxCores) {
+ // Remove the incoming image
+ queue.poll()
+ _waitingServers.add(-1)
+ _unscheduledServers.add(1)
+
+ logger.warn("Failed to spawn $server: does not fit [${clock.millis()}]")
+
+ server.state = ServerState.ERROR
+ continue
+ } else {
+ break
+ }
+ }
+
+ val host = hv.host
+
+ // Remove request from queue
+ queue.poll()
+ _waitingServers.add(-1)
+
+ logger.info { "Assigned server $server to host $host." }
+
+ // Speculatively update the hypervisor view information to prevent other images in the queue from
+ // deciding on stale values.
+ hv.instanceCount++
+ hv.provisionedCores += server.flavor.cpuCount
+ hv.availableMemory -= server.flavor.memorySize // XXX Temporary hack
+
+ scope.launch {
+ try {
+ server.host = host
+ host.spawn(server)
+ activeServers[server] = host
+ } catch (e: Throwable) {
+ logger.error("Failed to deploy VM", e)
+
+ hv.instanceCount--
+ hv.provisionedCores -= server.flavor.cpuCount
+ hv.availableMemory += server.flavor.memorySize
+ }
+ }
+ }
+ }
+
+ /**
+ * A request to schedule an [InternalServer] onto one of the [Host]s.
+ */
+ internal data class SchedulingRequest(val server: InternalServer) {
+ /**
+ * A flag to indicate that the request is cancelled.
+ */
+ var isCancelled: Boolean = false
+ }
+
+ override fun onStateChanged(host: Host, newState: HostState) {
+ when (newState) {
+ HostState.UP -> {
+ logger.debug { "[${clock.millis()}] Host ${host.uid} state changed: $newState" }
+
+ val hv = hostToView[host]
+ if (hv != null) {
+ // Corner case for when the hypervisor already exists
+ availableHosts += hv
+ _availableHostCount.add(1)
+ }
+
+ // Re-schedule on the new machine
+ requestSchedulingCycle()
+ }
+ HostState.DOWN -> {
+ logger.debug { "[${clock.millis()}] Host ${host.uid} state changed: $newState" }
+
+ val hv = hostToView[host] ?: return
+ availableHosts -= hv
+ _availableHostCount.add(-1)
+
+ requestSchedulingCycle()
+ }
+ }
+ }
+
+ override fun onStateChanged(host: Host, server: Server, newState: ServerState) {
+ require(server is InternalServer) { "Invalid server type passed to service" }
+
+ if (server.host != host) {
+ // This can happen when a server is rescheduled and started on another machine, while being deleted from
+ // the old machine.
+ return
+ }
+
+ server.state = newState
+
+ if (newState == ServerState.RUNNING) {
+ _runningServers.add(1)
+ } else if (newState == ServerState.TERMINATED || newState == ServerState.DELETED) {
+ logger.info { "[${clock.millis()}] Server ${server.uid} ${server.name} ${server.flavor} finished." }
+
+ activeServers -= server
+ _runningServers.add(-1)
+ _finishedServers.add(1)
+
+ val hv = hostToView[host]
+ if (hv != null) {
+ hv.provisionedCores -= server.flavor.cpuCount
+ hv.instanceCount--
+ hv.availableMemory += server.flavor.memorySize
+ } else {
+ logger.error { "Unknown host $host" }
+ }
+
+ // Try to reschedule if needed
+ requestSchedulingCycle()
+ }
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt
new file mode 100644
index 00000000..e2f33f11
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt
@@ -0,0 +1,44 @@
+/*
+ * 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 org.opendc.compute.service.internal
+
+import org.opendc.compute.service.ComputeService
+import org.opendc.compute.service.driver.Host
+import java.util.UUID
+
+/**
+ * A view of a [Host] as seen from the [ComputeService]
+ */
+public class HostView(public val host: Host) {
+ /**
+ * The unique identifier of the host.
+ */
+ public val uid: UUID
+ get() = host.uid
+
+ public var instanceCount: Int = 0
+ public var availableMemory: Long = host.model.memorySize
+ public var provisionedCores: Int = 0
+
+ override fun toString(): String = "HostView[host=$host]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt
new file mode 100644
index 00000000..b8fb6279
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.internal
+
+import org.opendc.compute.api.Flavor
+import java.util.*
+
+/**
+ * Internal stateful representation of a [Flavor].
+ */
+internal class InternalFlavor(
+ private val service: ComputeServiceImpl,
+ override val uid: UUID,
+ name: String,
+ cpuCount: Int,
+ memorySize: Long,
+ labels: Map<String, String>,
+ meta: Map<String, Any>
+) : Flavor {
+ override var name: String = name
+ private set
+
+ override var cpuCount: Int = cpuCount
+ private set
+
+ override var memorySize: Long = memorySize
+ private set
+
+ override val labels: MutableMap<String, String> = labels.toMutableMap()
+
+ override val meta: MutableMap<String, Any> = meta.toMutableMap()
+
+ override suspend fun refresh() {
+ // No-op: this object is the source-of-truth
+ }
+
+ override suspend fun delete() {
+ service.delete(this)
+ }
+
+ override fun equals(other: Any?): Boolean = other is Flavor && uid == other.uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Flavor[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt
new file mode 100644
index 00000000..d9ed5896
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.internal
+
+import org.opendc.compute.api.Image
+import java.util.*
+
+/**
+ * Internal stateful representation of an [Image].
+ */
+internal class InternalImage(
+ private val service: ComputeServiceImpl,
+ override val uid: UUID,
+ override val name: String,
+ labels: Map<String, String>,
+ meta: Map<String, Any>
+) : Image {
+
+ override val labels: MutableMap<String, String> = labels.toMutableMap()
+
+ override val meta: MutableMap<String, Any> = meta.toMutableMap()
+
+ override suspend fun refresh() {
+ // No-op: this object is the source-of-truth
+ }
+
+ override suspend fun delete() {
+ service.delete(this)
+ }
+
+ override fun equals(other: Any?): Boolean = other is Image && uid == other.uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Image[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt
new file mode 100644
index 00000000..d9d0f3fc
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.internal
+
+import mu.KotlinLogging
+import org.opendc.compute.api.*
+import org.opendc.compute.service.driver.Host
+import java.util.UUID
+
+/**
+ * Internal implementation of the [Server] interface.
+ */
+internal class InternalServer(
+ private val service: ComputeServiceImpl,
+ override val uid: UUID,
+ override val name: String,
+ override val flavor: InternalFlavor,
+ override val image: InternalImage,
+ override val labels: MutableMap<String, String>,
+ override val meta: MutableMap<String, Any>
+) : Server {
+ /**
+ * The logger instance of this server.
+ */
+ private val logger = KotlinLogging.logger {}
+
+ /**
+ * The watchers of this server object.
+ */
+ private val watchers = mutableListOf<ServerWatcher>()
+
+ /**
+ * The [Host] that has been assigned to host the server.
+ */
+ internal var host: Host? = null
+
+ /**
+ * The current scheduling request.
+ */
+ private var request: ComputeServiceImpl.SchedulingRequest? = null
+
+ override suspend fun start() {
+ when (state) {
+ ServerState.RUNNING -> {
+ logger.debug { "User tried to start server but server is already running" }
+ return
+ }
+ ServerState.PROVISIONING -> {
+ logger.debug { "User tried to start server but request is already pending: doing nothing" }
+ return
+ }
+ ServerState.DELETED -> {
+ logger.warn { "User tried to start terminated server" }
+ throw IllegalStateException("Server is terminated")
+ }
+ else -> {
+ logger.info { "User requested to start server $uid" }
+ state = ServerState.PROVISIONING
+ assert(request == null) { "Scheduling request already active" }
+ request = service.schedule(this)
+ }
+ }
+ }
+
+ override suspend fun stop() {
+ when (state) {
+ ServerState.PROVISIONING -> {
+ cancelProvisioningRequest()
+ state = ServerState.TERMINATED
+ }
+ ServerState.RUNNING, ServerState.ERROR -> {
+ val host = checkNotNull(host) { "Server not running" }
+ host.stop(this)
+ }
+ ServerState.TERMINATED, ServerState.DELETED -> {} // No work needed
+ }
+ }
+
+ override suspend fun delete() {
+ when (state) {
+ ServerState.PROVISIONING, ServerState.TERMINATED -> {
+ cancelProvisioningRequest()
+ service.delete(this)
+ state = ServerState.DELETED
+ }
+ ServerState.RUNNING, ServerState.ERROR -> {
+ val host = checkNotNull(host) { "Server not running" }
+ host.delete(this)
+ service.delete(this)
+ state = ServerState.DELETED
+ }
+ else -> {} // No work needed
+ }
+ }
+
+ override fun watch(watcher: ServerWatcher) {
+ watchers += watcher
+ }
+
+ override fun unwatch(watcher: ServerWatcher) {
+ watchers -= watcher
+ }
+
+ override suspend fun refresh() {
+ // No-op: this object is the source-of-truth
+ }
+
+ override var state: ServerState = ServerState.TERMINATED
+ set(value) {
+ if (value != field) {
+ watchers.forEach { it.onStateChanged(this, value) }
+ }
+
+ field = value
+ }
+
+ /**
+ * Cancel the provisioning request if active.
+ */
+ private fun cancelProvisioningRequest() {
+ val request = request
+ if (request != null) {
+ this.request = null
+ request.isCancelled = true
+ }
+ }
+
+ override fun equals(other: Any?): Boolean = other is Server && uid == other.uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Server[uid=$uid,state=$state]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt
new file mode 100644
index 00000000..a2ab3a2e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.ComputeService
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A generic scheduler interface used by the [ComputeService] to select hosts to place [Server]s on.
+ */
+public interface ComputeScheduler {
+ /**
+ * Register the specified [host] to be used for scheduling.
+ */
+ public fun addHost(host: HostView)
+
+ /**
+ * Remove the specified [host] to be removed from the scheduling pool.
+ */
+ public fun removeHost(host: HostView)
+
+ /**
+ * Select a host for the specified [server].
+ *
+ * @param server The server to select a host for.
+ * @return The host to schedule the server on or `null` if no server is available.
+ */
+ public fun select(server: Server): HostView?
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt
new file mode 100644
index 00000000..0fd5b2a4
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import org.opendc.compute.service.scheduler.filters.HostFilter
+import org.opendc.compute.service.scheduler.weights.HostWeigher
+
+/**
+ * A [ComputeScheduler] implementation that uses filtering and weighing passes to select
+ * the host to schedule a [Server] on.
+ *
+ * This implementation is based on the filter scheduler from OpenStack Nova.
+ * See: https://docs.openstack.org/nova/latest/user/filter-scheduler.html
+ */
+public class FilterScheduler(private val filters: List<HostFilter>, private val weighers: List<Pair<HostWeigher, Double>>) : ComputeScheduler {
+ /**
+ * The pool of hosts available to the scheduler.
+ */
+ private val hosts = mutableListOf<HostView>()
+
+ override fun addHost(host: HostView) {
+ hosts.add(host)
+ }
+
+ override fun removeHost(host: HostView) {
+ hosts.remove(host)
+ }
+
+ override fun select(server: Server): HostView? {
+ return hosts.asSequence()
+ .filter { host ->
+ for (filter in filters) {
+ if (!filter.test(host, server))
+ return@filter false
+ }
+
+ true
+ }
+ .sortedByDescending { host ->
+ weighers.sumByDouble { (weigher, factor) -> weigher.getWeight(host, server) * factor }
+ }
+ .firstOrNull()
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt
new file mode 100644
index 00000000..284c1f91
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt
@@ -0,0 +1,64 @@
+/*
+ * 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 org.opendc.compute.service.scheduler
+
+import mu.KotlinLogging
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * Policy replaying VM-cluster assignment.
+ *
+ * Within each cluster, the active servers on each node determine which node gets
+ * assigned the VM image.
+ */
+public class ReplayScheduler(private val vmPlacements: Map<String, String>) : ComputeScheduler {
+ private val logger = KotlinLogging.logger {}
+
+ /**
+ * The pool of hosts available to the scheduler.
+ */
+ private val hosts = mutableListOf<HostView>()
+
+ override fun addHost(host: HostView) {
+ hosts.add(host)
+ }
+
+ override fun removeHost(host: HostView) {
+ hosts.remove(host)
+ }
+
+ override fun select(server: Server): HostView? {
+ val clusterName = vmPlacements[server.name]
+ ?: throw IllegalStateException("Could not find placement data in VM placement file for VM ${server.name}")
+ val machinesInCluster = hosts.filter { it.host.name.contains(clusterName) }
+
+ if (machinesInCluster.isEmpty()) {
+ logger.info { "Could not find any machines belonging to cluster $clusterName for image ${server.name}, assigning randomly." }
+ return hosts.maxByOrNull { it.availableMemory }
+ }
+
+ return machinesInCluster.maxByOrNull { it.availableMemory }
+ ?: throw IllegalStateException("Cloud not find any machine and could not randomly assign")
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt
new file mode 100644
index 00000000..072440c5
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostFilter] that checks whether the capabilities provided by the host satisfies the requirements of the server
+ * flavor.
+ */
+public class ComputeCapabilitiesFilter : HostFilter {
+ override fun test(host: HostView, server: Server): Boolean {
+ val fitsMemory = host.availableMemory >= server.flavor.memorySize
+ val fitsCpu = host.host.model.cpuCount >= server.flavor.cpuCount
+ return fitsMemory && fitsCpu
+ }
+
+ override fun toString(): String = "ComputeCapabilitiesFilter"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt
new file mode 100644
index 00000000..fb842415
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.driver.HostState
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostFilter] that filters on active hosts.
+ */
+public class ComputeFilter : HostFilter {
+ override fun test(host: HostView, server: Server): Boolean {
+ return host.host.state == HostState.UP
+ }
+
+ override fun toString(): String = "ComputeFilter"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt
new file mode 100644
index 00000000..9e909ca6
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import org.opendc.compute.service.scheduler.FilterScheduler
+
+/**
+ * A filter used by the [FilterScheduler] to filter hosts.
+ */
+public fun interface HostFilter {
+ /**
+ * Test whether the specified [host] should be included in the selection
+ * for scheduling the specified [server].
+ */
+ public fun test(host: HostView, server: Server): Boolean
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt
new file mode 100644
index 00000000..ed6674b1
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostFilter] that filters hosts based on the number of instances on the host.
+ *
+ * @param limit The maximum number of instances on the host.
+ */
+public class InstanceCountFilter(private val limit: Int) : HostFilter {
+ override fun test(host: HostView, server: Server): Boolean {
+ return host.instanceCount < limit
+ }
+
+ override fun toString(): String = "InstanceCountFilter[limit=$limit]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt
new file mode 100644
index 00000000..12e6510e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the available memory per core on the host.
+ */
+public class CoreMemoryWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.availableMemory.toDouble() / host.host.model.cpuCount
+ }
+
+ override fun toString(): String = "CoreMemoryWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt
new file mode 100644
index 00000000..d48ee9e0
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import org.opendc.compute.service.scheduler.FilterScheduler
+
+/**
+ * An interface used by the [FilterScheduler] to weigh the pool of host for a scheduling request.
+ */
+public fun interface HostWeigher {
+ /**
+ * Obtain the weight of the specified [host] when scheduling the specified [server].
+ */
+ public fun getWeight(host: HostView, server: Server): Double
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt
new file mode 100644
index 00000000..2ef733e5
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the number of instances on the host.
+ */
+public class InstanceCountWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.instanceCount.toDouble()
+ }
+
+ override fun toString(): String = "InstanceCountWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt
new file mode 100644
index 00000000..115d8e4d
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the available memory on the host.
+ */
+public class MemoryWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.availableMemory.toDouble()
+ }
+
+ override fun toString(): String = "MemoryWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt
new file mode 100644
index 00000000..df5bcd6e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the number of provisioned cores on the host.
+ */
+public class ProvisionedCoresWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.provisionedCores.toDouble()
+ }
+
+ override fun toString(): String = "ProvisionedCoresWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.kt
new file mode 100644
index 00000000..1615df3a
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import java.util.*
+
+/**
+ * A [HostWeigher] that assigns random weights to each host every selection.
+ */
+public class RandomWeigher(private val random: Random) : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double = random.nextDouble()
+
+ override fun toString(): String = "RandomWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
new file mode 100644
index 00000000..a6258845
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service
+
+import io.mockk.*
+import io.opentelemetry.api.metrics.MeterProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.opendc.compute.api.*
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.driver.HostListener
+import org.opendc.compute.service.driver.HostModel
+import org.opendc.compute.service.driver.HostState
+import org.opendc.compute.service.scheduler.FilterScheduler
+import org.opendc.compute.service.scheduler.filters.ComputeCapabilitiesFilter
+import org.opendc.compute.service.scheduler.filters.ComputeFilter
+import org.opendc.compute.service.scheduler.weights.MemoryWeigher
+import org.opendc.simulator.core.SimulationCoroutineScope
+import org.opendc.simulator.core.runBlockingSimulation
+import java.util.*
+
+/**
+ * Test suite for the [ComputeService] interface.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class ComputeServiceTest {
+ lateinit var scope: SimulationCoroutineScope
+ lateinit var service: ComputeService
+
+ @BeforeEach
+ fun setUp() {
+ scope = SimulationCoroutineScope()
+ val clock = scope.clock
+ val computeScheduler = FilterScheduler(
+ filters = listOf(ComputeFilter(), ComputeCapabilitiesFilter()),
+ weighers = listOf(MemoryWeigher() to -1.0)
+ )
+ val meter = MeterProvider.noop().get("opendc-compute")
+ service = ComputeService(scope.coroutineContext, clock, meter, computeScheduler)
+ }
+
+ @Test
+ fun testClientClose() = scope.runBlockingSimulation {
+ val client = service.newClient()
+
+ assertEquals(emptyList<Flavor>(), client.queryFlavors())
+ assertEquals(emptyList<Image>(), client.queryImages())
+ assertEquals(emptyList<Server>(), client.queryServers())
+
+ client.close()
+
+ assertThrows<IllegalStateException> { client.queryFlavors() }
+ assertThrows<IllegalStateException> { client.queryImages() }
+ assertThrows<IllegalStateException> { client.queryServers() }
+
+ assertThrows<IllegalStateException> { client.findFlavor(UUID.randomUUID()) }
+ assertThrows<IllegalStateException> { client.findImage(UUID.randomUUID()) }
+ assertThrows<IllegalStateException> { client.findServer(UUID.randomUUID()) }
+
+ assertThrows<IllegalStateException> { client.newFlavor("test", 1, 2) }
+ assertThrows<IllegalStateException> { client.newImage("test") }
+ assertThrows<IllegalStateException> { client.newServer("test", mockk(), mockk()) }
+ }
+
+ @Test
+ fun testClientCreate() = scope.runBlockingSimulation {
+ val client = service.newClient()
+
+ val flavor = client.newFlavor("test", 1, 1024)
+ assertEquals(listOf(flavor), client.queryFlavors())
+ assertEquals(flavor, client.findFlavor(flavor.uid))
+ val image = client.newImage("test")
+ assertEquals(listOf(image), client.queryImages())
+ assertEquals(image, client.findImage(image.uid))
+ val server = client.newServer("test", image, flavor, start = false)
+ assertEquals(listOf(server), client.queryServers())
+ assertEquals(server, client.findServer(server.uid))
+
+ server.delete()
+ assertNull(client.findServer(server.uid))
+
+ image.delete()
+ assertNull(client.findImage(image.uid))
+
+ flavor.delete()
+ assertNull(client.findFlavor(flavor.uid))
+
+ assertThrows<IllegalStateException> { server.start() }
+ }
+
+ @Test
+ fun testClientOnClose() = scope.runBlockingSimulation {
+ service.close()
+ assertThrows<IllegalStateException> {
+ service.newClient()
+ }
+ }
+
+ @Test
+ fun testAddHost() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+
+ assertEquals(0, service.hostCount)
+ assertEquals(emptySet<Host>(), service.hosts)
+
+ service.addHost(host)
+
+ verify(exactly = 1) { host.addListener(any()) }
+
+ assertEquals(1, service.hostCount)
+ assertEquals(1, service.hosts.size)
+
+ service.removeHost(host)
+
+ verify(exactly = 1) { host.removeListener(any()) }
+ }
+
+ @Test
+ fun testAddHostDouble() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.DOWN
+
+ assertEquals(0, service.hostCount)
+ assertEquals(emptySet<Host>(), service.hosts)
+
+ service.addHost(host)
+ service.addHost(host)
+
+ verify(exactly = 1) { host.addListener(any()) }
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughCpus() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 0)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughMemory() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 0, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughResources() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerCancelRequest() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ server.stop()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testServerCannotFitOnHost() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(10 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify { host.canFit(server) }
+ }
+
+ @Test
+ fun testHostAvailableAfterSomeTime() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.DOWN
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+
+ every { host.state } returns HostState.UP
+ listeners.forEach { it.onStateChanged(host, HostState.UP) }
+
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify { host.canFit(server) }
+ }
+
+ @Test
+ fun testHostUnavailableAfterSomeTime() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ delay(5 * 60 * 1000)
+
+ every { host.state } returns HostState.DOWN
+ listeners.forEach { it.onStateChanged(host, HostState.DOWN) }
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify(exactly = 0) { host.canFit(server) }
+ }
+
+ @Test
+ fun testServerInvalidType() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ assertThrows<IllegalArgumentException> {
+ listeners.forEach { it.onStateChanged(host, server, ServerState.RUNNING) }
+ }
+ }
+
+ @Test
+ fun testServerDeploy() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+ val slot = slot<Server>()
+
+ val watcher = mockk<ServerWatcher>(relaxUnitFun = true)
+ server.watch(watcher)
+
+ // Start server
+ server.start()
+ delay(5 * 60 * 1000)
+ coVerify { host.spawn(capture(slot), true) }
+
+ listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.RUNNING) }
+
+ server.refresh()
+ assertEquals(ServerState.RUNNING, server.state)
+
+ verify { watcher.onStateChanged(server, ServerState.RUNNING) }
+
+ // Stop server
+ listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.TERMINATED) }
+
+ server.refresh()
+ assertEquals(ServerState.TERMINATED, server.state)
+
+ verify { watcher.onStateChanged(server, ServerState.TERMINATED) }
+ }
+
+ @Test
+ fun testServerDeployFailure() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ coEvery { host.spawn(any(), true) } throws IllegalStateException()
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt
new file mode 100644
index 00000000..18d698c6
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service
+
+import io.mockk.*
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.opendc.compute.api.Flavor
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import java.util.*
+
+/**
+ * Test suite for the [InternalFlavor] implementation.
+ */
+class InternalFlavorTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+ val b = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Flavor>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Flavor>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt
new file mode 100644
index 00000000..e1cb0128
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service
+
+import io.mockk.*
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.opendc.compute.api.Image
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import org.opendc.compute.service.internal.InternalImage
+import java.util.*
+
+/**
+ * Test suite for the [InternalFlavor] implementation.
+ */
+class InternalImageTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+ val b = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Image>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Image>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt
new file mode 100644
index 00000000..20ea8d20
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.service
+
+import io.mockk.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.yield
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import org.opendc.compute.service.internal.InternalImage
+import org.opendc.compute.service.internal.InternalServer
+import org.opendc.simulator.core.runBlockingSimulation
+import java.util.*
+
+/**
+ * Test suite for the [InternalServer] implementation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class InternalServerTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val b = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Server>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Server>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+
+ @Test
+ fun testStartTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ every { service.schedule(any()) } answers { ComputeServiceImpl.SchedulingRequest(it.invocation.args[0] as InternalServer) }
+
+ server.start()
+
+ verify(exactly = 1) { service.schedule(server) }
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+
+ @Test
+ fun testStartDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+
+ assertThrows<IllegalStateException> { server.start() }
+ }
+
+ @Test
+ fun testStartProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.PROVISIONING
+
+ server.start()
+
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+
+ @Test
+ fun testStartRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.RUNNING
+
+ server.start()
+
+ assertEquals(ServerState.RUNNING, server.state)
+ }
+
+ @Test
+ fun testStopProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val request = ComputeServiceImpl.SchedulingRequest(server)
+
+ every { service.schedule(any()) } returns request
+
+ server.start()
+ server.stop()
+
+ assertTrue(request.isCancelled)
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testStopTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.TERMINATED
+ server.stop()
+
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testStopDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+ server.stop()
+
+ assertEquals(ServerState.DELETED, server.state)
+ }
+
+ @Test
+ fun testStopRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ server.state = ServerState.RUNNING
+ server.host = host
+ server.stop()
+ yield()
+
+ coVerify { host.stop(server) }
+ }
+
+ @Test
+ fun testDeleteProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val request = ComputeServiceImpl.SchedulingRequest(server)
+
+ every { service.schedule(any()) } returns request
+
+ server.start()
+ server.delete()
+
+ assertTrue(request.isCancelled)
+ assertEquals(ServerState.DELETED, server.state)
+ verify { service.delete(server) }
+ }
+
+ @Test
+ fun testDeleteTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.TERMINATED
+ server.delete()
+
+ assertEquals(ServerState.DELETED, server.state)
+
+ verify { service.delete(server) }
+ }
+
+ @Test
+ fun testDeleteDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+ server.delete()
+
+ assertEquals(ServerState.DELETED, server.state)
+ }
+
+ @Test
+ fun testDeleteRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ server.state = ServerState.RUNNING
+ server.host = host
+ server.delete()
+ yield()
+
+ coVerify { host.delete(server) }
+ verify { service.delete(server) }
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml b/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml
new file mode 100644
index 00000000..0dfb75f2
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (c) 2021 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.
+ -->
+
+<Configuration status="WARN" packages="org.apache.logging.log4j.core">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%highlight{%-5level}] %logger{36} - %msg%n" disableAnsi="false"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Logger name="org.opendc" level="trace" additivity="false">
+ <AppenderRef ref="Console"/>
+ </Logger>
+ <Root level="info">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/opendc-compute/opendc-compute-simulator/build.gradle.kts b/opendc-compute/opendc-compute-simulator/build.gradle.kts
new file mode 100644
index 00000000..3bf8a114
--- /dev/null
+++ b/opendc-compute/opendc-compute-simulator/build.gradle.kts
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+description = "Simulator for OpenDC Compute"
+
+/* Build configuration */
+plugins {
+ `kotlin-library-conventions`
+ `testing-conventions`
+ `jacoco-conventions`
+}
+
+dependencies {
+ api(platform(project(":opendc-platform")))
+ api(project(":opendc-compute:opendc-compute-service"))
+ api(project(":opendc-simulator:opendc-simulator-compute"))
+ api(project(":opendc-simulator:opendc-simulator-failures"))
+ implementation(project(":opendc-utils"))
+ implementation("io.github.microutils:kotlin-logging")
+
+ testImplementation(project(":opendc-simulator:opendc-simulator-core"))
+ testImplementation(project(":opendc-telemetry:opendc-telemetry-sdk"))
+ testRuntimeOnly("org.slf4j:slf4j-simple:${versions.slf4j}")
+}
diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt
new file mode 100644
index 00000000..6d87e444
--- /dev/null
+++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt
@@ -0,0 +1,423 @@
+/*
+ * 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 org.opendc.compute.simulator
+
+import io.opentelemetry.api.metrics.Meter
+import io.opentelemetry.api.metrics.common.Labels
+import kotlinx.coroutines.*
+import mu.KotlinLogging
+import org.opendc.compute.api.Flavor
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+import org.opendc.compute.service.driver.*
+import org.opendc.simulator.compute.*
+import org.opendc.simulator.compute.cpufreq.PerformanceScalingGovernor
+import org.opendc.simulator.compute.cpufreq.ScalingDriver
+import org.opendc.simulator.compute.cpufreq.ScalingGovernor
+import org.opendc.simulator.compute.cpufreq.SimpleScalingDriver
+import org.opendc.simulator.compute.interference.IMAGE_PERF_INTERFERENCE_MODEL
+import org.opendc.simulator.compute.interference.PerformanceInterferenceModel
+import org.opendc.simulator.compute.model.MemoryUnit
+import org.opendc.simulator.compute.power.ConstantPowerModel
+import org.opendc.simulator.compute.power.PowerModel
+import org.opendc.simulator.failures.FailureDomain
+import java.time.Clock
+import java.util.*
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
+
+/**
+ * A [Host] that is simulates virtual machines on a physical machine using [SimHypervisor].
+ */
+public class SimHost(
+ override val uid: UUID,
+ override val name: String,
+ model: SimMachineModel,
+ override val meta: Map<String, Any>,
+ context: CoroutineContext,
+ clock: Clock,
+ meter: Meter,
+ hypervisor: SimHypervisorProvider,
+ scalingGovernor: ScalingGovernor,
+ scalingDriver: ScalingDriver,
+ private val mapper: SimWorkloadMapper = SimMetaWorkloadMapper(),
+) : Host, FailureDomain, AutoCloseable {
+
+ public constructor(
+ uid: UUID,
+ name: String,
+ model: SimMachineModel,
+ meta: Map<String, Any>,
+ context: CoroutineContext,
+ clock: Clock,
+ meter: Meter,
+ hypervisor: SimHypervisorProvider,
+ powerModel: PowerModel = ConstantPowerModel(0.0),
+ mapper: SimWorkloadMapper = SimMetaWorkloadMapper(),
+ ) : this(uid, name, model, meta, context, clock, meter, hypervisor, PerformanceScalingGovernor(), SimpleScalingDriver(powerModel), mapper)
+
+ /**
+ * The [CoroutineScope] of the host bounded by the lifecycle of the host.
+ */
+ override val scope: CoroutineScope = CoroutineScope(context + Job())
+
+ /**
+ * The logger instance of this server.
+ */
+ private val logger = KotlinLogging.logger {}
+
+ /**
+ * The event listeners registered with this host.
+ */
+ private val listeners = mutableListOf<HostListener>()
+
+ /**
+ * Current total memory use of the images on this hypervisor.
+ */
+ private var availableMemory: Long = model.memory.map { it.size }.sum()
+
+ /**
+ * The machine to run on.
+ */
+ public val machine: SimBareMetalMachine = SimBareMetalMachine(context, clock, model, scalingGovernor, scalingDriver)
+
+ /**
+ * The hypervisor to run multiple workloads.
+ */
+ public val hypervisor: SimHypervisor = hypervisor.create(
+ object : SimHypervisor.Listener {
+ override fun onSliceFinish(
+ hypervisor: SimHypervisor,
+ requestedWork: Long,
+ grantedWork: Long,
+ overcommittedWork: Long,
+ interferedWork: Long,
+ cpuUsage: Double,
+ cpuDemand: Double
+ ) {
+
+ _batch.put(_cpuWork, requestedWork.toDouble())
+ _batch.put(_cpuWorkGranted, grantedWork.toDouble())
+ _batch.put(_cpuWorkOvercommit, overcommittedWork.toDouble())
+ _batch.put(_cpuWorkInterference, interferedWork.toDouble())
+ _batch.put(_cpuUsage, cpuUsage)
+ _batch.put(_cpuDemand, cpuDemand)
+ _batch.put(_cpuPower, machine.powerDraw)
+ _batch.record()
+ }
+ }
+ )
+
+ /**
+ * The virtual machines running on the hypervisor.
+ */
+ private val guests = HashMap<Server, Guest>()
+
+ override val state: HostState
+ get() = _state
+ private var _state: HostState = HostState.DOWN
+ set(value) {
+ if (value != field) {
+ listeners.forEach { it.onStateChanged(this, value) }
+ }
+ field = value
+ }
+
+ override val model: HostModel = HostModel(model.cpus.size, model.memory.map { it.size }.sum())
+
+ /**
+ * The number of guests on the host.
+ */
+ private val _guests = meter.longUpDownCounterBuilder("guests.total")
+ .setDescription("Number of guests")
+ .setUnit("1")
+ .build()
+ .bind(Labels.of("host", uid.toString()))
+
+ /**
+ * The number of active guests on the host.
+ */
+ private val _activeGuests = meter.longUpDownCounterBuilder("guests.active")
+ .setDescription("Number of active guests")
+ .setUnit("1")
+ .build()
+ .bind(Labels.of("host", uid.toString()))
+
+ /**
+ * The CPU usage on the host.
+ */
+ private val _cpuUsage = meter.doubleValueRecorderBuilder("cpu.usage")
+ .setDescription("The amount of CPU resources used by the host")
+ .setUnit("MHz")
+ .build()
+
+ /**
+ * The CPU demand on the host.
+ */
+ private val _cpuDemand = meter.doubleValueRecorderBuilder("cpu.demand")
+ .setDescription("The amount of CPU resources the guests would use if there were no CPU contention or CPU limits")
+ .setUnit("MHz")
+ .build()
+
+ /**
+ * The requested work for the CPU.
+ */
+ private val _cpuPower = meter.doubleValueRecorderBuilder("power.usage")
+ .setDescription("The amount of power used by the CPU")
+ .setUnit("W")
+ .build()
+
+ /**
+ * The requested work for the CPU.
+ */
+ private val _cpuWork = meter.doubleValueRecorderBuilder("cpu.work.total")
+ .setDescription("The amount of work supplied to the CPU")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The work actually performed by the CPU.
+ */
+ private val _cpuWorkGranted = meter.doubleValueRecorderBuilder("cpu.work.granted")
+ .setDescription("The amount of work performed by the CPU")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The work that could not be performed by the CPU due to overcommitting resource.
+ */
+ private val _cpuWorkOvercommit = meter.doubleValueRecorderBuilder("cpu.work.overcommit")
+ .setDescription("The amount of work not performed by the CPU due to overcommitment")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The work that could not be performed by the CPU due to interference.
+ */
+ private val _cpuWorkInterference = meter.doubleValueRecorderBuilder("cpu.work.interference")
+ .setDescription("The amount of work not performed by the CPU due to interference")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The batch recorder used to record multiple metrics atomically.
+ */
+ private val _batch = meter.newBatchRecorder("host", uid.toString())
+
+ init {
+ // Launch hypervisor onto machine
+ scope.launch {
+ try {
+ _state = HostState.UP
+ machine.run(this@SimHost.hypervisor, emptyMap())
+ } catch (_: CancellationException) {
+ // Ignored
+ } catch (cause: Throwable) {
+ logger.error(cause) { "Host failed" }
+ throw cause
+ } finally {
+ _state = HostState.DOWN
+ }
+ }
+ }
+
+ override fun canFit(server: Server): Boolean {
+ val sufficientMemory = availableMemory > server.flavor.memorySize
+ val enoughCpus = machine.model.cpus.size >= server.flavor.cpuCount
+ val canFit = hypervisor.canFit(server.flavor.toMachineModel())
+
+ return sufficientMemory && enoughCpus && canFit
+ }
+
+ override suspend fun spawn(server: Server, start: Boolean) {
+ // Return if the server already exists on this host
+ if (server in this) {
+ return
+ }
+
+ require(canFit(server)) { "Server does not fit" }
+ val guest = Guest(server, hypervisor.createMachine(server.flavor.toMachineModel()))
+ guests[server] = guest
+ _guests.add(1)
+
+ if (start) {
+ guest.start()
+ }
+ }
+
+ override fun contains(server: Server): Boolean {
+ return server in guests
+ }
+
+ override suspend fun start(server: Server) {
+ val guest = requireNotNull(guests[server]) { "Unknown server ${server.uid} at host $uid" }
+ guest.start()
+ }
+
+ override suspend fun stop(server: Server) {
+ val guest = requireNotNull(guests[server]) { "Unknown server ${server.uid} at host $uid" }
+ guest.stop()
+ }
+
+ override suspend fun delete(server: Server) {
+ val guest = guests.remove(server) ?: return
+ guest.terminate()
+ _guests.add(-1)
+ }
+
+ override fun addListener(listener: HostListener) {
+ listeners.add(listener)
+ }
+
+ override fun removeListener(listener: HostListener) {
+ listeners.remove(listener)
+ }
+
+ override fun close() {
+ scope.cancel()
+ machine.close()
+ }
+
+ override fun toString(): String = "SimHost[uid=$uid,name=$name,model=$model]"
+
+ /**
+ * Convert flavor to machine model.
+ */
+ private fun Flavor.toMachineModel(): SimMachineModel {
+ val originalCpu = machine.model.cpus[0]
+ val processingNode = originalCpu.node.copy(coreCount = cpuCount)
+ val processingUnits = (0 until cpuCount).map { originalCpu.copy(id = it, node = processingNode) }
+ val memoryUnits = listOf(MemoryUnit("Generic", "Generic", 3200.0, memorySize))
+
+ return SimMachineModel(processingUnits, memoryUnits)
+ }
+
+ private fun onGuestStart(vm: Guest) {
+ guests.forEach { (_, guest) ->
+ if (guest.state == ServerState.RUNNING) {
+ vm.performanceInterferenceModel?.onStart(vm.server.image.name)
+ }
+ }
+
+ _activeGuests.add(1)
+ listeners.forEach { it.onStateChanged(this, vm.server, vm.state) }
+ }
+
+ private fun onGuestStop(vm: Guest) {
+ guests.forEach { (_, guest) ->
+ if (guest.state == ServerState.RUNNING) {
+ vm.performanceInterferenceModel?.onStop(vm.server.image.name)
+ }
+ }
+
+ _activeGuests.add(-1)
+ listeners.forEach { it.onStateChanged(this, vm.server, vm.state) }
+ }
+
+ override suspend fun fail() {
+ _state = HostState.DOWN
+ }
+
+ override suspend fun recover() {
+ _state = HostState.UP
+ }
+
+ /**
+ * A virtual machine instance that the driver manages.
+ */
+ private inner class Guest(val server: Server, val machine: SimMachine) {
+ val performanceInterferenceModel: PerformanceInterferenceModel? = server.meta[IMAGE_PERF_INTERFERENCE_MODEL] as? PerformanceInterferenceModel?
+
+ var state: ServerState = ServerState.TERMINATED
+
+ suspend fun start() {
+ when (state) {
+ ServerState.TERMINATED -> {
+ logger.info { "User requested to start server ${server.uid}" }
+ launch()
+ }
+ ServerState.RUNNING -> return
+ ServerState.DELETED -> {
+ logger.warn { "User tried to start terminated server" }
+ throw IllegalArgumentException("Server is terminated")
+ }
+ else -> assert(false) { "Invalid state transition" }
+ }
+ }
+
+ suspend fun stop() {
+ when (state) {
+ ServerState.RUNNING, ServerState.ERROR -> {
+ val job = job ?: throw IllegalStateException("Server should be active")
+ job.cancel()
+ job.join()
+ }
+ ServerState.TERMINATED, ServerState.DELETED -> return
+ else -> assert(false) { "Invalid state transition" }
+ }
+ }
+
+ suspend fun terminate() {
+ stop()
+ state = ServerState.DELETED
+ }
+
+ private var job: Job? = null
+
+ private suspend fun launch() = suspendCancellableCoroutine<Unit> { cont ->
+ assert(job == null) { "Concurrent job running" }
+ val workload = mapper.createWorkload(server)
+
+ job = scope.launch {
+ delay(1) // TODO Introduce boot time
+ init()
+ cont.resume(Unit)
+ try {
+ machine.run(workload, mapOf("driver" to this@SimHost, "server" to server))
+ exit(null)
+ } catch (cause: Throwable) {
+ exit(cause)
+ } finally {
+ machine.close()
+ job = null
+ }
+ }
+ }
+
+ private fun init() {
+ state = ServerState.RUNNING
+ onGuestStart(this)
+ }
+
+ private fun exit(cause: Throwable?) {
+ state =
+ if (cause == null)
+ ServerState.TERMINATED
+ else
+ ServerState.ERROR
+
+ availableMemory += server.flavor.memorySize
+ onGuestStop(this)
+ }
+ }
+}
diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimMetaWorkloadMapper.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimMetaWorkloadMapper.kt
new file mode 100644
index 00000000..c05f1a2c
--- /dev/null
+++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimMetaWorkloadMapper.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.simulator
+
+import org.opendc.compute.api.Server
+import org.opendc.simulator.compute.workload.SimWorkload
+
+/**
+ * A [SimWorkloadMapper] that maps a [Server] to a workload via the meta-data.
+ */
+public class SimMetaWorkloadMapper(private val key: String = "workload") : SimWorkloadMapper {
+ override fun createWorkload(server: Server): SimWorkload {
+ return requireNotNull(server.meta[key] ?: server.image.meta[key]) as SimWorkload
+ }
+}
diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt
new file mode 100644
index 00000000..7082c5cf
--- /dev/null
+++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021 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 org.opendc.compute.simulator
+
+import org.opendc.compute.api.Server
+import org.opendc.simulator.compute.workload.SimWorkload
+
+/**
+ * A [SimWorkloadMapper] is responsible for mapping a [Server] and [Image] to a [SimWorkload] that can be simulated.
+ */
+public fun interface SimWorkloadMapper {
+ /**
+ * Map the specified [server] to a [SimWorkload] that can be simulated.
+ */
+ public fun createWorkload(server: Server): SimWorkload
+}
diff --git a/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt b/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt
new file mode 100644
index 00000000..5594fd59
--- /dev/null
+++ b/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt
@@ -0,0 +1,227 @@
+/*
+ * 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 org.opendc.compute.simulator
+
+import io.opentelemetry.api.metrics.MeterProvider
+import io.opentelemetry.sdk.common.CompletableResultCode
+import io.opentelemetry.sdk.metrics.SdkMeterProvider
+import io.opentelemetry.sdk.metrics.data.MetricData
+import io.opentelemetry.sdk.metrics.export.MetricExporter
+import io.opentelemetry.sdk.metrics.export.MetricProducer
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertAll
+import org.opendc.compute.api.Flavor
+import org.opendc.compute.api.Image
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+import org.opendc.compute.api.ServerWatcher
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.driver.HostListener
+import org.opendc.simulator.compute.SimFairShareHypervisorProvider
+import org.opendc.simulator.compute.SimMachineModel
+import org.opendc.simulator.compute.model.MemoryUnit
+import org.opendc.simulator.compute.model.ProcessingNode
+import org.opendc.simulator.compute.model.ProcessingUnit
+import org.opendc.simulator.compute.workload.SimTraceWorkload
+import org.opendc.simulator.core.runBlockingSimulation
+import org.opendc.telemetry.sdk.metrics.export.CoroutineMetricReader
+import org.opendc.telemetry.sdk.toOtelClock
+import java.util.UUID
+import kotlin.coroutines.resume
+
+/**
+ * Basic test-suite for the hypervisor.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class SimHostTest {
+ private lateinit var machineModel: SimMachineModel
+
+ @BeforeEach
+ fun setUp() {
+ val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2)
+
+ machineModel = SimMachineModel(
+ cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) },
+ memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) }
+ )
+ }
+
+ /**
+ * Test overcommitting of resources by the hypervisor.
+ */
+ @Test
+ fun testOvercommitted() = runBlockingSimulation {
+ var requestedWork = 0L
+ var grantedWork = 0L
+ var overcommittedWork = 0L
+
+ val meterProvider: MeterProvider = SdkMeterProvider
+ .builder()
+ .setClock(clock.toOtelClock())
+ .build()
+
+ val virtDriver = SimHost(UUID.randomUUID(), "test", machineModel, emptyMap(), coroutineContext, clock, meterProvider.get("opendc-compute-simulator"), SimFairShareHypervisorProvider())
+ val duration = 5 * 60L
+ val vmImageA = MockImage(
+ UUID.randomUUID(),
+ "<unnamed>",
+ emptyMap(),
+ mapOf(
+ "workload" to SimTraceWorkload(
+ sequenceOf(
+ SimTraceWorkload.Fragment(duration * 1000, 2 * 28.0, 2),
+ SimTraceWorkload.Fragment(duration * 1000, 2 * 3500.0, 2),
+ SimTraceWorkload.Fragment(duration * 1000, 0.0, 2),
+ SimTraceWorkload.Fragment(duration * 1000, 2 * 183.0, 2)
+ ),
+ )
+ )
+ )
+ val vmImageB = MockImage(
+ UUID.randomUUID(),
+ "<unnamed>",
+ emptyMap(),
+ mapOf(
+ "workload" to SimTraceWorkload(
+ sequenceOf(
+ SimTraceWorkload.Fragment(duration * 1000, 2 * 28.0, 2),
+ SimTraceWorkload.Fragment(duration * 1000, 2 * 3100.0, 2),
+ SimTraceWorkload.Fragment(duration * 1000, 0.0, 2),
+ SimTraceWorkload.Fragment(duration * 1000, 2 * 73.0, 2)
+ )
+ )
+ )
+ )
+
+ val flavor = MockFlavor(2, 0)
+
+ // Setup metric reader
+ val reader = CoroutineMetricReader(
+ this, listOf(meterProvider as MetricProducer),
+ object : MetricExporter {
+ override fun export(metrics: Collection<MetricData>): CompletableResultCode {
+ val metricsByName = metrics.associateBy { it.name }
+ requestedWork += metricsByName.getValue("cpu.work.total").doubleSummaryData.points.first().sum.toLong()
+ grantedWork += metricsByName.getValue("cpu.work.granted").doubleSummaryData.points.first().sum.toLong()
+ overcommittedWork += metricsByName.getValue("cpu.work.overcommit").doubleSummaryData.points.first().sum.toLong()
+ return CompletableResultCode.ofSuccess()
+ }
+
+ override fun flush(): CompletableResultCode = CompletableResultCode.ofSuccess()
+
+ override fun shutdown(): CompletableResultCode = CompletableResultCode.ofSuccess()
+ },
+ exportInterval = duration * 1000
+ )
+
+ coroutineScope {
+ launch { virtDriver.spawn(MockServer(UUID.randomUUID(), "a", flavor, vmImageA)) }
+ launch { virtDriver.spawn(MockServer(UUID.randomUUID(), "b", flavor, vmImageB)) }
+
+ suspendCancellableCoroutine<Unit> { cont ->
+ virtDriver.addListener(object : HostListener {
+ private var finished = 0
+
+ override fun onStateChanged(host: Host, server: Server, newState: ServerState) {
+ if (newState == ServerState.TERMINATED && ++finished == 2) {
+ cont.resume(Unit)
+ }
+ }
+ })
+ }
+ }
+
+ // Ensure last cycle is collected
+ delay(1000 * duration)
+ virtDriver.close()
+ reader.close()
+
+ assertAll(
+ { assertEquals(4197600, requestedWork, "Requested work does not match") },
+ { assertEquals(2157600, grantedWork, "Granted work does not match") },
+ { assertEquals(2040000, overcommittedWork, "Overcommitted work does not match") },
+ { assertEquals(1500001, clock.millis()) }
+ )
+ }
+
+ private class MockFlavor(
+ override val cpuCount: Int,
+ override val memorySize: Long
+ ) : Flavor {
+ override val uid: UUID = UUID.randomUUID()
+ override val name: String = "test"
+ override val labels: Map<String, String> = emptyMap()
+ override val meta: Map<String, Any> = emptyMap()
+
+ override suspend fun delete() {
+ throw NotImplementedError()
+ }
+
+ override suspend fun refresh() {
+ throw NotImplementedError()
+ }
+ }
+
+ private class MockImage(
+ override val uid: UUID,
+ override val name: String,
+ override val labels: Map<String, String>,
+ override val meta: Map<String, Any>
+ ) : Image {
+ override suspend fun delete() {
+ throw NotImplementedError()
+ }
+
+ override suspend fun refresh() {
+ throw NotImplementedError()
+ }
+ }
+
+ private class MockServer(
+ override val uid: UUID,
+ override val name: String,
+ override val flavor: Flavor,
+ override val image: Image
+ ) : Server {
+ override val labels: Map<String, String> = emptyMap()
+
+ override val meta: Map<String, Any> = emptyMap()
+
+ override val state: ServerState = ServerState.TERMINATED
+
+ override suspend fun start() {}
+
+ override suspend fun stop() {}
+
+ override suspend fun delete() {}
+
+ override fun watch(watcher: ServerWatcher) {}
+
+ override fun unwatch(watcher: ServerWatcher) {}
+
+ override suspend fun refresh() {}
+ }
+}