From 5a96e73e156f29063ebecae0ae1601bd11cf84d3 Mon Sep 17 00:00:00 2001 From: ga111o Date: Tue, 16 Jun 2026 17:32:54 +0900 Subject: [PATCH 1/2] docs: update README.md --- README.md | 341 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 296 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index fd54678..76d3c4a 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,320 @@ # KWS Control -KWS Control +- Core 노드 - 실제로 VM을 구동하는 워커 머신. Control이 HTTP로 명령을 보냄 +- CMS - OVN 기반 가상 네트워크 자동화 서비스(OVS/OVN). VM 생성/삭제 시 IP / MAC / 서브넷(SDN UUID) 할당·해제 +- Apache Guacamole - 브라우저에서 VM에 SSH로 접속하게 해주는 게이트웨이 +- MySQL - 인스턴스 정보·서브넷 상태 영속화(메인 DB) + Guacamole 접속 설정 저장(Guacamole DB) +- Redis - VM의 최신 상태(상태값/타임스탬프) 캐시 -## system requirements +--- -- Go >= 1.21 -- libvirt -- Docker +## 목차 -## run +1. [아키텍처](#아키텍처) +2. [지원 환경](#지원-환경) +3. [실행](#실행) +4. [설정(Configuration)](#설정configuration) +5. [HTTP API](#http-api) +6. [데이터 저장소](#데이터-저장소) +7. [프로젝트 구조](#프로젝트-구조) +8. [테스트](#테스트) +9. [배포 (CI/CD)](#배포-cicd) -### 1. dependencies +--- -#### Ubuntu/Debian -```sh -sudo apt install libvirt-dev pkg-config -``` +## 아키텍처 -#### CentOS/RHEL -```sh -sudo yum install libvirt-devel pkgconfig -``` +### VM 생성 흐름 -#### I use Arch BTW -```sh -sudo pacman -S libvirt qemu dnsmasq openbsd-netcat -``` +VM 생성(`POST /vm`)요청 받았을 때.(`service/vm.go` 의 `CreateVM`) + +1. 자원 요구량(CPU/MEM/DISK)을 만족하는 Core 선택 (`structure/resource_manager.go`) +2. VM 접속용 SSH 키쌍 생성 (`pkg/ssh`) +3. CMS에 서브넷/IP 할당 요청 (`Add` = 기존 서브넷 재사용 / 그 외 = 신규 서브넷) +4. Guacamole DB에 접속 설정 기록 (`pkg/guacamole`) +5. Control 인메모리 자원 테이블 갱신 -> Redis에 초기 상태 저장 +6. Core에 VM 생성 요청 (`client.CoreClient`) +7. MySQL에 인스턴스 정보 영속화 + +Guacamole 설정·인메모리 자원 할당 단계는 `cleanupChain`(`service/cleanup.go`)에 롤백 함수를 등록하여, 이후 단계 실패 시 역순으로 정리. +단, CMS 서브넷/IP 할당은 아직 롤백이 등록되지 않아(`service/vm.go` 의 TODO), 이후 단계 실패 시 잡은 IP/서브넷은 수동 정리가 필요함. + +### 계층 구조 -### 3. Go mod download -```sh -go mod download +``` +api/ HTTP 핸들러 + 요청/응답 DTO (입력 검증, JSON 직렬화) + └─ service/ 비즈니스 로직 (오케스트레이션, 트랜잭션 흐름) + └─ client/ 외부 HTTP 클라이언트 (Core / CMS / Guacamole) + └─ structure/ 도메인 타입 · 인메모리 상태 · MySQL 영속화 + └─ pkg/ 순수 유틸리티 (crypto / ssh / network / guacamole DB) +util/ 로깅 · HTTP 응답 헬퍼 (전 계층 공용) +startup/ 부트스트랩 (config 읽기, DB/Redis 초기화, Core 폴링) ``` -## run +--- -### method1: build & run -```sh -make run +## 지원 환경 + +| 도구 | 버전 | 용도 | +| -------------- | ---------------------------- | -------------------------- | +| Go | 1.23 이상 (toolchain 1.23.4) | 빌드·로컬 실행 | +| Docker | 최신 | 컨테이너 실행·통합 테스트 | +| Docker Compose | v2 (`docker compose`) | 로컬 스택·테스트 스택 기동 | +| MySQL | 8.0 | 메인 DB + Guacamole DB | +| Redis | 7 | 상태 캐시 | + +- Core 노드, CMS, Apache Guacamole가 네트워크로 도달 가능해야 실제 VM 작업이 동작. +- Docker Compose는 Redis·MySQL·Control 만 제공하며, Core/CMS/Guacamole 는 `.env` 로 외부 주소를 지정해 연결. + +의존성 다운로드: `go mod download` -make build +--- -./kws +## 실행 -make clean +1. `.env.example` 참고하여 환경 변수 파일 작성 + +2. 실행 + + ```sh + docker compose up -d --build + ``` + + - `redis` (6379), `mysql` (3306), `control_dev` 컨테이너가 실행됨. + - MySQL 컨테이너는 첫 기동 시 `startup/init.sh` 가 자동 실행되어 DB·테이블 생성. + - Control API는 호스트의 `8082` 포트 → 컨테이너 `8081` 로 매핑됨. (`docker-compose.yml`) + +3. 동작 확인 + + ```sh + curl -i -X GET -H "Content-Type: application/json" \ + -d '{"uuid":"healthcheck"}' http://localhost:8082/vm/info + # 서비스가 떠 있으면 404(redis 없음) 또는 200 응답이 옴. + ``` + +--- + +## 설정 + +설정값은 환경 변수가 우선이고, 비어 있으면 `config.yaml` 값을 사용 (`startup/init.go`). +단, `REDIS_HOST` 는 예외로, 비어 있으면 `config.yaml` 의 `redis:` 가 아니라 하드코딩된 `localhost:6379` 로 폴백함 (`startup/redis.go`). + +### 환경 변수 (`.env.example` 참고) + +| 변수 | 설명 | +| --------------------------------------------------------------------- | ------------------------------------------------------------------ | +| `DB_USER` / `DB_PASSWORD` / `DB_HOST` / `DB_PORT` / `DB_NAME` | 메인 MySQL 접속 정보 (인스턴스·서브넷 저장) | +| `GUAC_DB_USER` / `GUAC_DB_PASSWORD` / `GUAC_DB_HOST` / `GUAC_DB_NAME` | Guacamole MySQL 접속 정보 | +| `REDIS_HOST` | Redis 주소 (예: `localhost:6379`) | +| `CORES` | Core 노드 주소 목록, 콤마 구분 (예: `10.0.0.5:8080,10.0.0.6:8080`) | +| `CMS_HOST` | CMS 서비스 주소 (예: `cms.internal:8080`) | +| `GUACAMOLE_BASE_URL` | Guacamole 베이스 URL (예: `http://host:8080/guacamole`) | + +### `resources/config.yaml` + +환경 변수가 없을 때 쓰이는 기본/폴백 설정. 구조(`structure/vm.go` 의 `Config`): + +```yaml +vm_internal_subnets: + - "127.0.0.1/24" +cores: + - "100.95.253.74:8080" # CORES 환경변수가 우선 +port: 8081 +db: # 메인 DB (DB_* 환경변수가 우선) + user: "root" + password: "password" + host: "100.101.247.128" + port: 3306 + name: "db" +guac_db: # Guacamole DB (GUAC_DB_* 환경변수가 우선) + user: "root" + password: "password" + host: "100.101.247.128" + port: 3306 + name: "guacamole_db" ``` -### method2: docker -```sh -docker build -t kws-control . +> `config.yaml`(루트), `.env`, `logs/`, 빌드 산출물(`kws`)은 `.gitignore` 처리되어 있음. + +--- -docker run -p 8081:8081 kws-control +## HTTP API + +모든 라우트는 `api/handlers.go` 의 `Server()` 에 등록되며, `X-Content-Type-Options: nosniff` 헤더가 붙음. +기본 포트는 8081 (Compose dev 스택에서는 호스트 8082). + +| 메서드 | 경로 | 본문 / 파라미터 | 설명 | +| -------- | -------------- | ------------------------------------------- | ------------------------------------------ | +| `POST` | `/vm` | 아래 생성 본문 | VM 생성 | +| `DELETE` | `/vm` | `{"uuid":"..."}` | VM 삭제 (Core·CMS·Guacamole·DB·Redis 정리) | +| `POST` | `/vm/start` | `{"uuid":"..."}` | VM 시작(부팅) | +| `POST` | `/vm/shutdown` | `{"uuid":"..."}` | VM 강제 종료 | +| `GET` | `/vm/status` | `{"uuid":"...","type":"cpu\|memory\|disk"}` | Core에서 실시간 자원 사용량 조회 | +| `GET` | `/vm/info` | `{"uuid":"..."}` | Redis에 캐시된 VM 정보 조회 | +| `GET` | `/vm/connect` | `?uuid=...` (쿼리 파라미터) | Guacamole 인증 토큰 발급 | +| `POST` | `/vm/redis` | `{"UUID":"...","status":"..."}` | VM 상태값 갱신 (Core가 콜백) | + +### VM 생성 요청 본문 예시 (`POST /vm`) + +```json +{ + "domType": "kvm", + "domName": "my-vm", + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "os": "ubuntu-22.04", + "HWInfo": { "cpu": 2, "memory": 4096, "disk": 20480 }, + "network": { "ips": [] }, + "users": [ + { "name": "alice", "groups": "sudo", "passWord": "secret", "ssh": [] } + ], + "Subnettype": "New" +} ``` -## tree +- `HWInfo.memory`, `HWInfo.disk` 단위는 MiB, `cpu` 는 논리 코어 수. +- `cpu`·`memory`·`disk` 는 0이면 안 됨 (`api/create_vm.go`). `users` 는 최소 1명 필요 (`service/vm.go` 의 `CreateVM`). +- `Subnettype` 이 `"Add"` 면 기존 VM이 속한 서브넷을 재사용, 그 외 값이면 신규 서브넷을 할당함. +- 첫 번째 사용자에게는 Control이 생성한 SSH 공개키가 자동 주입됨. + +> 참고: `/vm/status`, `/vm/info` 는 GET이지만 JSON 본문을 읽고, `/vm/connect` 만 쿼리 파라미터(`uuid`)를 씀. + +VM 상태값(`/vm/redis`)은 `unknown`, `prepare begin`, `start begin`, `started begin`, `stopped end`, +`release end`, `migrate begin`, `restort begin` 중 하나로 정규화됨 (`api/update_redis.go`). + +--- + +## 데이터 저장소 + +### MySQL - 메인 DB (`core_base`) + +`startup/init.sh` / `tests/init-test-db.sql` 가 생성: + +- `subnet(id, last_subnet)` - 마지막으로 할당된 서브넷 추적 (신규 서브넷 계산용) +- `inst_info(uuid, inst_ip, guac_pass, inst_mem, inst_vcpu, inst_disk)` - 인스턴스 스펙 +- `inst_loc(uuid, core)` - 인스턴스가 위치한 Core 인덱스 + +### MySQL - Guacamole DB (`guacamole_db`) + +Apache Guacamole 표준 스키마(`guacamole_entity`, `guacamole_user`, `guacamole_connection`, +`guacamole_connection_parameter`, `guacamole_connection_permission`). 전체 DDL은 [database.md](database.md) 참고. +Control은 VM 생성 시 이 테이블에 직접 사용자·SSH 커넥션을 기록함 (`pkg/guacamole/config.go`). + +### Redis + +키 = VM UUID, 값 = `{uuid, cpu, memory, disk, ip, status, time}` JSON (`service/redis.go`). + +--- + +## 프로젝트 구조 ``` KWS_Control/ -|── api/ -│ |── server/ # HTTP API server -│ |── workercont/ # worker control -|── config/ # config -|── vm/ # VM management -|── util/ # utils -|── main.go # main.go -|── Dockerfile # dockerfile -|── Makefile # biuld script -|── go.mod # go module setting -``` \ No newline at end of file +├── main.go # 진입점: Redis·Core 데이터 초기화 후 HTTP 서버 기동 +├── Makefile # build / run / clean 타깃 (kws 바이너리) +├── Dockerfile # golang:1.23 기반 빌드 이미지 +├── docker-compose.yml # 로컬 스택: redis + mysql + control_dev +├── .env.example # 환경 변수 템플릿 +├── database.md # Guacamole DB 스키마(DDL) 참고 문서 +├── go.mod / go.sum # Go 모듈 정의 +│ +├── api/ # ── HTTP 계층 (핸들러 + 요청/응답 DTO) ── +│ ├── handlers.go # 라우터: Server(), 모든 라우트 등록 +│ ├── middleware.go # 공통 보안 헤더 미들웨어 +│ ├── create_vm.go # POST /vm VM 생성 +│ ├── delete_vm.go # DELETE /vm VM 삭제 +│ ├── start_vm.go # POST /vm/start VM 시작 +│ ├── shutdown_vm.go # POST /vm/shutdown VM 강제 종료 +│ ├── get_vm_status.go # GET /vm/status 실시간 자원 조회 +│ ├── get_vm_info.go # GET /vm/info Redis 캐시 조회 +│ ├── connect_vm.go # GET /vm/connect Guacamole 토큰 발급 +│ └── update_redis.go # POST /vm/redis 상태 갱신 + 상태 상수 +│ +├── service/ # ── 비즈니스 로직 (오케스트레이션) ── +│ ├── vm.go # CreateVM/DeleteVM/StartVM/ShutdownVM/Get*Info +│ ├── network.go # CMS 서브넷 할당·삭제(Add/New), VM IP 조회 +│ ├── guacamole.go # Guacamole 인증 토큰 발급 로직 +│ ├── redis.go # VM 정보 Redis 저장/조회/갱신/삭제 + 상태 상수 +│ ├── dto.go # 서비스 계층 입출력 DTO +│ ├── cleanup.go # cleanupChain (단계별 롤백 체인) +│ └── core_allocation.go # (미사용) 코어 라운드로빈 할당 자리표시 +│ +├── client/ # ── 외부 시스템 HTTP 클라이언트 ── +│ ├── vm.go # CoreClient: Core 노드에 VM 명령/상태 조회 +│ ├── cms.go # CmsClient: 인스턴스(IP/MAC/서브넷) 할당·삭제 +│ ├── guacamole.go # GuacamoleClient: REST 인증 토큰 획득 +│ └── model/ +│ ├── vm.go # Core/VM 요청·응답 계약 + 상태 상수 +│ └── common.go # 제네릭 CoreResponse[T] + 에러 타입 +│ +├── structure/ # ── 도메인 타입 · 상태 · 영속화 ── +│ ├── vm.go # Config, Core, CoreInfo, VMInfo, UUID 등 핵심 타입 +│ ├── control_infra.go # ControlContext + UUID→Core 조회 +│ ├── resource_manager.go # 인메모리 자원 관리: 코어 선택, 할당/회수, 락 +│ ├── repository.go # VMRepository 인터페이스 +│ ├── mysql_vm_repository.go# VMRepository의 MySQL 구현체 +│ └── errors.go # 도메인 에러 생성자 +│ +├── pkg/ # ── 재사용 가능한 순수 유틸리티 ── +│ ├── crypto/password.go # Salt/SHA256 해시·랜덤 비밀번호 (Guacamole 호환) +│ ├── guacamole/config.go # Guacamole DB에 사용자·SSH 커넥션 생성/정리 +│ ├── network/network.go # 서브넷 계산 (다음 서브넷, IP→서브넷) +│ └── ssh/keygen.go # RSA SSH 키쌍 생성 +│ +├── startup/ # ── 부트스트랩 ── +│ ├── init.go # InitializeCoreData: config·DB 연결·Core 폴링·인스턴스 로드 +│ ├── redis.go # InitializeRedis: 연결·헬스 체크 +│ ├── core_ip_config.go # readConfig: config.yaml 파싱(+ resources/ 폴백) +│ └── init.sh # MySQL 컨테이너 초기 DB/테이블 생성 스크립트 +│ +├── util/ # ── 전 계층 공용 헬퍼 ── +│ ├── logger.go # logrus 기반 커스텀 로거(파일+stdout, 호출 위치 표기) +│ ├── response.go # RespondJSON / RespondError +│ └── util.go # (빈 자리표시 파일) +│ +├── tests/ # ── 블랙박스 통합 테스트 ── +│ ├── blackbox_io_test.sh # 전체 라이프사이클 검증 러너 +│ ├── docker-compose.test.yml# 테스트 스택(mysql/redis/mock-core/control) +│ ├── init-test-db.sql # 테스트 DB 스키마 시드 +│ └── mock_core.py # Core/CMS/Guacamole 목(mock) 서버 +│ +├── resources/ +│ └── config.yaml # 기본/폴백 설정값 +│ +├── .github/ +│ ├── workflows/dev.yaml # staging 푸시 → control_dev 이미지 빌드·배포 +│ ├── workflows/deploy.yaml# production 푸시 → control_deploy 이미지 빌드·배포 +│ ├── ISSUE_TEMPLATE/ # 이슈 템플릿(bug/feature/task) +│ └── PULL_REQUEST_TEMPLATE.md +│ +└── .vscode/launch.json # VS Code Go 디버그 설정 +``` + +--- + +## 테스트 + +외부 의존성을 모두 컨테이너(목 포함)로 띄우는 블랙박스 통합 테스트 제공. +Docker / Docker Compose v2 만 있으면 실행 가능. + +```sh +./tests/blackbox_io_test.sh +``` + +- `tests/docker-compose.test.yml` 로 MySQL · Redis · 목 Core(`mock_core.py`) · Control 을 기동함. +- HTTP 라우팅(405), 본문 검증(400), 필드 검증, 라이프사이클 시나리오를 차례로 검증. +- 종료 시 테스트 스택과 볼륨을 자동 정리함(`trap cleanup EXIT`). +- Control 테스트 서비스는 호스트 `18081` 포트로 노출. + +--- + +## 배포 (CI/CD) + +`self-hosted` 러너(로컬 Ubuntu)에서 Docker 이미지를 빌드·배포. + +| 브랜치 | 워크플로 | 컨테이너 | 포트(호스트→컨테이너) | +| ------------ | ------------------------------- | ---------------- | --------------------- | +| `staging` | `.github/workflows/dev.yaml` | `CONTROL_DEV` | `8082 → 8081` | +| `production` | `.github/workflows/deploy.yaml` | `CONTROL_DEPLOY` | `8081 → 8081` | + +- PR의 기본 대상 브랜치는 `staging`. +- 배포에 필요한 값(`CORES`, `DB_*`, `GUAC_DB_*`, `REDIS_HOST`, `CMS_HOST`, `GUAC_BASE_URL` 등)은 GitHub Actions Secrets 로 주입. From eb0de68365cc45c2503205a416e1672b3849fddb Mon Sep 17 00:00:00 2001 From: Kwon Date: Sun, 28 Jun 2026 16:49:53 +0900 Subject: [PATCH 2/2] feat: wire TakeExternalSnapshot to Core endpoint - Fix TakeSnapshotResponse to match Core response shape (uuid, snapKey) - Add CoreClient.TakeExternalSnapshot with 12m timeout for snapshot+upload - Fill in TakeSnapshot TODO: presigned PUT URL -> Core -> RustFS upload --- client/model/snapshot.go | 3 ++- client/vm.go | 14 ++++++++++++++ service/external_snap.go | 13 +++++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/client/model/snapshot.go b/client/model/snapshot.go index 123e024..e907181 100644 --- a/client/model/snapshot.go +++ b/client/model/snapshot.go @@ -10,5 +10,6 @@ type TakeSnapshotRequest struct { } type TakeSnapshotResponse struct { - Message string `json:"message,omitempty"` + UUID string `json:"uuid"` + SnapKey string `json:"snapKey"` } diff --git a/client/vm.go b/client/vm.go index 923f6fb..9821ab3 100644 --- a/client/vm.go +++ b/client/vm.go @@ -183,3 +183,17 @@ func (c *CoreClient) ForceShutdownVM(ctx context.Context, req model.ForceShutdow } return response, nil } + +// TakeExternalSnapshot asks Core to take a snapshot and upload it to RustFS via the given presigned PUT URL. +// Uses a dedicated HTTP client with a 12-minute timeout because snapshot creation + upload can take several minutes. +func (c *CoreClient) TakeExternalSnapshot(ctx context.Context, req model.TakeSnapshotRequest) (model.TakeSnapshotResponse, error) { + snapClient := &CoreClient{ + baseURL: c.baseURL, + client: &http.Client{Timeout: 12 * time.Minute}, + } + var response model.TakeSnapshotResponse + if err := snapClient.doRequest(ctx, http.MethodPost, "/TakeExternalSnapshot", req, &response); err != nil { + return model.TakeSnapshotResponse{}, err + } + return response, nil +} diff --git a/service/external_snap.go b/service/external_snap.go index e4da6a5..98cd9ae 100644 --- a/service/external_snap.go +++ b/service/external_snap.go @@ -6,6 +6,7 @@ import ( "time" "github.com/easy-cloud-Knet/KWS_Control/client" + "github.com/easy-cloud-Knet/KWS_Control/client/model" vms "github.com/easy-cloud-Knet/KWS_Control/structure" ) @@ -52,9 +53,13 @@ func TakeSnapshot(uuid vms.UUID, snapName string, ctx *vms.ControlContext) error return fmt.Errorf("TakeSnapshot %s: failed to generate presigned URL: %w", uuid, err) } - // TODO: coreClient.TakeSnapshot(context.Background(), model.TakeSnapshotRequest{ - // UUID: uuid, SnapKey: snapName, PresignedURL: presignedURL, - // }) - _ = presignedURL + coreClient := client.NewCoreClient(core) + if _, err := coreClient.TakeExternalSnapshot(context.Background(), model.TakeSnapshotRequest{ + UUID: uuid, + SnapKey: snapName, + PresignedURL: presignedURL, + }); err != nil { + return fmt.Errorf("TakeSnapshot %s: core failed: %w", uuid, err) + } return nil }