diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index cb19a0e..3e30dbf 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build and tag Docker image run: | @@ -21,8 +21,22 @@ jobs: - name: Deploy container locally run: | - IMAGE=${{ secrets.DOCKER_HUB_USERNAME }}/control_deploy:latest + IMAGE=${{ secrets.DOCKER_HUB_USERNAME }}/control_dev:latest docker stop CONTROL_DEPLOY || true docker rm CONTROL_DEPLOY || true - docker run -d --name CONTROL_DEPLOY -p 8081:8081 $IMAGE + docker run -d --name CONTROL_DEPLOY -p 8083:8081 --restart=always \ + --add-host host.docker.internal:host-gateway \ + -e CORES="${{ secrets.CORES }}" \ + -e DB_USER="${{ secrets.DB_USER }}" \ + -e DB_PASSWORD="${{ secrets.DB_PASSWORD }}" \ + -e DB_HOST="${{ secrets.DB_HOST }}" \ + -e DB_NAME="${{ secrets.DB_NAME }}" \ + -e GUAC_DB_USER="${{ secrets.GUAC_DB_USER }}" \ + -e GUAC_DB_PASSWORD="${{ secrets.GUAC_DB_PASSWORD }}" \ + -e GUAC_DB_HOST="${{ secrets.GUAC_DB_HOST }}" \ + -e GUAC_DB_NAME="${{ secrets.GUAC_DB_NAME }}" \ + -e REDIS_HOST="${{ secrets.REDIS_HOST }}" \ + -e CMS_HOST="${{ secrets.CMS_HOST }}" \ + -e GUAC_BASE_URL="${{ secrets.GUAC_BASE_URL }}" \ + $IMAGE docker system prune -af diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 1d7064d..62c7d7b 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build and tag Docker image run: | diff --git a/.github/workflows/pr-build-check.yaml b/.github/workflows/pr-build-check.yaml new file mode 100644 index 0000000..2be349c --- /dev/null +++ b/.github/workflows/pr-build-check.yaml @@ -0,0 +1,40 @@ +name: PR Build Verification + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - main + - Dev + +jobs: + build-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.x' + + - name: Download Go modules + run: go mod download + + - name: Build + run: go build -o main . + + - name: Comment on failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + event: 'COMMENT', + body: 'Failed To BUILD! Please check the build logs for details and Resolve the issues before merging.' + }); \ No newline at end of file diff --git a/api/connect_vm.go b/api/connect_vm.go new file mode 100644 index 0000000..ef09ccd --- /dev/null +++ b/api/connect_vm.go @@ -0,0 +1,44 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/structure" + "github.com/easy-cloud-Knet/KWS_Control/util" +) + +type ApiVmConnectRequest struct { + UUID structure.UUID `json:"uuid"` +} + +type ApiVmConnectResponse struct { + AuthToken string `json:"authToken"` +} + +func (c *handlerContext) vmConnect(w http.ResponseWriter, r *http.Request) { + log := util.GetLogger() + + uuidStr := r.URL.Query().Get("uuid") + if uuidStr == "" { + http.Error(w, "Missing 'uuid' query parameter", http.StatusBadRequest) + log.Error("Missing 'uuid' query parameter", nil, true) + return + } + + uuid := structure.UUID(uuidStr) + authToken, err := service.GetGuacamoleToken(uuid, c.context) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Error("Failed to get Guacamole token: %v", err, true) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(ApiVmConnectResponse{AuthToken: authToken}); err != nil { + log.Error("Failed to encode response: %v", err, true) + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } +} diff --git a/api/create_vm.go b/api/create_vm.go new file mode 100644 index 0000000..f52ef47 --- /dev/null +++ b/api/create_vm.go @@ -0,0 +1,28 @@ +package api + +import ( + "net/http" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/util" +) + +func (c *handlerContext) createVm(w http.ResponseWriter, r *http.Request) { + log := util.GetLogger() + + err := service.CreateVM(w, r, c.context, c.rdb) + if err != nil { + h := w.Header() + h.Del("Content-Length") + h.Set("Content-Type", "text/plain; charset=utf-8") + h.Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(http.StatusMethodNotAllowed) + + log.Error("Failed to create VM: %v", err, true) + return + } + defer r.Body.Close() + + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("VM created successfully")) +} diff --git a/api/delete_vm.go b/api/delete_vm.go new file mode 100644 index 0000000..ab2d183 --- /dev/null +++ b/api/delete_vm.go @@ -0,0 +1,30 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/structure" +) + +type ApiDeleteVmRequest struct { + UUID structure.UUID `json:"uuid"` +} + +func (c *handlerContext) deleteVm(w http.ResponseWriter, r *http.Request) { + var req ApiDeleteVmRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + defer r.Body.Close() + + err := service.DeleteVM(req.UUID, c.context, c.rdb) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/api/get_vm_info.go b/api/get_vm_info.go new file mode 100644 index 0000000..2efbf2e --- /dev/null +++ b/api/get_vm_info.go @@ -0,0 +1,58 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/structure" + "github.com/easy-cloud-Knet/KWS_Control/util" +) + +type ApiVmInfoRequest struct { + UUID structure.UUID `json:"uuid"` +} + +type ApiVmInfoResponse struct { + UUID structure.UUID `json:"uuid"` + CPU uint32 `json:"cpu"` + Memory uint32 `json:"memory"` // MiB + Disk uint32 `json:"disk"` // MiB + IP string `json:"ip"` +} + +func (c *handlerContext) vmInfo(w http.ResponseWriter, r *http.Request) { + log := util.GetLogger() + + var req ApiVmInfoRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + log.Error("Invalid request body: %v", err, true) + return + } + defer r.Body.Close() + + vmInfo, err := service.GetVMInfoFromRedis(r.Context(), c.rdb, req.UUID) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + log.Error("failed to get vm info from redis: %v", err, true) + return + } + + response := ApiVmInfoResponse{ + UUID: vmInfo.UUID, + CPU: vmInfo.CPU, + Memory: vmInfo.Memory, + Disk: vmInfo.Disk, + IP: vmInfo.IP, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Error("failed to encode vm info response: %v", err, true) + http.Error(w, "failed to encode response", http.StatusInternalServerError) + return + } + + log.Info("retrieved vm info from redis: UUID=%s", string(req.UUID), true) +} diff --git a/api/get_vm_status.go b/api/get_vm_status.go new file mode 100644 index 0000000..3aae810 --- /dev/null +++ b/api/get_vm_status.go @@ -0,0 +1,58 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/structure" + "github.com/easy-cloud-Knet/KWS_Control/util" +) + +type ApiVmStatusRequest struct { + UUID structure.UUID `json:"uuid"` + Type string `json:"type"` // "cpu", "memory", or "disk" +} + +func (c *handlerContext) vmStatus(w http.ResponseWriter, r *http.Request) { + log := util.GetLogger() + + var req ApiVmStatusRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + log.Error("Failed to decode request body: %v", err, true) + return + } + defer r.Body.Close() + + statusType := req.Type + if statusType != "cpu" && statusType != "memory" && statusType != "disk" { + http.Error(w, "Invalid status type. Must be 'cpu', 'memory', or 'disk'", http.StatusBadRequest) + return + } + + var data any + var err error + + switch statusType { + case "cpu": + data, err = service.GetVMCpuInfo(req.UUID, c.context) + case "memory": + data, err = service.GetVMMemoryInfo(req.UUID, c.context) + case "disk": + data, err = service.GetVMDiskInfo(req.UUID, c.context) + } + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Error("Failed to get VM status: %v", err, true) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(data); err != nil { + log.Error("Failed to encode response: %v", err, true) + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } +} diff --git a/api/model/vm.go b/api/model/vm.go deleted file mode 100644 index 5774610..0000000 --- a/api/model/vm.go +++ /dev/null @@ -1,75 +0,0 @@ -package model - -import "github.com/easy-cloud-Knet/KWS_Control/structure" - -type ApiDeleteVmRequest struct { - UUID structure.UUID `json:"uuid"` -} - -type ApiShutdownVmRequest struct { - UUID structure.UUID `json:"uuid"` -} -type ApiShutdownVmResponse struct { - Message string `json:"message"` -} -type ApiForceShutdownVmRequest struct { - UUID structure.UUID `json:"uuid"` -} -type ApiForceShutdownVmResponse struct { - Message string `json:"message"` -} -type ApiStartVmRequest struct { - UUID structure.UUID `json:"uuid"` -} - -type ApiVmStatusRequest struct { - UUID structure.UUID `json:"uuid"` - Type string `json:"type"` // "cpu", "memory", or "disk" -} - -type ApiVmConnectRequest struct { - UUID structure.UUID `json:"uuid"` -} - -type ApiVmInfoRequest struct { - UUID structure.UUID `json:"uuid"` -} - -type ApiVmInfoResponse struct { - UUID structure.UUID `json:"uuid"` - CPU uint32 `json:"cpu"` - Memory uint32 `json:"memory"` // MiB - Disk uint32 `json:"disk"` // MiB - IP string `json:"ip"` -} - -// request/model/vm.go 와 동일하게 유지 -const ( - VMStatusPrepareBegin = "prepare begin" - VMStatusStartBegin = "start begin" - VMStatusStarted = "started begin" - VMStatusStopped = "stopped end" - VMStatusRelease = "release end" - VMStatusMigrate = "migrate begin" - VMStatusRestore = "restort begin" - VMStatusUnknown = "unknown" -) - -type Redis struct { - UUID structure.UUID `json:"UUID"` - Status string `json:"status"` -} - -func ValidateAndNormalizeStatus(status string) string { - if status == "" || status == "null" { - return VMStatusUnknown - } - - switch status { - case VMStatusPrepareBegin, VMStatusStartBegin, VMStatusStarted, VMStatusStopped, - VMStatusRelease, VMStatusMigrate, VMStatusRestore, VMStatusUnknown: - return status - default: - return VMStatusUnknown - } -} diff --git a/api/shutdown_vm.go b/api/shutdown_vm.go new file mode 100644 index 0000000..615bd44 --- /dev/null +++ b/api/shutdown_vm.go @@ -0,0 +1,42 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/structure" +) + +type ApiShutdownVmRequest struct { + UUID structure.UUID `json:"uuid"` +} + +type ApiShutdownVmResponse struct { + Message string `json:"message"` +} + +type ApiForceShutdownVmRequest struct { + UUID structure.UUID `json:"uuid"` +} + +type ApiForceShutdownVmResponse struct { + Message string `json:"message"` +} + +func (c *handlerContext) shutdownVm(w http.ResponseWriter, r *http.Request) { + var req ApiShutdownVmRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + err := service.ShutdownVM(req.UUID, c.context, c.rdb) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/api/start_vm.go b/api/start_vm.go new file mode 100644 index 0000000..eb242d7 --- /dev/null +++ b/api/start_vm.go @@ -0,0 +1,30 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/structure" +) + +type ApiStartVmRequest struct { + UUID structure.UUID `json:"uuid"` +} + +func (c *handlerContext) startVm(w http.ResponseWriter, r *http.Request) { + var req ApiStartVmRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + err := service.StartVM(req.UUID, c.context) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/api/update_redis.go b/api/update_redis.go new file mode 100644 index 0000000..99ea1a4 --- /dev/null +++ b/api/update_redis.go @@ -0,0 +1,80 @@ +package api + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/easy-cloud-Knet/KWS_Control/service" + "github.com/easy-cloud-Knet/KWS_Control/structure" + "github.com/easy-cloud-Knet/KWS_Control/util" +) + +const ( + VMStatusPrepareBegin = "prepare begin" + VMStatusStartBegin = "start begin" + VMStatusStarted = "started begin" + VMStatusStopped = "stopped end" + VMStatusRelease = "release end" + VMStatusMigrate = "migrate begin" + VMStatusRestore = "restort begin" + VMStatusUnknown = "unknown" +) + +type RedisStatusRequest struct { + UUID structure.UUID `json:"UUID"` + Status string `json:"status"` +} + +func ValidateAndNormalizeStatus(status string) string { + if status == "" || status == "null" { + return VMStatusUnknown + } + + switch status { + case VMStatusPrepareBegin, VMStatusStartBegin, VMStatusStarted, VMStatusStopped, + VMStatusRelease, VMStatusMigrate, VMStatusRestore, VMStatusUnknown: + return status + default: + return VMStatusUnknown + } +} + +func (c *handlerContext) redis(w http.ResponseWriter, r *http.Request) { + log := util.GetLogger() + + var req RedisStatusRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + log.Error("Invalid request body: %v", err, true) + return + } + defer r.Body.Close() + + originalStatus := req.Status + normalizedStatus := ValidateAndNormalizeStatus(req.Status) + if originalStatus != normalizedStatus { + log.Warn("VM status normalized: UUID=%s, original='%s', normalized='%s'", + req.UUID, originalStatus, normalizedStatus, true) + } + + ctx := r.Context() + err := service.UpdateVMStatusInRedis(ctx, c.rdb, req.UUID, normalizedStatus, time.Now().Unix()) + if err != nil { + http.Error(w, "Failed to update VM status in Redis", http.StatusInternalServerError) + log.Error("Failed to update VM status in Redis: %v", err, true) + return + } + + storedValue, err := c.rdb.Get(ctx, string(req.UUID)).Result() + if err != nil { + http.Error(w, "Failed to get value from Redis", http.StatusInternalServerError) + log.Error("Redis GET failed: %v", err, true) + return + } + + log.DebugInfo("Redis 확인 완료 - key: %s, value: %s", req.UUID, storedValue) + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("VM status updated in Redis")) +} diff --git a/api/vm.go b/api/vm.go deleted file mode 100644 index 26f9406..0000000 --- a/api/vm.go +++ /dev/null @@ -1,231 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/easy-cloud-Knet/KWS_Control/api/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" -) - -func (c *handlerContext) createVm(w http.ResponseWriter, r *http.Request) { - log := util.GetLogger() - - err := service.CreateVM(w, r, c.context, c.rdb) - if err != nil { - h := w.Header() - h.Del("Content-Length") - h.Set("Content-Type", "text/plain; charset=utf-8") - h.Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusMethodNotAllowed) - - log.Error("Failed to create VM: %v", err, true) - - return - } - defer r.Body.Close() - - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("VM created successfully")) -} - -func (c *handlerContext) deleteVm(w http.ResponseWriter, r *http.Request) { - var req model.ApiDeleteVmRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - defer r.Body.Close() - - err := service.DeleteVM(req.UUID, c.context, c.rdb) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) // TODO: 코어가 없는 경우 처리 - return - } - - w.WriteHeader(http.StatusOK) -} - -func (c *handlerContext) shutdownVm(w http.ResponseWriter, r *http.Request) { - var req model.ApiShutdownVmRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - err := service.ShutdownVM(req.UUID, c.context, c.rdb) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (c *handlerContext) startVm(w http.ResponseWriter, r *http.Request) { - var req model.ApiStartVmRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - err := service.StartVM(req.UUID, c.context) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (c *handlerContext) vmStatus(w http.ResponseWriter, r *http.Request) { - log := util.GetLogger() - - var req model.ApiVmStatusRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - log.Error("Failed to decode request body: %v", err, true) - return - } - defer r.Body.Close() - - statusType := req.Type - if statusType != "cpu" && statusType != "memory" && statusType != "disk" { - http.Error(w, "Invalid status type. Must be 'cpu', 'memory', or 'disk'", http.StatusBadRequest) - return - } - - var data any - var err error - - switch statusType { - case "cpu": - data, err = service.GetVMCpuInfo(req.UUID, c.context) - case "memory": - data, err = service.GetVMMemoryInfo(req.UUID, c.context) - case "disk": - data, err = service.GetVMDiskInfo(req.UUID, c.context) - } - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - log.Error("Failed to get VM status: %v", err, true) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(data); err != nil { - log.Error("Failed to encode response: %v", err, true) - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } -} - -func (c *handlerContext) vmConnect(w http.ResponseWriter, r *http.Request) { - log := util.GetLogger() - - uuidStr := r.URL.Query().Get("uuid") - if uuidStr == "" { - http.Error(w, "Missing 'uuid' query parameter", http.StatusBadRequest) - log.Error("Missing 'uuid' query parameter", nil, true) - return - } - - var uuid = structure.UUID(uuidStr) - - authToken, err := service.GetGuacamoleToken(uuid, c.context) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - log.Error("Failed to get Guacamole token: %v", err, true) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]string{"authToken": authToken}); err != nil { - log.Error("Failed to encode response: %v", err, true) - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } -} -func (c *handlerContext) redis(w http.ResponseWriter, r *http.Request) { - log := util.GetLogger() - - var req model.Redis - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - log.Error("Invalid request body: %v", err, true) - return - } - defer r.Body.Close() - originalStatus := req.Status - normalizedStatus := model.ValidateAndNormalizeStatus(req.Status) - - if originalStatus != normalizedStatus { - log.Warn("VM status normalized: UUID=%s, original='%s', normalized='%s'", - req.UUID, originalStatus, normalizedStatus, true) - } - - ctx := r.Context() - - err := service.UpdateVMStatusInRedis(ctx, c.rdb, req.UUID, normalizedStatus, time.Now().Unix()) - if err != nil { - http.Error(w, "Failed to update VM status in Redis", http.StatusInternalServerError) - log.Error("Failed to update VM status in Redis: %v", err, true) - return - } - - storedValue, err := c.rdb.Get(ctx, string(req.UUID)).Result() - if err != nil { - http.Error(w, "Failed to get value from Redis", http.StatusInternalServerError) - log.Error("Redis GET failed: %v", err, true) - return - } - - log.DebugInfo("Redis 확인 완료 - key: %s, value: %s", req.UUID, storedValue) - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("VM status updated in Redis")) -} - -func (c *handlerContext) vmInfo(w http.ResponseWriter, r *http.Request) { - log := util.GetLogger() - - var req model.ApiVmInfoRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "invalid request body", http.StatusBadRequest) - log.Error("Invalid request body: %v", err, true) - return - } - defer r.Body.Close() - - vmInfo, err := service.GetVMInfoFromRedis(r.Context(), c.rdb, req.UUID) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - log.Error("failed to get vm info from redis: %v", err, true) - return - } - - response := model.ApiVmInfoResponse{ - UUID: vmInfo.UUID, - CPU: vmInfo.CPU, - Memory: vmInfo.Memory, - Disk: vmInfo.Disk, - IP: vmInfo.IP, - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(response); err != nil { - log.Error("failed to encode vm info response: %v", err, true) - http.Error(w, "failed to encode response", http.StatusInternalServerError) - return - } - - log.Info("retrieved vm info from redis: UUID=%s", string(req.UUID), true) -} diff --git a/request/guacamole.go b/client/guacamole.go similarity index 99% rename from request/guacamole.go rename to client/guacamole.go index 6c22e75..aa302a5 100644 --- a/request/guacamole.go +++ b/client/guacamole.go @@ -1,4 +1,4 @@ -package request +package client import ( "context" diff --git a/request/model/common.go b/client/model/common.go similarity index 100% rename from request/model/common.go rename to client/model/common.go diff --git a/request/model/vm.go b/client/model/vm.go similarity index 100% rename from request/model/vm.go rename to client/model/vm.go diff --git a/request/vm.go b/client/vm.go similarity index 98% rename from request/vm.go rename to client/vm.go index ea20e05..923f6fb 100644 --- a/request/vm.go +++ b/client/vm.go @@ -1,4 +1,4 @@ -package request +package client import ( "bytes" @@ -9,7 +9,7 @@ import ( "net/http" "time" - "github.com/easy-cloud-Knet/KWS_Control/request/model" + "github.com/easy-cloud-Knet/KWS_Control/client/model" "github.com/easy-cloud-Knet/KWS_Control/structure" "github.com/easy-cloud-Knet/KWS_Control/util" ) diff --git a/pkg/crypto/password.go b/pkg/crypto/password.go new file mode 100644 index 0000000..055ee25 --- /dev/null +++ b/pkg/crypto/password.go @@ -0,0 +1,35 @@ +package crypto + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "strings" +) + +func HashPasswordWithSalt(password string, salt []byte) []byte { + hash := sha256.New() + temp := strings.ToUpper(hex.EncodeToString(salt)) + hash.Write([]byte(password)) + hash.Write([]byte(temp)) + return hash.Sum(nil) +} + +func GenerateRandomSalt(length int) ([]byte, error) { + salt := make([]byte, length) + _, err := rand.Read(salt) + if err != nil { + return nil, err + } + return salt, nil +} + +func GenerateRandomPassword(length int) (string, error) { + bytes := make([]byte, length) + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(bytes)[:length], nil +} diff --git a/service/guacamol_config.go b/pkg/guacamole/config.go similarity index 68% rename from service/guacamol_config.go rename to pkg/guacamole/config.go index 7d6cbb4..8c1ba72 100644 --- a/service/guacamol_config.go +++ b/pkg/guacamole/config.go @@ -1,19 +1,15 @@ -package service +package guacamole import ( - "crypto/rand" - "crypto/sha256" "database/sql" - "encoding/base64" "encoding/hex" "fmt" - "strings" + "github.com/easy-cloud-Knet/KWS_Control/pkg/crypto" "github.com/easy-cloud-Knet/KWS_Control/util" - _ "github.com/go-sql-driver/mysql" ) -func GuacamoleConfig(Username string, UUID string, Ip string, PrivateKey string, db *sql.DB) string { +func Configure(username, uuid, ip, privateKey string, db *sql.DB) string { log := util.GetLogger() if db == nil { @@ -22,24 +18,23 @@ func GuacamoleConfig(Username string, UUID string, Ip string, PrivateKey string, } // 1. 무작위 비밀번호 생성 - userPass, err := generateRandomPassword(12) + userPass, err := crypto.GenerateRandomPassword(12) if err != nil { log.Error("guacamole: failed to generate random password:", err, true) return "" } - fmt.Println("생성된 비밀번호:", userPass) // 2. 32바이트 Salt 생성 - salt, err := generateRandomSalt(32) + salt, err := crypto.GenerateRandomSalt(32) if err != nil { log.Error("guacamole: failed to create random salt:", err, true) return "" } - fmt.Println("생성된 salt:", hex.EncodeToString(salt)) // 3. 해시 계산: SHA256(salt + password) - passwordHash := fmt.Sprintf("%x", hashPasswordWithSalt(userPass, salt)) - saltHex := fmt.Sprintf("%x", salt) + passwordHash := fmt.Sprintf("%x", crypto.HashPasswordWithSalt(userPass, salt)) + saltHex := hex.EncodeToString(salt) + tx, err := db.Begin() if err != nil { log.Error("guacamole: failed to start transaction: %v", err, true) @@ -59,15 +54,13 @@ func GuacamoleConfig(Username string, UUID string, Ip string, PrivateKey string, // 중복 확인 이후 var entityID int64 var res sql.Result - err = tx.QueryRow(`SELECT entity_id FROM guacamole_entity WHERE name = ? AND type = 'USER'`, UUID).Scan(&entityID) + err = tx.QueryRow(`SELECT entity_id FROM guacamole_entity WHERE name = ? AND type = 'USER'`, uuid).Scan(&entityID) if err == sql.ErrNoRows { - // Entity가 없으면 새로 생성 - res, err = tx.Exec(`INSERT INTO guacamole_entity (name, type) VALUES (?, 'USER')`, UUID) + res, err = tx.Exec(`INSERT INTO guacamole_entity (name, type) VALUES (?, 'USER')`, uuid) if err != nil { log.Error("guacamole: failed to create an entity:", err, true) return "" } - entityID, err = res.LastInsertId() if err != nil { log.Error("guacamole: failed to retrieve entity id:", err, true) @@ -78,7 +71,6 @@ func GuacamoleConfig(Username string, UUID string, Ip string, PrivateKey string, log.Error("guacamole: failed to check existing entity:", err, true) return "" } else { - // Entity가 이미 존재 log.DebugInfo("guacamole: using existing entity with ID: %d", entityID) } @@ -97,7 +89,7 @@ func GuacamoleConfig(Username string, UUID string, Ip string, PrivateKey string, } // 6. Connection 생성 - connectionName := fmt.Sprintf("%s-ssh", UUID) + connectionName := fmt.Sprintf("%s-ssh", uuid) res, err = tx.Exec(` INSERT INTO guacamole_connection (connection_name, protocol) VALUES (?, 'ssh') @@ -116,10 +108,10 @@ func GuacamoleConfig(Username string, UUID string, Ip string, PrivateKey string, // 7. Connection parameters 설정 parameters := map[string]string{ - "hostname": Ip, + "hostname": ip, "port": "22", - "username": Username, - "private-key": PrivateKey, + "username": username, + "private-key": privateKey, } for name, value := range parameters { @@ -154,40 +146,7 @@ func GuacamoleConfig(Username string, UUID string, Ip string, PrivateKey string, return userPass } -// SHA256 해시 함수 (salt 포함) -func hashPasswordWithSalt(password string, salt []byte) []byte { - hash := sha256.New() - - var temp = hex.EncodeToString(salt) - - temp = strings.ToUpper(temp) - hash.Write([]byte(password)) - hash.Write([]byte(temp)) - return hash.Sum(nil) -} - -// 랜덤 salt 생성 (32바이트) -func generateRandomSalt(length int) ([]byte, error) { - salt := make([]byte, length) - _, err := rand.Read(salt) - if err != nil { - return nil, err - } - return salt, nil -} - -// 안전한 랜덤 비밀번호 생성 함수 -func generateRandomPassword(length int) (string, error) { - bytes := make([]byte, length) - _, err := rand.Read(bytes) - if err != nil { - return "", err - } - return base64.RawURLEncoding.EncodeToString(bytes)[:length], nil -} - -// 뭔가뭔가 문제가 생겼을 때, 지우는 무언가 -func CleanupGuacamoleConfig(UUID string, db *sql.DB) error { +func Cleanup(uuid string, db *sql.DB) error { log := util.GetLogger() if db == nil { @@ -215,28 +174,25 @@ func CleanupGuacamoleConfig(UUID string, db *sql.DB) error { } }() - // Entity ID 찾기 var entityID int64 - err = tx.QueryRow(`SELECT entity_id FROM guacamole_entity WHERE name = ? AND type = 'USER'`, UUID).Scan(&entityID) + err = tx.QueryRow(`SELECT entity_id FROM guacamole_entity WHERE name = ? AND type = 'USER'`, uuid).Scan(&entityID) if err == sql.ErrNoRows { - log.DebugInfo("no entity found for UUID %s, nothing to clean up", UUID) + log.DebugInfo("no entity found for UUID %s, nothing to clean up", uuid) return nil } else if err != nil { - log.Error("failed to find entity for UUID %s: %v", UUID, err, true) - return fmt.Errorf("failed to find entity for UUID %s: %w", UUID, err) + log.Error("failed to find entity for UUID %s: %v", uuid, err, true) + return fmt.Errorf("failed to find entity for UUID %s: %w", uuid, err) } - // Entity 삭제 // cascade로 user도 같이 삭제 _, err = tx.Exec(`DELETE FROM guacamole_entity WHERE entity_id = ?`, entityID) if err != nil { - log.Error("failed to delete entity for UUID %s: %v", UUID, err, true) - return fmt.Errorf("failed to delete entity for UUID %s: %w", UUID, err) + log.Error("failed to delete entity for UUID %s: %v", uuid, err, true) + return fmt.Errorf("failed to delete entity for UUID %s: %w", uuid, err) } - // UUID와 관련된 orphaned connections 정리 - connectionName := fmt.Sprintf("%s-ssh", UUID) + connectionName := fmt.Sprintf("%s-ssh", uuid) _, err = tx.Exec(` - DELETE FROM guacamole_connection + DELETE FROM guacamole_connection WHERE connection_name = ? AND connection_id NOT IN ( SELECT DISTINCT connection_id FROM guacamole_connection_permission ) @@ -251,6 +207,6 @@ func CleanupGuacamoleConfig(UUID string, db *sql.DB) error { return fmt.Errorf("failed to commit transaction: %w", err) } - log.Info("successfully cleaned up configuration for UUID %s", UUID, true) + log.Info("successfully cleaned up configuration for UUID %s", uuid, true) return nil } diff --git a/pkg/network/network.go b/pkg/network/network.go new file mode 100644 index 0000000..f9df305 --- /dev/null +++ b/pkg/network/network.go @@ -0,0 +1,47 @@ +package network + +import ( + "fmt" + "strconv" + "strings" +) + +func FindSubnet(lastSubnet string) string { + value := make([]int, 3) + j := 0 + for i := 0; i < 3; i++ { + var temp string + for lastSubnet[j] != '.' { + temp = temp + string(lastSubnet[j]) + j++ + } + value[i], _ = strconv.Atoi(temp) + j++ + } + + if value[2] >= 255 { + if value[1] >= 255 { + if value[0] >= 255 { + return "err" + } + value[0]++ + value[1] = 0 + value[2] = 0 + } else { + value[1]++ + value[2] = 0 + } + } else { + value[2]++ + } + + return fmt.Sprintf("%s.%s.%s.", strconv.Itoa(value[0]), strconv.Itoa(value[1]), strconv.Itoa(value[2])) +} + +func GetSubnetFromIP(ip string) (string, error) { + parts := strings.Split(ip, ".") + if len(parts) != 4 { + return "", fmt.Errorf("invalid IP format: %s", ip) + } + return strings.Join(parts[:3], ".") + ".", nil +} diff --git a/service/ssh_key_gen.go b/pkg/ssh/keygen.go similarity index 89% rename from service/ssh_key_gen.go rename to pkg/ssh/keygen.go index 27c6965..105258c 100644 --- a/service/ssh_key_gen.go +++ b/pkg/ssh/keygen.go @@ -1,14 +1,15 @@ -package service +package ssh import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" + "golang.org/x/crypto/ssh" ) -func GenerateSshKey() (string, string, error) { +func GenerateSSHKey() (string, string, error) { privateKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return "", "", err diff --git a/service/guacamole.go b/service/guacamole.go index 26e5594..66019aa 100644 --- a/service/guacamole.go +++ b/service/guacamole.go @@ -2,7 +2,7 @@ package service import ( "context" - "github.com/easy-cloud-Knet/KWS_Control/request" + "github.com/easy-cloud-Knet/KWS_Control/client" "github.com/easy-cloud-Knet/KWS_Control/structure" ) @@ -13,14 +13,14 @@ func GetGuacamoleToken(uuid structure.UUID, ctx *structure.ControlContext) (stri } if vm, exists := core.VMInfoIdx[uuid]; exists { - client := request.NewGuacamoleClient(&ctx.Config) + guacClient := client.NewGuacamoleClient(&ctx.Config) - err := client.Authenticate(context.Background(), string(uuid), vm.GuacPassword) + err := guacClient.Authenticate(context.Background(), string(uuid), vm.GuacPassword) if err != nil { return "", err } - return client.AuthToken(), nil + return guacClient.AuthToken(), nil } else { return "", structure.ErrVmNotFound(uuid) } diff --git a/service/network.go b/service/network.go index 14ba779..17201a4 100644 --- a/service/network.go +++ b/service/network.go @@ -6,10 +6,9 @@ import ( "fmt" "net/http" "os" - "strconv" - "strings" "time" + pkgnetwork "github.com/easy-cloud-Knet/KWS_Control/pkg/network" vms "github.com/easy-cloud-Knet/KWS_Control/structure" "github.com/easy-cloud-Knet/KWS_Control/util" ) @@ -99,7 +98,7 @@ func (c *CmsClient) AddCmsSubnet(ctx *vms.ControlContext, uuid vms.UUID) (*CmsRe log.Error("AddCmsSubnet : GetVMIPByUUID: %w", err) return nil, err } - subnet, err := GetSubnetFromIP(ip) + subnet, err := pkgnetwork.GetSubnetFromIP(ip) if err != nil { log.Error("AddCmsSubnet : GetSubnetFromIP: %v", err) return nil, err @@ -118,7 +117,7 @@ func (c *CmsClient) NewCmsSubnet(ctx *vms.ControlContext) (*CmsResponse, error) log := util.GetLogger() last_subnet := ctx.Last_subnet - next_last_subnet := Find_subnet(last_subnet) + next_last_subnet := pkgnetwork.FindSubnet(last_subnet) log.Info("NewCmsSubnet : next_last_subnet: %s", next_last_subnet) temp, err := c.CmsRequest(next_last_subnet) @@ -135,40 +134,6 @@ func (c *CmsClient) NewCmsSubnet(ctx *vms.ControlContext) (*CmsResponse, error) return temp, nil } -func Find_subnet(last_subnet string) string { - value := make([]int, 3) - j := 0 - for i := 0; i < 3; i++ { - var temp string - for last_subnet[j] != '.' { - temp = temp + string(last_subnet[j]) - j++ - } - value[i], _ = strconv.Atoi(temp) - j++ - } - - if value[2] >= 255 { - if value[1] >= 255 { - if value[0] >= 255 { - return "err" - } else { - value[0]++ - value[1] = 0 - value[2] = 0 - } - } else { - value[1]++ - value[2] = 0 - } - } else { - value[2]++ - } - - result := fmt.Sprintf("%s.%s.%s.", strconv.Itoa(value[0]), strconv.Itoa(value[1]), strconv.Itoa(value[2])) - return result -} - func GetVMIPByUUID(ctx *vms.ControlContext, uuid vms.UUID) (string, error) { core, ok := ctx.VMLocation[uuid] if !ok { @@ -183,11 +148,3 @@ func GetVMIPByUUID(ctx *vms.ControlContext, uuid vms.UUID) (string, error) { return vmInfo.IP_VM, nil } -func GetSubnetFromIP(ip string) (string, error) { - parts := strings.Split(ip, ".") - if len(parts) != 4 { - return "", fmt.Errorf("invalid IP format: %s", ip) - } - - return strings.Join(parts[:3], ".") + ".", nil -} diff --git a/service/redis.go b/service/redis.go index 4aa2491..e953dcb 100644 --- a/service/redis.go +++ b/service/redis.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" - "github.com/easy-cloud-Knet/KWS_Control/request/model" + "github.com/easy-cloud-Knet/KWS_Control/client/model" "github.com/easy-cloud-Knet/KWS_Control/structure" "github.com/easy-cloud-Knet/KWS_Control/util" "github.com/redis/go-redis/v9" diff --git a/service/vm.go b/service/vm.go index 67f5ce7..30cd79d 100644 --- a/service/vm.go +++ b/service/vm.go @@ -10,8 +10,10 @@ import ( "strings" "time" - "github.com/easy-cloud-Knet/KWS_Control/request" - "github.com/easy-cloud-Knet/KWS_Control/request/model" + "github.com/easy-cloud-Knet/KWS_Control/client" + "github.com/easy-cloud-Knet/KWS_Control/client/model" + "github.com/easy-cloud-Knet/KWS_Control/pkg/guacamole" + internalssh "github.com/easy-cloud-Knet/KWS_Control/pkg/ssh" "github.com/easy-cloud-Knet/KWS_Control/util" "github.com/redis/go-redis/v9" @@ -62,6 +64,7 @@ func CreateVM(w http.ResponseWriter, r *http.Request, contextStruct *vms.Control aliveCount := 0 for i := range contextStruct.Cores { + core := &contextStruct.Cores[i] log.DebugInfo("core %s checking: FreeMemory=%d, FreeCPU=%d, FreeDisk=%d, IsAlive=%t", core.IP, core.FreeMemory, core.FreeCPU, core.FreeDisk, core.IsAlive) @@ -118,7 +121,7 @@ func CreateVM(w http.ResponseWriter, r *http.Request, contextStruct *vms.Control return errors.New("selectedCore == nil") } - var privateKeyPEM, publicKeyOpenSSH, err = GenerateSshKey() + var privateKeyPEM, publicKeyOpenSSH, err = internalssh.GenerateSSHKey() if err != nil { log.Error("GenerateSshKey() failed: %v", err, true) return err @@ -136,7 +139,7 @@ func CreateVM(w http.ResponseWriter, r *http.Request, contextStruct *vms.Control cleanup := func() { if guacamoleConfigured { log.Info("clean up clean up") - if cleanupErr := CleanupGuacamoleConfig(string(uuid), contextStruct.GuacDB); cleanupErr != nil { + if cleanupErr := guacamole.Cleanup(string(uuid), contextStruct.GuacDB); cleanupErr != nil { log.Error("Failed to cleanup Guacamole config during rollback: %v", cleanupErr) } } @@ -169,7 +172,7 @@ func CreateVM(w http.ResponseWriter, r *http.Request, contextStruct *vms.Control fmt.Printf("%s\n", cmsResp.MacAddr) fmt.Printf("%s\n", cmsResp.SdnUUID) - userPass := GuacamoleConfig(req.Users[0].Name, string(req.UUID), cmsResp.IP, privateKeyPEM, contextStruct.GuacDB) + userPass := guacamole.Configure(req.Users[0].Name, string(req.UUID), cmsResp.IP, privateKeyPEM, contextStruct.GuacDB) if userPass == "" { log.Error("Failed to configure Guacamole", true) @@ -222,8 +225,8 @@ func CreateVM(w http.ResponseWriter, r *http.Request, contextStruct *vms.Control } // Redis 저장 완료 후 HTTP 전송 (Core에서 Redis 업데이트 가능) - client := request.NewCoreClient(selectedCore) - _, err = client.CreateVM(context.Background(), req) + coreClient := client.NewCoreClient(selectedCore) + _, err = coreClient.CreateVM(context.Background(), req) if err != nil { log.Error("Error creating VM on core %s: %v", selectedCore.IP, err, true) cleanup() // 직접 지우지 말고 요 함수 하나로-- @@ -263,8 +266,8 @@ func DeleteVM(uuid vms.UUID, contextStruct *vms.ControlContext, rdb *redis.Clien return fmt.Errorf("VM with UUID %s not found", string(uuid)) } - client := request.NewCoreClient(core) - _, err := client.DeleteVM(context.Background(), model.DeleteVMRequest{ + coreClient := client.NewCoreClient(core) + _, err := coreClient.DeleteVM(context.Background(), model.DeleteVMRequest{ UUID: uuid, Type: model.HardDelete, }) @@ -278,7 +281,7 @@ func DeleteVM(uuid vms.UUID, contextStruct *vms.ControlContext, rdb *redis.Clien log.Error("error deleting instance %s from ControlContext: %v", uuid, err) return err } - if cleanupErr := CleanupGuacamoleConfig(string(uuid), contextStruct.GuacDB); cleanupErr != nil { + if cleanupErr := guacamole.Cleanup(string(uuid), contextStruct.GuacDB); cleanupErr != nil { log.Error("Failed to cleanup Guacamole config during rollback: %v", cleanupErr) } @@ -298,8 +301,8 @@ func StartVM(uuid vms.UUID, contextStruct *vms.ControlContext) error { return fmt.Errorf("VM with UUID %s not found", string(uuid)) } - client := request.NewCoreClient(core) - _, err := client.StartVM(context.Background(), model.StartVMRequest{ + coreClient := client.NewCoreClient(core) + _, err := coreClient.StartVM(context.Background(), model.StartVMRequest{ UUID: uuid, }) if err != nil { @@ -316,8 +319,8 @@ func ShutdownVM(uuid vms.UUID, contextStruct *vms.ControlContext, rdb *redis.Cli return fmt.Errorf("VM with UUID %s not found", string(uuid)) } - client := request.NewCoreClient(core) - _, err := client.ForceShutdownVM(context.Background(), model.ForceShutdownVMRequest{ + coreClient := client.NewCoreClient(core) + _, err := coreClient.ForceShutdownVM(context.Background(), model.ForceShutdownVMRequest{ UUID: uuid, }) @@ -355,9 +358,9 @@ func GetVMCpuInfo(uuid vms.UUID, contextStruct *vms.ControlContext) (model.CoreM return model.CoreMachineCpuInfoResponse{}, errors.New(msg) } - client := request.NewCoreClient(core) + coreClient := client.NewCoreClient(core) - cpuInfo, err := client.GetVMCpuInfo(context.Background(), uuid) + cpuInfo, err := coreClient.GetVMCpuInfo(context.Background(), uuid) if err != nil { msg := fmt.Sprintf("Error getting CPU info for VM %s on core %s: %v", uuid, core.IP, err) log.Error(msg, true) @@ -378,9 +381,9 @@ func GetVMMemoryInfo(uuid vms.UUID, contextStruct *vms.ControlContext) (model.Co return model.CoreMachineMemoryInfoResponse{}, errors.New(msg) } - client := request.NewCoreClient(core) + coreClient := client.NewCoreClient(core) - memoryInfo, err := client.GetVMMemoryInfo(context.Background(), uuid) + memoryInfo, err := coreClient.GetVMMemoryInfo(context.Background(), uuid) if err != nil { msg := fmt.Sprintf("Error getting memory info for VM %s on core %s: %v", uuid, core.IP, err) log.Error(msg, true) @@ -401,9 +404,9 @@ func GetVMDiskInfo(uuid vms.UUID, contextStruct *vms.ControlContext) (model.Core return model.CoreMachineDiskInfoResponse{}, errors.New(msg) } - client := request.NewCoreClient(core) + coreClient := client.NewCoreClient(core) - diskInfo, err := client.GetVMDiskInfo(context.Background(), uuid) + diskInfo, err := coreClient.GetVMDiskInfo(context.Background(), uuid) if err != nil { msg := fmt.Sprintf("Error getting disk info for VM %s on core %s: %v", uuid, core.IP, err) log.Error(msg, true) diff --git a/startup/init.go b/startup/init.go index 9cce1e3..1c3ebd9 100644 --- a/startup/init.go +++ b/startup/init.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/easy-cloud-Knet/KWS_Control/request" + "github.com/easy-cloud-Knet/KWS_Control/client" "github.com/easy-cloud-Knet/KWS_Control/structure" "golang.org/x/sync/errgroup" @@ -201,14 +201,14 @@ func InitializeCoreData(configPath string) (structure.ControlContext, error) { currentCore := core g.Go(func() error { - client := request.NewCoreClient(currentCore) + coreClient := client.NewCoreClient(currentCore) - memResp, err := client.GetCoreMachineMemoryInfo(ctx) + memResp, err := coreClient.GetCoreMachineMemoryInfo(ctx) if err != nil { currentCore.IsAlive = false return fmt.Errorf("failed to get Memory info for core %s:%d: %w", currentCore.IP, currentCore.Port, err) } - diskResp, err := client.GetCoreMachineDiskInfo(ctx) + diskResp, err := coreClient.GetCoreMachineDiskInfo(ctx) if err != nil { currentCore.IsAlive = false return fmt.Errorf("failed to get Disk info for core %s:%d: %w", currentCore.IP, currentCore.Port, err) diff --git a/tests/blackbox_io_test.sh b/tests/blackbox_io_test.sh new file mode 100755 index 0000000..405b7b3 --- /dev/null +++ b/tests/blackbox_io_test.sh @@ -0,0 +1,417 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================ +# KWS_Control Black-Box Integration Test +# ============================================================ + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +COMPOSE_FILE="$SCRIPT_DIR/docker-compose.test.yml" +PROJECT_NAME="kws-test" +BASE_URL="http://localhost:18081" +REDIS_CONTAINER="kws-test-redis" +MAX_WAIT=90 + +# --- counters --- +PASS=0 +FAIL=0 +TOTAL=0 + +# --- colors --- +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# ============================================================ +# Helper functions +# ============================================================ + +pass() { + ((PASS++)) || true + ((TOTAL++)) || true + echo -e " ${GREEN}PASS${NC} $1" +} + +fail() { + ((FAIL++)) || true + ((TOTAL++)) || true + echo -e " ${RED}FAIL${NC} $1 (expected=$2, got=$3)" +} + +section() { + echo -e "\n${CYAN}${BOLD}=== $1 ===${NC}" +} + +# assert_status +assert_status() { + local desc="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + pass "$desc" + else + fail "$desc" "$expected" "$actual" + fi +} + +# assert_body_contains +assert_body_contains() { + local desc="$1" substr="$2" body="$3" + if echo "$body" | grep -q "$substr"; then + pass "$desc" + else + fail "$desc" "body contains '$substr'" "body='$body'" + fi +} + +# http [body] -> sets HTTP_CODE and HTTP_BODY +http() { + local method="$1" path="$2" body="${3:-}" + local tmp + tmp=$(mktemp) + + if [ -n "$body" ]; then + HTTP_CODE=$(curl -s -o "$tmp" -w "%{http_code}" \ + -X "$method" \ + -H "Content-Type: application/json" \ + -d "$body" \ + "${BASE_URL}${path}" 2>/dev/null) || HTTP_CODE="000" + else + HTTP_CODE=$(curl -s -o "$tmp" -w "%{http_code}" \ + -X "$method" \ + "${BASE_URL}${path}" 2>/dev/null) || HTTP_CODE="000" + fi + + HTTP_BODY=$(cat "$tmp" 2>/dev/null || echo "") + rm -f "$tmp" +} + +# redis_exec -> runs redis-cli inside the redis container +redis_exec() { + docker exec "$REDIS_CONTAINER" redis-cli "$@" 2>/dev/null +} + +# ============================================================ +# Lifecycle +# ============================================================ + +cleanup() { + echo -e "\n${YELLOW}Cleaning up test environment...${NC}" + docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" down -v --remove-orphans 2>/dev/null || true +} +trap cleanup EXIT + +echo -e "${BOLD}Starting test environment...${NC}" +docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build + +echo -n "Waiting for KWS_Control to be ready" +for i in $(seq 1 $MAX_WAIT); do + if curl -sf -o /dev/null -X GET -H "Content-Type: application/json" -d '{"uuid":"healthcheck"}' "${BASE_URL}/vm/info" 2>/dev/null; then + echo -e " ${GREEN}ready${NC} (${i}s)" + break + fi + # 404 also means the service is up (vm not found in redis) + code=$(curl -s -o /dev/null -w "%{http_code}" -X GET -H "Content-Type: application/json" -d '{"uuid":"healthcheck"}' "${BASE_URL}/vm/info" 2>/dev/null) || code="000" + if [ "$code" = "404" ]; then + echo -e " ${GREEN}ready${NC} (${i}s)" + break + fi + if [ "$i" -eq "$MAX_WAIT" ]; then + echo -e " ${RED}TIMEOUT${NC}" + echo "Container logs:" + docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" logs control-test 2>/dev/null | tail -50 + exit 1 + fi + echo -n "." + sleep 1 +done + +# ============================================================ +# Section 1: HTTP Method Routing +# ============================================================ +section "Section 1: HTTP Method Routing" + +http GET "/vm" +assert_status "GET /vm should be 405" "405" "$HTTP_CODE" + +http PUT "/vm" '{"test":true}' +assert_status "PUT /vm should be 405" "405" "$HTTP_CODE" + +http GET "/vm/shutdown" +assert_status "GET /vm/shutdown should be 405" "405" "$HTTP_CODE" + +http DELETE "/vm/redis" +assert_status "DELETE /vm/redis should be 405" "405" "$HTTP_CODE" + +http POST "/vm/status" '{"uuid":"test","type":"cpu"}' +assert_status "POST /vm/status should be 405" "405" "$HTTP_CODE" + +http POST "/vm/connect" +assert_status "POST /vm/connect should be 405" "405" "$HTTP_CODE" + +http POST "/vm/info" '{"uuid":"test"}' +assert_status "POST /vm/info should be 405" "405" "$HTTP_CODE" + +http DELETE "/vm/start" +assert_status "DELETE /vm/start should be 405" "405" "$HTTP_CODE" + +# ============================================================ +# Section 2: Invalid JSON Body +# ============================================================ +section "Section 2: Invalid JSON Body" + +http POST "/vm" '{invalid-json}' +assert_status "POST /vm with invalid JSON should be 405" "405" "$HTTP_CODE" + +http DELETE "/vm" 'not-json' +assert_status "DELETE /vm with invalid JSON should be 400" "400" "$HTTP_CODE" + +http POST "/vm/shutdown" '{bad' +assert_status "POST /vm/shutdown with invalid JSON should be 400" "400" "$HTTP_CODE" + +http POST "/vm/start" '!!!' +assert_status "POST /vm/start with invalid JSON should be 400" "400" "$HTTP_CODE" + +http GET "/vm/status" 'null' +assert_status "GET /vm/status with invalid JSON should be 400" "400" "$HTTP_CODE" + +http POST "/vm/redis" '{nope' +assert_status "POST /vm/redis with invalid JSON should be 400" "400" "$HTTP_CODE" + +http GET "/vm/info" 'xyz' +assert_status "GET /vm/info with invalid JSON should be 400" "400" "$HTTP_CODE" + +# ============================================================ +# Section 3: Field Validation +# ============================================================ +section "Section 3: Field Validation" + +http GET "/vm/status" '{"uuid":"test-uuid"}' +assert_status "GET /vm/status without type field should be 400" "400" "$HTTP_CODE" + +http GET "/vm/status" '{"uuid":"test-uuid","type":"network"}' +assert_status "GET /vm/status with invalid type should be 400" "400" "$HTTP_CODE" + +# vmConnect uses query parameter +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${BASE_URL}/vm/connect" 2>/dev/null) || HTTP_CODE="000" +assert_status "GET /vm/connect without uuid param should be 400" "400" "$HTTP_CODE" + +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${BASE_URL}/vm/connect?uuid=" 2>/dev/null) || HTTP_CODE="000" +assert_status "GET /vm/connect with empty uuid should be 400" "400" "$HTTP_CODE" + +# ============================================================ +# Section 4: Non-existent UUID (Core-dependent endpoints) +# ============================================================ +section "Section 4: Non-existent UUID" + +http DELETE "/vm" '{"uuid":"nonexistent-uuid-0000"}' +assert_status "DELETE /vm with unknown UUID should be 500" "500" "$HTTP_CODE" + +http POST "/vm/shutdown" '{"uuid":"nonexistent-uuid-0000"}' +assert_status "POST /vm/shutdown with unknown UUID should be 500" "500" "$HTTP_CODE" + +http POST "/vm/start" '{"uuid":"nonexistent-uuid-0000"}' +assert_status "POST /vm/start with unknown UUID should be 500" "500" "$HTTP_CODE" + +http GET "/vm/status" '{"uuid":"nonexistent-uuid-0000","type":"cpu"}' +assert_status "GET /vm/status with unknown UUID should be 500" "500" "$HTTP_CODE" + +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${BASE_URL}/vm/connect?uuid=nonexistent-uuid-0000" 2>/dev/null) || HTTP_CODE="000" +assert_status "GET /vm/connect with unknown UUID should be 500" "500" "$HTTP_CODE" + +# ============================================================ +# Section 5: Happy Path - VM Lifecycle +# ============================================================ +section "Section 5: Happy Path - VM Lifecycle" + +HAPPY_UUID="happy-test-$(date +%s)" + +# 5-1. Create VM +echo -e " ${CYAN}-- Create VM --${NC}" +http POST "/vm" "{ + \"domType\": \"kvm\", + \"domName\": \"test-vm-happy\", + \"uuid\": \"${HAPPY_UUID}\", + \"os\": \"ubuntu\", + \"HWInfo\": {\"cpu\": 2, \"memory\": 2048, \"disk\": 20}, + \"network\": {\"ips\": [], \"NetType\": 0}, + \"users\": [{\"name\": \"ubuntu\", \"groups\": \"sudo\", \"passWord\": \"testpass\", \"ssh\": []}], + \"Subnettype\": \"\" +}" +assert_status "POST /vm create VM should be 201" "201" "$HTTP_CODE" + +# 5-2. Verify VM info in Redis after creation +http GET "/vm/info" "{\"uuid\":\"${HAPPY_UUID}\"}" +assert_status "GET /vm/info after create should be 200" "200" "$HTTP_CODE" +assert_body_contains "VM info has correct uuid" "$HAPPY_UUID" "$HTTP_BODY" +assert_body_contains "VM info has correct cpu" '"cpu":2' "$HTTP_BODY" +assert_body_contains "VM info has correct memory" '"memory":2048' "$HTTP_BODY" +assert_body_contains "VM info has correct disk" '"disk":20' "$HTTP_BODY" + +# 5-3. Start VM +echo -e "\n ${CYAN}-- Start VM --${NC}" +http POST "/vm/start" "{\"uuid\":\"${HAPPY_UUID}\"}" +assert_status "POST /vm/start should be 200" "200" "$HTTP_CODE" + +# 5-4. Get VM status (cpu) +echo -e "\n ${CYAN}-- VM Status --${NC}" +http GET "/vm/status" "{\"uuid\":\"${HAPPY_UUID}\",\"type\":\"cpu\"}" +assert_status "GET /vm/status cpu should be 200" "200" "$HTTP_CODE" + +http GET "/vm/status" "{\"uuid\":\"${HAPPY_UUID}\",\"type\":\"memory\"}" +assert_status "GET /vm/status memory should be 200" "200" "$HTTP_CODE" + +http GET "/vm/status" "{\"uuid\":\"${HAPPY_UUID}\",\"type\":\"disk\"}" +assert_status "GET /vm/status disk should be 200" "200" "$HTTP_CODE" + +# 5-5. Shutdown VM +echo -e "\n ${CYAN}-- Shutdown VM --${NC}" +http POST "/vm/shutdown" "{\"uuid\":\"${HAPPY_UUID}\"}" +assert_status "POST /vm/shutdown should be 200" "200" "$HTTP_CODE" + +# Verify Redis status updated to "stopped end" after shutdown +redis_val=$(redis_exec GET "$HAPPY_UUID") +if echo "$redis_val" | grep -q '"status":"stopped end"'; then + pass "Redis status is 'stopped end' after shutdown" +else + fail "Redis status should be 'stopped end' after shutdown" '"status":"stopped end"' "$redis_val" +fi + +# 5-6. Delete VM +echo -e "\n ${CYAN}-- Delete VM --${NC}" +http DELETE "/vm" "{\"uuid\":\"${HAPPY_UUID}\"}" +assert_status "DELETE /vm should be 200" "200" "$HTTP_CODE" + +# Verify VM removed from Redis after deletion +http GET "/vm/info" "{\"uuid\":\"${HAPPY_UUID}\"}" +assert_status "GET /vm/info after delete should be 404" "404" "$HTTP_CODE" + +# ============================================================ +# Section 6: Redis Endpoints (E2E) +# ============================================================ +section "Section 6: Redis Endpoints" + +# 6a. Seed Redis with test VM data +echo -e " ${YELLOW}Seeding Redis with test data...${NC}" + +redis_exec SET "test-uuid-001" \ + '{"uuid":"test-uuid-001","cpu":4,"memory":8192,"disk":40960,"ip":"10.0.0.50","status":"unknown","time":1700000000}' \ + > /dev/null + +redis_exec SET "test-uuid-002" \ + '{"uuid":"test-uuid-002","cpu":2,"memory":4096,"disk":20480,"ip":"10.0.0.99","status":"prepare begin","time":1700000000}' \ + > /dev/null + +# 6b. POST /vm/redis -- Update VM status +echo -e "\n ${CYAN}-- POST /vm/redis tests --${NC}" + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"started begin"}' +assert_status "Update status to 'started begin' should be 200" "200" "$HTTP_CODE" +assert_body_contains "Response body contains 'VM status updated'" "VM status updated" "$HTTP_BODY" + +# Verify in Redis +redis_val=$(redis_exec GET "test-uuid-001") +if echo "$redis_val" | grep -q '"status":"started begin"'; then + pass "Redis value has status 'started begin'" +else + fail "Redis value should have status 'started begin'" '"status":"started begin"' "$redis_val" +fi + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"stopped end"}' +assert_status "Update status to 'stopped end' should be 200" "200" "$HTTP_CODE" + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"prepare begin"}' +assert_status "Update status to 'prepare begin' should be 200" "200" "$HTTP_CODE" + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"start begin"}' +assert_status "Update status to 'start begin' should be 200" "200" "$HTTP_CODE" + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"release end"}' +assert_status "Update status to 'release end' should be 200" "200" "$HTTP_CODE" + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"migrate begin"}' +assert_status "Update status to 'migrate begin' should be 200" "200" "$HTTP_CODE" + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"restort begin"}' +assert_status "Update status to 'restort begin' should be 200" "200" "$HTTP_CODE" + +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"unknown"}' +assert_status "Update status to 'unknown' should be 200" "200" "$HTTP_CODE" + +# Invalid status -> normalized to "unknown" +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"bogus-status"}' +assert_status "Update with invalid status should be 200 (normalized)" "200" "$HTTP_CODE" + +redis_val=$(redis_exec GET "test-uuid-001") +if echo "$redis_val" | grep -q '"status":"unknown"'; then + pass "Invalid status normalized to 'unknown' in Redis" +else + fail "Invalid status should normalize to 'unknown'" '"status":"unknown"' "$redis_val" +fi + +# Empty status -> normalized to "unknown" +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":""}' +assert_status "Update with empty status should be 200 (normalized)" "200" "$HTTP_CODE" + +# "null" string -> normalized to "unknown" +http POST "/vm/redis" '{"UUID":"test-uuid-001","status":"null"}' +assert_status "Update with 'null' string status should be 200 (normalized)" "200" "$HTTP_CODE" + +# Non-existent UUID -> 500 (UpdateVMStatusInRedis requires existing data) +http POST "/vm/redis" '{"UUID":"no-such-uuid-999","status":"started begin"}' +assert_status "Update status for non-existent UUID should be 500" "500" "$HTTP_CODE" + +# 6c. GET /vm/info tests +echo -e "\n ${CYAN}-- GET /vm/info tests --${NC}" + +http GET "/vm/info" '{"uuid":"test-uuid-001"}' +assert_status "Get info for existing UUID should be 200" "200" "$HTTP_CODE" +assert_body_contains "Response contains uuid field" "test-uuid-001" "$HTTP_BODY" +assert_body_contains "Response contains cpu field" '"cpu":4' "$HTTP_BODY" +assert_body_contains "Response contains memory field" '"memory":8192' "$HTTP_BODY" +assert_body_contains "Response contains disk field" '"disk":40960' "$HTTP_BODY" +assert_body_contains "Response contains ip field" '"ip":"10.0.0.50"' "$HTTP_BODY" + +http GET "/vm/info" '{"uuid":"does-not-exist"}' +assert_status "Get info for non-existent UUID should be 404" "404" "$HTTP_CODE" + +http GET "/vm/info" '{}' +assert_status "Get info with empty body should be 404" "404" "$HTTP_CODE" + +# 6d. Write-then-read flow +echo -e "\n ${CYAN}-- Write-Read Flow --${NC}" + +http POST "/vm/redis" '{"UUID":"test-uuid-002","status":"started begin"}' +assert_status "Flow: update test-uuid-002 status should be 200" "200" "$HTTP_CODE" + +http GET "/vm/info" '{"uuid":"test-uuid-002"}' +assert_status "Flow: read test-uuid-002 info should be 200" "200" "$HTTP_CODE" +assert_body_contains "Flow: response has correct cpu" '"cpu":2' "$HTTP_BODY" +assert_body_contains "Flow: response has correct ip" '"ip":"10.0.0.99"' "$HTTP_BODY" + +redis_val=$(redis_exec GET "test-uuid-002") +if echo "$redis_val" | grep -q '"status":"started begin"'; then + pass "Flow: Redis confirms status updated to 'started begin'" +else + fail "Flow: Redis should have status 'started begin'" '"status":"started begin"' "$redis_val" +fi + +# ============================================================ +# Results +# ============================================================ +section "Test Results" + +echo -e " Total: ${BOLD}${TOTAL}${NC}" +echo -e " Passed: ${GREEN}${PASS}${NC}" +echo -e " Failed: ${RED}${FAIL}${NC}" +echo "" + +if [ "$FAIL" -gt 0 ]; then + echo -e "${RED}${BOLD}SOME TESTS FAILED${NC}" + exit 1 +else + echo -e "${GREEN}${BOLD}ALL TESTS PASSED${NC}" + exit 0 +fi diff --git a/tests/docker-compose.test.yml b/tests/docker-compose.test.yml new file mode 100644 index 0000000..f596fd7 --- /dev/null +++ b/tests/docker-compose.test.yml @@ -0,0 +1,65 @@ +services: + mysql-test: + image: mysql:8.0 + container_name: kws-test-mysql + environment: + MYSQL_ROOT_PASSWORD: testpass + volumes: + - ./init-test-db.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ptestpass"] + interval: 3s + timeout: 5s + retries: 20 + + redis-test: + image: redis:7-alpine + container_name: kws-test-redis + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 2s + timeout: 3s + retries: 10 + + mock-core: + image: python:3.12-alpine + container_name: kws-test-mock-core + working_dir: /app + volumes: + - ./mock_core.py:/app/mock_core.py + command: ["python", "mock_core.py"] + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/')"] + interval: 2s + timeout: 3s + retries: 10 + + control-test: + build: + context: .. + dockerfile: Dockerfile + container_name: kws-test-control + ports: + - "18081:8081" + environment: + DB_USER: root + DB_PASSWORD: testpass + DB_HOST: mysql-test + DB_PORT: "3306" + DB_NAME: core_base + GUAC_DB_USER: root + GUAC_DB_PASSWORD: testpass + GUAC_DB_HOST: mysql-test + GUAC_DB_PORT: "3306" + GUAC_DB_NAME: guacamole_db + REDIS_HOST: redis-test:6379 + CORES: mock-core:8080 + GUACAMOLE_BASE_URL: http://mock-core:8080/guacamole + CMS_HOST: mock-core:8080 + depends_on: + mysql-test: + condition: service_healthy + redis-test: + condition: service_healthy + mock-core: + condition: service_healthy diff --git a/tests/init-test-db.sql b/tests/init-test-db.sql new file mode 100644 index 0000000..eb68065 --- /dev/null +++ b/tests/init-test-db.sql @@ -0,0 +1,87 @@ +-- =========================================== +-- KWS_Control Integration Test DB Init +-- =========================================== + +-- ---- core_base database ---- +CREATE DATABASE IF NOT EXISTS core_base; +USE core_base; + +CREATE TABLE IF NOT EXISTS subnet ( + id INT PRIMARY KEY, + last_subnet VARCHAR(64) NOT NULL +); +INSERT INTO subnet (id, last_subnet) VALUES (1, '10.0.0.1'); + +CREATE TABLE IF NOT EXISTS inst_info ( + uuid VARCHAR(128) PRIMARY KEY, + inst_ip VARCHAR(64), + guac_pass VARCHAR(256), + inst_mem INT, + inst_vcpu INT, + inst_disk INT +); + +CREATE TABLE IF NOT EXISTS inst_loc ( + uuid VARCHAR(128) PRIMARY KEY, + core INT +); + +-- ---- guacamole_db database ---- +CREATE DATABASE IF NOT EXISTS guacamole_db; +USE guacamole_db; + +CREATE TABLE IF NOT EXISTS guacamole_entity ( + entity_id INT(11) NOT NULL AUTO_INCREMENT, + name VARCHAR(128) NOT NULL, + type ENUM('USER') NOT NULL, + PRIMARY KEY (entity_id), + UNIQUE KEY UK_guacamole_entity_name_scope (type, name) +); + +CREATE TABLE IF NOT EXISTS guacamole_user ( + user_id INT(11) NOT NULL AUTO_INCREMENT, + entity_id INT(11) NOT NULL, + password_hash BINARY(32) NOT NULL, + password_salt BINARY(32), + password_date DATETIME NOT NULL, + full_name VARCHAR(256), + PRIMARY KEY (user_id), + UNIQUE KEY UK_guacamole_user_single_entity (entity_id), + CONSTRAINT FK_guacamole_user_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS guacamole_connection ( + connection_id INT(11) NOT NULL AUTO_INCREMENT, + connection_name VARCHAR(128) NOT NULL, + protocol VARCHAR(32) NOT NULL, + PRIMARY KEY (connection_id) +); + +CREATE TABLE IF NOT EXISTS guacamole_connection_parameter ( + connection_id INT(11) NOT NULL, + parameter_name VARCHAR(128) NOT NULL, + parameter_value VARCHAR(4096) NOT NULL, + PRIMARY KEY (connection_id, parameter_name), + CONSTRAINT FK_guacamole_connection_parameter_connection + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS guacamole_connection_permission ( + entity_id INT(11) NOT NULL, + connection_id INT(11) NOT NULL, + permission ENUM('READ') NOT NULL, + PRIMARY KEY (entity_id, connection_id, permission), + CONSTRAINT FK_guacamole_connection_permission_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) + ON DELETE CASCADE, + CONSTRAINT FK_guacamole_connection_permission_connection + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) + ON DELETE CASCADE +); diff --git a/tests/mock_core.py b/tests/mock_core.py new file mode 100644 index 0000000..cc5255a --- /dev/null +++ b/tests/mock_core.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Minimal mock server for KWS_Core. Responds to /getStatusHost and other paths.""" + +import json +from http.server import HTTPServer, BaseHTTPRequestHandler + +MEMORY_RESP = { + "information": { + "total_gb": 64, + "used_gb": 16, + "available_gb": 48, + "used_percent": 25.0, + }, + "message": "Host Status Return operation success", +} + +DISK_RESP = { + "information": { + "total_gb": 500, + "used_gb": 100, + "free_gb": 400, + "used_percent": 20.0, + }, + "message": "Host Status Return operation success", +} + +CPU_RESP = { + "information": { + "system_time": 100.0, + "idle_time": 5000.0, + "usage_percent": 5.0, + }, + "message": "Host Status Return operation success", +} + +GENERIC_RESP = {"message": "mock ok"} + +# CMS mock response for POST /New/Instance +CMS_RESP = { + "ip": "10.0.1.100", + "macAddr": "52:54:00:aa:bb:cc", + "sdnUUID": "mock-sdn-uuid-0001", +} + +# VM status mock for GET /getStatusUUID +VM_STATUS_RESP = { + "information": { + "system_time": 50.0, + "idle_time": 3000.0, + "usage_percent": 3.5, + }, + "message": "domain Status UUID operation success", +} + + +class Handler(BaseHTTPRequestHandler): + def _read_body(self): + length = int(self.headers.get("Content-Length", 0)) + if length: + return json.loads(self.rfile.read(length)) + return {} + + def _respond(self, data, code=200): + body = json.dumps(data).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.path == "/getStatusHost": + body = self._read_body() + dt = body.get("host_dataType", -1) + if dt == 0: + return self._respond(CPU_RESP) + elif dt == 1: + return self._respond(MEMORY_RESP) + elif dt == 2: + return self._respond(DISK_RESP) + else: + return self._respond(MEMORY_RESP) + elif self.path == "/getStatusUUID": + return self._respond(VM_STATUS_RESP) + self._respond(GENERIC_RESP) + + def do_POST(self): + self._read_body() + if self.path == "/New/Instance": + return self._respond(CMS_RESP) + self._respond(GENERIC_RESP) + + def do_DELETE(self): + self._read_body() + self._respond(GENERIC_RESP) + + def log_message(self, fmt, *args): + print(f"[mock-core] {fmt % args}") + + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", 8080), Handler) + print("[mock-core] Listening on :8080") + server.serve_forever() diff --git a/util/logger.go b/util/logger.go index 945addc..d43c968 100644 --- a/util/logger.go +++ b/util/logger.go @@ -293,6 +293,7 @@ func parseLogArgs(args ...interface{}) (string, bool) { return message, save } +// 현재 미사용중 func GetEnhancedLogger() *Logger { return NewEnhancedLogger() } diff --git a/util/util.go b/util/util.go index df323f4..13caf0e 100644 --- a/util/util.go +++ b/util/util.go @@ -4,6 +4,8 @@ import ( "net/http" ) +// 현재 미사용중 +// 얜 뭐임? 어따쓰는거지? func CheckMethod(w http.ResponseWriter, r *http.Request, expectedMethod string) bool { if r.Method != expectedMethod { log := GetLogger()