diff --git a/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/VirtualMachine.kt b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/VirtualMachine.kt index 23e9f521..aab56284 100644 --- a/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/VirtualMachine.kt +++ b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/VirtualMachine.kt @@ -142,6 +142,7 @@ interface VirtualMachine : Entity, IEntity { var name: String var isTemplate: Boolean var host: String +// var hostId: String // course related var adminId: String diff --git a/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/vm/VmOptions.kt b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/vm/VmOptions.kt index 667504b7..73100304 100644 --- a/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/vm/VmOptions.kt +++ b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/vm/VmOptions.kt @@ -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( diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateVmApplyRequest.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateVmApplyRequest.kt index b0a633e5..396cbd47 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateVmApplyRequest.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateVmApplyRequest.kt @@ -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 使用截止时间 @@ -34,6 +35,8 @@ data class CreateVmApplyRequest( val diskSize: kotlin.Long, /* 创建虚拟机所使用的模板的UUID */ val templateUuid: kotlin.String, +// /* 创建虚拟机所使用的模板名称 */ +// val templateName: kotlin.String, /* 申请理由 */ val description: kotlin.String, /* 生成的虚拟机的名称的前缀 */ diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt index e2f41f25..1dac7519 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt @@ -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 @@ -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) @@ -62,6 +63,7 @@ data class VirtualMachineSpec( disNum = this.diskNum, diskSize = this.diskSize, powerOn = false, + hostId = authRedis.getValueByKey(name) ?: "", ) fun toCrd(): VirtualMachine { @@ -75,7 +77,6 @@ data class VirtualMachineSpec( this.spec = vmSpec } } - } @JsonInclude(JsonInclude.Include.ALWAYS) @@ -102,6 +103,7 @@ fun VirtualMachineModel.toCrdSpec(): VirtualMachineSpec { diskNum = this.diskNum, diskSize = this.diskSize, powerState = null, + hostId = this.host ) } @@ -128,6 +130,7 @@ class VirtualMachineReconciler(val client: KubernetesClient) : Reconciler? ): UpdateControl { + val logger = logger("CRD reconcile")() val vm = resource ?: return UpdateControl.noUpdate() val vmClient = newVMClient(vm.spec.platform) if (vm.spec.deleted) { @@ -205,6 +208,29 @@ class VirtualMachineReconciler(val client: KubernetesClient) : Reconciler 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}!!!" } + } + } + } } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/TemplateUUIDs.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/TemplateUUIDs.kt new file mode 100644 index 00000000..af992726 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/TemplateUUIDs.kt @@ -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 { + companion object : Entity.Factory() + var templateName: String + var platform: String + var uuid: String +} + +object TemplateUUIDs : Table("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) \ No newline at end of file diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/VmApply.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/VmApply.kt index f06ab8e0..1e113b92 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/VmApply.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/model/VmApply.kt @@ -26,6 +26,7 @@ interface VmApply : Entity, 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: 拒绝 @@ -58,6 +59,7 @@ object VmApplyList : Table("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 } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt index 29f5f3cd..713ee59b 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt @@ -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 @@ -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.* @@ -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) } @@ -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() @@ -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()) @@ -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 { @@ -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?): VmApply { val vmApply = mysql.vmApplyList.find { it.id.eq(id) } ?: throw NotFoundException() call.user().assertWrite(vmApply) @@ -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 @@ -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) { @@ -381,11 +432,15 @@ fun VmApply.namespaceName(): String { return this.id.lowercase() } + + fun VmApply.toVmCrdSpec(initial: Boolean = false, platform: String = "vcenter", extraStudentList: List? = null): List { 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( @@ -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 -> @@ -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( diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/test/RouteTest.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/test/RouteTest.kt index b196eb77..8d0fc985 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/test/RouteTest.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/test/RouteTest.kt @@ -1,5 +1,17 @@ package cn.edu.buaa.scs.utils.test +import cn.edu.buaa.scs.kube.crd.v1alpha1.VirtualMachineSpec +import cn.edu.buaa.scs.model.VirtualMachine +import cn.edu.buaa.scs.model.VirtualMachineExtraInfo +import cn.edu.buaa.scs.utils.logger +import cn.edu.buaa.scs.vm.CreateVmOptions +import cn.edu.buaa.scs.vm.GeneticAlgorithm +import cn.edu.buaa.scs.vm.PhysicalHost +import cn.edu.buaa.scs.vm.vcenter.VCenterClient +import cn.edu.buaa.scs.vm.vcenter.VCenterClient.getVcenterHostsUsage +import cn.edu.buaa.scs.service.VmService +import cn.edu.buaa.scs.vm.sangfor.SangforClient +import cn.edu.buaa.scs.vm.vcenter.VCenterClient.transferHost import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -8,7 +20,103 @@ fun Route.test() { // just for test route("/test") { get { - call.respondText("Hello, world!") + val logger = logger("UNIT TEST")() + logger.info { "......TESTING FUNCTIONS......" } + + + // 👍深信服平台获取资源占用 getHostsUsage +// logger.info {"TEST getSangforHostsUsage()"} +// var usage = getHostsUsage().getOrThrow() +// logger.info {usage.size} +// usage.forEach{ +// logger.info { "This VM: IP=" + it["ip"] + ", ID=" + it["hostId"]} +// logger.info { it["cpu_total_mhz"]+ " " + it["cpu_used_mhz"]+ " " + it["cpu_ratio"] } +// logger.info { it["memory_total_mb"]+ " " + it["memory_used_mb"]+ " " + it["memory_ratio"] } +// } + + // 👍深信服平台迁移虚拟机 +// logger.info {"TEST migrateVmToNewHost()"} +// val vmUuid = "e44a24b3-4c15-4608-ab95-1b16dfb5d880" +// val hostId = "host-e03676dde96c" +// val vmName = "centos-base_克隆" +// val code = migrateVmToNewHost(vmName, vmUuid, hostId) +// logger.info { "code = $code" } + + // 👍深信服平台创建(克隆)并迁移虚拟机到指定主机 +// val option = CreateVmOptions("zmqVM", VirtualMachineExtraInfo(templateUuid="8d907a8a-f34f-4dc1-b755-47b5aec9e98a"), +// 1024, 1, 100000000, 10, false, "host-e03676dde96c") +// val vm = SangforClient.createVM(option).getOrThrow() +// logger.info { vm.name } +// logger.info { vm.uuid } +// logger.info { vm.platform } +// logger.info { vm.host } + + // 👍vCenter平台获取资源占用 +// logger.info {"TEST getVcenterHostsUsage()"} +// val hosts = getVcenterHostsUsage().getOrThrow() +// logger.info { hosts.size } +// hosts.forEach{ +// logger.info { "This host: NAME=" + it.hostId} +// logger.info { it.cpu_total_mhz.toString()+ " " + it.cpu_used_mhz.toString()+ " " + it.cpu_ratio.toString() } +// logger.info { it.memory_total_mb.toString()+ " " + it.memory_used_mb.toString()+ " " + it.memory_ratio.toString() } +// } + + // 👍vCenter平台直接创建(克隆)虚拟机到指定主机 +// val vm = VCenterClient.createVM( +// CreateVmOptions( +// name = "zmqVM-test", +// extraInfo = VirtualMachineExtraInfo(templateUuid="4207e974-8edd-8555-abdc-a664fabf92a3"), +// memory = 2048, +// cpu = 2, +// diskSize = 16106127360, +// powerOn = false, +// hostId = "10.251.254.22" +// ) +// ).getOrThrow() +// logger.info { vm.name } +// logger.info { vm.uuid } +// logger.info { vm.platform } +// logger.info { vm.host } + + + // 👍测试遗传算法 +// val vm1 = VirtualMachineSpec("name1", "sangfor", false, "extraInfo", +// 4, 256, 1, 2048, VirtualMachine.PowerState.PoweredOff) +// val vm2 = VirtualMachineSpec("name2", "sangfor", false, "extraInfo", +// 4, 256, 1, 2048, VirtualMachine.PowerState.PoweredOff) +// val vm3 = VirtualMachineSpec("name2", "vcenter", false, "extraInfo", +// 4, 256, 1, 2048, VirtualMachine.PowerState.PoweredOff) +// val host1 = PhysicalHost("hostid1", 1000, 900, 0.9, +// 2000, 1800, 0.9) +// val host2 = PhysicalHost("hostid2", 1000, 4, 0.1, +// 2000, 256, 0.5) +// val host3 = PhysicalHost("hostid3", 1000, 0, 0.0, +// 2000, 0, 0.0) +// val vmList = listOf(vm1, vm2, vm3) +// val hostList = listOf(host1, host2, host3) +// val ga = GeneticAlgorithm(vmList, hostList, +// 20, 0.2, 0.2, 3, 20) +// ga.evolve() +// ga.getBestSolution().getAllocation().forEach { logger.info {hostList[it].hostId} } +// + + // 测试虚拟机是否创建 +// val vs = VmService() +// vs.getExperimentVms() + + // 测试动态迁移 + logger.info { "Testing hotMigrate VM......" } + val hostR = mapOf( + "hostRefType" to "HostSystem", + "hostRefValue" to "host-180" // 10.251.254.22 + ) +// val vmUuid = "42194af9-4cae-0681-185e-1b7bd7094e4a" // "zmq-test001-99131004" + val vmUuid = "4219fd26-3670-90cd-db1c-06d76ed2b306" + transferHost(vmUuid, hostR) + logger.info { "VM Migrate FINISH." } + + + call.respondText("All Finish!") } } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/PhysicalHost.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/PhysicalHost.kt new file mode 100644 index 00000000..d2bdf569 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/PhysicalHost.kt @@ -0,0 +1,12 @@ +package cn.edu.buaa.scs.vm + +data class PhysicalHost (val hostId: String, + val cpu_total_mhz: Int, + var cpu_used_mhz: Int, + var cpu_ratio: Double, + val memory_total_mb: Int, + var memory_used_mb: Int, + var memory_ratio: Double, + var hostRefType: String?, + var hostRefValue: String?){ +} diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/VMScheduler.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/VMScheduler.kt new file mode 100644 index 00000000..b895408a --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/VMScheduler.kt @@ -0,0 +1,225 @@ +package cn.edu.buaa.scs.vm + +import cn.edu.buaa.scs.controller.models.VirtualMachine +import cn.edu.buaa.scs.kube.crd.v1alpha1.VirtualMachineSpec +import kotlin.random.Random +import kotlin.math.sqrt + +class Chromosome(private val vmList: List, private val hostList: List) { + private var allocation: IntArray // 该染色体的基因(一种分配方案) + private var fitnessValue: Double = 0.0 // 适应度值,表示该染色体的优劣程度 + + init { + // 随机初始化分配方案 + this.allocation = IntArray(vmList.size) { Random.nextInt(hostList.size) } // 分配方案,每个元素表示对应虚拟机分配给哪个主机 + evaluateFitness() // 计算适应度值 + } + + constructor(allocation: IntArray, vmList: List, hostList: List) : this(vmList, hostList) { + this.allocation = allocation + evaluateFitness() + } + + // 交叉操作,返回生成的新染色体 + fun crossWith(other: Chromosome): Chromosome { + val length = allocation.size + val cutPoint = (Math.random() * (length - 1)).toInt() + 1 // 随机选择一个交叉点,保证至少有一个基因片段来自每个染色体 + val newAllocation = allocation.copyOf() // 复制当前染色体的基因 + + for (i in cutPoint until length) { + newAllocation[i] = other.allocation[i] // 将当前染色体的后半部分替换为另一个染色体的后半部分 + } + return Chromosome(newAllocation, vmList, hostList) + } + + // 单点变异操作,返回生成的新染色体 + fun mutate() { + // 随机选择一个虚拟机,并重新随机分配它的主机 + val vmIndex = Random.nextInt(vmList.size) + allocation[vmIndex] = Random.nextInt(hostList.size) + evaluateFitness() + } + + // 计算各主机CPU/内存利用率%:U(i)CPU, U(i)RAM + // 所有主机的CPU/内存利用率均值%:U(avg)CPU = ∑U(i)CPU/n, U(avg)RAM = ∑U(i)RAM/n + // 计算CPU/内存的利用均衡度(样本标准差):D = sqrt (1/n)*∑[(U(i)-U(avg))^2] + // 适应度值 = a*Dcpu + b*Dram + private fun evaluateFitness() { + val n = allocation.size // 总共虚拟机个数 + val hosts = mutableListOf() // 用于统计每个主机的资源利用情况 + val cpuUsages = mutableListOf() // 存储每个主机的 CPU 利用率 + val ramUsages = mutableListOf() // 存储每个主机的内存利用率 + + hostList.forEach { // 建立临时的主机列表用于本次计算 + hosts.add(it.copy()) + } + + // 遍历所有的虚拟机,将其占用资源叠加到对应的物理主机上 + for (i in 0 until n) { + val hostIndex = allocation[i] // host在hostList里的下标 + val vmCpu = vmList[i].cpu + val vmRam = vmList[i].memory +// val vmDisk = vmList[i].diskSize + + // 更新主机的资源占用情况 + hosts[hostIndex].cpu_used_mhz += vmCpu * 10 // vmCpu是虚拟机CPU内核数,只能近似转换为mhz + hosts[hostIndex].memory_used_mb += vmRam +// host.disk += vmDisk + } + + // 计算每个主机的 CPU&内存 利用率 + for (host in hosts) { + cpuUsages.add(host.cpu_used_mhz.toDouble() / host.cpu_total_mhz) + ramUsages.add(host.memory_used_mb.toDouble() / host.memory_total_mb) + } + + // 计算所有主机的CPU/内存利用率均值 + val avgCpuUsage = cpuUsages.average() + val avgRamUsage = ramUsages.average() + + // 计算CPU/内存的利用均衡度(样本标准差) + val cpuStd = sqrt(cpuUsages.sumOf { (it - avgCpuUsage) * (it - avgCpuUsage) } / n) + val ramStd = sqrt(ramUsages.sumOf { (it - avgRamUsage) * (it - avgRamUsage) } / n) + + // 根据给定的参数计算适应度值 + val a = 0.6 + val b = 0.4 + val fitness = 2 - (a * cpuStd + b * ramStd) + this.fitnessValue = fitness + } + + // 获取染色体的分配方案 + fun getAllocation(): IntArray { + return allocation + } + + fun getFitness(): Double { + return fitnessValue + } +} + + +class GeneticAlgorithm(private val vmList: List, private val hostList: List, private val populationSize: Int, + private val mutationRate: Double, private val crossoverRate: Double, private val elitismCount: Int, private val evolveCycles: Int) { + + private var population: MutableList = mutableListOf() + + init { + for (i in 0 until populationSize) { + population.add(Chromosome(vmList, hostList)) + } + } + + fun evolve() { + repeat(evolveCycles) { + var newPopulation: MutableList = mutableListOf() // 下一代种群 + // elitism,选择适应度最大的(精英)生成下一代种群 + for (i in 0 until elitismCount) { + val elite = population.maxByOrNull { it.getFitness() }!! + newPopulation.add(elite) + } + + // crossover,交叉【拷贝突变】 + while (newPopulation.size < populationSize) { + val parent1 = selectParent() + val parent2 = selectParent() + + val child1 = parent1.crossWith(parent2) + val child2 = parent2.crossWith(parent1) + + if (Math.random() < crossoverRate) { // 以某概率执行条件 + newPopulation.add(child1) + } + if (Math.random() < crossoverRate) { + newPopulation.add(child2) + } + } + + // mutation,突变【原地突变】 + for (i in elitismCount until newPopulation.size) { + if (Math.random() < mutationRate) { + newPopulation[i].mutate() + } + } + + // 新种群替换旧种群 + population = newPopulation + } + } + + fun getBestSolution(): Chromosome { + return population.maxByOrNull { it.getFitness() }!! + } + + // 轮盘赌法选择可以遗传下一代的染色体 + private fun selectParent(): Chromosome { + var sum = 0.0 + for (chromosome in population) { + sum += chromosome.getFitness() + } + var rouletteWheelPosition = Math.random() * sum + for (chromosome in population) { + rouletteWheelPosition -= chromosome.getFitness() + if (rouletteWheelPosition <= 0) { + return chromosome + } + } + return population.last() + } +} + +// class Chromosome(var genes: List) { +// +// constructor() : this(List(NUMBER_OF_VIRTUAL_MACHINES) { Random.nextInt(NUMBER_OF_PHYSICAL_MACHINES) }) +// +// constructor(parent1: Chromosome, parent2: Chromosome) { +// val crossoverPoint = Random.nextInt(NUMBER_OF_VIRTUAL_MACHINES) +// genes = parent1.genes.take(crossoverPoint) + parent2.genes.drop(crossoverPoint) +// } +// +// var fitness: Double = 0.0 +// private set +// +// init { +// calculateFitness() +// } +// +// fun mutate() { +// val index = Random.nextInt(NUMBER_OF_VIRTUAL_MACHINES) +// genes = genes.toMutableList().apply { set(index, Random.nextInt(NUMBER_OF_PHYSICAL_MACHINES)) } +// calculateFitness() +// } +// +// fun evaluateFitness() { +// // 获取所有主机的资源利用率 +// val hostUsage = platform.getHostUsage() +// +// // 计算所有CPU、内存、磁盘的平均利用率 +// val cpuAvg = hostUsage.map { it.cpuUsage }.average() +// val memoryAvg = hostUsage.map { it.memoryUsage }.average() +// val diskAvg = hostUsage.map { it.diskUsage }.average() +// +// // 计算CPU、内存、磁盘的利用率方差 +// val cpuVariance = hostUsage.map { it.cpuUsage }.let { usage -> +// usage.map { (it - cpuAvg).pow(2) }.sum() / usage.size +// } +// val memoryVariance = hostUsage.map { it.memoryUsage }.let { usage -> +// usage.map { (it - memoryAvg).pow(2) }.sum() / usage.size +// } +// val diskVariance = hostUsage.map { it.diskUsage }.let { usage -> +// usage.map { (it - diskAvg).pow(2) }.sum() / usage.size +// } +// +// // 计算负载均衡度和节点间资源平衡度的加权平均值 +// val loadBalanceWeight = 0.5 // 负载均衡度权重 +// val resourceBalanceWeight = 0.5 // 资源平衡度权重 +// val loadBalance = 1 - (cpuVariance + memoryVariance + diskVariance) / 3 +// val resourceBalance = (hostUsage.map { abs(it.cpuUsage - cpuAvg) }.sum() + +// hostUsage.map { abs(it.memoryUsage - memoryAvg) }.sum() + +// hostUsage.map { abs(it.diskUsage - diskAvg) }.sum()) / (3 * numHosts) +// val fitness = loadBalanceWeight * loadBalance + resourceBalanceWeight * resourceBalance +// +// this.fitness = fitness +// } +// } +//} diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/sangfor/SangforClient.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/sangfor/SangforClient.kt index 8b555ba5..1ed7df0a 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/sangfor/SangforClient.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/sangfor/SangforClient.kt @@ -13,6 +13,7 @@ import cn.edu.buaa.scs.utils.schedule.waitForDone import cn.edu.buaa.scs.utils.setExpireKey import cn.edu.buaa.scs.vm.CreateVmOptions import cn.edu.buaa.scs.vm.IVMClient +import cn.edu.buaa.scs.vm.PhysicalHost import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* @@ -80,6 +81,29 @@ object SangforClient : IVMClient { return jsonMapper.readTree(body).get("access").get("token").get("id").toString().split('"')[1] } + suspend fun connectAdmin(): String { + val response = client.post("openstack/identity/v2.0/tokens") { + contentType(ContentType.Application.Json) + val username = application.getConfigString("vm.sangfor_admin.username") + val password = application.getConfigString("vm.sangfor_admin.password") + setBody( + """ + { + "auth": { + "tenantName": "$username", + "passwordCredentials": { + "username": "$username", + "password": "$password" + } + } + } + """.trimIndent() + ) + } + val body: String = response.body() + return jsonMapper.readTree(body).get("access").get("token").get("id").toString().split('"')[1] + } + suspend fun fetchTicket(token: String): Token { val resBody: String = client.get("summary") { header("Cookie", "aCMPAuthToken=$token") @@ -107,6 +131,24 @@ object SangforClient : IVMClient { return Token(token, ticket, sid) } + suspend fun getTokenAdmin(): Token { + tokenLock.lock() + var token = authRedis.getValueByKey("sangfor_token_admin") + var ticket = authRedis.getValueByKey("sangfor_ticket_admin") ?: "" + var sid = authRedis.getValueByKey("sangfor_sid_admin") ?: "" + if (token == null) { + token = connectAdmin() + authRedis.setExpireKey("sangfor_token_admin", token, 3500) + val tokenBody = fetchTicket(token) + ticket = tokenBody.ticket + sid = tokenBody.sid + authRedis.setExpireKey("sangfor_ticket_admin", ticket, 4000) + authRedis.setExpireKey("sangfor_sid_admin", sid, 4000) + } + tokenLock.unlock() + return Token(token, ticket, sid) + } + override suspend fun getAllVMs(): Result> { // Get all virtual machines val token = getToken().id @@ -305,6 +347,18 @@ object SangforClient : IVMClient { }.body() jsonMapper.readTree(vmRes)["server"]["OS-EXT-STS:task_state"].toString() == "\"\"" } + // 克隆完毕,接下来迁移到指定的host上去!(sangfor只能先克隆再迁移) + if (options.hostId != "") { + migrateVmToNewHost(options.name, uuid, options.hostId) + // Wait the migration to be done + waitForDone(20000L, 500L) { + token = getToken() + val vmRes: String = client.get("openstack/compute/v2/servers/$uuid") { + header("X-Auth-Token", token.id) + }.body() + jsonMapper.readTree(vmRes)["server"]["hostId"].toString() == options.hostId + } + } createLock.unlock() // Initialize settings of the new virtual machine. token = getToken() @@ -542,6 +596,70 @@ object SangforClient : IVMClient { ) }.status.value } + + suspend fun getSangforHostsUsage(): Result> { + /* + 返回sangfor资源池中每个主机的实时cpu和内存占用情况 + */ + val token = getTokenAdmin() + val vmRes: String = client.get("admin/view/host-list") { + header("Cookie", "aCMPAuthToken=${token.id}") + }.body() + val vmJSON = jsonMapper.readTree(vmRes) + val vmInfos = mutableListOf() + vmJSON["data"].forEach{ + if (it["status"].toString().split('"')[1] == "running") { + val hostId = it["id"].toString().split('"')[1] // 字符串接收到的是“”双重引号“”,需要去除一层 + val ip = it["ip"].toString().split('"')[1] + val cpuTotalMhz = it["cpu"]["total_mhz"].intValue() + val cpuUsedMhz = it["cpu"]["used_mhz"].intValue() + val cpuRatio = it["cpu_ratio"].doubleValue() + val memoryTotalMb = it["memory"]["total_mb"].intValue() + val memoryUsedMb = it["memory"]["used_mb"].intValue() + val memoryRatio = it["memory_ratio"].doubleValue() +// val vmInfo = mapOf( +// "ip" to ip, "hostId" to hostId, +// "cpu_total_mhz" to cpuTotalMhz, "cpu_used_mhz" to cpuUsedMhz, "cpu_ratio" to cpuRatio, +// "memory_total_mb" to memoryTotalMb, "memory_used_mb" to memoryUsedMb, "memory_ratio" to memoryRatio +// ) + val hostInfo = PhysicalHost(hostId, cpuTotalMhz, cpuUsedMhz, cpuRatio, + memoryTotalMb, memoryUsedMb, memoryRatio, hostRefType = "sangforHost", hostRefValue = ip) + vmInfos.add(hostInfo) + } + } + return Result.success(vmInfos) + } + + suspend fun migrateVmToNewHost(vmName: String, + vmUuid: String, + hostId: String + ) { + val token = getTokenAdmin() + val resCode = client.post("admin/servers/$vmUuid/migrate-server") { + contentType(ContentType.Application.Json) + header("Cookie", "aCMPAuthToken=${token.id}") + header("CSRFPreventionToken", token.ticket) + header("sid", token.sid) + setBody( + """ + { + "server_id": "$vmUuid", + "migrate_server_info": { + "name": "$vmName", + "cluster_id": "e4c588df-48c4-4f48-9d94-011e36c82c73", + "cluster_type": "hci", + "storage_location": "3600d0231000859694803abfa3b686284", + "compute_location": { + "id": "$hostId" + }, + "auto_shutdown": 1, + "auto_poweron": 0 + } + } + """.trimIndent() + ) + }.status.value + } } data class Token( diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt index 2b14dfd0..2dac7f34 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt @@ -1,6 +1,5 @@ package cn.edu.buaa.scs.vm.vcenter -import cn.edu.buaa.scs.config.globalConfig import cn.edu.buaa.scs.error.NotFoundException import cn.edu.buaa.scs.model.VirtualMachine import cn.edu.buaa.scs.utils.HttpClientWrapper @@ -32,7 +31,8 @@ object VCenterClient : IVMClient { requestTimeoutMillis = 100000000L } }, - basePath = globalConfig.vcenter.serviceUrl +// basePath = globalConfig.vcenter.serviceUrl + basePath = "http://localhost:9977/api/v2/vcenter" ) } @@ -74,6 +74,10 @@ object VCenterClient : IVMClient { client.post("/vm/$uuid/powerOff") } + suspend fun transferHost(uuid: String, hostRefDict: Map) { + client.post("/vm/$uuid/transHost", hostRefDict) + } + override suspend fun configVM( uuid: String, experimentId: Int?, @@ -112,4 +116,7 @@ object VCenterClient : IVMClient { getVM(uuid).getOrThrow() } + suspend fun getVcenterHostsUsage(): Result> = runCatching { + client.get>("/hosts").getOrThrow() + } } diff --git a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/PhysicalHost.kt b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/PhysicalHost.kt new file mode 100644 index 00000000..043bd0f2 --- /dev/null +++ b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/PhysicalHost.kt @@ -0,0 +1,16 @@ +package cn.edu.buaa.scs.vcenter + +import com.vmware.vim25.ManagedObjectReference + +data class PhysicalHost (val hostId: String, + val cpu_total_mhz: Int, + var cpu_used_mhz: Int, + var cpu_ratio: Double, + val memory_total_mb: Int, + var memory_used_mb: Int, + var memory_ratio: Double, + var hostRefType: String, + var hostRefValue: String + ){ + +} diff --git a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt index 0f02fcc5..861ef4cf 100644 --- a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt +++ b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt @@ -1,6 +1,7 @@ package cn.edu.buaa.scs.vcenter import cn.edu.buaa.scs.vm.ConfigVmOptions +import com.vmware.vim25.ManagedObjectReference import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* @@ -53,7 +54,20 @@ fun Application.vcenterRouting() { VCenterWrapper.convertVMToTemplate(call.getVmUuid()).getOrThrow() call.respond("OK") } + + post("/transHost") { + val hostRefDict = call.receive>() + VCenterWrapper.transferHost(call.getVmUuid(), hostRefDict).getOrThrow() + call.respond("OK") + } + } + + route("/hosts") { + get() { + call.respond(VCenterWrapper.getAllHostsUsage().getOrThrow()) + } } + } } } diff --git a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt index 8e4d60f6..e960d9dd 100644 --- a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt +++ b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt @@ -198,6 +198,7 @@ object VCenterWrapper { suspend fun create(options: CreateVmOptions): Result { return baseSyncTask { connection -> val task = clone( + // TODO: TASK-VCENTER - 修改clone的过程,指定host主机 connection, options.name, options.extraInfo.templateUuid, @@ -206,6 +207,7 @@ object VCenterWrapper { options.memory, options.diskSize, options.powerOn, + options.hostId ) waitForTaskResult(connection, task).getOrThrow() getAllVmsFromVCenter(connection).find { vm -> @@ -214,6 +216,11 @@ object VCenterWrapper { } } + /* + 通过查询主机列表和数据存储的空闲空间等信息,找到一个合适的主机作为克隆的目标位置, + 并配置虚拟机的元信息和位置信息, + 最终调用vimPort.cloneVMTask来创建并克隆虚拟机。 + */ private fun clone( connection: Connection, name: String, @@ -223,6 +230,7 @@ object VCenterWrapper { memoryMb: Int, diskSizeBytes: Long, powerOn: Boolean, + hostId: String, ): ManagedObjectReference { val datacenterRef = connection.getDatacenterRef() val getMoRef = connection.getMoRef() @@ -239,20 +247,46 @@ object VCenterWrapper { arrayOf("datastore", "summary", "name"), RetrieveOptions() ) + + val logger = logger("clone-vm-to-host")() + logger.info { "try to clone a vm from template to a host......" } + logger.info { "We have ${hostList.size} hosts in total......" } + + // TODO: 这一段原本是找到第一个磁盘空间够用的主机分配给虚拟机 var host: ManagedObjectReference? = null var datastore: ManagedObjectReference? = null + var datastoreSummaryFinal: DatastoreSummary? = null val diskRequired = Long.MIN_VALUE + var maxFreeSpace: Long = 0 hostList.forEach { (hostRef, hostProps) -> val hostSummary = hostProps["summary"]!! as HostListSummary - if (hostSummary.runtime.connectionState === HostSystemConnectionState.CONNECTED) { + val hostConfig = hostSummary.config + val hostName = hostConfig.name + if (hostId == "" || hostId == hostName) { +/* +HostSystem -> Datastore : A collection of references to the subset of datastore objects in the datacenter that are available in this HostSystem. +每个主机都连接了若干datastore,可以把VM创建在任意连接的datastore上,同时每个datastore可以同时被许多主机连接。 +Represents a storage location for virtual machine files. A storage location can be a VMFS volume, a directory on Network Attached Storage, or a local file system path. +A datastore is platform-independent and host-independent. Therefore, datastores do not change when the virtual machines they contain are moved between hosts. The scope of a datastore is a datacenter; the datastore is uniquely named within the datacenter. +Datastores are configured per host. As part of host configuration, a HostSystem can be configured to mount a set of network drives. Multiple hosts may be configured to point to the same storage location. There exists only one Datastore object per Datacenter, for each such shared location. Each Datastore object keeps a reference to the set of hosts that have mounted the datastore. A Datastore object can be removed only if no hosts currently have the datastore mounted. +Thus, managing datastores is done both at the host level and the datacenter level. Each host is configured explicitly with the set of datastores it can access. At the datacenter, a view of the datastores across the datacenter is shown. + */ + // TODO: 分配给最空闲的datastore! val dataStores = (hostProps["datastore"] as ArrayOfManagedObjectReference?)!!.managedObjectReference + logger.info { "This host has ${dataStores.size} dataStores, let's find which is okay......" } for (ds in dataStores) { val datastoreSummary = getMoRef.entityProps(ds, "summary")["summary"]!! as DatastoreSummary if (datastoreSummary.isAccessible && datastoreSummary.freeSpace / (1024 * 1024) > 1024 * 1024 && datastoreSummary.freeSpace > diskRequired) { - host = hostRef - datastore = datastoreSummary.datastore + if (datastoreSummary.freeSpace > maxFreeSpace) { + maxFreeSpace = datastoreSummary.freeSpace + datastore = datastoreSummary.datastore + datastoreSummaryFinal = datastoreSummary + host = hostRef + logger.info { "Found a better datastore, vm requires ${diskRequired}, this ds has ${datastoreSummary.freeSpace}" } + } } } + logger.info( "Host [${hostName}]'s datastore has connected to [${datastoreSummaryFinal!!.name}], which free space is [${datastoreSummaryFinal!!.freeSpace}]." ) } } if (host == null) { @@ -288,10 +322,11 @@ object VCenterWrapper { } configSpec.annotation = vmExtraInfo.toJson() val cloneSpec = VirtualMachineCloneSpec() - cloneSpec.location = relocateSpec + cloneSpec.location = relocateSpec // 指定host和datastore的位置信息 cloneSpec.isPowerOn = powerOn cloneSpec.isTemplate = false cloneSpec.config = configSpec + logger.info { "Success config! We're creating the VM!" } return vimPort.cloneVMTask(templateRef, connection.getCreateVmSubFolder(), name, cloneSpec) } @@ -307,7 +342,6 @@ object VCenterWrapper { baseSyncTask { connection -> val vmRef = connection.getVmRefByUuid(uuid) val vmProps = connection.getMoRef().entityProps(vmRef, "summary", "config.hardware.device", "guest.net") - val vmSummary = vmProps["summary"]!! as VirtualMachineSummary val hostRef = vmSummary.runtime.host val hostProps = connection.getMoRef().entityProps(hostRef, "name") @@ -354,6 +388,80 @@ object VCenterWrapper { } } } + + fun getAllHostsFromVCenter(connection: Connection): List? { + val logger = logger("get-all-hosts")() + logger.info { "try to fetch host list from vcenter......" } + val datacenterRef = connection.getDatacenterRef() + val getMoRef = connection.getMoRef() + val hostList = + getMoRef.inContainerByType(datacenterRef, + "HostSystem", + arrayOf("name", "summary"), + RetrieveOptions() + ) + val finalHostUsageList = mutableListOf() + hostList.forEach { (hostRef, hostProps) -> + try { + val hostName = hostProps["name"]!! as String // IP addr + val summary = hostProps["summary"] as HostListSummary + if (summary.runtime.connectionState === HostSystemConnectionState.CONNECTED) { + val cpuUsage = summary.quickStats.overallCpuUsage as Int // in MHz + val cpuMhz = summary.hardware.cpuMhz + val numCpuCores = summary.hardware.numCpuCores + val cpuTotalMhz = cpuMhz * numCpuCores + val cpuUsagePercent = cpuUsage.toDouble() / cpuTotalMhz + + val memoryUsage = summary.quickStats.overallMemoryUsage as Int // in MB + val totalMemoryMB = (summary.hardware.memorySize / 1000000).toInt() // memorySize is in bytes + val memoryUsagePercent = memoryUsage.toDouble() / totalMemoryMB + finalHostUsageList.add( + PhysicalHost( + hostName, cpuTotalMhz, cpuUsage, cpuUsagePercent, + totalMemoryMB, memoryUsage, memoryUsagePercent, + hostRefType=hostRef.type, hostRefValue=hostRef.value + ) + ) + } + } catch (e: Throwable) { + logger.error { e.stackTraceToString() } + } + } + logger.info { "done:) fetch host list from vcenter" } + // TODO 临时跳过10.251.254.23 + finalHostUsageList.removeIf { it.hostId == "10.251.254.23" } + return finalHostUsageList.toList() + } + fun getAllHostsUsage(): Result> { + if (getAllVmsConnection == null) { + getAllVmsConnection = vcenterConnect() + } + var hosts: List? = null + try { + hosts = getAllHostsFromVCenter(getAllVmsConnection!!) + } catch (_: Throwable) { + getAllVmsConnection = vcenterConnect() + try { + getAllVmsConnection = vcenterConnect() + hosts = getAllHostsFromVCenter(getAllVmsConnection!!) + } catch (e: Throwable) { + logger("vm-worker-$getAllVmsConnection")().error { e.stackTraceToString() } + } + } + return Result.success(hosts ?: listOf()) + } + + suspend fun transferHost(uuid: String, hostRefDict: Map): Result { + val reloSpec = VirtualMachineRelocateSpec() + val hostRef = ManagedObjectReference() + hostRef.type = hostRefDict["hostRefType"] + hostRef.value = hostRefDict["hostRefValue"] + reloSpec.host = hostRef + return baseSyncTask { connection -> + val task = connection.vimPort.relocateVMTask(connection.getVmRefByUuid(uuid), reloSpec, VirtualMachineMovePriority.DEFAULT_PRIORITY) + waitForTaskResult(connection, task).getOrThrow() + } + } } internal fun convertVMModel( @@ -409,4 +517,4 @@ internal fun convertVMModel( vm.applyExtraInfo(extraInfo) return vm -} +} \ No newline at end of file