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:
type: object
required: [name]
required: [name, cidr]
properties:
name:
type: string
description: Unique name for the VPC, must follow the format vp-[id]
pattern: '^vp-.+'
example: vp-00001
cidr:
type: string
description: CIDR block for the entire VPC address space
example: "10.0.0.0/16"
VPC:
type: object
@ -312,10 +316,13 @@ components:
type: string
enum: [creating, created, deleting, deleted]
example: created
cidr:
type: string
example: "10.0.0.0/16"
SubnetCreateRequest:
type: object
required: [name, vpc, gateway_ip, cidr]
required: [name, vpc, interface_ip, cidr]
properties:
name:
type: string
@ -342,7 +349,7 @@ components:
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.
example: vms
gateway_ip:
interface_ip:
type: string
format: ipv4
description: Gateway IP for the subnet
@ -351,6 +358,12 @@ components:
type: string
description: Subnet CIDR block
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:
type: object
@ -377,23 +390,23 @@ components:
type: string
description: Resolved interface name from agent config
example: br-000000
gateway_ip:
interface_ip:
type: string
example: "10.10.10.1"
cidr:
type: string
example: "10.10.10.0/24"
default_route:
type: boolean
example: false
VMCreateRequest:
type: object
required: [name, metadata_port, interfaces, storage]
required: [name, interfaces, storage]
properties:
name:
type: string
example: vm-00001
metadata_port:
type: string
example: "80"
memory:
type: integer
description: Memory in MB (default 512)

View file

@ -2,32 +2,36 @@ package agentapi
type VPCCreateRequest struct {
Name string `json:"name"`
CIDR string `json:"cidr"`
}
type VPC struct {
Name string `json:"name"`
State string `json:"state"`
CIDR string `json:"cidr"`
}
type SubnetCreateRequest struct {
Name string `json:"name"`
VPC string `json:"vpc"`
Mode string `json:"mode"`
VxlanID int `json:"vxlan_id"`
IfaceType string `json:"iface_type"`
GatewayIP string `json:"gateway_ip"`
CIDR string `json:"cidr"`
Name string `json:"name"`
VPC string `json:"vpc"`
Mode string `json:"mode"`
VxlanID int `json:"vxlan_id"`
IfaceType string `json:"iface_type"`
InterfaceIP string `json:"interface_ip"`
CIDR string `json:"cidr"`
DefaultRoute bool `json:"default_route"`
}
type Subnet struct {
Name string `json:"name"`
State string `json:"state"`
VPC string `json:"vpc"`
Mode string `json:"mode"`
VxlanID int `json:"vxlan_id"`
LocalIface string `json:"local_iface"`
GatewayIP string `json:"gateway_ip"`
CIDR string `json:"cidr"`
Name string `json:"name"`
State string `json:"state"`
VPC string `json:"vpc"`
Mode string `json:"mode"`
VxlanID int `json:"vxlan_id"`
LocalIface string `json:"local_iface"`
InterfaceIP string `json:"interface_ip"`
CIDR string `json:"cidr"`
DefaultRoute bool `json:"default_route"`
}
type VMInterface struct {
@ -42,14 +46,13 @@ type VMStorage struct {
}
type VMCreateRequest struct {
Name string `json:"name"`
MetadataPort string `json:"metadata_port"`
Memory int `json:"memory"`
CPUs int `json:"cpus"`
Password string `json:"password"`
SSHKey string `json:"sshkey"`
Interfaces []VMInterface `json:"interfaces"`
Storage []VMStorage `json:"storage"`
Name string `json:"name"`
Memory int `json:"memory"`
CPUs int `json:"cpus"`
Password string `json:"password"`
SSHKey string `json:"sshkey"`
Interfaces []VMInterface `json:"interfaces"`
Storage []VMStorage `json:"storage"`
}
type VM struct {

View file

@ -53,10 +53,12 @@ func (s *Server) getSubnet(w http.ResponseWriter, _ *http.Request, name string)
sub.VxlanID, _ = strconv.Atoi(value)
case "local_iface":
sub.LocalIface = value
case "gateway_ip":
sub.GatewayIP = value
case "interface_ip":
sub.InterfaceIP = value
case "cidr":
sub.CIDR = value
case "default_route":
sub.DefaultRoute = value == "true"
}
}
w.WriteHeader(http.StatusOK)

View file

@ -60,7 +60,7 @@ func TestPostSubnet_Created(t *testing.T) {
Name: "sn-new",
VPC: "vpc-1",
IfaceType: "vms",
GatewayIP: "10.0.0.1",
InterfaceIP: "10.0.0.1",
CIDR: "10.0.0.0/24",
}
body, _ := json.Marshal(req)
@ -95,7 +95,7 @@ func TestPostSubnet_IfaceTypeOptional(t *testing.T) {
req := SubnetCreateRequest{
Name: "sn-opt",
VPC: "vpc-1",
GatewayIP: "10.0.0.1",
InterfaceIP: "10.0.0.1",
CIDR: "10.0.0.0/24",
// IfaceType omis — doit utiliser default_interface
}
@ -113,7 +113,7 @@ func TestPostSubnet_VPCNotFound(t *testing.T) {
Name: "sn-1",
VPC: "vpc-inexistant",
IfaceType: "vms",
GatewayIP: "10.0.0.1",
InterfaceIP: "10.0.0.1",
CIDR: "10.0.0.0/24",
}
body, _ := json.Marshal(req)
@ -132,7 +132,7 @@ func TestPostSubnet_Duplicate(t *testing.T) {
Name: "sn-exist",
VPC: "vpc-1",
IfaceType: "vms",
GatewayIP: "10.0.0.1",
InterfaceIP: "10.0.0.1",
CIDR: "10.0.0.0/24",
}
body, _ := json.Marshal(req)
@ -150,7 +150,52 @@ func TestPostSubnet_VPCDeleting(t *testing.T) {
Name: "sn-1",
VPC: "vpc-dying",
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",
}
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/vpc", "vpc-1")
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)
w := httptest.NewRecorder()
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)
case "local_iface":
subnets[name].LocalIface = value
case "gateway_ip":
subnets[name].GatewayIP = value
case "interface_ip":
subnets[name].InterfaceIP = value
case "cidr":
subnets[name].CIDR = value
case "default_route":
subnets[name].DefaultRoute = value == "true"
}
}
result := make([]Subnet, 0, len(subnets))
@ -71,19 +73,20 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"})
return
}
if req.Name == "" || req.VPC == "" || req.GatewayIP == "" || req.CIDR == "" {
if req.Name == "" || req.VPC == "" || req.InterfaceIP == "" || req.CIDR == "" {
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
}
cmd := dispatcher.CreateSubnetCommand{
Name: req.Name,
VPC: req.VPC,
Mode: req.Mode,
VxlanID: req.VxlanID,
IfaceType: req.IfaceType,
GatewayIP: req.GatewayIP,
CIDR: req.CIDR,
Name: req.Name,
VPC: req.VPC,
Mode: req.Mode,
VxlanID: req.VxlanID,
IfaceType: req.IfaceType,
InterfaceIP: req.InterfaceIP,
CIDR: req.CIDR,
DefaultRoute: req.DefaultRoute,
}
if err := s.dispatcher.Prepare(cmd); err != 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)
case "local_iface":
sub.LocalIface = value
case "gateway_ip":
sub.GatewayIP = value
case "interface_ip":
sub.InterfaceIP = value
case "cidr":
sub.CIDR = value
case "default_route":
sub.DefaultRoute = value == "true"
}
}
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"})
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)
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
}
@ -78,15 +78,14 @@ func (s *Server) startVM(w http.ResponseWriter, r *http.Request) {
}
cmd := dispatcher.StartVMCommand{
Name: req.Name,
Subnet: primary.Subnet,
IP: primary.IP,
MetadataPort: req.MetadataPort,
VolumePath: req.Storage[0].Path,
Memory: req.Memory,
CPUs: req.CPUs,
Password: req.Password,
SSHKey: req.SSHKey,
Name: req.Name,
Subnet: primary.Subnet,
IP: primary.IP,
VolumePath: req.Storage[0].Path,
Memory: req.Memory,
CPUs: req.CPUs,
Password: req.Password,
SSHKey: req.SSHKey,
}
if err := s.dispatcher.Prepare(cmd); err != nil {

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"})
return
}
cidr, _ := kv.GetFromDB(s.db, "vpc/"+name+"/cidr")
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) {

View file

@ -53,7 +53,7 @@ func TestListVpcs_InvalidMethod(t *testing.T) {
func TestPostVpc_Created(t *testing.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()
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
if w.Code != http.StatusAccepted {
@ -67,11 +67,34 @@ func TestPostVpc_Created(t *testing.T) {
if result.State != "creating" {
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) {
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()
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
if w.Code != http.StatusBadRequest {
@ -82,7 +105,7 @@ func TestPostVpc_MissingName(t *testing.T) {
func TestPostVpc_Duplicate(t *testing.T) {
s, db := newTestServer(t)
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()
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
if w.Code != http.StatusConflict {

View file

@ -2,6 +2,7 @@ package agentapi
import (
"encoding/json"
"net"
"net/http"
"strings"
@ -38,8 +39,11 @@ func (s *Server) listVpcs(w http.ResponseWriter, _ *http.Request) {
if _, ok := vpcs[name]; !ok {
vpcs[name] = &VPC{Name: name}
}
if parts[2] == "state" {
switch parts[2] {
case "state":
vpcs[name].State = value
case "cidr":
vpcs[name].CIDR = value
}
}
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"})
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 {
w.WriteHeader(http.StatusConflict)
json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()})
@ -76,5 +90,5 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) {
return
}
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,11 +51,15 @@ func TestIncrementIP_Carry(t *testing.T) {
func newConf(t *testing.T, cidr string) Config {
t.Helper()
_, network, _ := net.ParseCIDR(cidr)
_, vpcNet, _ := net.ParseCIDR("10.0.0.0/16")
gw := net.ParseIP("192.168.1.1").To4()
return Config{
Network: network,
Gateway: net.ParseIP("192.168.1.1").To4(),
Name: "test",
ConfDir: t.TempDir(),
Network: network,
VPCGateway: gw,
VPCRoute: vpcNet,
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")
path, _, _ := GenerateConfig(conf)
content, _ := os.ReadFile(path)
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")
conf := Config{
Network: network,
Gateway: net.ParseIP("10.10.0.1").To4(),
Name: "vpc1",
ConfDir: t.TempDir(),
}
@ -144,7 +179,6 @@ func TestGenerateConfig_CreatesConfDir(t *testing.T) {
_, network, _ := net.ParseCIDR("10.0.0.0/30")
conf := Config{
Network: network,
Gateway: net.ParseIP("10.0.0.1").To4(),
Name: "net",
ConfDir: dir,
}

View file

@ -14,7 +14,12 @@ func GenerateConfig(c Config) (string, map[string]string, error) {
var sb strings.Builder
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-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")
entries := make(map[string]string)

View file

@ -5,8 +5,10 @@ import (
)
type Config struct {
Network *net.IPNet
Gateway net.IP
Name string
ConfDir string
Network *net.IPNet
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
ConfDir string
}

View file

@ -12,13 +12,14 @@ import (
)
type CreateSubnetCommand struct {
Name string
VPC string
Mode string
VxlanID int
IfaceType string
GatewayIP string
CIDR string
Name string
VPC string
Mode string
VxlanID int
IfaceType string
InterfaceIP string
CIDR string
DefaultRoute bool
}
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+"/mode", c.Mode)
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+"/default_route", strconv.FormatBool(c.DefaultRoute))
if c.Mode == "vxlan" {
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")
cmd := CreateSubnetCommand{
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 {
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")
cmd := CreateSubnetCommand{
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())
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")
cmd := CreateSubnetCommand{
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())
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")
cmd := CreateSubnetCommand{
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 {
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)
cmd := CreateSubnetCommand{
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 {
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")
cmd := CreateSubnetCommand{
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 {
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")
cmd := CreateSubnetCommand{
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 {
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 ---
func TestDeleteSubnetCommand_Prepare_Success(t *testing.T) {

View file

@ -2,7 +2,9 @@ package dispatcher
import (
"fmt"
"math/rand"
"strconv"
"strings"
"time"
configuration "git.g3e.fr/syonad/two/internal/config/agent"
@ -12,15 +14,14 @@ import (
)
type StartVMCommand struct {
Name string
Subnet string
IP string
MetadataPort string
VolumePath string
Memory int
CPUs int
Password string
SSHKey string
Name string
Subnet string
IP string
VolumePath string
Memory int
CPUs int
Password string
SSHKey string
}
func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
@ -34,10 +35,14 @@ func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
if subnetState == "deleting" || subnetState == "deleted" {
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+"/subnet", c.Subnet)
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+"/memory", strconv.Itoa(c.Memory))
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
}
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 {
timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second)
for {

View file

@ -2,6 +2,7 @@ package dispatcher
import (
"fmt"
"net"
"strings"
"time"
@ -13,12 +14,19 @@ import (
type CreateVPCCommand struct {
Name string
CIDR string
}
func (c CreateVPCCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
if _, err := kv.GetFromDB(db, "vpc/"+c.Name+"/state"); err == nil {
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")
}

View file

@ -10,7 +10,7 @@ import (
func TestCreateVPCCommand_Prepare_NewVPC(t *testing.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 {
t.Fatalf("Prepare a échoué : %v", err)
}
@ -21,17 +21,32 @@ func TestCreateVPCCommand_Prepare_NewVPC(t *testing.T) {
if state != "creating" {
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) {
_, db := newTestDispatcher(t)
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 {
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 ---
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()
}
func DropARPToGateway(bridge, gatewayIP string) error {
func DropARPToGateway(iface, ip string) error {
if err := addRule("FORWARD",
"--out-interface", bridge,
"--out-interface", iface,
"-p", "arp",
"--arp-op", "Request",
"--arp-ip-dst", gatewayIP,
"--arp-ip-dst", ip,
"-j", "DROP"); err != nil {
return fmt.Errorf("ebtables arp rule: %w", err)
}
return nil
}
func DropDHCP(bridge string) error {
func DropDHCP(iface, ip string) error {
if err := addRule("FORWARD",
"--out-interface", bridge,
"--out-interface", iface,
"-p", "IPv4",
"--ip-protocol", "udp",
"--ip-source-port", "67:68",
"--ip-destination-port", "67:68",
"--ip-source", ip,
"-j", "DROP"); err != nil {
return fmt.Errorf("ebtables dhcp rule: %w", err)
}
return nil
}
func DeleteARPToGateway(bridge, gatewayIP string) error {
func DeleteARPToGateway(iface, ip string) error {
return deleteRule("FORWARD",
"--out-interface", bridge,
"--out-interface", iface,
"-p", "arp",
"--arp-op", "Request",
"--arp-ip-dst", gatewayIP,
"--arp-ip-dst", ip,
"-j", "DROP")
}
func DeleteDHCP(bridge string) error {
func DeleteDHCP(iface, ip string) error {
return deleteRule("FORWARD",
"--out-interface", bridge,
"--out-interface", iface,
"-p", "IPv4",
"--ip-protocol", "udp",
"--ip-source-port", "67:68",
"--ip-destination-port", "67:68",
"--ip-source", ip,
"-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 {
case "vxlan":
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 {
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("add addr to bridge in netns: %w", err)
return fmt.Errorf("configure netns: %w", err)
}
case "bridge":
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 {
return fmt.Errorf("add route in 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)
return fmt.Errorf("configure netns: %w", err)
}
}
@ -139,10 +137,22 @@ func setupVxlanHost(d subnetData, vethE string) error {
func startDHCP(db *badger.DB, subnetName string, d subnetData) error {
conf := dhcp.Config{
Network: d.cidr,
Gateway: d.gatewayIP,
Name: d.vpc + "_" + d.bridge,
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)
if err != nil {
return fmt.Errorf("generate dhcp config: %w", err)

View file

@ -11,14 +11,16 @@ import (
)
type subnetData struct {
vpc string
subnetID string
bridge string
mode string
vxlanID int
localIface string
gatewayIP net.IP
cidr *net.IPNet
vpc string
subnetID string
bridge string
mode string
vxlanID int
localIface string
interfaceIP net.IP
cidr *net.IPNet
vpcCIDR *net.IPNet
defaultRoute bool
}
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
gatewayIPStr, err := kv.GetFromDB(db, "subnet/"+name+"/gateway_ip")
interfaceIPStr, err := kv.GetFromDB(db, "subnet/"+name+"/interface_ip")
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)
if gatewayIP == nil {
return d, fmt.Errorf("invalid gateway_ip: %s", gatewayIPStr)
interfaceIP := net.ParseIP(interfaceIPStr)
if interfaceIP == nil {
return d, fmt.Errorf("invalid interface_ip: %s", interfaceIPStr)
}
d.gatewayIP = gatewayIP
d.interfaceIP = interfaceIP
cidrStr, err := kv.GetFromDB(db, "subnet/"+name+"/cidr")
if err != nil {
@ -77,5 +79,21 @@ func loadSubnet(db *badger.DB, name string) (subnetData, error) {
}
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
}

View file

@ -70,18 +70,18 @@ func stopDHCP(db *badger.DB, subnetName string, d subnetData) error {
func deleteSubnetVxlan(d subnetData) error {
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
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)
}
vethI := "v-" + d.subnetID + "-i"
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)
}); 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 {
@ -99,19 +99,15 @@ func deleteSubnetVxlan(d subnetData) error {
}
func deleteSubnetBridge(d subnetData) error {
if err := netns.Call(d.vpc, func() error {
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)
}
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 bridge in netns: %w", err)
return fmt.Errorf("delete netns resources: %w", err)
}
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 {
return iptables.AddMetadataRedirect(d.ip, d.gatewayIP, d.metadataPort)
return iptables.AddMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort)
}); err != nil {
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{
Name: name,
VpcName: d.vpcName,
BindIP: d.gatewayIP,
BindIP: d.interfaceIP,
BindPort: d.metadataPort,
Password: d.password,
SSHKEY: d.sshkey,

View file

@ -14,7 +14,7 @@ import (
type vmData struct {
subnetName string
vpcName string
gatewayIP string
interfaceIP string
bridge string
tapID int
ip string
@ -43,11 +43,11 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
}
d.vpcName = vpcName
gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip")
interfaceIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/interface_ip")
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")
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 {
return iptables.DeleteMetadataRedirect(d.ip, d.gatewayIP, d.metadataPort)
return iptables.DeleteMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort)
}); err != nil {
return fmt.Errorf("delete metadata redirect: %w", err)
}