diff --git a/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/Host.kt b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/Host.kt new file mode 100644 index 0000000..103887f --- /dev/null +++ b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/Host.kt @@ -0,0 +1,13 @@ +package cn.edu.buaa.scs.model + +data class Host( + val ip: String, + val status: String, + val totalMem: Double, + val usedMem: Double, + val totalCPU: Double, + val usedCPU: Double, + val totalStorage: Long, + val usedStorage: Long, + val count: Int, +) diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt index b80d3d8..11aec03 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt @@ -14,13 +14,20 @@ package cn.edu.buaa.scs.controller.models /** * - * @param id + * @param id 学工号 + * @param departmentId 所在单位 * @param role 1 for student, 2 for teacher, 4 for admin + * @param name 姓名 */ data class CreateUserRequest( + /* 学工号 */ val id: kotlin.String, + /* 所在单位 */ + val departmentId: kotlin.Int, /* 1 for student, 2 for teacher, 4 for admin */ - val role: CreateUserRequest.Role + val role: CreateUserRequest.Role, + /* 姓名 */ + val name: kotlin.String? = null ) { /** diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/DepartmentModel.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/DepartmentModel.kt new file mode 100644 index 0000000..32013c9 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/DepartmentModel.kt @@ -0,0 +1,24 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param id + * @param name + */ +data class DepartmentModel( + val id: kotlin.String, + val name: kotlin.String +) + diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/Host.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/Host.kt new file mode 100644 index 0000000..6c82b32 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/Host.kt @@ -0,0 +1,38 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param ip + * @param status + * @param totalMem + * @param usedMem + * @param totalCPU + * @param usedCPU + * @param totalStorage + * @param usedStorage + * @param count + */ +data class Host( + val ip: kotlin.String, + val status: kotlin.String, + val totalMem: kotlin.Double, + val usedMem: kotlin.Double, + val totalCPU: kotlin.Double, + val usedCPU: kotlin.Double, + val totalStorage: kotlin.Long, + val usedStorage: kotlin.Long, + val count: kotlin.Int +) + diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TermModel.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TermModel.kt index 821399e..7ff3022 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TermModel.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TermModel.kt @@ -16,9 +16,13 @@ package cn.edu.buaa.scs.controller.models * * @param id * @param name + * @param courseCount + * @param expCount */ data class TermModel( val id: kotlin.Int? = null, - val name: kotlin.String? = null + val name: kotlin.String? = null, + val courseCount: kotlin.Int? = null, + val expCount: kotlin.Int? = null ) diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TermsGetRequest.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TermsGetRequest.kt new file mode 100644 index 0000000..a83ab27 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TermsGetRequest.kt @@ -0,0 +1,23 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param name 学期名称 + */ +data class TermsGetRequest( + /* 学期名称 */ + val name: kotlin.String? = null +) + diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt index d975bfa..3aac6c3 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt @@ -13,12 +13,13 @@ import io.ktor.server.routing.* fun Route.adminRoute() { route("/admin") { - route("/user") { post { val req = call.receive() val role = UserRole.fromLevel(req.role.value) - call.respond(convertUserModel(call.admin.addUser(req.id, role))) + val name = req.name + val departmentId = req.departmentId + call.respond(convertUserModel(call.admin.addUser(req.id, name, role, departmentId))) } } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Term.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Term.kt index 78081d1..31c59fb 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Term.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Term.kt @@ -1,21 +1,38 @@ package cn.edu.buaa.scs.route +import cn.edu.buaa.scs.controller.models.CreateUserRequest import cn.edu.buaa.scs.controller.models.TermModel +import cn.edu.buaa.scs.error.BadRequestException import cn.edu.buaa.scs.model.Term +import cn.edu.buaa.scs.model.UserRole +import cn.edu.buaa.scs.service.admin import cn.edu.buaa.scs.service.term import io.ktor.server.application.* +import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* fun Route.termRoute() { route("/terms") { get { - call.respond(call.term.getAllTerms().map { convertTermModel(it) }) + call.respond(call.term.getAllTerms()) } get("/latest") { call.respond(convertTermModel(call.term.getLatestTerm())) } + + post { + val req = call.receive() + call.term.createTerm(req.name ?: "") + call.respond("OK") + } + + delete("/{id}") { + val id = call.parameters["id"] ?: throw BadRequestException("学期 ID 不得为空") + call.term.deleteTerm(id.toInt()) + call.respond("OK") + } } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/User.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/User.kt index 0a7f766..22eeb19 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/User.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/User.kt @@ -25,7 +25,8 @@ fun Route.userRoute() { route("/stuffs") { get { val search = call.parameters["search"] - call.respond(call.userService.getTeachersAndStudents(search).map { convertUserModel(it) }) + val limit = call.parameters["limit"]?.toIntOrNull() ?: 10 + call.respond(call.userService.getTeachersAndStudents(search, limit).map { convertUserModel(it) }) } } @@ -54,6 +55,12 @@ fun Route.userRoute() { } } + + route("/departments") { + get { + call.respond(call.userService.getAllDepartments()) + } + } } internal fun convertUserModel(user: User): UserModel { diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt index 5663b2f..c9441e9 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt @@ -79,6 +79,12 @@ fun Route.vmRoute() { } } + route("/hosts") { + get { + call.respond(call.vm.getHosts()) + } + } + route("/vms") { get { call.respond(call.vm.adminGetAllVms().map { convertVirtualMachineResponse(it) }) diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt index 3590d0e..5c85ddc 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt @@ -12,11 +12,11 @@ val ApplicationCall.admin: AdminService class AdminService(val call: ApplicationCall) : IService { companion object : IService.Caller() - fun addUser(id: String, role: UserRole): User { + fun addUser(id: String, name: String?, role: UserRole, departmentId: Int): User { if (!call.user().isAdmin()) { throw AuthorizationException("only admin can add user") } - return User.createNewUnActiveUser(id, null, role) + return User.createNewUnActiveUser(id, name, role, departmentId) } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt index ca0f517..5fb431d 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt @@ -180,7 +180,7 @@ class CourseService(val call: ApplicationCall) : IService { // make sure students exist studentIdList.forEachAsync { studentId -> if (!mysql.users.exists { it.id.inList(studentId.lowerUpperNormal()) }) { - User.createNewUnActiveUser(studentId, null, UserRole.STUDENT) + User.createNewUnActiveUser(studentId, null, UserRole.STUDENT, 0) } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Term.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Term.kt index fe37413..8de410e 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Term.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Term.kt @@ -1,15 +1,16 @@ package cn.edu.buaa.scs.service +import cn.edu.buaa.scs.controller.models.TermModel import cn.edu.buaa.scs.error.BusinessException +import cn.edu.buaa.scs.model.Courses +import cn.edu.buaa.scs.model.Experiments import cn.edu.buaa.scs.model.Term import cn.edu.buaa.scs.model.terms import cn.edu.buaa.scs.storage.mysql import io.ktor.server.application.* -import org.ktorm.dsl.eq -import org.ktorm.entity.find -import org.ktorm.entity.first -import org.ktorm.entity.sortedByDescending -import org.ktorm.entity.toList +import org.ktorm.database.asIterable +import org.ktorm.dsl.* +import org.ktorm.entity.* fun Term.Companion.id(id: Int): Term = mysql.terms.find { it.id eq id } ?: throw BusinessException("find term($id) from mysql error") @@ -20,11 +21,43 @@ val ApplicationCall.term class TermService(val call: ApplicationCall) : IService { companion object : IService.Caller() - fun getAllTerms(): List { - return mysql.terms.toList().sortedByDescending { it.id } + fun getAllTerms(): List { + val terms = mysql.useConnection { conn -> + val sql = """ + select term.id, term.name, count(1), sum(c.exp_cnt) + from term left join ( + select c.id, c.term_id, count(1) exp_cnt + from course c left join experiment e on c.id = e.course_id + group by c.id + ) c on c.term_id=term.id + group by term.id + order by term.id desc + """.trimIndent() + conn.prepareStatement(sql).use { statement -> + statement.executeQuery().asIterable().map { TermModel( + id = it.getInt(1), + name = it.getString(2), + courseCount = it.getInt(3), + expCount = it.getInt(4), + ) } + } + } + return terms } fun getLatestTerm(): Term { return mysql.terms.sortedByDescending { it.id }.first() } + + fun createTerm(name: String) { + val term = Term { + this.name = name + } + mysql.terms.add(term) + } + + fun deleteTerm(id: Int) { + val term = mysql.terms.find { it.id.eq(id) } ?: return + term.delete() + } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt index c770b54..984e791 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt @@ -1,6 +1,7 @@ package cn.edu.buaa.scs.service import cn.edu.buaa.scs.controller.models.AssistantModel +import cn.edu.buaa.scs.controller.models.DepartmentModel import cn.edu.buaa.scs.controller.models.PatchUserRequest import cn.edu.buaa.scs.error.AuthorizationException import cn.edu.buaa.scs.error.BadRequestException @@ -22,11 +23,12 @@ fun User.Companion.getUerListByIdList(idList: List): List { else mysql.users.filter { it.id.inList(idList) }.toList() } -fun User.Companion.createNewUnActiveUser(id: String, name: String?, role: UserRole): User { +fun User.Companion.createNewUnActiveUser(id: String, name: String?, role: UserRole, departmentId: Int): User { val user = User { this.id = id this.name = name ?: "未激活用户" this.role = role + this.departmentId = departmentId } mysql.users.add(user) return user @@ -58,18 +60,30 @@ class UserService(val call: ApplicationCall) : IService { fun getTeachersAndStudents(search: String?, limit: Int = 10): List { if (call.user().isStudent()) return listOf() - if (search.isNullOrBlank()) return listOf() - - var query = mysql.users - .filter { - (it.id.like("%$search%").or(it.name.like("%$search%"))) - .and((it.role eq UserRole.STUDENT).or(it.role eq UserRole.TEACHER)) + val ret: List + if (search.isNullOrBlank()) { + var query = mysql.users + .filter { + (it.role eq UserRole.STUDENT).or(it.role eq UserRole.TEACHER) + } + .sortedBy { it.id } + if (limit != -1) { + query = query.take(limit) } - .sortedBy { it.id } - if (limit != -1) { - query = query.take(limit) + ret = query.toList() + } else { + var query = mysql.users + .filter { + (it.id.like("%$search%").or(it.name.like("%$search%"))) + .and((it.role eq UserRole.STUDENT).or(it.role eq UserRole.TEACHER)) + } + .sortedBy { it.id } + if (limit != -1) { + query = query.take(limit) + } + ret = query.toList() } - return query.toList() + return ret } fun patchUser(userId: String, req: PatchUserRequest) { @@ -97,7 +111,8 @@ class UserService(val call: ApplicationCall) : IService { } val user = User.id(userId) - if (user.password != old) { + // 如果是管理员修改密码,无需检查旧密码 + if (!call.user().isAdmin() && user.password != old) { throw BadRequestException("旧密码错误") } user.password = new @@ -132,4 +147,13 @@ class UserService(val call: ApplicationCall) : IService { .flatten() .sortedByDescending { it.rawId } } + + fun getAllDepartments(): List { + return mysql.departments.map { department -> + DepartmentModel( + id = department.id, + name = department.name, + ) + } + } } 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 dead026..aa766bf 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 @@ -50,6 +50,10 @@ class VmService(val call: ApplicationCall) : IService { vmKubeClient.resource(vm).patch() } + suspend fun getHosts(): List { + return sfClient.getHosts().getOrThrow() + } + fun getPersonalVms(): List { val vmApplyList = mysql.vmApplyList.filter { ((it.studentId eq call.userId()) or (it.teacherId eq call.userId())) and (it.experimentId eq 0) } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/IVMClient.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/IVMClient.kt index d21c73c..76d7fc8 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/IVMClient.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/IVMClient.kt @@ -1,10 +1,13 @@ package cn.edu.buaa.scs.vm +import cn.edu.buaa.scs.model.Host import cn.edu.buaa.scs.model.VirtualMachine import cn.edu.buaa.scs.vm.sangfor.SangforClient import cn.edu.buaa.scs.vm.vcenter.VCenterClient interface IVMClient { + suspend fun getHosts(): Result> + suspend fun getAllVMs(): Result> suspend fun getVM(uuid: String): Result 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 8b555ba..f7415e1 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 @@ -4,6 +4,7 @@ import cn.edu.buaa.scs.application import cn.edu.buaa.scs.cache.authRedis import cn.edu.buaa.scs.error.NotFoundException +import cn.edu.buaa.scs.model.Host import cn.edu.buaa.scs.model.VirtualMachine import cn.edu.buaa.scs.model.applySangforExtraInfo import cn.edu.buaa.scs.utils.getConfigString @@ -32,6 +33,7 @@ import javax.net.ssl.X509TrustManager object SangforClient : IVMClient { val username = application.getConfigString("vm.sangfor.username") val password = application.getConfigString("vm.sangfor.password") + val adminPassword = application.getConfigString("vm.sangfor.adminPassword") private val tokenLock = Mutex() private val createLock = Mutex() @@ -59,16 +61,16 @@ object SangforClient : IVMClient { } } - suspend fun connect(): String { + private suspend fun connect(user: String, password: String): String { val response = client.post("openstack/identity/v2.0/tokens") { contentType(ContentType.Application.Json) setBody( """ { "auth": { - "tenantName": "$username", + "tenantName": "$user", "passwordCredentials": { - "username": "$username", + "username": "$user", "password": "$password" } } @@ -80,7 +82,7 @@ object SangforClient : IVMClient { return jsonMapper.readTree(body).get("access").get("token").get("id").toString().split('"')[1] } - suspend fun fetchTicket(token: String): Token { + private suspend fun fetchTicket(token: String): Token { val resBody: String = client.get("summary") { header("Cookie", "aCMPAuthToken=$token") }.body() @@ -89,13 +91,13 @@ object SangforClient : IVMClient { return Token(token, ticket, sid) } - suspend fun getToken(): Token { + private suspend fun getToken(): Token { tokenLock.lock() var token = authRedis.getValueByKey("sangfor_token") var ticket = authRedis.getValueByKey("sangfor_ticket") ?: "" var sid = authRedis.getValueByKey("sangfor_sid") ?: "" if (token == null) { - token = connect() + token = connect(username, password) authRedis.setExpireKey("sangfor_token", token, 3500) val tokenBody = fetchTicket(token) ticket = tokenBody.ticket @@ -107,6 +109,59 @@ object SangforClient : IVMClient { return Token(token, ticket, sid) } + suspend fun getAdminToken(): Token { + tokenLock.lock() + var token = authRedis.getValueByKey("sangfor_admin_token") + var ticket = authRedis.getValueByKey("sangfor_admin_ticket") ?: "" + var sid = authRedis.getValueByKey("sangfor_admin_sid") ?: "" + if (token == null) { + token = connect("admin", adminPassword) + authRedis.setExpireKey("sangfor_admin_token", token, 3500) + val tokenBody = fetchTicket(token) + ticket = tokenBody.ticket + sid = tokenBody.sid + authRedis.setExpireKey("sangfor_admin_ticket", ticket, 4000) + authRedis.setExpireKey("sangfor_admin_sid", sid, 4000) + } + tokenLock.unlock() + return Token(token, ticket, sid) + } + + override suspend fun getHosts(): Result> { + val token = getAdminToken().id + val clusterRes: String = client.get("admin/view/cluster-list") { + header("Cookie", "aCMPAuthToken=${token}") + }.body() + val clusters = jsonMapper.readTree(clusterRes)["data"] + val hostList = mutableListOf() + for (cluster in clusters) { + val cid = cluster["id"].toString().split('"')[1] + val hostRes: String = client.get("admin/view/host-list?cluster_id=$cid") { + header("Cookie", "aCMPAuthToken=${token}") + }.body() + val hosts = jsonMapper.readTree(hostRes)["data"] + for (hostJSON in hosts) { + val host = Host( + ip = hostJSON["ip"].toString().split('"')[1], + status = hostJSON["status"].toString().split('"')[1], + totalMem = hostJSON["memory"]["total_mb"].doubleValue(), + usedMem = hostJSON["memory"]["used_mb"].doubleValue(), + totalCPU = hostJSON["cpu"]["total_mhz"].doubleValue(), + usedCPU = hostJSON["cpu"]["used_mhz"].doubleValue(), + totalStorage = if (hostJSON["disks"].size() != 0) { + hostJSON["disks"].map { + it["disk_size_byte"].longValue() + }.reduce { s, s1 -> s + s1 } + } else { 0 }, + usedStorage = 0L, + count = hostJSON["count"].intValue(), + ) + hostList.add(host) + } + } + return Result.success(hostList) + } + override suspend fun getAllVMs(): Result> { // Get all virtual machines val token = getToken().id 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 ef7527d..34acb48 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 @@ -2,6 +2,7 @@ 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.Host import cn.edu.buaa.scs.model.VirtualMachine import cn.edu.buaa.scs.utils.HttpClientWrapper import cn.edu.buaa.scs.utils.schedule.waitForDone @@ -38,6 +39,10 @@ object VCenterClient : IVMClient { private fun vmNotFound(uuid: String): NotFoundException = NotFoundException("virtualMachine($uuid) not found") + override suspend fun getHosts(): Result> { + TODO("Not yet implemented") + } + override suspend fun getAllVMs(): Result> = runCatching { client.get>("/vms").getOrThrow() } diff --git a/openapi/cloudapi_v2.yaml b/openapi/cloudapi_v2.yaml index 73aef4c..101e1ed 100644 --- a/openapi/cloudapi_v2.yaml +++ b/openapi/cloudapi_v2.yaml @@ -1291,6 +1291,24 @@ paths: security: - Authorization: [] parameters: [] + /hosts: + get: + summary: 获取全部物理主机 + tags: + - 虚拟机 + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Host' + operationId: get-hosts + description: 获取全部物理主机 + security: + - Authorization: [] /paasUser: post: summary: 在各个PaaS相关后端中创建用户 @@ -1868,6 +1886,24 @@ paths: in: query name: limit description: '-1表示无限制。默认为10' + /departments: + get: + summary: 获取单位列表 + tags: + - 人员 + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DepartmentModel' + operationId: get-departments + description: 获取单位列表 + security: + - Authorization: [ ] /resourcePools: parameters: [] get: @@ -2545,6 +2581,38 @@ paths: description: 获取所有学期 security: - Authorization: [] + post: + summary: 添加学期 + tags: + - 学期 + description: 添加新的学期 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: 学期名称 + responses: + '200': + description: 添加学期成功 + '/terms/{id}': + delete: + summary: 删除学期 + description: 删除特定学期 + parameters: + - name: id + in: path + description: 要删除的学期 ID + required: true + schema: + type: string + responses: + '200': + description: 学期删除成功 '/course/{courseId}/assistants': parameters: - schema: @@ -3082,6 +3150,17 @@ components: - email - role - departmentName + DepartmentModel: + title: DepartmentModel + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name TermModel: title: TermModel type: object @@ -3091,6 +3170,12 @@ components: format: int32 name: type: string + courseCount: + type: integer + format: int32 + expCount: + type: integer + format: int32 ExpVmInfo: title: ExpVmInfo type: object @@ -3524,6 +3609,45 @@ components: - diskNum - diskSize - overallStatus + Host: + title: Host + type: object + properties: + ip: + type: string + status: + type: string + totalMem: + type: number + format: double + usedMem: + type: number + format: double + totalCPU: + type: number + format: double + usedCPU: + type: number + format: double + totalStorage: + type: integer + format: int64 + usedStorage: + type: integer + format: int64 + count: + type: integer + format: int32 + required: + - ip + - status + - totalMem + - usedMem + - totalCPU + - usedCPU + - totalStorage + - usedStorage + - count StatExpAssignmentResponseAssignments: title: StatExpAssignmentResponseAssignments type: object @@ -4517,6 +4641,14 @@ components: properties: id: type: string + description: '学工号' + name: + type: string + description: '姓名' + departmentId: + type: integer + format: int32 + description: '所在单位' role: type: integer format: int32 @@ -4528,6 +4660,7 @@ components: required: - id - role + - departmentId ActiveUserRequest: title: ActiveUserRequest x-stoplight: 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 b609725..fb6f4b2 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 @@ -21,6 +21,12 @@ fun Application.vcenterRouting() { } + route("/hosts") { + get { + call.respond(VCenterWrapper.getHosts()) + } + } + route("/vm/{uuid}") { fun ApplicationCall.getVmUuid() = parameters["uuid"]!! 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 a3269ec..cdeef06 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 @@ -8,6 +8,7 @@ import cn.edu.buaa.scs.utils.jsonMapper import cn.edu.buaa.scs.utils.logger import cn.edu.buaa.scs.vm.ConfigVmOptions import cn.edu.buaa.scs.vm.CreateVmOptions +import cn.edu.buaa.scs.model.Host import com.fasterxml.jackson.module.kotlin.readValue import com.vmware.photon.controller.model.adapters.vsphere.util.connection.BasicConnection import com.vmware.photon.controller.model.adapters.vsphere.util.connection.Connection @@ -118,6 +119,41 @@ object VCenterWrapper { } } + suspend fun getHosts(): List { + return baseSyncTask { connection -> + val datacenterRef = connection.getDatacenterRef() + val getMoRef = connection.getMoRef() + val hostList = + getMoRef.inContainerByType(datacenterRef, "HostSystem", arrayOf("name", "summary", "datastore", "vm"), RetrieveOptions()) + hostList.map { (hostRef, hostProps) -> + val hostSummary = hostProps["summary"]!! as HostListSummary + val hostHardwareSummary = hostSummary.hardware + var usedMem = 0.0 + var usedCPU = 0.0 + if (hostSummary.runtime.connectionState === HostSystemConnectionState.CONNECTED) { + usedMem = 1.0 * hostSummary.quickStats.overallMemoryUsage + usedCPU = 1.0 * hostSummary.quickStats.overallCpuUsage + } + val dataStores = (hostProps["datastore"] as ArrayOfManagedObjectReference?)!!.managedObjectReference + val (usedStorage, totalStorage) = dataStores.map { getMoRef.entityProps(it, "summary")["summary"]!! as DatastoreSummary } + .filter { it.isAccessible } + .map { Pair(it.capacity - it.freeSpace, it.capacity) } + .reduceOrNull { (a, b), (a1, b1) -> Pair(a + a1, b + b1) } ?: Pair(0L, 0L) + Host( + ip = hostProps["name"]!! as String, + status = hostSummary.runtime.connectionState.value(), + totalMem = 1.0 * hostHardwareSummary.memorySize, + usedMem = usedMem, + totalCPU = 1.0 * hostHardwareSummary.cpuMhz * hostHardwareSummary.numCpuCores, + usedCPU = usedCPU, + totalStorage = totalStorage, + usedStorage = usedStorage, + count = (hostProps["vm"]!! as ArrayOfManagedObjectReference).managedObjectReference.size, + ) + } + }.getOrThrow() + } + suspend fun configVM( uuid: String, opt: ConfigVmOptions,