Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ interface VirtualMachine : Entity<VirtualMachine>, IEntity {
var name: String
var isTemplate: Boolean
var host: String
// var hostId: String

// course related
var adminId: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ data class CreateVmOptions(
@JsonProperty("memory") val memory: Int, // MB
@JsonProperty("cpu") val cpu: Int,
@JsonProperty("diskNum") val disNum: Int = 1,
@JsonProperty("diskSize") val diskSize: Long, // bytes
@JsonProperty("diskSize") val diskSize: Long, // Bytes

@JsonProperty("powerOn") val powerOn: Boolean = false,

@JsonProperty("hostId") val hostId: String = "",
)

data class ConfigVmOptions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package cn.edu.buaa.scs.controller.models
* @param memory MB
* @param diskSize bytes
* @param templateUuid 创建虚拟机所使用的模板的UUID
// * @param templateName 创建虚拟机所使用的模板的名称
* @param description 申请理由
* @param namePrefix 生成的虚拟机的名称的前缀
* @param dueTime 使用截止时间
Expand All @@ -34,6 +35,8 @@ data class CreateVmApplyRequest(
val diskSize: kotlin.Long,
/* 创建虚拟机所使用的模板的UUID */
val templateUuid: kotlin.String,
// /* 创建虚拟机所使用的模板名称 */
// val templateName: kotlin.String,
/* 申请理由 */
val description: kotlin.String,
/* 生成的虚拟机的名称的前缀 */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package cn.edu.buaa.scs.kube.crd.v1alpha1

import cn.edu.buaa.scs.cache.authRedis
import cn.edu.buaa.scs.kube.vmKubeClient
import cn.edu.buaa.scs.model.VirtualMachineExtraInfo
import cn.edu.buaa.scs.model.VmApply
import cn.edu.buaa.scs.model.vmApplyList
import cn.edu.buaa.scs.service.namespaceName
import cn.edu.buaa.scs.storage.mysql
import cn.edu.buaa.scs.utils.jsonMapper
import cn.edu.buaa.scs.utils.jsonReadValue
import cn.edu.buaa.scs.utils.*
import cn.edu.buaa.scs.vm.CreateVmOptions
import cn.edu.buaa.scs.vm.newVMClient
import cn.edu.buaa.scs.vm.vcenter.VCenterClient
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
Expand Down Expand Up @@ -47,8 +48,8 @@ data class VirtualMachineSpec(
@JsonProperty("powerState") @PrinterColumn(name = "spec_powerState") val powerState: VirtualMachineModel.PowerState?,

@JsonProperty("deleted") val deleted: Boolean = false,
@JsonProperty("hostId") var hostId: String = "",
) : KubernetesResource {

@JsonIgnore
fun getVmExtraInfo(): VirtualMachineExtraInfo {
return jsonReadValue(this.extraInfo)
Expand All @@ -62,6 +63,7 @@ data class VirtualMachineSpec(
disNum = this.diskNum,
diskSize = this.diskSize,
powerOn = false,
hostId = authRedis.getValueByKey(name) ?: "",
)

fun toCrd(): VirtualMachine {
Expand All @@ -75,7 +77,6 @@ data class VirtualMachineSpec(
this.spec = vmSpec
}
}

}

@JsonInclude(JsonInclude.Include.ALWAYS)
Expand All @@ -102,6 +103,7 @@ fun VirtualMachineModel.toCrdSpec(): VirtualMachineSpec {
diskNum = this.diskNum,
diskSize = this.diskSize,
powerState = null,
hostId = this.host
)
}

Expand All @@ -128,6 +130,7 @@ class VirtualMachineReconciler(val client: KubernetesClient) : Reconciler<Virtua
resource: VirtualMachine?,
context: Context<VirtualMachine>?
): UpdateControl<VirtualMachine> {
val logger = logger("CRD reconcile")()
val vm = resource ?: return UpdateControl.noUpdate()
val vmClient = newVMClient(vm.spec.platform)
if (vm.spec.deleted) {
Expand Down Expand Up @@ -205,6 +208,29 @@ class VirtualMachineReconciler(val client: KubernetesClient) : Reconciler<Virtua
vm.spec = vm.spec.copy(deleted = true)
} else {
vm.status = vmModel.toCrdStatus()
if (vm.spec.platform == "vcenter") {
val hostId = vm.status.host
if (authRedis.getValueByKey(hostId) == null) {
// 如果缓存中没有这个主机,那么就去查询一下这个主机
authRedis.setExpireKey(hostId, "true", 600)
val vCenterClient = vmClient as VCenterClient
val hosts = vCenterClient.getVcenterHostsUsage().getOrNull()
val hostUsage = hosts!!.find { it.hostId == hostId }
logger.info { "动态调度发现虚拟机${vm.spec.name}对应的主机${hostUsage!!.hostId}," +
"其中CPU利用率是${hostUsage.cpu_ratio},内存利用率是${hostUsage.memory_ratio}" }
// 如果主机负载超标,需要调度
if (hostUsage!!.cpu_ratio > 0.9 || hostUsage!!.memory_ratio > 0.9) {
// 迁移!到CPU占用最小的上
val targetHost = hosts.minBy { it.cpu_ratio }
val hostRefDict = mapOf(
"hostRefType" to targetHost.hostRefType!!,
"hostRefValue" to targetHost.hostRefValue!!
)
vCenterClient.transferHost(vmUuid, hostRefDict)
logger.info { "The vm ${vm.spec.name} has moved from ${hostId} to ${targetHost}!!!" }
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cn.edu.buaa.scs.model

import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.*

interface TemplateUUID : Entity<TemplateUUID> {
companion object : Entity.Factory<TemplateUUID>()
var templateName: String
var platform: String
var uuid: String
}

object TemplateUUIDs : Table<TemplateUUID>("template_uuids") {
val templateName = varchar("template_name").primaryKey().bindTo { it.templateName }
val platform = varchar("platform").bindTo { it.platform }
val uuid = varchar("uuid").bindTo { it.uuid }
}

val Database.templateUUIDs get() = this.sequenceOf(TemplateUUIDs)
2 changes: 2 additions & 0 deletions cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/VmApply.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface VmApply : Entity<VmApply>, IEntity {
var memory: Int // MB
var diskSize: Long // bytes
var templateUuid: String
// var templateName: String
var description: String
var applyTime: Long
var status: Int // 0: 还未处理; 1: 允许; 2: 拒绝
Expand Down Expand Up @@ -58,6 +59,7 @@ object VmApplyList : Table<VmApply>("vm_apply") {
val memory = int("memory").bindTo { it.memory }
val diskSize = long("disk_size").bindTo { it.diskSize }
val templateUuid = varchar("template_uuid").bindTo { it.templateUuid }
// val templateName = varchar("template_name").bindTo { it.templateName }
val description = text("description").bindTo { it.description }
val applyTime = long("apply_time").bindTo { it.applyTime }
val status = int("status").bindTo { it.status }
Expand Down
72 changes: 68 additions & 4 deletions cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cn.edu.buaa.scs.service
import cn.edu.buaa.scs.auth.assertRead
import cn.edu.buaa.scs.auth.assertWrite
import cn.edu.buaa.scs.auth.authRead
import cn.edu.buaa.scs.cache.authRedis
import cn.edu.buaa.scs.controller.models.CreateVmApplyRequest
import cn.edu.buaa.scs.error.AuthorizationException
import cn.edu.buaa.scs.kube.crd.v1alpha1.VirtualMachineSpec
Expand All @@ -13,6 +14,8 @@ import cn.edu.buaa.scs.model.*
import cn.edu.buaa.scs.storage.mysql
import cn.edu.buaa.scs.utils.*
import cn.edu.buaa.scs.vm.*
import cn.edu.buaa.scs.vm.sangfor.SangforClient.getSangforHostsUsage
import cn.edu.buaa.scs.vm.vcenter.VCenterClient.getVcenterHostsUsage
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.websocket.*
Expand All @@ -22,6 +25,8 @@ import org.ktorm.dsl.*
import org.ktorm.entity.*
import org.ktorm.schema.ColumnDeclaring
import java.util.*
import cn.edu.buaa.scs.utils.logger
import cn.edu.buaa.scs.vm.sangfor.SangforClient

val ApplicationCall.vm
get() = VmService.getSvc(this) { VmService(this) }
Expand Down Expand Up @@ -141,7 +146,7 @@ class VmService(val call: ApplicationCall) : IService {
return mysql.vmApplyList.find { it.id.eq(id) } ?: throw NotFoundException()
}

fun handleApply(id: String, approve: Boolean, replyMsg: String): VmApply {
suspend fun handleApply(id: String, approve: Boolean, replyMsg: String): VmApply {
if (!call.user().isAdmin()) throw AuthorizationException()

val vmApply = mysql.vmApplyList.find { it.id.eq(id) } ?: throw NotFoundException()
Expand All @@ -158,6 +163,8 @@ class VmService(val call: ApplicationCall) : IService {
call.user().assertWrite(vmApply)
if (!vmApply.isApproved()) throw BadRequestException("the VMApply(${vmApply.id} is not approved")
vmApply.namespaceName().ensureNamespace(kubeClient)
// TODO: 这个函数给已有的vmApply添加更多的学生虚拟机,通过studentIdList直接在k8s中添加crd spec
// vmApply.toVmCrdSpec返回一个包含若干等待创建的虚拟机spec的列表
vmApply.toVmCrdSpec(false, platform, studentIdList).forEach { spec ->
vmKubeClient
.inNamespace(vmApply.namespaceName())
Expand All @@ -167,7 +174,8 @@ class VmService(val call: ApplicationCall) : IService {
return vmApply
}

private fun approveApply(vmApply: VmApply, approve: Boolean, replyMsg: String): VmApply {
private suspend fun approveApply(vmApply: VmApply, approve: Boolean, replyMsg: String): VmApply {
val logger = logger("approveApply")()
if (approve) {
vmApply.status = 1
} else {
Expand All @@ -176,25 +184,51 @@ class VmService(val call: ApplicationCall) : IService {
vmApply.replyMsg = replyMsg
vmApply.handleTime = System.currentTimeMillis()

//// 原来的版本
val templateVM = mysql.virtualMachines.find { it.uuid.eq(vmApply.templateUuid) }
var platform = "vcenter"
templateVM?.let {
platform = it.platform
}
////

if (approve) {
logger.info { "This apply is approved! Platform is ${platform}......" }
val hostList = if (platform == "vcenter") {
getVcenterHostsUsage().getOrThrow()

} else {
getSangforHostsUsage().getOrThrow()
}
// 所有平台主机列表
val vmSpecList = vmApply.toVmCrdSpec(true, platform) // 一次性要创建的所有虚拟机列表!

logger.info { "We have ${hostList.size} hosts alive and ${vmSpecList.size} vms to be create......" }

// begin GA algo
logger.info { "Begin running GeneticAlgorithm......" }
val gA = GeneticAlgorithm(vmSpecList, hostList, 50, 0.3, 0.3, 5, 20)
gA.evolve()
val alloc = gA.getBestSolution().getAllocation()

logger.info { "GeneticAlgorithm complete, result is: " }
alloc.forEach { logger.info {hostList[it].hostId} }

vmApply.namespaceName().ensureNamespace(kubeClient)
vmApply.toVmCrdSpec(true, platform).forEach { spec ->
vmSpecList.forEachIndexed { i, spec ->
// TODO: 这个vmApply创建了很多个虚拟机spec(如果是实验用的,就会根据studentIdList创建非常多的spec),对于每个即将被创建的虚拟机:
spec.hostId = hostList[alloc[i]].hostId // 给即将要被创建的vmSpec里写入分配到的hostID
authRedis.setExpireKey(spec.name, hostList[alloc[i]].hostId, 3500)
vmKubeClient
.inNamespace(vmApply.namespaceName())
.resource(spec.toCrd())
.createOrReplace()
}
}
logger.info { "VM Apply Process finish! ALL VM create INTO CRD!" }
mysql.vmApplyList.update(vmApply)
return vmApply
}

fun deleteFromApply(id: String, studentId: String?, teacherId: String?, studentIdList: List<String>?): VmApply {
val vmApply = mysql.vmApplyList.find { it.id.eq(id) } ?: throw NotFoundException()
call.user().assertWrite(vmApply)
Expand Down Expand Up @@ -247,6 +281,8 @@ class VmService(val call: ApplicationCall) : IService {
this.memory = request.memory
this.diskSize = request.diskSize
this.templateUuid = request.templateUuid
// TODO: 要求这个接口前端传来的request里也是templateName
// this.templateName = request.templateName
this.description = request.description
this.applyTime = System.currentTimeMillis()
this.status = 0
Expand Down Expand Up @@ -352,6 +388,21 @@ class VmService(val call: ApplicationCall) : IService {
studentId = studentId,
).getOrThrow()
}

fun recordTemplateName2Uuid(vcenter_uuid: String, sangfor_uuid: String, name: String) {
val sangforTemplate = TemplateUUID {
platform = "sangfor"
uuid = sangfor_uuid
templateName = name
}
val vcenterTemplate = TemplateUUID {
platform = "vcenter"
uuid = vcenter_uuid
templateName = name
}
mysql.templateUUIDs.add(sangforTemplate)
mysql.templateUUIDs.add(vcenterTemplate)
}
}

suspend fun DefaultWebSocketServerSession.sshWS(uuid: String) {
Expand Down Expand Up @@ -381,11 +432,15 @@ fun VmApply.namespaceName(): String {
return this.id.lowercase()
}



fun VmApply.toVmCrdSpec(initial: Boolean = false, platform: String = "vcenter", extraStudentList: List<String>? = null): List<VirtualMachineSpec> {
val vmApply = this
val baseExtraInfo = VirtualMachineExtraInfo(
applyId = vmApply.id,
templateUuid = vmApply.templateUuid,
// 查数据表,查出这个platform中 这个模板名对应的模板uuid
// templateUuid = mysql.templateUUIDs.find { it.platform.eq(platform) and it.templateName.eq(vmApply.templateName)}.uuid,
initial = initial,
)
val baseSpec = VirtualMachineSpec(
Expand All @@ -399,6 +454,14 @@ fun VmApply.toVmCrdSpec(initial: Boolean = false, platform: String = "vcenter",
template = false,
extraInfo = jsonMapper.writeValueAsString(baseExtraInfo),
)
/*
根据传入的 extraStudentList 参数是否为空,分别处理不同的情况:
如果 extraStudentList 不为空,则表示需要创建多个学生专用的虚拟机。在这种情况下,对于列表中的每一个学生,都需要创建一个新的 VirtualMachineSpec 对象,并将其加入到返回值列表中。
如果 extraStudentList 为空,则根据传入的 VmApply 对象中的其他信息,创建一个或多个 VirtualMachineSpec 对象。具体来说,有以下几种情况:
如果 VmApply 对象中的 studentId 不为空,且不等于 "default",则表示需要创建一个学生专用的虚拟机。
如果 VmApply 对象中的 teacherId 不为空,且不等于 "default",则表示需要创建一个老师专用的虚拟机。
如果 VmApply 对象中的 experimentId 不为 0,则表示需要创建多个学生专用的虚拟机,且这些虚拟机都属于同一个实验。
*/
return if (extraStudentList != null) {
val experiment = Experiment.id(vmApply.experimentId)
extraStudentList.map { studentId ->
Expand Down Expand Up @@ -440,6 +503,7 @@ fun VmApply.toVmCrdSpec(initial: Boolean = false, platform: String = "vcenter",
)

vmApply.experimentId != 0 -> {
// 如果是实验用的虚拟机,就会根据studentIdList参数,创建很多个虚拟机spec,返回给K8S CRD!
val experiment = Experiment.id(vmApply.experimentId)
vmApply.studentIdList.map { studentId ->
baseSpec.copy(
Expand Down
Loading