Compare commits

...

10 commits

Author SHA1 Message Date
b41b4f2518
f-28: generate metadata_port automatically at vm creation
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 23:28:18 +02:00
1b56a42627
f-28: refactor dhcp config to use VPCRoute and DefaultGateway
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 23:06:52 +02:00
c0caf1a24c
f-28: add GetDefaultGateway to netif
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 23:06:45 +02:00
bb5698fdda
f-28: add subnet default_route field
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 23:06:41 +02:00
76a840b80a
f-28: add vpc cidr field
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 23:06:36 +02:00
2504b435a3
f-28: fix: dhcp do not emit local default route
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 23:06:29 +02:00
32b78a84f9
f-28: fix: add proper dhcp handle
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 22:28:51 +02:00
848f965883
f-28: fix: renomage api param
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 21:58:33 +02:00
9492de7a2b
f-28: test: add tests
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 21:50:29 +02:00
91a5d7ac78
f-28: bridge: fix ebtables
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-18 21:11:09 +02:00
26 changed files with 536 additions and 181 deletions

View file

@ -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, gateway_ip, cidr] required: [name, vpc, interface_ip, cidr]
properties: properties:
name: name:
type: string type: string
@ -342,7 +349,7 @@ components:
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
@ -351,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
@ -377,23 +390,23 @@ components:
type: string type: string
description: Resolved interface name from agent config 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)

View file

@ -2,11 +2,13 @@ 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 {
@ -15,8 +17,9 @@ type SubnetCreateRequest struct {
Mode string `json:"mode"` Mode string `json:"mode"`
VxlanID int `json:"vxlan_id"` VxlanID int `json:"vxlan_id"`
IfaceType string `json:"iface_type"` IfaceType string `json:"iface_type"`
GatewayIP string `json:"gateway_ip"` InterfaceIP string `json:"interface_ip"`
CIDR string `json:"cidr"` CIDR string `json:"cidr"`
DefaultRoute bool `json:"default_route"`
} }
type Subnet struct { type Subnet struct {
@ -26,8 +29,9 @@ type Subnet struct {
Mode string `json:"mode"` Mode string `json:"mode"`
VxlanID int `json:"vxlan_id"` VxlanID int `json:"vxlan_id"`
LocalIface string `json:"local_iface"` LocalIface string `json:"local_iface"`
GatewayIP string `json:"gateway_ip"` InterfaceIP string `json:"interface_ip"`
CIDR string `json:"cidr"` CIDR string `json:"cidr"`
DefaultRoute bool `json:"default_route"`
} }
type VMInterface struct { type VMInterface struct {
@ -43,7 +47,6 @@ 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"`
Password string `json:"password"` Password string `json:"password"`

View file

@ -53,10 +53,12 @@ func (s *Server) getSubnet(w http.ResponseWriter, _ *http.Request, name string)
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)

View file

@ -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)

View file

@ -50,10 +50,12 @@ func (s *Server) listSubnets(w http.ResponseWriter, _ *http.Request) {
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))
@ -71,9 +73,9 @@ 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{
@ -82,8 +84,9 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) {
Mode: req.Mode, Mode: req.Mode,
VxlanID: req.VxlanID, VxlanID: req.VxlanID,
IfaceType: req.IfaceType, IfaceType: req.IfaceType,
GatewayIP: req.GatewayIP, InterfaceIP: req.InterfaceIP,
CIDR: req.CIDR, 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 {
@ -118,10 +121,12 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) {
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)

View file

@ -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
} }
@ -81,7 +81,6 @@ func (s *Server) startVM(w http.ResponseWriter, r *http.Request) {
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,

View file

@ -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) {

View file

@ -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 {

View file

@ -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})
} }

View file

@ -51,9 +51,13 @@ 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,
VPCRoute: vpcNet,
DefaultGateway: gw,
Name: "test", Name: "test",
ConfDir: t.TempDir(), 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,
} }

View file

@ -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)

View file

@ -6,7 +6,9 @@ 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)
VPCRoute *net.IPNet // if non-nil, emit dhcp-option=121,VPCRoute,VPCGateway
DefaultGateway net.IP // if non-nil, emit dhcp-option=3,DefaultGateway
Name string Name string
ConfDir string ConfDir string
} }

View file

@ -17,8 +17,9 @@ type CreateSubnetCommand struct {
Mode string Mode string
VxlanID int VxlanID int
IfaceType string IfaceType string
GatewayIP string InterfaceIP string
CIDR 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 {
@ -46,8 +47,9 @@ func (c CreateSubnetCommand) Prepare(db *badger.DB, cfg *configuration.Config) e
kv.AddInDB(db, "subnet/"+c.Name+"/vpc", c.VPC) kv.AddInDB(db, "subnet/"+c.Name+"/vpc", c.VPC)
kv.AddInDB(db, "subnet/"+c.Name+"/mode", c.Mode) 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" { if c.Mode == "vxlan" {
kv.AddInDB(db, "subnet/"+c.Name+"/vxlan_id", strconv.Itoa(c.VxlanID)) kv.AddInDB(db, "subnet/"+c.Name+"/vxlan_id", strconv.Itoa(c.VxlanID))
} }

View file

@ -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) {

View file

@ -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"
@ -15,7 +17,6 @@ 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
@ -34,10 +35,14 @@ 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))
@ -50,6 +55,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 {

View file

@ -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")
} }

View file

@ -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) {

View file

@ -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")
} }

View file

@ -0,0 +1,23 @@
//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.Dst == nil && r.Gw != nil {
return r.Gw, nil
}
}
return nil, fmt.Errorf("no default gateway found")
}

View 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")
}

View file

@ -81,32 +81,30 @@ func createSubnet(db *badger.DB, subnetName string, d subnetData) error {
switch d.mode { switch d.mode {
case "vxlan": case "vxlan":
if err := netns.Call(d.vpc, func() error { if err := netns.Call(d.vpc, func() error {
return netif.AddrAdd(d.bridge, d.gatewayIP) if err := netif.AddrAdd(d.bridge, d.interfaceIP); err != nil {
}); err != nil { return fmt.Errorf("add addr: %w", err)
return fmt.Errorf("add addr to bridge in netns: %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 := netns.Call(d.vpc, func() error {
return netif.RouteAdd(d.bridge, d.cidr) 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 { }); err != nil {
return fmt.Errorf("add route in netns: %w", err) return fmt.Errorf("configure netns: %w", err)
}
case "bridge":
}
applyEbtables := func() error {
if err := ebtables.DropARPToGateway(d.bridge, d.gatewayIP.String()); err != nil {
return err
}
return ebtables.DropDHCP(d.bridge)
}
switch d.mode {
case "vxlan":
if err := applyEbtables(); err != nil {
return err
}
case "bridge":
if err := netns.Call(d.vpc, applyEbtables); err != nil {
return fmt.Errorf("set ebtables in netns: %w", err)
} }
} }
@ -139,10 +137,22 @@ func setupVxlanHost(d subnetData, vethE string) error {
func startDHCP(db *badger.DB, subnetName string, d subnetData) error { 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)

View file

@ -17,8 +17,10 @@ type subnetData struct {
mode string mode string
vxlanID int vxlanID int
localIface string localIface string
gatewayIP net.IP interfaceIP net.IP
cidr *net.IPNet 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) {
@ -57,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 {
@ -77,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
} }

View file

@ -70,18 +70,18 @@ func stopDHCP(db *badger.DB, subnetName string, d subnetData) error {
func deleteSubnetVxlan(d subnetData) error { func deleteSubnetVxlan(d subnetData) error {
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID) vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
vethI := "v-" + d.subnetID + "-i"
if err := ebtables.DeleteARPToGateway(d.bridge, d.gatewayIP.String()); err != nil {
return fmt.Errorf("delete ebtables arp rule: %w", err)
}
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 {
@ -99,19 +99,15 @@ func deleteSubnetVxlan(d subnetData) error {
} }
func deleteSubnetBridge(d subnetData) error { func deleteSubnetBridge(d subnetData) error {
if err := netns.Call(d.vpc, func() error { vethI := "v-" + d.subnetID + "-i"
if err := ebtables.DeleteARPToGateway(d.bridge, d.gatewayIP.String()); err != nil {
return fmt.Errorf("delete ebtables arp rule: %w", err)
}
return ebtables.DeleteDHCP(d.bridge)
}); err != nil {
return fmt.Errorf("delete ebtables in netns: %w", err)
}
if err := netns.Call(d.vpc, func() error { 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) 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("v-" + d.subnetID + "-e"); err != nil { if err := netif.DeleteLink("v-" + d.subnetID + "-e"); err != nil {

View file

@ -33,7 +33,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 +41,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,

View file

@ -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
@ -43,11 +43,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 {

View file

@ -52,7 +52,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)
} }