Merge branch 'feature-28'
This commit is contained in:
commit
437766d812
32 changed files with 842 additions and 226 deletions
|
|
@ -294,13 +294,17 @@ components:
|
||||||
|
|
||||||
VPCCreateRequest:
|
VPCCreateRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [name]
|
required: [name, cidr]
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: Unique name for the VPC, must follow the format vp-[id]
|
description: Unique name for the VPC, must follow the format vp-[id]
|
||||||
pattern: '^vp-.+'
|
pattern: '^vp-.+'
|
||||||
example: vp-00001
|
example: vp-00001
|
||||||
|
cidr:
|
||||||
|
type: string
|
||||||
|
description: CIDR block for the entire VPC address space
|
||||||
|
example: "10.0.0.0/16"
|
||||||
|
|
||||||
VPC:
|
VPC:
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -312,10 +316,13 @@ components:
|
||||||
type: string
|
type: string
|
||||||
enum: [creating, created, deleting, deleted]
|
enum: [creating, created, deleting, deleted]
|
||||||
example: created
|
example: created
|
||||||
|
cidr:
|
||||||
|
type: string
|
||||||
|
example: "10.0.0.0/16"
|
||||||
|
|
||||||
SubnetCreateRequest:
|
SubnetCreateRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [name, vpc, vxlan_id, gateway_ip, cidr]
|
required: [name, vpc, interface_ip, cidr]
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -325,15 +332,24 @@ components:
|
||||||
type: string
|
type: string
|
||||||
description: Parent VPC name
|
description: Parent VPC name
|
||||||
example: vpc1
|
example: vpc1
|
||||||
|
mode:
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Subnet mode. "vxlan" (default): creates a VXLAN tunnel and a host bridge.
|
||||||
|
"bridge": attaches directly to an existing bridge resolved from iface_type in the agent config.
|
||||||
|
"vlan" is reserved for future use.
|
||||||
|
enum: [vxlan, bridge]
|
||||||
|
default: vxlan
|
||||||
|
example: vxlan
|
||||||
vxlan_id:
|
vxlan_id:
|
||||||
type: integer
|
type: integer
|
||||||
description: VXLAN VNI identifier
|
description: VXLAN VNI identifier. Required when mode is "vxlan", ignored otherwise.
|
||||||
example: 100
|
example: 100
|
||||||
iface_type:
|
iface_type:
|
||||||
type: string
|
type: string
|
||||||
description: Interface type key defined in the agent config (e.g. vms, internet, admin). Falls back to default_interface if omitted or unknown.
|
description: Interface type key defined in the agent config (e.g. vms, internet, admin). Falls back to default_interface if omitted or unknown.
|
||||||
example: vms
|
example: vms
|
||||||
gateway_ip:
|
interface_ip:
|
||||||
type: string
|
type: string
|
||||||
format: ipv4
|
format: ipv4
|
||||||
description: Gateway IP for the subnet
|
description: Gateway IP for the subnet
|
||||||
|
|
@ -342,6 +358,12 @@ components:
|
||||||
type: string
|
type: string
|
||||||
description: Subnet CIDR block
|
description: Subnet CIDR block
|
||||||
example: "10.10.10.0/24"
|
example: "10.10.10.0/24"
|
||||||
|
default_route:
|
||||||
|
type: boolean
|
||||||
|
description: >
|
||||||
|
If true, advertise a default route via DHCP. For vxlan mode the gateway is the interface IP.
|
||||||
|
For bridge mode the gateway is read from the host routing table.
|
||||||
|
default: false
|
||||||
|
|
||||||
Subnet:
|
Subnet:
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -356,30 +378,35 @@ components:
|
||||||
vpc:
|
vpc:
|
||||||
type: string
|
type: string
|
||||||
example: vpc1
|
example: vpc1
|
||||||
|
mode:
|
||||||
|
type: string
|
||||||
|
enum: [vxlan, bridge]
|
||||||
|
example: vxlan
|
||||||
vxlan_id:
|
vxlan_id:
|
||||||
type: integer
|
type: integer
|
||||||
|
description: VXLAN VNI. Present only when mode is "vxlan".
|
||||||
example: 100
|
example: 100
|
||||||
local_iface:
|
local_iface:
|
||||||
type: string
|
type: string
|
||||||
description: Resolved interface name
|
description: Resolved interface name from agent config
|
||||||
example: br-000000
|
example: br-000000
|
||||||
gateway_ip:
|
interface_ip:
|
||||||
type: string
|
type: string
|
||||||
example: "10.10.10.1"
|
example: "10.10.10.1"
|
||||||
cidr:
|
cidr:
|
||||||
type: string
|
type: string
|
||||||
example: "10.10.10.0/24"
|
example: "10.10.10.0/24"
|
||||||
|
default_route:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
|
||||||
VMCreateRequest:
|
VMCreateRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [name, metadata_port, interfaces, storage]
|
required: [name, interfaces, storage]
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
example: vm-00001
|
example: vm-00001
|
||||||
metadata_port:
|
|
||||||
type: string
|
|
||||||
example: "80"
|
|
||||||
memory:
|
memory:
|
||||||
type: integer
|
type: integer
|
||||||
description: Memory in MB (default 512)
|
description: Memory in MB (default 512)
|
||||||
|
|
@ -403,6 +430,10 @@ components:
|
||||||
minItems: 1
|
minItems: 1
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/VMStorage"
|
$ref: "#/components/schemas/VMStorage"
|
||||||
|
uefi:
|
||||||
|
type: boolean
|
||||||
|
description: Boot with UEFI firmware (OVMF). Defaults to false (SeaBIOS).
|
||||||
|
example: false
|
||||||
|
|
||||||
VMInterface:
|
VMInterface:
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -460,6 +491,9 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/VMStorage"
|
$ref: "#/components/schemas/VMStorage"
|
||||||
|
uefi:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
|
||||||
Error:
|
Error:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,18 @@ interfaces:
|
||||||
metadata:
|
metadata:
|
||||||
run_dir: "/run/two/metadata"
|
run_dir: "/run/two/metadata"
|
||||||
|
|
||||||
|
# QEMU runtime paths
|
||||||
|
qemu:
|
||||||
|
# UEFI firmware (requires apt install ovmf on Debian/Ubuntu)
|
||||||
|
ovmf_code_path: "/usr/share/OVMF/OVMF_CODE.fd"
|
||||||
|
ovmf_vars_template: "/usr/share/OVMF/OVMF_VARS.fd"
|
||||||
|
# Per-VM UEFI variable store (writable copy, created at start / deleted at stop)
|
||||||
|
uefi_vars_dir: "/run/two/vms/efi"
|
||||||
|
# QEMU Unix socket directories
|
||||||
|
serial_dir: "/run/two/vms/serial"
|
||||||
|
monitor_dir: "/run/two/vms/monitor"
|
||||||
|
qmp_dir: "/run/two/vms/qmp"
|
||||||
|
|
||||||
# Admin API (read-only DB inspection, loopback only)
|
# Admin API (read-only DB inspection, loopback only)
|
||||||
admin:
|
admin:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,36 @@ package agentapi
|
||||||
|
|
||||||
type VPCCreateRequest struct {
|
type VPCCreateRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
CIDR string `json:"cidr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VPC struct {
|
type VPC struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
CIDR string `json:"cidr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubnetCreateRequest struct {
|
type SubnetCreateRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
VPC string `json:"vpc"`
|
VPC string `json:"vpc"`
|
||||||
VxlanID int `json:"vxlan_id"`
|
Mode string `json:"mode"`
|
||||||
IfaceType string `json:"iface_type"`
|
VxlanID int `json:"vxlan_id"`
|
||||||
GatewayIP string `json:"gateway_ip"`
|
IfaceType string `json:"iface_type"`
|
||||||
CIDR string `json:"cidr"`
|
InterfaceIP string `json:"interface_ip"`
|
||||||
|
CIDR string `json:"cidr"`
|
||||||
|
DefaultRoute bool `json:"default_route"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subnet struct {
|
type Subnet struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
VPC string `json:"vpc"`
|
VPC string `json:"vpc"`
|
||||||
VxlanID int `json:"vxlan_id"`
|
Mode string `json:"mode"`
|
||||||
LocalIface string `json:"local_iface"`
|
VxlanID int `json:"vxlan_id"`
|
||||||
GatewayIP string `json:"gateway_ip"`
|
LocalIface string `json:"local_iface"`
|
||||||
CIDR string `json:"cidr"`
|
InterfaceIP string `json:"interface_ip"`
|
||||||
|
CIDR string `json:"cidr"`
|
||||||
|
DefaultRoute bool `json:"default_route"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VMInterface struct {
|
type VMInterface struct {
|
||||||
|
|
@ -40,14 +46,14 @@ type VMStorage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type VMCreateRequest struct {
|
type VMCreateRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
MetadataPort string `json:"metadata_port"`
|
Memory int `json:"memory"`
|
||||||
Memory int `json:"memory"`
|
CPUs int `json:"cpus"`
|
||||||
CPUs int `json:"cpus"`
|
UEFI bool `json:"uefi"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
SSHKey string `json:"sshkey"`
|
SSHKey string `json:"sshkey"`
|
||||||
Interfaces []VMInterface `json:"interfaces"`
|
Interfaces []VMInterface `json:"interfaces"`
|
||||||
Storage []VMStorage `json:"storage"`
|
Storage []VMStorage `json:"storage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VM struct {
|
type VM struct {
|
||||||
|
|
@ -56,6 +62,7 @@ type VM struct {
|
||||||
MetadataPort string `json:"metadata_port"`
|
MetadataPort string `json:"metadata_port"`
|
||||||
Memory int `json:"memory"`
|
Memory int `json:"memory"`
|
||||||
CPUs int `json:"cpus"`
|
CPUs int `json:"cpus"`
|
||||||
|
UEFI bool `json:"uefi"`
|
||||||
Interfaces []VMInterface `json:"interfaces"`
|
Interfaces []VMInterface `json:"interfaces"`
|
||||||
Storage []VMStorage `json:"storage"`
|
Storage []VMStorage `json:"storage"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,18 @@ func (s *Server) getSubnet(w http.ResponseWriter, _ *http.Request, name string)
|
||||||
sub.State = value
|
sub.State = value
|
||||||
case "vpc":
|
case "vpc":
|
||||||
sub.VPC = value
|
sub.VPC = value
|
||||||
|
case "mode":
|
||||||
|
sub.Mode = value
|
||||||
case "vxlan_id":
|
case "vxlan_id":
|
||||||
sub.VxlanID, _ = strconv.Atoi(value)
|
sub.VxlanID, _ = strconv.Atoi(value)
|
||||||
case "local_iface":
|
case "local_iface":
|
||||||
sub.LocalIface = value
|
sub.LocalIface = value
|
||||||
case "gateway_ip":
|
case "interface_ip":
|
||||||
sub.GatewayIP = value
|
sub.InterfaceIP = value
|
||||||
case "cidr":
|
case "cidr":
|
||||||
sub.CIDR = value
|
sub.CIDR = value
|
||||||
|
case "default_route":
|
||||||
|
sub.DefaultRoute = value == "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ func TestPostSubnet_Created(t *testing.T) {
|
||||||
Name: "sn-new",
|
Name: "sn-new",
|
||||||
VPC: "vpc-1",
|
VPC: "vpc-1",
|
||||||
IfaceType: "vms",
|
IfaceType: "vms",
|
||||||
GatewayIP: "10.0.0.1",
|
InterfaceIP: "10.0.0.1",
|
||||||
CIDR: "10.0.0.0/24",
|
CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(req)
|
body, _ := json.Marshal(req)
|
||||||
|
|
@ -95,7 +95,7 @@ func TestPostSubnet_IfaceTypeOptional(t *testing.T) {
|
||||||
req := SubnetCreateRequest{
|
req := SubnetCreateRequest{
|
||||||
Name: "sn-opt",
|
Name: "sn-opt",
|
||||||
VPC: "vpc-1",
|
VPC: "vpc-1",
|
||||||
GatewayIP: "10.0.0.1",
|
InterfaceIP: "10.0.0.1",
|
||||||
CIDR: "10.0.0.0/24",
|
CIDR: "10.0.0.0/24",
|
||||||
// IfaceType omis — doit utiliser default_interface
|
// IfaceType omis — doit utiliser default_interface
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +113,7 @@ func TestPostSubnet_VPCNotFound(t *testing.T) {
|
||||||
Name: "sn-1",
|
Name: "sn-1",
|
||||||
VPC: "vpc-inexistant",
|
VPC: "vpc-inexistant",
|
||||||
IfaceType: "vms",
|
IfaceType: "vms",
|
||||||
GatewayIP: "10.0.0.1",
|
InterfaceIP: "10.0.0.1",
|
||||||
CIDR: "10.0.0.0/24",
|
CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(req)
|
body, _ := json.Marshal(req)
|
||||||
|
|
@ -132,7 +132,7 @@ func TestPostSubnet_Duplicate(t *testing.T) {
|
||||||
Name: "sn-exist",
|
Name: "sn-exist",
|
||||||
VPC: "vpc-1",
|
VPC: "vpc-1",
|
||||||
IfaceType: "vms",
|
IfaceType: "vms",
|
||||||
GatewayIP: "10.0.0.1",
|
InterfaceIP: "10.0.0.1",
|
||||||
CIDR: "10.0.0.0/24",
|
CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(req)
|
body, _ := json.Marshal(req)
|
||||||
|
|
@ -150,7 +150,52 @@ func TestPostSubnet_VPCDeleting(t *testing.T) {
|
||||||
Name: "sn-1",
|
Name: "sn-1",
|
||||||
VPC: "vpc-dying",
|
VPC: "vpc-dying",
|
||||||
IfaceType: "vms",
|
IfaceType: "vms",
|
||||||
GatewayIP: "10.0.0.1",
|
InterfaceIP: "10.0.0.1",
|
||||||
|
CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusUnprocessableEntity {
|
||||||
|
t.Errorf("attendu 422, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_BridgeMode_Success(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
req := SubnetCreateRequest{
|
||||||
|
Name: "sn-br",
|
||||||
|
VPC: "vpc-1",
|
||||||
|
Mode: "bridge",
|
||||||
|
IfaceType: "vms",
|
||||||
|
InterfaceIP: "10.0.0.1",
|
||||||
|
CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
var result Subnet
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if result.Mode != "bridge" {
|
||||||
|
t.Errorf("mode attendu bridge, obtenu %q", result.Mode)
|
||||||
|
}
|
||||||
|
if result.VxlanID != 0 {
|
||||||
|
t.Errorf("vxlan_id devrait être 0 en mode bridge, obtenu %d", result.VxlanID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_UnknownMode(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
req := SubnetCreateRequest{
|
||||||
|
Name: "sn-1",
|
||||||
|
VPC: "vpc-1",
|
||||||
|
Mode: "vlan",
|
||||||
|
InterfaceIP: "10.0.0.1",
|
||||||
CIDR: "10.0.0.0/24",
|
CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(req)
|
body, _ := json.Marshal(req)
|
||||||
|
|
@ -177,7 +222,7 @@ func TestGetSubnet_Found(t *testing.T) {
|
||||||
kv.AddInDB(db, "subnet/sn-1/state", "created")
|
kv.AddInDB(db, "subnet/sn-1/state", "created")
|
||||||
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
|
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
|
||||||
kv.AddInDB(db, "subnet/sn-1/cidr", "10.0.0.0/24")
|
kv.AddInDB(db, "subnet/sn-1/cidr", "10.0.0.0/24")
|
||||||
kv.AddInDB(db, "subnet/sn-1/gateway_ip", "10.0.0.1")
|
kv.AddInDB(db, "subnet/sn-1/interface_ip", "10.0.0.1")
|
||||||
req := httptest.NewRequest(http.MethodGet, "/subnets/sn-1", nil)
|
req := httptest.NewRequest(http.MethodGet, "/subnets/sn-1", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.SubnetByNameHandler(w, req)
|
s.SubnetByNameHandler(w, req)
|
||||||
|
|
|
||||||
|
|
@ -44,14 +44,18 @@ func (s *Server) listSubnets(w http.ResponseWriter, _ *http.Request) {
|
||||||
subnets[name].State = value
|
subnets[name].State = value
|
||||||
case "vpc":
|
case "vpc":
|
||||||
subnets[name].VPC = value
|
subnets[name].VPC = value
|
||||||
|
case "mode":
|
||||||
|
subnets[name].Mode = value
|
||||||
case "vxlan_id":
|
case "vxlan_id":
|
||||||
subnets[name].VxlanID, _ = strconv.Atoi(value)
|
subnets[name].VxlanID, _ = strconv.Atoi(value)
|
||||||
case "local_iface":
|
case "local_iface":
|
||||||
subnets[name].LocalIface = value
|
subnets[name].LocalIface = value
|
||||||
case "gateway_ip":
|
case "interface_ip":
|
||||||
subnets[name].GatewayIP = value
|
subnets[name].InterfaceIP = value
|
||||||
case "cidr":
|
case "cidr":
|
||||||
subnets[name].CIDR = value
|
subnets[name].CIDR = value
|
||||||
|
case "default_route":
|
||||||
|
subnets[name].DefaultRoute = value == "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result := make([]Subnet, 0, len(subnets))
|
result := make([]Subnet, 0, len(subnets))
|
||||||
|
|
@ -69,18 +73,20 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) {
|
||||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"})
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Name == "" || req.VPC == "" || req.GatewayIP == "" || req.CIDR == "" {
|
if req.Name == "" || req.VPC == "" || req.InterfaceIP == "" || req.CIDR == "" {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, gateway_ip and cidr are required"})
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, interface_ip and cidr are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cmd := dispatcher.CreateSubnetCommand{
|
cmd := dispatcher.CreateSubnetCommand{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
VPC: req.VPC,
|
VPC: req.VPC,
|
||||||
VxlanID: req.VxlanID,
|
Mode: req.Mode,
|
||||||
IfaceType: req.IfaceType,
|
VxlanID: req.VxlanID,
|
||||||
GatewayIP: req.GatewayIP,
|
IfaceType: req.IfaceType,
|
||||||
CIDR: req.CIDR,
|
InterfaceIP: req.InterfaceIP,
|
||||||
|
CIDR: req.CIDR,
|
||||||
|
DefaultRoute: req.DefaultRoute,
|
||||||
}
|
}
|
||||||
if err := s.dispatcher.Prepare(cmd); err != nil {
|
if err := s.dispatcher.Prepare(cmd); err != nil {
|
||||||
if _, dbErr := kv.GetFromDB(s.db, "subnet/"+req.Name+"/state"); dbErr == nil {
|
if _, dbErr := kv.GetFromDB(s.db, "subnet/"+req.Name+"/state"); dbErr == nil {
|
||||||
|
|
@ -109,14 +115,18 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) {
|
||||||
sub.State = value
|
sub.State = value
|
||||||
case "vpc":
|
case "vpc":
|
||||||
sub.VPC = value
|
sub.VPC = value
|
||||||
|
case "mode":
|
||||||
|
sub.Mode = value
|
||||||
case "vxlan_id":
|
case "vxlan_id":
|
||||||
sub.VxlanID, _ = strconv.Atoi(value)
|
sub.VxlanID, _ = strconv.Atoi(value)
|
||||||
case "local_iface":
|
case "local_iface":
|
||||||
sub.LocalIface = value
|
sub.LocalIface = value
|
||||||
case "gateway_ip":
|
case "interface_ip":
|
||||||
sub.GatewayIP = value
|
sub.InterfaceIP = value
|
||||||
case "cidr":
|
case "cidr":
|
||||||
sub.CIDR = value
|
sub.CIDR = value
|
||||||
|
case "default_route":
|
||||||
|
sub.DefaultRoute = value == "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ func vmFromDB(name string, entries map[string]string) (VM, error) {
|
||||||
vm.MetadataPort = entries[prefix+"metadata_port"]
|
vm.MetadataPort = entries[prefix+"metadata_port"]
|
||||||
vm.Memory, _ = strconv.Atoi(entries[prefix+"memory"])
|
vm.Memory, _ = strconv.Atoi(entries[prefix+"memory"])
|
||||||
vm.CPUs, _ = strconv.Atoi(entries[prefix+"cpus"])
|
vm.CPUs, _ = strconv.Atoi(entries[prefix+"cpus"])
|
||||||
|
vm.UEFI = entries[prefix+"uefi"] == "true"
|
||||||
|
|
||||||
subnet := entries[prefix+"subnet"]
|
subnet := entries[prefix+"subnet"]
|
||||||
ip := entries[prefix+"ip"]
|
ip := entries[prefix+"ip"]
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,9 @@ func (s *Server) startVM(w http.ResponseWriter, r *http.Request) {
|
||||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"})
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Name == "" || req.MetadataPort == "" || len(req.Interfaces) == 0 || len(req.Storage) == 0 {
|
if req.Name == "" || len(req.Interfaces) == 0 || len(req.Storage) == 0 {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "name, metadata_port, interfaces and storage are required"})
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "name, interfaces and storage are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,15 +78,15 @@ func (s *Server) startVM(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := dispatcher.StartVMCommand{
|
cmd := dispatcher.StartVMCommand{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Subnet: primary.Subnet,
|
Subnet: primary.Subnet,
|
||||||
IP: primary.IP,
|
IP: primary.IP,
|
||||||
MetadataPort: req.MetadataPort,
|
VolumePath: req.Storage[0].Path,
|
||||||
VolumePath: req.Storage[0].Path,
|
Memory: req.Memory,
|
||||||
Memory: req.Memory,
|
CPUs: req.CPUs,
|
||||||
CPUs: req.CPUs,
|
UEFI: req.UEFI,
|
||||||
Password: req.Password,
|
Password: req.Password,
|
||||||
SSHKey: req.SSHKey,
|
SSHKey: req.SSHKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.dispatcher.Prepare(cmd); err != nil {
|
if err := s.dispatcher.Prepare(cmd); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,9 @@ func (s *Server) getVpc(w http.ResponseWriter, _ *http.Request, name string) {
|
||||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "vpc not found"})
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "vpc not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cidr, _ := kv.GetFromDB(s.db, "vpc/"+name+"/cidr")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(VPC{Name: name, State: state})
|
json.NewEncoder(w).Encode(VPC{Name: name, State: state, CIDR: cidr})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) {
|
func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func TestListVpcs_InvalidMethod(t *testing.T) {
|
||||||
|
|
||||||
func TestPostVpc_Created(t *testing.T) {
|
func TestPostVpc_Created(t *testing.T) {
|
||||||
s, _ := newTestServer(t)
|
s, _ := newTestServer(t)
|
||||||
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-new"})
|
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-new", CIDR: "10.0.0.0/16"})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
if w.Code != http.StatusAccepted {
|
if w.Code != http.StatusAccepted {
|
||||||
|
|
@ -67,11 +67,34 @@ func TestPostVpc_Created(t *testing.T) {
|
||||||
if result.State != "creating" {
|
if result.State != "creating" {
|
||||||
t.Errorf("state attendu creating, obtenu %q", result.State)
|
t.Errorf("state attendu creating, obtenu %q", result.State)
|
||||||
}
|
}
|
||||||
|
if result.CIDR != "10.0.0.0/16" {
|
||||||
|
t.Errorf("cidr attendu 10.0.0.0/16, obtenu %q", result.CIDR)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostVpc_MissingName(t *testing.T) {
|
func TestPostVpc_MissingName(t *testing.T) {
|
||||||
s, _ := newTestServer(t)
|
s, _ := newTestServer(t)
|
||||||
body, _ := json.Marshal(VPCCreateRequest{})
|
body, _ := json.Marshal(VPCCreateRequest{CIDR: "10.0.0.0/16"})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("attendu 400, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostVpc_MissingCIDR(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-new"})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("attendu 400, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostVpc_InvalidCIDR(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-new", CIDR: "not-a-cidr"})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
if w.Code != http.StatusBadRequest {
|
if w.Code != http.StatusBadRequest {
|
||||||
|
|
@ -82,7 +105,7 @@ func TestPostVpc_MissingName(t *testing.T) {
|
||||||
func TestPostVpc_Duplicate(t *testing.T) {
|
func TestPostVpc_Duplicate(t *testing.T) {
|
||||||
s, db := newTestServer(t)
|
s, db := newTestServer(t)
|
||||||
kv.AddInDB(db, "vpc/vpc-exist/state", "created")
|
kv.AddInDB(db, "vpc/vpc-exist/state", "created")
|
||||||
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-exist"})
|
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-exist", CIDR: "10.0.0.0/16"})
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
if w.Code != http.StatusConflict {
|
if w.Code != http.StatusConflict {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package agentapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -38,8 +39,11 @@ func (s *Server) listVpcs(w http.ResponseWriter, _ *http.Request) {
|
||||||
if _, ok := vpcs[name]; !ok {
|
if _, ok := vpcs[name]; !ok {
|
||||||
vpcs[name] = &VPC{Name: name}
|
vpcs[name] = &VPC{Name: name}
|
||||||
}
|
}
|
||||||
if parts[2] == "state" {
|
switch parts[2] {
|
||||||
|
case "state":
|
||||||
vpcs[name].State = value
|
vpcs[name].State = value
|
||||||
|
case "cidr":
|
||||||
|
vpcs[name].CIDR = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result := make([]VPC, 0, len(vpcs))
|
result := make([]VPC, 0, len(vpcs))
|
||||||
|
|
@ -62,7 +66,17 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) {
|
||||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "name is required"})
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "name is required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cmd := dispatcher.CreateVPCCommand{Name: req.Name}
|
if req.CIDR == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "cidr is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, _, err := net.ParseCIDR(req.CIDR); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid cidr"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := dispatcher.CreateVPCCommand{Name: req.Name, CIDR: req.CIDR}
|
||||||
if err := s.dispatcher.Prepare(cmd); err != nil {
|
if err := s.dispatcher.Prepare(cmd); err != nil {
|
||||||
w.WriteHeader(http.StatusConflict)
|
w.WriteHeader(http.StatusConflict)
|
||||||
json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()})
|
json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()})
|
||||||
|
|
@ -76,5 +90,5 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
json.NewEncoder(w).Encode(VPC{Name: req.Name, State: state})
|
json.NewEncoder(w).Encode(VPC{Name: req.Name, State: state, CIDR: req.CIDR})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,14 @@ type Config struct {
|
||||||
Address string `mapstructure:"address"`
|
Address string `mapstructure:"address"`
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
} `mapstructure:"admin"`
|
} `mapstructure:"admin"`
|
||||||
|
QEMU struct {
|
||||||
|
OVMFCodePath string `mapstructure:"ovmf_code_path"`
|
||||||
|
OVMFVarsTemplate string `mapstructure:"ovmf_vars_template"`
|
||||||
|
UEFIVarsDir string `mapstructure:"uefi_vars_dir"`
|
||||||
|
SerialDir string `mapstructure:"serial_dir"`
|
||||||
|
MonitorDir string `mapstructure:"monitor_dir"`
|
||||||
|
QMPDir string `mapstructure:"qmp_dir"`
|
||||||
|
} `mapstructure:"qemu"`
|
||||||
DefaultInterface string `mapstructure:"default_interface"`
|
DefaultInterface string `mapstructure:"default_interface"`
|
||||||
Interfaces map[string]string `mapstructure:"interfaces"`
|
Interfaces map[string]string `mapstructure:"interfaces"`
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +63,12 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
v.SetDefault("dispatcher.timeout_seconds", 300)
|
v.SetDefault("dispatcher.timeout_seconds", 300)
|
||||||
v.SetDefault("dispatcher.poll_seconds", 2)
|
v.SetDefault("dispatcher.poll_seconds", 2)
|
||||||
v.SetDefault("metadata.run_dir", "/run/two/metadata")
|
v.SetDefault("metadata.run_dir", "/run/two/metadata")
|
||||||
|
v.SetDefault("qemu.ovmf_code_path", "/usr/share/OVMF/OVMF_CODE.fd")
|
||||||
|
v.SetDefault("qemu.ovmf_vars_template", "/usr/share/OVMF/OVMF_VARS.fd")
|
||||||
|
v.SetDefault("qemu.uefi_vars_dir", "/run/two/vms/uefi")
|
||||||
|
v.SetDefault("qemu.serial_dir", "/run/two/vms/serial")
|
||||||
|
v.SetDefault("qemu.monitor_dir", "/run/two/vms/monitor")
|
||||||
|
v.SetDefault("qemu.qmp_dir", "/run/two/vms/qmp")
|
||||||
v.SetDefault("admin.enabled", false)
|
v.SetDefault("admin.enabled", false)
|
||||||
v.SetDefault("admin.address", "127.0.0.1")
|
v.SetDefault("admin.address", "127.0.0.1")
|
||||||
v.SetDefault("admin.port", 9091)
|
v.SetDefault("admin.port", 9091)
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,15 @@ func TestIncrementIP_Carry(t *testing.T) {
|
||||||
func newConf(t *testing.T, cidr string) Config {
|
func newConf(t *testing.T, cidr string) Config {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
_, network, _ := net.ParseCIDR(cidr)
|
_, network, _ := net.ParseCIDR(cidr)
|
||||||
|
_, vpcNet, _ := net.ParseCIDR("10.0.0.0/16")
|
||||||
|
gw := net.ParseIP("192.168.1.1").To4()
|
||||||
return Config{
|
return Config{
|
||||||
Network: network,
|
Network: network,
|
||||||
Gateway: net.ParseIP("192.168.1.1").To4(),
|
VPCGateway: gw,
|
||||||
Name: "test",
|
VPCRoute: vpcNet,
|
||||||
ConfDir: t.TempDir(),
|
DefaultGateway: gw,
|
||||||
|
Name: "test",
|
||||||
|
ConfDir: t.TempDir(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,13 +88,45 @@ func TestGenerateConfig_FilenameMatchesName(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateConfig_ContainsGateway(t *testing.T) {
|
func TestGenerateConfig_ContainsDefaultGateway(t *testing.T) {
|
||||||
conf := newConf(t, "192.168.1.0/29")
|
conf := newConf(t, "192.168.1.0/29")
|
||||||
path, _, _ := GenerateConfig(conf)
|
path, _, _ := GenerateConfig(conf)
|
||||||
content, _ := os.ReadFile(path)
|
content, _ := os.ReadFile(path)
|
||||||
|
|
||||||
if !strings.Contains(string(content), "dhcp-option=3,192.168.1.1") {
|
if !strings.Contains(string(content), "dhcp-option=3,192.168.1.1") {
|
||||||
t.Errorf("gateway absente du fichier généré :\n%s", content)
|
t.Errorf("dhcp-option=3 absente du fichier généré :\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateConfig_NoDefaultGateway(t *testing.T) {
|
||||||
|
conf := newConf(t, "192.168.1.0/29")
|
||||||
|
conf.DefaultGateway = nil
|
||||||
|
path, _, _ := GenerateConfig(conf)
|
||||||
|
content, _ := os.ReadFile(path)
|
||||||
|
|
||||||
|
if strings.Contains(string(content), "dhcp-option=3,") {
|
||||||
|
t.Errorf("dhcp-option=3 présente alors que DefaultGateway=nil :\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateConfig_ContainsVPCRoute(t *testing.T) {
|
||||||
|
conf := newConf(t, "192.168.1.0/29")
|
||||||
|
path, _, _ := GenerateConfig(conf)
|
||||||
|
content, _ := os.ReadFile(path)
|
||||||
|
|
||||||
|
if !strings.Contains(string(content), "dhcp-option=121,10.0.0.0/16,192.168.1.1") {
|
||||||
|
t.Errorf("dhcp-option=121 absente ou incorrecte :\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateConfig_NoVPCRoute(t *testing.T) {
|
||||||
|
conf := newConf(t, "192.168.1.0/29")
|
||||||
|
conf.VPCRoute = nil
|
||||||
|
path, _, _ := GenerateConfig(conf)
|
||||||
|
content, _ := os.ReadFile(path)
|
||||||
|
|
||||||
|
if strings.Contains(string(content), "dhcp-option=121,") {
|
||||||
|
t.Errorf("dhcp-option=121 présente alors que VPCRoute=nil :\n%s", content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +134,6 @@ func TestGenerateConfig_ContainsDhcpRange(t *testing.T) {
|
||||||
_, network, _ := net.ParseCIDR("10.10.0.0/24")
|
_, network, _ := net.ParseCIDR("10.10.0.0/24")
|
||||||
conf := Config{
|
conf := Config{
|
||||||
Network: network,
|
Network: network,
|
||||||
Gateway: net.ParseIP("10.10.0.1").To4(),
|
|
||||||
Name: "vpc1",
|
Name: "vpc1",
|
||||||
ConfDir: t.TempDir(),
|
ConfDir: t.TempDir(),
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +179,6 @@ func TestGenerateConfig_CreatesConfDir(t *testing.T) {
|
||||||
_, network, _ := net.ParseCIDR("10.0.0.0/30")
|
_, network, _ := net.ParseCIDR("10.0.0.0/30")
|
||||||
conf := Config{
|
conf := Config{
|
||||||
Network: network,
|
Network: network,
|
||||||
Gateway: net.ParseIP("10.0.0.1").To4(),
|
|
||||||
Name: "net",
|
Name: "net",
|
||||||
ConfDir: dir,
|
ConfDir: dir,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,12 @@ func GenerateConfig(c Config) (string, map[string]string, error) {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
fmt.Fprintf(&sb, "no-resolv\n")
|
fmt.Fprintf(&sb, "no-resolv\n")
|
||||||
fmt.Fprintf(&sb, "dhcp-range=%s,static,%s,12h\n", c.Network.IP.String(), mask)
|
fmt.Fprintf(&sb, "dhcp-range=%s,static,%s,12h\n", c.Network.IP.String(), mask)
|
||||||
fmt.Fprintf(&sb, "dhcp-option=3,%s\n", c.Gateway.String())
|
if c.VPCRoute != nil {
|
||||||
|
fmt.Fprintf(&sb, "dhcp-option=121,%s,%s\n", c.VPCRoute.String(), c.VPCGateway.String())
|
||||||
|
}
|
||||||
|
if c.DefaultGateway != nil {
|
||||||
|
fmt.Fprintf(&sb, "dhcp-option=3,%s\n", c.DefaultGateway.String())
|
||||||
|
}
|
||||||
fmt.Fprintf(&sb, "dhcp-option=6,1.1.1.1,8.8.8.8\n\n")
|
fmt.Fprintf(&sb, "dhcp-option=6,1.1.1.1,8.8.8.8\n\n")
|
||||||
|
|
||||||
entries := make(map[string]string)
|
entries := make(map[string]string)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Network *net.IPNet
|
Network *net.IPNet
|
||||||
Gateway net.IP
|
VPCGateway net.IP // next-hop for VPCRoute (option 121)
|
||||||
Name string
|
VPCRoute *net.IPNet // if non-nil, emit dhcp-option=121,VPCRoute,VPCGateway
|
||||||
ConfDir string
|
DefaultGateway net.IP // if non-nil, emit dhcp-option=3,DefaultGateway
|
||||||
|
Name string
|
||||||
|
ConfDir string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,23 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateSubnetCommand struct {
|
type CreateSubnetCommand struct {
|
||||||
Name string
|
Name string
|
||||||
VPC string
|
VPC string
|
||||||
VxlanID int
|
Mode string
|
||||||
IfaceType string
|
VxlanID int
|
||||||
GatewayIP string
|
IfaceType string
|
||||||
CIDR string
|
InterfaceIP string
|
||||||
|
CIDR string
|
||||||
|
DefaultRoute bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreateSubnetCommand) Prepare(db *badger.DB, cfg *configuration.Config) error {
|
func (c CreateSubnetCommand) Prepare(db *badger.DB, cfg *configuration.Config) error {
|
||||||
|
if c.Mode == "" {
|
||||||
|
c.Mode = "vxlan"
|
||||||
|
}
|
||||||
|
if c.Mode != "vxlan" && c.Mode != "bridge" {
|
||||||
|
return fmt.Errorf("unknown subnet mode %q", c.Mode)
|
||||||
|
}
|
||||||
if _, err := kv.GetFromDB(db, "subnet/"+c.Name+"/state"); err == nil {
|
if _, err := kv.GetFromDB(db, "subnet/"+c.Name+"/state"); err == nil {
|
||||||
return fmt.Errorf("subnet %q already exists", c.Name)
|
return fmt.Errorf("subnet %q already exists", c.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -37,10 +45,14 @@ func (c CreateSubnetCommand) Prepare(db *badger.DB, cfg *configuration.Config) e
|
||||||
}
|
}
|
||||||
kv.AddInDB(db, "subnet/"+c.Name+"/state", "creating")
|
kv.AddInDB(db, "subnet/"+c.Name+"/state", "creating")
|
||||||
kv.AddInDB(db, "subnet/"+c.Name+"/vpc", c.VPC)
|
kv.AddInDB(db, "subnet/"+c.Name+"/vpc", c.VPC)
|
||||||
kv.AddInDB(db, "subnet/"+c.Name+"/vxlan_id", strconv.Itoa(c.VxlanID))
|
kv.AddInDB(db, "subnet/"+c.Name+"/mode", c.Mode)
|
||||||
kv.AddInDB(db, "subnet/"+c.Name+"/local_iface", localIface)
|
kv.AddInDB(db, "subnet/"+c.Name+"/local_iface", localIface)
|
||||||
kv.AddInDB(db, "subnet/"+c.Name+"/gateway_ip", c.GatewayIP)
|
kv.AddInDB(db, "subnet/"+c.Name+"/interface_ip", c.InterfaceIP)
|
||||||
kv.AddInDB(db, "subnet/"+c.Name+"/cidr", c.CIDR)
|
kv.AddInDB(db, "subnet/"+c.Name+"/cidr", c.CIDR)
|
||||||
|
kv.AddInDB(db, "subnet/"+c.Name+"/default_route", strconv.FormatBool(c.DefaultRoute))
|
||||||
|
if c.Mode == "vxlan" {
|
||||||
|
kv.AddInDB(db, "subnet/"+c.Name+"/vxlan_id", strconv.Itoa(c.VxlanID))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ func TestCreateSubnetCommand_Prepare_Success(t *testing.T) {
|
||||||
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
cmd := CreateSubnetCommand{
|
cmd := CreateSubnetCommand{
|
||||||
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
if err := cmd.Prepare(db, testCfg()); err != nil {
|
if err := cmd.Prepare(db, testCfg()); err != nil {
|
||||||
t.Fatalf("Prepare a échoué : %v", err)
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
|
|
@ -40,7 +40,7 @@ func TestCreateSubnetCommand_Prepare_UsesIfaceTypeMapping(t *testing.T) {
|
||||||
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
cmd := CreateSubnetCommand{
|
cmd := CreateSubnetCommand{
|
||||||
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
cmd.Prepare(db, testCfg())
|
cmd.Prepare(db, testCfg())
|
||||||
iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface")
|
iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface")
|
||||||
|
|
@ -54,7 +54,7 @@ func TestCreateSubnetCommand_Prepare_UsesDefaultIfaceWhenTypeUnknown(t *testing.
|
||||||
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
cmd := CreateSubnetCommand{
|
cmd := CreateSubnetCommand{
|
||||||
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
IfaceType: "inconnu", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
IfaceType: "inconnu", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
cmd.Prepare(db, testCfg())
|
cmd.Prepare(db, testCfg())
|
||||||
iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface")
|
iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface")
|
||||||
|
|
@ -69,7 +69,7 @@ func TestCreateSubnetCommand_Prepare_Duplicate(t *testing.T) {
|
||||||
kv.AddInDB(db, "subnet/sn-exist/state", "created")
|
kv.AddInDB(db, "subnet/sn-exist/state", "created")
|
||||||
cmd := CreateSubnetCommand{
|
cmd := CreateSubnetCommand{
|
||||||
Name: "sn-exist", VPC: "vpc-1", VxlanID: 100,
|
Name: "sn-exist", VPC: "vpc-1", VxlanID: 100,
|
||||||
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
if err := cmd.Prepare(db, testCfg()); err == nil {
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
t.Error("Prepare devrait échouer sur un subnet déjà existant")
|
t.Error("Prepare devrait échouer sur un subnet déjà existant")
|
||||||
|
|
@ -80,7 +80,7 @@ func TestCreateSubnetCommand_Prepare_VPCNotFound(t *testing.T) {
|
||||||
_, db := newTestDispatcher(t)
|
_, db := newTestDispatcher(t)
|
||||||
cmd := CreateSubnetCommand{
|
cmd := CreateSubnetCommand{
|
||||||
Name: "sn-1", VPC: "vpc-inexistant", VxlanID: 100,
|
Name: "sn-1", VPC: "vpc-inexistant", VxlanID: 100,
|
||||||
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
if err := cmd.Prepare(db, testCfg()); err == nil {
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
t.Error("Prepare devrait échouer si le VPC n'existe pas")
|
t.Error("Prepare devrait échouer si le VPC n'existe pas")
|
||||||
|
|
@ -92,7 +92,7 @@ func TestCreateSubnetCommand_Prepare_VPCDeleting(t *testing.T) {
|
||||||
kv.AddInDB(db, "vpc/vpc-dying/state", "deleting")
|
kv.AddInDB(db, "vpc/vpc-dying/state", "deleting")
|
||||||
cmd := CreateSubnetCommand{
|
cmd := CreateSubnetCommand{
|
||||||
Name: "sn-1", VPC: "vpc-dying", VxlanID: 100,
|
Name: "sn-1", VPC: "vpc-dying", VxlanID: 100,
|
||||||
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
if err := cmd.Prepare(db, testCfg()); err == nil {
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
t.Error("Prepare devrait échouer si le VPC est en cours de suppression")
|
t.Error("Prepare devrait échouer si le VPC est en cours de suppression")
|
||||||
|
|
@ -104,13 +104,109 @@ func TestCreateSubnetCommand_Prepare_VPCDeleted(t *testing.T) {
|
||||||
kv.AddInDB(db, "vpc/vpc-gone/state", "deleted")
|
kv.AddInDB(db, "vpc/vpc-gone/state", "deleted")
|
||||||
cmd := CreateSubnetCommand{
|
cmd := CreateSubnetCommand{
|
||||||
Name: "sn-1", VPC: "vpc-gone", VxlanID: 100,
|
Name: "sn-1", VPC: "vpc-gone", VxlanID: 100,
|
||||||
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
}
|
}
|
||||||
if err := cmd.Prepare(db, testCfg()); err == nil {
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
t.Error("Prepare devrait échouer si le VPC est supprimé")
|
t.Error("Prepare devrait échouer si le VPC est supprimé")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_DefaultsToVxlanMode(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
cmd.Prepare(db, testCfg())
|
||||||
|
mode, _ := kv.GetFromDB(db, "subnet/sn-1/mode")
|
||||||
|
if mode != "vxlan" {
|
||||||
|
t.Errorf("mode attendu vxlan, obtenu %q", mode)
|
||||||
|
}
|
||||||
|
if _, err := kv.GetFromDB(db, "subnet/sn-1/vxlan_id"); err != nil {
|
||||||
|
t.Error("vxlan_id devrait être écrit en mode vxlan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_BridgeMode_Success(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", Mode: "bridge",
|
||||||
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err != nil {
|
||||||
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
|
}
|
||||||
|
mode, _ := kv.GetFromDB(db, "subnet/sn-1/mode")
|
||||||
|
if mode != "bridge" {
|
||||||
|
t.Errorf("mode attendu bridge, obtenu %q", mode)
|
||||||
|
}
|
||||||
|
iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface")
|
||||||
|
if iface != "br-vms" {
|
||||||
|
t.Errorf("local_iface attendu br-vms, obtenu %q", iface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_BridgeMode_NoVxlanID(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", Mode: "bridge",
|
||||||
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
cmd.Prepare(db, testCfg())
|
||||||
|
if _, err := kv.GetFromDB(db, "subnet/sn-1/vxlan_id"); err == nil {
|
||||||
|
t.Error("vxlan_id ne devrait pas être écrit en mode bridge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_UnknownMode(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", Mode: "vlan",
|
||||||
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer pour un mode inconnu")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_DefaultRouteStored(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
DefaultRoute: true,
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err != nil {
|
||||||
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
|
}
|
||||||
|
val, err := kv.GetFromDB(db, "subnet/sn-1/default_route")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("default_route non écrit en DB : %v", err)
|
||||||
|
}
|
||||||
|
if val != "true" {
|
||||||
|
t.Errorf("default_route attendu true, obtenu %q", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_DefaultRouteFalseByDefault(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
|
IfaceType: "vms", InterfaceIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
cmd.Prepare(db, testCfg())
|
||||||
|
val, _ := kv.GetFromDB(db, "subnet/sn-1/default_route")
|
||||||
|
if val != "false" {
|
||||||
|
t.Errorf("default_route attendu false, obtenu %q", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- DeleteSubnetCommand.Prepare ---
|
// --- DeleteSubnetCommand.Prepare ---
|
||||||
|
|
||||||
func TestDeleteSubnetCommand_Prepare_Success(t *testing.T) {
|
func TestDeleteSubnetCommand_Prepare_Success(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package dispatcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||||
|
|
@ -12,15 +14,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type StartVMCommand struct {
|
type StartVMCommand struct {
|
||||||
Name string
|
Name string
|
||||||
Subnet string
|
Subnet string
|
||||||
IP string
|
IP string
|
||||||
MetadataPort string
|
VolumePath string
|
||||||
VolumePath string
|
Memory int
|
||||||
Memory int
|
CPUs int
|
||||||
CPUs int
|
UEFI bool
|
||||||
Password string
|
Password string
|
||||||
SSHKey string
|
SSHKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
||||||
|
|
@ -34,13 +36,20 @@ func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
||||||
if subnetState == "deleting" || subnetState == "deleted" {
|
if subnetState == "deleting" || subnetState == "deleted" {
|
||||||
return fmt.Errorf("subnet %q is %s", c.Subnet, subnetState)
|
return fmt.Errorf("subnet %q is %s", c.Subnet, subnetState)
|
||||||
}
|
}
|
||||||
|
port, err := allocateMetadataPort(db)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("allocate metadata port: %w", err)
|
||||||
|
}
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/state", "starting")
|
kv.AddInDB(db, "vm/"+c.Name+"/state", "starting")
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/subnet", c.Subnet)
|
kv.AddInDB(db, "vm/"+c.Name+"/subnet", c.Subnet)
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/ip", c.IP)
|
kv.AddInDB(db, "vm/"+c.Name+"/ip", c.IP)
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/metadata_port", c.MetadataPort)
|
kv.AddInDB(db, "vm/"+c.Name+"/metadata_port", strconv.Itoa(port))
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/volume_path", c.VolumePath)
|
kv.AddInDB(db, "vm/"+c.Name+"/volume_path", c.VolumePath)
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/memory", strconv.Itoa(c.Memory))
|
kv.AddInDB(db, "vm/"+c.Name+"/memory", strconv.Itoa(c.Memory))
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/cpus", strconv.Itoa(c.CPUs))
|
kv.AddInDB(db, "vm/"+c.Name+"/cpus", strconv.Itoa(c.CPUs))
|
||||||
|
if c.UEFI {
|
||||||
|
kv.AddInDB(db, "vm/"+c.Name+"/uefi", "true")
|
||||||
|
}
|
||||||
if c.Password != "" {
|
if c.Password != "" {
|
||||||
kv.AddInDB(db, "vm/"+c.Name+"/password", c.Password)
|
kv.AddInDB(db, "vm/"+c.Name+"/password", c.Password)
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +59,28 @@ func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allocateMetadataPort(db *badger.DB) (int, error) {
|
||||||
|
entries, err := kv.ListByPrefix(db, "vm/")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
used := make(map[int]struct{})
|
||||||
|
for key, value := range entries {
|
||||||
|
if strings.HasSuffix(key, "/metadata_port") {
|
||||||
|
if p, err := strconv.Atoi(value); err == nil {
|
||||||
|
used[p] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for range 100 {
|
||||||
|
p := rand.Intn(9000) + 1000
|
||||||
|
if _, taken := used[p]; !taken {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("no free metadata port available in [1000, 9999]")
|
||||||
|
}
|
||||||
|
|
||||||
func (c StartVMCommand) Execute(db *badger.DB, cfg *configuration.Config) error {
|
func (c StartVMCommand) Execute(db *badger.DB, cfg *configuration.Config) error {
|
||||||
timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second)
|
timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second)
|
||||||
for {
|
for {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package dispatcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -13,12 +14,19 @@ import (
|
||||||
|
|
||||||
type CreateVPCCommand struct {
|
type CreateVPCCommand struct {
|
||||||
Name string
|
Name string
|
||||||
|
CIDR string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreateVPCCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
func (c CreateVPCCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
||||||
if _, err := kv.GetFromDB(db, "vpc/"+c.Name+"/state"); err == nil {
|
if _, err := kv.GetFromDB(db, "vpc/"+c.Name+"/state"); err == nil {
|
||||||
return fmt.Errorf("vpc %q already exists", c.Name)
|
return fmt.Errorf("vpc %q already exists", c.Name)
|
||||||
}
|
}
|
||||||
|
if _, _, err := net.ParseCIDR(c.CIDR); err != nil {
|
||||||
|
return fmt.Errorf("invalid cidr %q: %w", c.CIDR, err)
|
||||||
|
}
|
||||||
|
if err := kv.AddInDB(db, "vpc/"+c.Name+"/cidr", c.CIDR); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return kv.AddInDB(db, "vpc/"+c.Name+"/state", "creating")
|
return kv.AddInDB(db, "vpc/"+c.Name+"/state", "creating")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
func TestCreateVPCCommand_Prepare_NewVPC(t *testing.T) {
|
func TestCreateVPCCommand_Prepare_NewVPC(t *testing.T) {
|
||||||
_, db := newTestDispatcher(t)
|
_, db := newTestDispatcher(t)
|
||||||
cmd := CreateVPCCommand{Name: "vpc-1"}
|
cmd := CreateVPCCommand{Name: "vpc-1", CIDR: "10.0.0.0/16"}
|
||||||
if err := cmd.Prepare(db, nil); err != nil {
|
if err := cmd.Prepare(db, nil); err != nil {
|
||||||
t.Fatalf("Prepare a échoué : %v", err)
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -21,17 +21,32 @@ func TestCreateVPCCommand_Prepare_NewVPC(t *testing.T) {
|
||||||
if state != "creating" {
|
if state != "creating" {
|
||||||
t.Errorf("state attendu creating, obtenu %q", state)
|
t.Errorf("state attendu creating, obtenu %q", state)
|
||||||
}
|
}
|
||||||
|
cidr, err := kv.GetFromDB(db, "vpc/vpc-1/cidr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cidr non écrit en DB : %v", err)
|
||||||
|
}
|
||||||
|
if cidr != "10.0.0.0/16" {
|
||||||
|
t.Errorf("cidr attendu 10.0.0.0/16, obtenu %q", cidr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateVPCCommand_Prepare_Duplicate(t *testing.T) {
|
func TestCreateVPCCommand_Prepare_Duplicate(t *testing.T) {
|
||||||
_, db := newTestDispatcher(t)
|
_, db := newTestDispatcher(t)
|
||||||
kv.AddInDB(db, "vpc/vpc-exist/state", "created")
|
kv.AddInDB(db, "vpc/vpc-exist/state", "created")
|
||||||
cmd := CreateVPCCommand{Name: "vpc-exist"}
|
cmd := CreateVPCCommand{Name: "vpc-exist", CIDR: "10.0.0.0/16"}
|
||||||
if err := cmd.Prepare(db, nil); err == nil {
|
if err := cmd.Prepare(db, nil); err == nil {
|
||||||
t.Error("Prepare devrait échouer sur un VPC déjà existant")
|
t.Error("Prepare devrait échouer sur un VPC déjà existant")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateVPCCommand_Prepare_InvalidCIDR(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
cmd := CreateVPCCommand{Name: "vpc-bad", CIDR: "not-a-cidr"}
|
||||||
|
if err := cmd.Prepare(db, nil); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer avec un CIDR invalide")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- DeleteVPCCommand.Prepare ---
|
// --- DeleteVPCCommand.Prepare ---
|
||||||
|
|
||||||
func TestDeleteVPCCommand_Prepare_Success(t *testing.T) {
|
func TestDeleteVPCCommand_Prepare_Success(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -13,46 +13,48 @@ func deleteRule(args ...string) error {
|
||||||
return exec.Command("ebtables", append([]string{"-D"}, args...)...).Run()
|
return exec.Command("ebtables", append([]string{"-D"}, args...)...).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func DropARPToGateway(bridge, gatewayIP string) error {
|
func DropARPToGateway(iface, ip string) error {
|
||||||
if err := addRule("FORWARD",
|
if err := addRule("FORWARD",
|
||||||
"--out-interface", bridge,
|
"--out-interface", iface,
|
||||||
"-p", "arp",
|
"-p", "arp",
|
||||||
"--arp-op", "Request",
|
"--arp-op", "Request",
|
||||||
"--arp-ip-dst", gatewayIP,
|
"--arp-ip-dst", ip,
|
||||||
"-j", "DROP"); err != nil {
|
"-j", "DROP"); err != nil {
|
||||||
return fmt.Errorf("ebtables arp rule: %w", err)
|
return fmt.Errorf("ebtables arp rule: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DropDHCP(bridge string) error {
|
func DropDHCP(iface, ip string) error {
|
||||||
if err := addRule("FORWARD",
|
if err := addRule("FORWARD",
|
||||||
"--out-interface", bridge,
|
"--out-interface", iface,
|
||||||
"-p", "IPv4",
|
"-p", "IPv4",
|
||||||
"--ip-protocol", "udp",
|
"--ip-protocol", "udp",
|
||||||
"--ip-source-port", "67:68",
|
"--ip-source-port", "67:68",
|
||||||
"--ip-destination-port", "67:68",
|
"--ip-destination-port", "67:68",
|
||||||
|
"--ip-source", ip,
|
||||||
"-j", "DROP"); err != nil {
|
"-j", "DROP"); err != nil {
|
||||||
return fmt.Errorf("ebtables dhcp rule: %w", err)
|
return fmt.Errorf("ebtables dhcp rule: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteARPToGateway(bridge, gatewayIP string) error {
|
func DeleteARPToGateway(iface, ip string) error {
|
||||||
return deleteRule("FORWARD",
|
return deleteRule("FORWARD",
|
||||||
"--out-interface", bridge,
|
"--out-interface", iface,
|
||||||
"-p", "arp",
|
"-p", "arp",
|
||||||
"--arp-op", "Request",
|
"--arp-op", "Request",
|
||||||
"--arp-ip-dst", gatewayIP,
|
"--arp-ip-dst", ip,
|
||||||
"-j", "DROP")
|
"-j", "DROP")
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDHCP(bridge string) error {
|
func DeleteDHCP(iface, ip string) error {
|
||||||
return deleteRule("FORWARD",
|
return deleteRule("FORWARD",
|
||||||
"--out-interface", bridge,
|
"--out-interface", iface,
|
||||||
"-p", "IPv4",
|
"-p", "IPv4",
|
||||||
"--ip-protocol", "udp",
|
"--ip-protocol", "udp",
|
||||||
"--ip-source-port", "67:68",
|
"--ip-source-port", "67:68",
|
||||||
"--ip-destination-port", "67:68",
|
"--ip-destination-port", "67:68",
|
||||||
|
"--ip-source", ip,
|
||||||
"-j", "DROP")
|
"-j", "DROP")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,8 @@
|
||||||
users:
|
users:
|
||||||
- name: syonad
|
- name: syonad
|
||||||
lock_passwd: false
|
lock_passwd: false
|
||||||
gecos: alpine Cloud User
|
|
||||||
groups: [adm, wheel]
|
|
||||||
doas:
|
|
||||||
- permit nopass syonad
|
|
||||||
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
||||||
shell: /bin/ash
|
shell: /bin/bash
|
||||||
passwd: "{{ .Password }}"
|
passwd: "{{ .Password }}"
|
||||||
ssh_authorized_keys:
|
ssh_authorized_keys:
|
||||||
- "{{ .SSHKEY }}"
|
- "{{ .SSHKEY }}"
|
||||||
|
|
|
||||||
29
internal/netif/gateway_linux.go
Normal file
29
internal/netif/gateway_linux.go
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package netif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDefaultGateway() (net.IP, error) {
|
||||||
|
routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list routes: %w", err)
|
||||||
|
}
|
||||||
|
for _, r := range routes {
|
||||||
|
if r.Gw == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.Dst == nil {
|
||||||
|
return r.Gw, nil
|
||||||
|
}
|
||||||
|
if ones, _ := r.Dst.Mask.Size(); ones == 0 {
|
||||||
|
return r.Gw, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no default gateway found")
|
||||||
|
}
|
||||||
12
internal/netif/gateway_other.go
Normal file
12
internal/netif/gateway_other.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package netif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDefaultGateway() (net.IP, error) {
|
||||||
|
return nil, fmt.Errorf("not supported on this platform")
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,9 @@ package qemu
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
@ -14,6 +16,11 @@ type Config struct {
|
||||||
VolumePath string
|
VolumePath string
|
||||||
Memory int
|
Memory int
|
||||||
CPUs int
|
CPUs int
|
||||||
|
UEFICodePath string
|
||||||
|
UEFIVarsPath string
|
||||||
|
SerialDir string
|
||||||
|
MonitorDir string
|
||||||
|
QMPDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(cfg Config) error {
|
func Start(cfg Config) error {
|
||||||
|
|
@ -27,20 +34,44 @@ func Start(cfg Config) error {
|
||||||
cpus = 1
|
cpus = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("qemu-system-x86_64",
|
for _, dir := range []string{cfg.SerialDir, cfg.MonitorDir, cfg.QMPDir} {
|
||||||
|
if dir != "" {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serialSock := filepath.Join(cfg.SerialDir, cfg.Name+".sock")
|
||||||
|
monitorSock := filepath.Join(cfg.MonitorDir, cfg.Name+".sock")
|
||||||
|
qmpSock := filepath.Join(cfg.QMPDir, cfg.Name+".sock")
|
||||||
|
|
||||||
|
args := []string{
|
||||||
"-enable-kvm",
|
"-enable-kvm",
|
||||||
"-cpu", "host",
|
"-cpu", "host",
|
||||||
"-m", fmt.Sprintf("%d", memory),
|
"-m", fmt.Sprintf("%d", memory),
|
||||||
"-smp", fmt.Sprintf("%d", cpus),
|
"-smp", fmt.Sprintf("%d", cpus),
|
||||||
"-serial", fmt.Sprintf("unix:/tmp/%s.sock,server,nowait", cfg.Name),
|
"-serial", fmt.Sprintf("unix:%s,server,nowait", serialSock),
|
||||||
"-monitor", fmt.Sprintf("unix:/tmp/%s.mon-sock,server,nowait", cfg.Name),
|
"-monitor", fmt.Sprintf("unix:%s,server,nowait", monitorSock),
|
||||||
"-qmp", fmt.Sprintf("unix:/tmp/%s.qmp-sock,server,nowait", cfg.Name),
|
"-qmp", fmt.Sprintf("unix:%s,server,nowait", qmpSock),
|
||||||
"-display", "none",
|
"-display", "none",
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.UEFICodePath != "" && cfg.UEFIVarsPath != "" {
|
||||||
|
args = append(args,
|
||||||
|
"-drive", fmt.Sprintf("if=pflash,format=raw,readonly=on,file=%s", cfg.UEFICodePath),
|
||||||
|
"-drive", fmt.Sprintf("if=pflash,format=raw,file=%s", cfg.UEFIVarsPath),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args,
|
||||||
"-drive", fmt.Sprintf("file=%s,if=virtio", cfg.VolumePath),
|
"-drive", fmt.Sprintf("file=%s,if=virtio", cfg.VolumePath),
|
||||||
"-netdev", fmt.Sprintf("tap,id=net0,ifname=tap%d,script=no,downscript=no", cfg.TapID),
|
"-netdev", fmt.Sprintf("tap,id=net0,ifname=tap%d,script=no,downscript=no", cfg.TapID),
|
||||||
"-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s", cfg.Mac),
|
"-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s", cfg.Mac),
|
||||||
"-daemonize",
|
"-daemonize",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cmd := exec.Command("qemu-system-x86_64", args...)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("qemu-system-x86_64: %w", err)
|
return fmt.Errorf("qemu-system-x86_64: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,11 @@ import "errors"
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Name, Mac, VolumePath string
|
Name, Mac, VolumePath string
|
||||||
TapID, Memory, CPUs int
|
TapID, Memory, CPUs int
|
||||||
|
UEFICodePath string
|
||||||
|
UEFIVarsPath string
|
||||||
|
SerialDir string
|
||||||
|
MonitorDir string
|
||||||
|
QMPDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(_ Config) error {
|
func Start(_ Config) error {
|
||||||
|
|
|
||||||
|
|
@ -27,46 +27,48 @@ func CreateSubnet(db *badger.DB, subnetName string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
|
if err := createSubnet(db, subnetName, d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := netif.CreateVethToNetns("v-"+d.subnetID+"-e", "v-"+d.subnetID+"-i", "/var/run/netns/"+d.vpc, 1500); err != nil {
|
return kv.AddInDB(db, "subnet/"+subnetName+"/state", "created")
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSubnet(db *badger.DB, subnetName string, d subnetData) error {
|
||||||
|
vethE := "v-" + d.subnetID + "-e"
|
||||||
|
vethI := "v-" + d.subnetID + "-i"
|
||||||
|
|
||||||
|
if err := netif.CreateVethToNetns(vethE, vethI, "/var/run/netns/"+d.vpc, 1500); err != nil {
|
||||||
return fmt.Errorf("create veth: %w", err)
|
return fmt.Errorf("create veth: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netif.CreateBridge(d.bridge, 1500); err != nil {
|
switch d.mode {
|
||||||
return fmt.Errorf("create bridge: %w", err)
|
case "vxlan":
|
||||||
}
|
if err := setupVxlanHost(d, vethE); err != nil {
|
||||||
|
return err
|
||||||
if err := netns.Call(d.vpc, func() error {
|
|
||||||
return netif.CreateBridge(d.bridge, 1500)
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("create bridge in netns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := netif.CreateVxlan(vxlanIface, d.vxlanID, d.localIface, 1500); err != nil {
|
|
||||||
return fmt.Errorf("create vxlan: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := netif.BridgeSetMaster("v-"+d.subnetID+"-e", d.bridge); err != nil {
|
|
||||||
return fmt.Errorf("add veth-e to bridge: %w", err)
|
|
||||||
}
|
|
||||||
if err := netns.Call(d.vpc, func() error {
|
|
||||||
return netif.BridgeSetMaster("v-"+d.subnetID+"-i", d.bridge)
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("add veth-i to bridge in netns: %w", err)
|
|
||||||
}
|
|
||||||
if err := netif.BridgeSetMaster(vxlanIface, d.bridge); err != nil {
|
|
||||||
return fmt.Errorf("add vxlan to bridge: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, iface := range []string{"v-" + d.subnetID + "-e", vxlanIface, d.bridge} {
|
|
||||||
if err := netif.LinkSetUp(iface); err != nil {
|
|
||||||
return fmt.Errorf("set up %s: %w", iface, err)
|
|
||||||
}
|
}
|
||||||
|
case "bridge":
|
||||||
|
if err := netif.BridgeSetMaster(vethE, d.localIface); err != nil {
|
||||||
|
return fmt.Errorf("add veth-e to bridge: %w", err)
|
||||||
|
}
|
||||||
|
if err := netif.LinkSetUp(vethE); err != nil {
|
||||||
|
return fmt.Errorf("set up %s: %w", vethE, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown subnet mode %q", d.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netns.Call(d.vpc, func() error {
|
if err := netns.Call(d.vpc, func() error {
|
||||||
for _, iface := range []string{"v-" + d.subnetID + "-i", d.bridge} {
|
if err := netif.CreateBridge(d.bridge, 1500); err != nil {
|
||||||
|
return fmt.Errorf("create bridge: %w", err)
|
||||||
|
}
|
||||||
|
return netif.BridgeSetMaster(vethI, d.bridge)
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("setup bridge in netns: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netns.Call(d.vpc, func() error {
|
||||||
|
for _, iface := range []string{vethI, d.bridge} {
|
||||||
if err := netif.LinkSetUp(iface); err != nil {
|
if err := netif.LinkSetUp(iface); err != nil {
|
||||||
return fmt.Errorf("set up %s: %w", iface, err)
|
return fmt.Errorf("set up %s: %w", iface, err)
|
||||||
}
|
}
|
||||||
|
|
@ -76,31 +78,81 @@ func CreateSubnet(db *badger.DB, subnetName string) error {
|
||||||
return fmt.Errorf("set up interfaces in netns: %w", err)
|
return fmt.Errorf("set up interfaces in netns: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netns.Call(d.vpc, func() error {
|
switch d.mode {
|
||||||
return netif.AddrAdd(d.bridge, d.gatewayIP)
|
case "vxlan":
|
||||||
}); err != nil {
|
if err := netns.Call(d.vpc, func() error {
|
||||||
return fmt.Errorf("add addr to bridge in netns: %w", err)
|
if err := netif.AddrAdd(d.bridge, d.interfaceIP); err != nil {
|
||||||
|
return fmt.Errorf("add addr: %w", err)
|
||||||
|
}
|
||||||
|
if err := netif.RouteAdd(d.bridge, d.cidr); err != nil {
|
||||||
|
return fmt.Errorf("add route: %w", err)
|
||||||
|
}
|
||||||
|
if err := ebtables.DropARPToGateway(vethI, d.interfaceIP.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ebtables.DropDHCP(vethI, d.interfaceIP.String())
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("configure netns: %w", err)
|
||||||
|
}
|
||||||
|
case "bridge":
|
||||||
|
if err := netns.Call(d.vpc, func() error {
|
||||||
|
if err := netif.AddrAdd(d.bridge, d.interfaceIP); err != nil {
|
||||||
|
return fmt.Errorf("add addr: %w", err)
|
||||||
|
}
|
||||||
|
if err := netif.RouteAdd(d.bridge, d.cidr); err != nil {
|
||||||
|
return fmt.Errorf("add route: %w", err)
|
||||||
|
}
|
||||||
|
return ebtables.DropDHCP(vethI, d.interfaceIP.String())
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("configure netns: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netns.Call(d.vpc, func() error {
|
return startDHCP(db, subnetName, d)
|
||||||
return netif.RouteAdd(d.bridge, d.cidr)
|
}
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("add route in netns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ebtables.DropARPToGateway(d.bridge, d.gatewayIP.String()); err != nil {
|
func setupVxlanHost(d subnetData, vethE string) error {
|
||||||
return err
|
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
|
||||||
}
|
|
||||||
if err := ebtables.DropDHCP(d.bridge); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if err := netif.CreateBridge(d.bridge, 1500); err != nil {
|
||||||
|
return fmt.Errorf("create bridge: %w", err)
|
||||||
|
}
|
||||||
|
if err := netif.CreateVxlan(vxlanIface, d.vxlanID, d.localIface, 1500); err != nil {
|
||||||
|
return fmt.Errorf("create vxlan: %w", err)
|
||||||
|
}
|
||||||
|
if err := netif.BridgeSetMaster(vethE, d.bridge); err != nil {
|
||||||
|
return fmt.Errorf("add veth-e to bridge: %w", err)
|
||||||
|
}
|
||||||
|
if err := netif.BridgeSetMaster(vxlanIface, d.bridge); err != nil {
|
||||||
|
return fmt.Errorf("add vxlan to bridge: %w", err)
|
||||||
|
}
|
||||||
|
for _, iface := range []string{vethE, vxlanIface, d.bridge} {
|
||||||
|
if err := netif.LinkSetUp(iface); err != nil {
|
||||||
|
return fmt.Errorf("set up %s: %w", iface, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startDHCP(db *badger.DB, subnetName string, d subnetData) error {
|
||||||
conf := dhcp.Config{
|
conf := dhcp.Config{
|
||||||
Network: d.cidr,
|
Network: d.cidr,
|
||||||
Gateway: d.gatewayIP,
|
|
||||||
Name: d.vpc + "_" + d.bridge,
|
Name: d.vpc + "_" + d.bridge,
|
||||||
ConfDir: "/etc/dnsmasq.d",
|
ConfDir: "/etc/dnsmasq.d",
|
||||||
}
|
}
|
||||||
|
switch d.mode {
|
||||||
|
case "vxlan":
|
||||||
|
conf.VPCGateway = d.interfaceIP
|
||||||
|
conf.VPCRoute = d.vpcCIDR
|
||||||
|
case "bridge":
|
||||||
|
if d.defaultRoute {
|
||||||
|
gw, err := netif.GetDefaultGateway()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get default gateway: %w", err)
|
||||||
|
}
|
||||||
|
conf.DefaultGateway = gw
|
||||||
|
}
|
||||||
|
}
|
||||||
_, entries, err := dhcp.GenerateConfig(conf)
|
_, entries, err := dhcp.GenerateConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generate dhcp config: %w", err)
|
return fmt.Errorf("generate dhcp config: %w", err)
|
||||||
|
|
@ -118,6 +170,5 @@ func CreateSubnet(db *badger.DB, subnetName string) error {
|
||||||
if err := svc.Start("dnsmasq@" + conf.Name + ".service"); err != nil {
|
if err := svc.Start("dnsmasq@" + conf.Name + ".service"); err != nil {
|
||||||
return fmt.Errorf("start dnsmasq: %w", err)
|
return fmt.Errorf("start dnsmasq: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return kv.AddInDB(db, "subnet/"+subnetName+"/state", "created")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type subnetData struct {
|
type subnetData struct {
|
||||||
vpc string
|
vpc string
|
||||||
subnetID string
|
subnetID string
|
||||||
bridge string
|
bridge string
|
||||||
vxlanID int
|
mode string
|
||||||
localIface string
|
vxlanID int
|
||||||
gatewayIP net.IP
|
localIface string
|
||||||
cidr *net.IPNet
|
interfaceIP net.IP
|
||||||
|
cidr *net.IPNet
|
||||||
|
vpcCIDR *net.IPNet
|
||||||
|
defaultRoute bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSubnet(db *badger.DB, name string) (subnetData, error) {
|
func loadSubnet(db *badger.DB, name string) (subnetData, error) {
|
||||||
|
|
@ -32,15 +35,23 @@ func loadSubnet(db *badger.DB, name string) (subnetData, error) {
|
||||||
}
|
}
|
||||||
d.vpc = vpc
|
d.vpc = vpc
|
||||||
|
|
||||||
vxlanIDStr, err := kv.GetFromDB(db, "subnet/"+name+"/vxlan_id")
|
mode, err := kv.GetFromDB(db, "subnet/"+name+"/mode")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return d, fmt.Errorf("get vxlan_id: %w", err)
|
return d, fmt.Errorf("get mode: %w", err)
|
||||||
}
|
}
|
||||||
vxlanID, err := strconv.Atoi(vxlanIDStr)
|
d.mode = mode
|
||||||
if err != nil {
|
|
||||||
return d, fmt.Errorf("parse vxlan_id: %w", err)
|
if d.mode == "vxlan" {
|
||||||
|
vxlanIDStr, err := kv.GetFromDB(db, "subnet/"+name+"/vxlan_id")
|
||||||
|
if err != nil {
|
||||||
|
return d, fmt.Errorf("get vxlan_id: %w", err)
|
||||||
|
}
|
||||||
|
vxlanID, err := strconv.Atoi(vxlanIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return d, fmt.Errorf("parse vxlan_id: %w", err)
|
||||||
|
}
|
||||||
|
d.vxlanID = vxlanID
|
||||||
}
|
}
|
||||||
d.vxlanID = vxlanID
|
|
||||||
|
|
||||||
localIface, err := kv.GetFromDB(db, "subnet/"+name+"/local_iface")
|
localIface, err := kv.GetFromDB(db, "subnet/"+name+"/local_iface")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -48,15 +59,15 @@ func loadSubnet(db *badger.DB, name string) (subnetData, error) {
|
||||||
}
|
}
|
||||||
d.localIface = localIface
|
d.localIface = localIface
|
||||||
|
|
||||||
gatewayIPStr, err := kv.GetFromDB(db, "subnet/"+name+"/gateway_ip")
|
interfaceIPStr, err := kv.GetFromDB(db, "subnet/"+name+"/interface_ip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return d, fmt.Errorf("get gateway_ip: %w", err)
|
return d, fmt.Errorf("get interface_ip: %w", err)
|
||||||
}
|
}
|
||||||
gatewayIP := net.ParseIP(gatewayIPStr)
|
interfaceIP := net.ParseIP(interfaceIPStr)
|
||||||
if gatewayIP == nil {
|
if interfaceIP == nil {
|
||||||
return d, fmt.Errorf("invalid gateway_ip: %s", gatewayIPStr)
|
return d, fmt.Errorf("invalid interface_ip: %s", interfaceIPStr)
|
||||||
}
|
}
|
||||||
d.gatewayIP = gatewayIP
|
d.interfaceIP = interfaceIP
|
||||||
|
|
||||||
cidrStr, err := kv.GetFromDB(db, "subnet/"+name+"/cidr")
|
cidrStr, err := kv.GetFromDB(db, "subnet/"+name+"/cidr")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -68,5 +79,21 @@ func loadSubnet(db *badger.DB, name string) (subnetData, error) {
|
||||||
}
|
}
|
||||||
d.cidr = ipNet
|
d.cidr = ipNet
|
||||||
|
|
||||||
|
defaultRouteStr, err := kv.GetFromDB(db, "subnet/"+name+"/default_route")
|
||||||
|
if err != nil {
|
||||||
|
return d, fmt.Errorf("get default_route: %w", err)
|
||||||
|
}
|
||||||
|
d.defaultRoute = defaultRouteStr == "true"
|
||||||
|
|
||||||
|
vpcCIDRStr, err := kv.GetFromDB(db, "vpc/"+d.vpc+"/cidr")
|
||||||
|
if err != nil {
|
||||||
|
return d, fmt.Errorf("get vpc cidr: %w", err)
|
||||||
|
}
|
||||||
|
_, vpcIPNet, err := net.ParseCIDR(vpcCIDRStr)
|
||||||
|
if err != nil {
|
||||||
|
return d, fmt.Errorf("parse vpc cidr: %w", err)
|
||||||
|
}
|
||||||
|
d.vpcCIDR = vpcIPNet
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,36 +27,64 @@ func DeleteSubnet(db *badger.DB, subnetName string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
|
if err := stopDHCP(db, subnetName, d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch d.mode {
|
||||||
|
case "vxlan":
|
||||||
|
if err := deleteSubnetVxlan(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "bridge":
|
||||||
|
if err := deleteSubnetBridge(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown subnet mode %q", d.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kv.AddInDB(db, "subnet/"+subnetName+"/state", "deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopDHCP(db *badger.DB, subnetName string, d subnetData) error {
|
||||||
svc, err := systemd.New()
|
svc, err := systemd.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to systemd: %w", err)
|
return fmt.Errorf("connect to systemd: %w", err)
|
||||||
}
|
}
|
||||||
defer svc.Close()
|
defer svc.Close()
|
||||||
|
|
||||||
if err := svc.Stop("dnsmasq@" + d.vpc + "_" + d.bridge + ".service"); err != nil {
|
svcName := "dnsmasq@" + d.vpc + "_" + d.bridge + ".service"
|
||||||
return fmt.Errorf("stop dnsmasq: %w", err)
|
if status, err := svc.Status(svcName); err == nil && status.ActiveState == "active" {
|
||||||
|
if err := svc.Stop(svcName); err != nil {
|
||||||
|
return fmt.Errorf("stop dnsmasq: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Remove("/etc/dnsmasq.d/" + d.vpc + "_" + d.bridge + ".conf"); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove("/etc/dnsmasq.d/" + d.vpc + "_" + d.bridge + ".conf"); err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("remove dnsmasq config: %w", err)
|
return fmt.Errorf("remove dnsmasq config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := kv.DeleteInDB(db, "subnet/"+subnetName+"/dhcp"); err != nil {
|
if err := kv.DeleteInDB(db, "subnet/"+subnetName+"/dhcp"); err != nil {
|
||||||
return fmt.Errorf("delete dhcp entries: %w", err)
|
return fmt.Errorf("delete dhcp entries: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := ebtables.DeleteARPToGateway(d.bridge, d.gatewayIP.String()); err != nil {
|
func deleteSubnetVxlan(d subnetData) error {
|
||||||
return fmt.Errorf("delete ebtables arp rule: %w", err)
|
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
|
||||||
}
|
vethI := "v-" + d.subnetID + "-i"
|
||||||
if err := ebtables.DeleteDHCP(d.bridge); err != nil {
|
|
||||||
return fmt.Errorf("delete ebtables dhcp rule: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := netns.Call(d.vpc, func() error {
|
if err := netns.Call(d.vpc, func() error {
|
||||||
|
if err := ebtables.DeleteARPToGateway(vethI, d.interfaceIP.String()); err != nil {
|
||||||
|
return fmt.Errorf("delete ebtables arp rule: %w", err)
|
||||||
|
}
|
||||||
|
if err := ebtables.DeleteDHCP(vethI, d.interfaceIP.String()); err != nil {
|
||||||
|
return fmt.Errorf("delete ebtables dhcp rule: %w", err)
|
||||||
|
}
|
||||||
return netif.DeleteLink(d.bridge)
|
return netif.DeleteLink(d.bridge)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("delete bridge in netns: %w", err)
|
return fmt.Errorf("delete netns resources: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netif.DeleteLink(vxlanIface); err != nil {
|
if err := netif.DeleteLink(vxlanIface); err != nil {
|
||||||
|
|
@ -70,6 +98,23 @@ func DeleteSubnet(db *badger.DB, subnetName string) error {
|
||||||
if err := netif.DeleteLink(d.bridge); err != nil {
|
if err := netif.DeleteLink(d.bridge); err != nil {
|
||||||
return fmt.Errorf("delete bridge: %w", err)
|
return fmt.Errorf("delete bridge: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return kv.AddInDB(db, "subnet/"+subnetName+"/state", "deleted")
|
}
|
||||||
|
|
||||||
|
func deleteSubnetBridge(d subnetData) error {
|
||||||
|
vethI := "v-" + d.subnetID + "-i"
|
||||||
|
|
||||||
|
if err := netns.Call(d.vpc, func() error {
|
||||||
|
if err := ebtables.DeleteDHCP(vethI, d.interfaceIP.String()); err != nil {
|
||||||
|
return fmt.Errorf("delete ebtables dhcp rule: %w", err)
|
||||||
|
}
|
||||||
|
return netif.DeleteLink(d.bridge)
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("delete netns resources: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netif.DeleteLink("v-" + d.subnetID + "-e"); err != nil {
|
||||||
|
return fmt.Errorf("delete veth: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ package vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||||
"git.g3e.fr/syonad/two/internal/iptables"
|
"git.g3e.fr/syonad/two/internal/iptables"
|
||||||
|
|
@ -33,7 +36,7 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netns.Call(d.vpcName, func() error {
|
if err := netns.Call(d.vpcName, func() error {
|
||||||
return iptables.AddMetadataRedirect(d.ip, d.gatewayIP, d.metadataPort)
|
return iptables.AddMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("add metadata redirect: %w", err)
|
return fmt.Errorf("add metadata redirect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +44,7 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
if err := metadata.StartMetadata(metadata.NoCloudConfig{
|
if err := metadata.StartMetadata(metadata.NoCloudConfig{
|
||||||
Name: name,
|
Name: name,
|
||||||
VpcName: d.vpcName,
|
VpcName: d.vpcName,
|
||||||
BindIP: d.gatewayIP,
|
BindIP: d.interfaceIP,
|
||||||
BindPort: d.metadataPort,
|
BindPort: d.metadataPort,
|
||||||
Password: d.password,
|
Password: d.password,
|
||||||
SSHKEY: d.sshkey,
|
SSHKEY: d.sshkey,
|
||||||
|
|
@ -49,18 +52,54 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
return fmt.Errorf("start metadata: %w", err)
|
return fmt.Errorf("start metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qcfg := qemu.Config{
|
||||||
|
Name: name,
|
||||||
|
TapID: d.tapID,
|
||||||
|
Mac: d.mac,
|
||||||
|
VolumePath: d.volumePath,
|
||||||
|
Memory: d.memory,
|
||||||
|
CPUs: d.cpus,
|
||||||
|
SerialDir: cfg.QEMU.SerialDir,
|
||||||
|
MonitorDir: cfg.QEMU.MonitorDir,
|
||||||
|
QMPDir: cfg.QEMU.QMPDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.uefi {
|
||||||
|
varsPath := filepath.Join(cfg.QEMU.UEFIVarsDir, name+"-uefi-vars.fd")
|
||||||
|
if err := copyFile(cfg.QEMU.OVMFVarsTemplate, varsPath); err != nil {
|
||||||
|
return fmt.Errorf("copy uefi vars: %w", err)
|
||||||
|
}
|
||||||
|
qcfg.UEFICodePath = cfg.QEMU.OVMFCodePath
|
||||||
|
qcfg.UEFIVarsPath = varsPath
|
||||||
|
}
|
||||||
|
|
||||||
if err := netns.Call(d.vpcName, func() error {
|
if err := netns.Call(d.vpcName, func() error {
|
||||||
return qemu.Start(qemu.Config{
|
return qemu.Start(qcfg)
|
||||||
Name: name,
|
|
||||||
TapID: d.tapID,
|
|
||||||
Mac: d.mac,
|
|
||||||
VolumePath: d.volumePath,
|
|
||||||
Memory: d.memory,
|
|
||||||
CPUs: d.cpus,
|
|
||||||
})
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("start qemu: %w", err)
|
return fmt.Errorf("start qemu: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return kv.AddInDB(db, "vm/"+name+"/state", "started")
|
return kv.AddInDB(db, "vm/"+name+"/state", "started")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return out.Sync()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
type vmData struct {
|
type vmData struct {
|
||||||
subnetName string
|
subnetName string
|
||||||
vpcName string
|
vpcName string
|
||||||
gatewayIP string
|
interfaceIP string
|
||||||
bridge string
|
bridge string
|
||||||
tapID int
|
tapID int
|
||||||
ip string
|
ip string
|
||||||
|
|
@ -23,6 +23,7 @@ type vmData struct {
|
||||||
volumePath string
|
volumePath string
|
||||||
memory int
|
memory int
|
||||||
cpus int
|
cpus int
|
||||||
|
uefi bool
|
||||||
password string
|
password string
|
||||||
sshkey string
|
sshkey string
|
||||||
}
|
}
|
||||||
|
|
@ -43,11 +44,11 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
|
||||||
}
|
}
|
||||||
d.vpcName = vpcName
|
d.vpcName = vpcName
|
||||||
|
|
||||||
gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip")
|
interfaceIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/interface_ip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return d, fmt.Errorf("get gateway_ip: %w", err)
|
return d, fmt.Errorf("get interface_ip: %w", err)
|
||||||
}
|
}
|
||||||
d.gatewayIP = gatewayIP
|
d.interfaceIP = interfaceIP
|
||||||
|
|
||||||
tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id")
|
tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -105,6 +106,9 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
|
||||||
return d, fmt.Errorf("parse cpus: %w", err)
|
return d, fmt.Errorf("parse cpus: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, _ := kv.GetFromDB(db, "vm/"+name+"/uefi"); v == "true" {
|
||||||
|
d.uefi = true
|
||||||
|
}
|
||||||
d.password, _ = kv.GetFromDB(db, "vm/"+name+"/password")
|
d.password, _ = kv.GetFromDB(db, "vm/"+name+"/password")
|
||||||
d.sshkey, _ = kv.GetFromDB(db, "vm/"+name+"/sshkey")
|
d.sshkey, _ = kv.GetFromDB(db, "vm/"+name+"/sshkey")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||||
|
|
@ -29,7 +31,7 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
socketPath := fmt.Sprintf("/tmp/%s.qmp-sock", name)
|
socketPath := filepath.Join(cfg.QEMU.QMPDir, name+".sock")
|
||||||
|
|
||||||
if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err != nil {
|
if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err != nil {
|
||||||
return fmt.Errorf("qmp system_powerdown: %w", err)
|
return fmt.Errorf("qmp system_powerdown: %w", err)
|
||||||
|
|
@ -52,7 +54,7 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netns.Call(d.vpcName, func() error {
|
if err := netns.Call(d.vpcName, func() error {
|
||||||
return iptables.DeleteMetadataRedirect(d.ip, d.gatewayIP, d.metadataPort)
|
return iptables.DeleteMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("delete metadata redirect: %w", err)
|
return fmt.Errorf("delete metadata redirect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -65,5 +67,10 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
return fmt.Errorf("delete tap: %w", err)
|
return fmt.Errorf("delete tap: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.uefi {
|
||||||
|
varsPath := filepath.Join(cfg.QEMU.UEFIVarsDir, name+"-uefi-vars.fd")
|
||||||
|
os.Remove(varsPath)
|
||||||
|
}
|
||||||
|
|
||||||
return kv.AddInDB(db, "vm/"+name+"/state", "stopped")
|
return kv.AddInDB(db, "vm/"+name+"/state", "stopped")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue