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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ DB_HOST=
REDIS_HOST=
CORES=
GUACAMOLE_BASE_URL=
CMS_HOST=
CMS_HOST=
CPU_OVERCOMMIT=
MEM_RESERVE_PCT=
DISK_RESERVE_PCT=
68 changes: 5 additions & 63 deletions api/create_vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,16 @@ import (
"errors"
"net/http"

"github.com/easy-cloud-Knet/KWS_Control/client/model"
"github.com/easy-cloud-Knet/KWS_Control/service"
"github.com/easy-cloud-Knet/KWS_Control/structure"
"github.com/easy-cloud-Knet/KWS_Control/util"
)

// ApiCreateVmRequest for POST /vm HTTP Request Body contract.
type ApiCreateVmRequest struct {
DomType string `json:"domType"`
DomName string `json:"domName"`
UUID structure.UUID `json:"uuid"`
OS string `json:"os"`
HWInfo ApiHardwareInfo `json:"HWInfo"`
Network ApiNetworkInfo `json:"network"`
Users []ApiUserInfo `json:"users"`
SubnetType string `json:"Subnettype"`
}

type ApiHardwareInfo struct {
CPU uint32 `json:"cpu"`
Memory uint32 `json:"memory"` // MiB
Disk uint32 `json:"disk"` // MiB
}

type ApiNetworkInfo struct {
IPs []string `json:"ips"`
// NetType은 내부에서 0 고정 — API 클라이언트가 전송하더라도 무시됨
}

type ApiUserInfo struct {
Name string `json:"name"`
Groups string `json:"groups"`
Password string `json:"passWord"`
SSHAuthorizedKeys []string `json:"ssh"`
}

// ToServiceInput은 HTTP DTO를 서비스 계층 DTO로 변환
func (r *ApiCreateVmRequest) ToServiceInput() service.CreateVMInput {
users := make([]service.UserSpec, len(r.Users))
for i, u := range r.Users {
users[i] = service.UserSpec{
Name: u.Name,
Groups: u.Groups,
Password: u.Password,
SSHAuthorizedKeys: u.SSHAuthorizedKeys,
}
}
return service.CreateVMInput{
UUID: r.UUID,
DomType: r.DomType,
DomName: r.DomName,
OS: r.OS,
HardwareInfo: service.HardwareSpec{
CPU: r.HWInfo.CPU,
Memory: r.HWInfo.Memory,
Disk: r.HWInfo.Disk,
},
Network: service.NetworkSpec{
IPs: r.Network.IPs,
},
Users: users,
SubnetType: r.SubnetType,
}
}

func (c *handlerContext) createVm(w http.ResponseWriter, r *http.Request) {
log := util.GetLogger()
defer r.Body.Close()

var req ApiCreateVmRequest
var req model.CreateVMRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Error("createVm: failed to parse request body: %v", err, true)
var syntaxErr *json.SyntaxError
Expand All @@ -85,12 +26,13 @@ func (c *handlerContext) createVm(w http.ResponseWriter, r *http.Request) {
return
}

if req.HWInfo.Memory == 0 || req.HWInfo.CPU == 0 || req.HWInfo.Disk == 0 {
if req.HardwareInfo.Memory == 0 || req.HardwareInfo.CPU == 0 || req.HardwareInfo.Disk == 0 {
util.RespondError(w, http.StatusBadRequest, "Memory, CPU, and Disk must be non-zero")
return
}

if err := service.CreateVM(req.ToServiceInput(), c.context, c.rdb); err != nil {
err := service.CreateVM(req, c.context, c.rdb)
if err != nil {
log.Error("createVm: failed to create VM: %v", err, true)
util.RespondError(w, http.StatusInternalServerError, err.Error())
return
Expand Down
29 changes: 3 additions & 26 deletions api/get_vm_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,6 @@ type ApiVmStatusRequest struct {
Type string `json:"type"` // "cpu", "memory", or "disk"
}

type ApiVmCpuStatusResponse struct {
System float64 `json:"system_time"`
Idle float64 `json:"idle_time"`
Usage float64 `json:"usage_percent"`
}

type ApiVmMemoryStatusResponse struct {
Total uint64 `json:"total_gb"`
Used uint64 `json:"used_gb"`
Available uint64 `json:"available_gb"`
UsedPercent float64 `json:"used_percent"`
}

type ApiVmDiskStatusResponse struct {
Total uint64 `json:"total_gb"`
Used uint64 `json:"used_gb"`
Free uint64 `json:"free_gb"`
UsedPercent float64 `json:"used_percent"`
}

func (c *handlerContext) vmStatus(w http.ResponseWriter, r *http.Request) {
log := util.GetLogger()
defer r.Body.Close()
Expand All @@ -56,14 +36,11 @@ func (c *handlerContext) vmStatus(w http.ResponseWriter, r *http.Request) {

switch statusType {
case "cpu":
cpu, e := service.GetVMCpuInfo(req.UUID, c.context)
data, err = ApiVmCpuStatusResponse{System: cpu.System, Idle: cpu.Idle, Usage: cpu.Usage}, e
data, err = service.GetVMCpuInfo(req.UUID, c.context)
case "memory":
mem, e := service.GetVMMemoryInfo(req.UUID, c.context)
data, err = ApiVmMemoryStatusResponse{Total: mem.Total, Used: mem.Used, Available: mem.Available, UsedPercent: mem.UsedPercent}, e
data, err = service.GetVMMemoryInfo(req.UUID, c.context)
case "disk":
disk, e := service.GetVMDiskInfo(req.UUID, c.context)
data, err = ApiVmDiskStatusResponse{Total: disk.Total, Used: disk.Used, Free: disk.Free, UsedPercent: disk.UsedPercent}, e
data, err = service.GetVMDiskInfo(req.UUID, c.context)
}

if err != nil {
Expand Down
95 changes: 24 additions & 71 deletions client/cms.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,102 +11,54 @@ import (
"github.com/easy-cloud-Knet/KWS_Control/util"
)

// CmsClient는 CMS서비스에 서브넷/인스턴스 생성을 요청하는 HTTP 클라이언트.
type CmsClient struct {
baseURL string
client *http.Client
}

type CmsNewInstanceResponse struct {
IP string `json:"IP"`
type NewSubnetRequest struct {
IP string `json:"ip"`
MacAddr string `json:"macAddr"`
SdnUUID string `json:"sdnUUID"`
}

type CmsDeleteInstanceResponse struct {
Detail string `json:"detail,omitempty"`
}

type cmsNewInstanceRequestBody struct {
type subnetRequest struct {
Subnet string `json:"Subnet"`
}

type cmsDeleteInstanceRequestBody struct {
IP string `json:"IP"`
}

func NewCmsClient() *CmsClient {
host := os.Getenv("CMS_HOST")
if host == "" {
CMS_HOST := os.Getenv("CMS_HOST")
if CMS_HOST == "" {
log := util.GetLogger()
log.Error("CMS_HOST Re:Check your env variable", true)
host = "localhost:8080"
log.Warn("CMS_HOST set: %s", host, true)
CMS_HOST = "localhost:8080"
log.Warn("CMS_HOST set: %s", CMS_HOST, true)
}
return &CmsClient{
baseURL: host,
baseURL: CMS_HOST,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}

func (c *CmsClient) RequestDeleteInstance(ip string) (*CmsDeleteInstanceResponse, error) {
log := util.GetLogger()

reqURL := fmt.Sprintf("http://%s/New/Instance", c.baseURL)
jsonBody, err := json.Marshal(cmsDeleteInstanceRequestBody{IP: ip})
if err != nil {
log.Error("CmsClient.RequestDeleteInstance : failed to marshal JSON: %v", err)
return nil, fmt.Errorf("CmsClient.RequestDeleteInstance: failed to marshal JSON: %w", err)
}
req, err := http.NewRequest("DELETE", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
log.Error("CmsClient.RequestDeleteInstance : failed to NewRequest: %v", err)
return nil, fmt.Errorf("CmsClient.RequestDeleteInstance: failed to create HTTP request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

log.DebugInfo("Making request to: %s", reqURL)
log.DebugInfo("Request body: %s", string(jsonBody))

resp, err := c.client.Do(req)
if err != nil {
log.Error("CmsClient.RequestDeleteInstance : failed to send request: %v", err)
return nil, fmt.Errorf("CmsClient.RequestDeleteInstance: failed to send request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
log.Error("CmsClient.RequestDeleteInstance : CMS returned status: %s", resp.Status)
return nil, fmt.Errorf("CmsClient.RequestDeleteInstance: CMS server returned non-OK status: %s", resp.Status)
}

var addrResp CmsDeleteInstanceResponse
if err := json.NewDecoder(resp.Body).Decode(&addrResp); err != nil {
log.Error("CmsClient.RequestDeleteInstance : failed to decode CMS response: %v", err)
return nil, fmt.Errorf("CmsClient.RequestDeleteInstance: failed to decode response: %w", err)
}
return &addrResp, nil
}

// RequestNewInstance는 주어진 서브넷에 대해 CMS에 새 인스턴스 할당을 요청한다.
func (c *CmsClient) RequestNewInstance(subnet string) (*CmsNewInstanceResponse, error) {
func (c *CmsClient) RequestSubnet(subnet string) (*NewSubnetRequest, error) {
log := util.GetLogger()

reqURL := fmt.Sprintf("http://%s/New/Instance", c.baseURL)
jsonBody, err := json.Marshal(cmsNewInstanceRequestBody{Subnet: subnet})
reqBody := subnetRequest{Subnet: subnet}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
log.Error("CmsClient.RequestNewInstance : failed to marshal JSON: %v", err)
return nil, fmt.Errorf("CmsClient.RequestNewInstance: failed to marshal JSON: %w", err)
log.Error("CMS : failed to marshal JSON: %v", err)
return nil, fmt.Errorf("RequestSubnet: failed to marshal JSON: %w", err)
}

req, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
log.Error("CmsClient.RequestNewInstance : failed to NewRequest: %v", err)
return nil, fmt.Errorf("CmsClient.RequestNewInstance: failed to create HTTP request: %w", err)
log.Error("CMS : failed to NewRequest: %v", err)
return nil, fmt.Errorf("RequestSubnet: failed to create HTTP request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

Expand All @@ -115,20 +67,21 @@ func (c *CmsClient) RequestNewInstance(subnet string) (*CmsNewInstanceResponse,

resp, err := c.client.Do(req)
if err != nil {
log.Error("CmsClient.RequestNewInstance : failed to send request: %v", err)
return nil, fmt.Errorf("CmsClient.RequestNewInstance: failed to send request: %w", err)
log.Error("CMS : failed to send request: %v", err)
return nil, fmt.Errorf("RequestSubnet: failed to send request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
log.Error("CmsClient.RequestNewInstance : CMS returned status: %s", resp.Status)
return nil, fmt.Errorf("CmsClient.RequestNewInstance: CMS server returned non-OK status: %s", resp.Status)
log.Error("CMS : CMS returned status: %s", resp.Status)
return nil, fmt.Errorf("CMS server returned non-OK status: %s", resp.Status)
}

var addrResp CmsNewInstanceResponse
var addrResp NewSubnetRequest
if err := json.NewDecoder(resp.Body).Decode(&addrResp); err != nil {
log.Error("CmsClient.RequestNewInstance : failed to decode CMS response: %v", err)
return nil, fmt.Errorf("CmsClient.RequestNewInstance: failed to decode response: %w", err)
log.Error("CMS : failed to decode CMS response: %v", err)
return nil, fmt.Errorf("RequestSubnet: failed to decode response: %w", err)
}

return &addrResp, nil
}
7 changes: 7 additions & 0 deletions client/model/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ type CoreMachineCpuInfoResponse struct {
System float64 `json:"system_time"`
Idle float64 `json:"idle_time"`
Usage float64 `json:"usage_percent"`
// Desc는 호스트 /getStatusHost(host_dataType=0) 응답에만 존재(runtime.NumCPU()).
// VM별 /getStatusUUID 응답에는 없으므로 포인터로 두어 미존재를 nil로 감지한다.
Desc *VCPUStatus `json:"vcpu_status"`
}

type VCPUStatus struct {
Total int `json:"total"` // 코어의 논리 CPU 총 개수
}

type CoreMachineMemoryInfoResponse struct {
Expand Down
5 changes: 2 additions & 3 deletions client/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ func (c *CoreClient) DeleteVM(context context.Context, req model.DeleteVMRequest
return response, nil
}

// 현재 미사용 중
// 코어 vcpu 갯수 가져오는 함수
// 코어에 문의 해봐야함 옛날에 구현 안됬다고 해서 컨트롤에서 9999로 하드코딩 했던거 같음
// 코어의 논리 CPU 총 개수를 /getStatusHost(host_dataType=0)에서 가져온다.
// 응답의 vcpu_status.total(=runtime.NumCPU())을 CoreInfoIdx.Cpu 캐시로 사용.
func (c *CoreClient) GetCoreMachineCpuInfo(context context.Context) (*model.CoreMachineCpuInfoResponse, error) {
var response model.CoreResponse[model.CoreMachineCpuInfoResponse]
err := c.doRequest(context, http.MethodGet, "/getStatusHost", model.GetMachineStatusRequest{
Expand Down
14 changes: 12 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package main
import (
"context"
"fmt"
"time"

"github.com/easy-cloud-Knet/KWS_Control/structure"

"github.com/easy-cloud-Knet/KWS_Control/api"
"github.com/easy-cloud-Knet/KWS_Control/service"
"github.com/easy-cloud-Knet/KWS_Control/startup"
"github.com/easy-cloud-Knet/KWS_Control/util"
)
Expand All @@ -16,7 +18,7 @@ func main() {

ctx := context.Background()

//Redis 초기화
//Redis 초기화 (VM status 저장 + 코어별 할당 집계 공용)
rdb, err := startup.InitializeRedis(ctx)
if err != nil {
log.Error("Failed to initialize Redis: %v", err, true)
Expand All @@ -30,7 +32,15 @@ func main() {
log.Error("Failed to initialize: %v", err, true)
panic(err)
}
printCores(contextStruct.Resources.Cores)
printCores(contextStruct.Cores)

// DB 인스턴스 합계로 코어별 할당(core:{ip}:{port}:alloc) 재구성(시작 시 1회, 멱등). 실패해도 기동은 계속.
if err := service.RebuildCoreAllocFromDB(ctx, &contextStruct, rdb); err != nil {
log.Error("Failed to rebuild core alloc from DB: %v", err, true)
}

// 주기적 헬스체크(코어 가용성/용량 갱신)
go service.StartHealthcheck(ctx, &contextStruct, 30*time.Second)

go func() {
err := api.Server(contextStruct.Config.Port, &contextStruct, rdb)
Expand Down
5 changes: 5 additions & 0 deletions resources/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ cores:

port: 8081

# 코어 선택 알고리즘 파라미터
cpu_overcommit: 4.0 # vCPU 오버커밋 배수 (logical_cpu * cpu_overcommit = 가용 vCPU)
mem_reserve_pct: 0.1 # 메모리 여유분 비율 (0..1)
disk_reserve_pct: 0.1 # 디스크 여유분 비율 (0..1)

db:
user: "root"
password: "password"
Expand Down
Loading