Compare commits

..

8 Commits

Author SHA1 Message Date
amery 9cf8239abb Merge branch 'pr-amery-chores' into next-amery 2024-05-25 21:37:35 +00:00
amery ea23312d4a rings: RingTwoPrefix()
Ring 2 is the service network shared by all kubernetes clusters.

Signed-off-by: Alejandro Mery <amery@jpi.io>
2024-05-25 21:31:21 +00:00
amery 57251ce0af rings: RingThreePrefix()
Ring 3 corresponds to the pods of the kubernetes cluster of a region

Signed-off-by: Alejandro Mery <amery@jpi.io>
2024-05-25 21:31:21 +00:00
amery dd22c8f72a rings: RingZeroPrefix()/RingZeroAddress()
Ring zero corresponds to the backbone that connects all zones.

Signed-off-by: Alejandro Mery <amery@jpi.io>
2024-05-25 21:31:21 +00:00
amery 30bb65b7a0 rings: RingOnePrefix()/RingOneAddress()
Ring one designates the (virtual) local network of a zone
within a region.

Signed-off-by: Alejandro Mery <amery@jpi.io>
2024-05-25 21:31:21 +00:00
amery d3f9eed548 rings: introduce generic ErrOutOfRange() factory
Signed-off-by: Alejandro Mery <amery@jpi.io>
2024-05-25 21:27:31 +00:00
amery 47c4e8f8c4 Merge branch 'pr-amery-cidr' into pr-amery-rings 2024-05-25 21:17:48 +00:00
amery e71b8b313f rings: introduce RegionID, ZoneID and NodeID
and a Valid() method to check if their value is within the
valid range.

Signed-off-by: Alejandro Mery <amery@jpi.io>
2024-05-25 21:15:50 +00:00
18 changed files with 177 additions and 281 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ func populateDNSManager(mgr *dns.Manager, m *cluster.Cluster) error {
m.ForEachZone(func(z *cluster.Zone) bool {
z.ForEachMachine(func(p *cluster.Machine) bool {
err = mgr.AddHost(ctx, z.Name, int(p.ID), p.IsActive(), p.PublicAddresses...)
err = mgr.AddHost(ctx, z.Name, p.ID, p.IsActive(), p.PublicAddresses...)
return err != nil
})
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
@@ -127,7 +128,7 @@ func gatewayListAll(zi cluster.ZoneIterator) error {
return false
}
for _, i := range ids {
sIDs = append(sIDs, i.String())
sIDs = append(sIDs, strconv.Itoa(i))
}
b.WriteString(strings.Join(sIDs, ", "))
b.WriteString("\n")
+2 -2
View File
@@ -45,7 +45,7 @@ func (m *Cluster) initZones(opts *ScanOptions) error {
func (m *Cluster) initZone(z *Zone, _ *ScanOptions) error {
var hasMissing bool
var lastMachineID NodeID
var lastMachineID int
z.zones = m
z.logger = m
@@ -58,7 +58,7 @@ func (m *Cluster) initZone(z *Zone, _ *ScanOptions) error {
case p.ID == 0:
hasMissing = true
case p.ID > lastMachineID:
lastMachineID = p.ID
lastMachineID = z.ID
}
return false
+1 -1
View File
@@ -114,7 +114,7 @@ func (m *Cluster) scanMachines(opts *ScanOptions) error {
func (m *Cluster) scanZoneIDs(_ *ScanOptions) error {
var hasMissing bool
var lastZoneID ZoneID
var lastZoneID int
m.ForEachZone(func(z *Zone) bool {
switch {
+32 -23
View File
@@ -35,8 +35,8 @@ func (m *Cluster) Env(export bool) (*Env, error) {
}
// Zones returns the list of Zone IDs
func (m *Env) Zones() []ZoneID {
var zones []ZoneID
func (m *Env) Zones() []int {
var zones []int
m.ForEachZone(func(z *Zone) bool {
zones = append(zones, z.ID)
@@ -70,8 +70,8 @@ func (m *Env) WriteTo(w io.Writer) (int64, error) {
m.writeEnvVar(&buf, m.cephFSID, "FSID")
}
m.writeEnvVar(&buf, genEnvStrings(m.Regions()), "REGIONS")
m.writeEnvVar(&buf, genEnvInts(m.Zones()), "ZONES")
m.writeEnvVarStrings(&buf, m.Regions(), "REGIONS")
m.writeEnvVarInts(&buf, m.Zones(), "ZONES")
m.ForEachZone(func(z *Zone) bool {
m.writeEnvZone(&buf, z)
@@ -92,7 +92,7 @@ func (m *Env) writeEnvZone(w io.Writer, z *Zone) {
// ZONE{zoneID}_GW
gateways, _ := z.GatewayIDs()
m.writeEnvVar(w, genEnvInts(gateways), "ZONE%v_%s", zoneID, "GW")
m.writeEnvVarInts(w, gateways, "ZONE%v_%s", zoneID, "GW")
// ZONE{zoneID}_REGION
m.writeEnvVar(w, genEnvZoneRegion(z), "ZONE%v_%s", zoneID, "REGION")
@@ -107,6 +107,32 @@ func (m *Env) writeEnvZone(w io.Writer, z *Zone) {
m.writeEnvVar(w, genEnvZoneCephMonIDs(monitors), "MON%v_%s", zoneID, "ID")
}
func (m *Env) writeEnvVarInts(w io.Writer, value []int, name string, args ...any) {
var buf bytes.Buffer
for _, v := range value {
if buf.Len() > 0 {
_, _ = fmt.Fprint(&buf, " ")
}
_, _ = fmt.Fprintf(&buf, "%v", v)
}
m.writeEnvVar(w, buf.String(), name, args...)
}
func (m *Env) writeEnvVarStrings(w io.Writer, value []string, name string, args ...any) {
var buf bytes.Buffer
for _, v := range value {
if buf.Len() > 0 {
_, _ = fmt.Fprint(&buf, " ")
}
_, _ = fmt.Fprintf(&buf, "%s", v)
}
m.writeEnvVar(w, buf.String(), name, args...)
}
func (m *Env) writeEnvVar(w io.Writer, value string, name string, args ...any) {
var prefix string
@@ -129,23 +155,6 @@ func (m *Env) writeEnvVar(w io.Writer, value string, name string, args ...any) {
}
}
func genEnvInts[T ~int | ~uint](values []T) string {
var buf bytes.Buffer
for _, v := range values {
if buf.Len() > 0 {
_, _ = buf.WriteRune(' ')
}
_, _ = buf.WriteString(fmt.Sprintf("%v", v))
}
return buf.String()
}
func genEnvStrings(values []string) string {
return strings.Join(values, " ")
}
func genEnvZoneNodes(z *Zone) string {
if n := z.Len(); n > 0 {
s := make([]string, 0, n)
@@ -155,7 +164,7 @@ func genEnvZoneNodes(z *Zone) string {
return false
})
return genEnvStrings(s)
return strings.Join(s, " ")
}
return ""
}
+2 -2
View File
@@ -12,7 +12,7 @@ type Machine struct {
zone *Zone
logger `json:"-" yaml:"-"`
ID NodeID
ID int
Name string `json:"-" yaml:"-"`
Inactive bool `json:"inactive,omitempty" yaml:"inactive,omitempty"`
@@ -74,7 +74,7 @@ func (m *Machine) SetGateway(enabled bool) error {
}
// Zone indicates the [Zone] this machine belongs to
func (m *Machine) Zone() ZoneID {
func (m *Machine) Zone() int {
return m.zone.ID
}
+1 -1
View File
@@ -223,7 +223,7 @@ func (m *Machine) applyWireguardPeerConfig(ring int, pc wireguard.PeerConfig) er
}
}
func (m *Machine) applyZoneNodeID(zoneID ZoneID, nodeID NodeID) error {
func (m *Machine) applyZoneNodeID(zoneID, nodeID int) error {
switch {
case zoneID == 0:
return fmt.Errorf("invalid %s", "zoneID")
+1 -1
View File
@@ -65,7 +65,7 @@ func (m *Machine) setID() error {
return err
}
m.ID = NodeID(id)
m.ID = int(id)
return nil
}
+11 -11
View File
@@ -81,8 +81,8 @@ func canMergeKeyPairs(p1, p2 wireguard.KeyPair) bool {
type RingAddressEncoder struct {
ID int
Port uint16
Encode func(zoneID ZoneID, nodeID NodeID) (netip.Addr, bool)
Decode func(addr netip.Addr) (zoneID ZoneID, nodeID NodeID, ok bool)
Encode func(zoneID, nodeID int) (netip.Addr, bool)
Decode func(addr netip.Addr) (zoneID, nodeID int, ok bool)
}
var (
@@ -110,7 +110,7 @@ var (
// ValidZoneID checks if the given zoneID is a valid 4 bit zone number.
//
// 0 is reserved, and only allowed when composing CIDRs.
func ValidZoneID(zoneID ZoneID) bool {
func ValidZoneID(zoneID int) bool {
switch {
case zoneID < 0 || zoneID > MaxZoneID:
return false
@@ -122,7 +122,7 @@ func ValidZoneID(zoneID ZoneID) bool {
// ValidNodeID checks if the given nodeID is a valid 8 bit number.
// nodeID is unique within a Zone.
// 0 is reserved, and only allowed when composing CIDRs.
func ValidNodeID(nodeID NodeID) bool {
func ValidNodeID(nodeID int) bool {
switch {
case nodeID < 0 || nodeID > MaxNodeID:
return false
@@ -133,19 +133,19 @@ func ValidNodeID(nodeID NodeID) bool {
// ParseRingZeroAddress extracts zone and node ID from a wg0 [netip.Addr]
// wg0 addresses are of the form `10.0.{{zoneID}}.{{nodeID}}`
func ParseRingZeroAddress(addr netip.Addr) (zoneID ZoneID, nodeID NodeID, ok bool) {
func ParseRingZeroAddress(addr netip.Addr) (zoneID int, nodeID int, ok bool) {
if addr.IsValid() {
a4 := addr.As4()
if a4[0] == 10 && a4[1] == 0 {
return ZoneID(a4[2]), NodeID(a4[3]), true
return int(a4[2]), int(a4[3]), true
}
}
return 0, 0, false
}
// RingZeroAddress returns a wg0 IP address
func RingZeroAddress(zoneID ZoneID, nodeID NodeID) (netip.Addr, bool) {
func RingZeroAddress(zoneID, nodeID int) (netip.Addr, bool) {
switch {
case !ValidZoneID(zoneID) || !ValidNodeID(nodeID):
return netip.Addr{}, false
@@ -157,13 +157,13 @@ func RingZeroAddress(zoneID ZoneID, nodeID NodeID) (netip.Addr, bool) {
// ParseRingOneAddress extracts zone and node ID from a wg1 [netip.Addr]
// wg1 addresses are of the form `10.{{zoneID << 4}}.{{nodeID}}`
func ParseRingOneAddress(addr netip.Addr) (zoneID ZoneID, nodeID NodeID, ok bool) {
func ParseRingOneAddress(addr netip.Addr) (zoneID int, nodeID int, ok bool) {
if addr.IsValid() {
a4 := addr.As4()
if a4[0] == 10 && a4[2] == 0 {
zoneID = ZoneID(a4[1] >> 4)
nodeID = NodeID(a4[3])
zoneID = int(a4[1] >> 4)
nodeID = int(a4[3])
return zoneID, nodeID, true
}
}
@@ -171,7 +171,7 @@ func ParseRingOneAddress(addr netip.Addr) (zoneID ZoneID, nodeID NodeID, ok bool
}
// RingOneAddress returns a wg1 IP address
func RingOneAddress(zoneID ZoneID, nodeID NodeID) (netip.Addr, bool) {
func RingOneAddress(zoneID, nodeID int) (netip.Addr, bool) {
switch {
case !ValidZoneID(zoneID) || !ValidNodeID(nodeID):
return netip.Addr{}, false
-25
View File
@@ -1,25 +0,0 @@
package cluster
import "strconv"
type (
// ZoneID is the identifier of a [Zone]
ZoneID int
// NodeID is the identifier of a [Machine]
NodeID int
)
func (z ZoneID) String() string {
return idString(int(z))
}
func (n NodeID) String() string {
return idString(int(n))
}
func idString(v int) string {
if v > 0 {
return strconv.Itoa(v)
}
return ""
}
+4 -4
View File
@@ -19,7 +19,7 @@ type Zone struct {
zones *Cluster
logger `json:"-" yaml:"-"`
ID ZoneID
ID int
Name string
Regions []string `json:",omitempty" yaml:",omitempty"`
@@ -31,7 +31,7 @@ func (z *Zone) String() string {
}
// SetGateway configures a machine to be the zone's ring0 gateway
func (z *Zone) SetGateway(gatewayID NodeID, enabled bool) error {
func (z *Zone) SetGateway(gatewayID int, enabled bool) error {
var err error
var found bool
@@ -56,8 +56,8 @@ func (z *Zone) SetGateway(gatewayID NodeID, enabled bool) error {
}
// GatewayIDs returns the list of IDs of machines that act as ring0 gateways
func (z *Zone) GatewayIDs() ([]NodeID, int) {
var out []NodeID
func (z *Zone) GatewayIDs() ([]int, int) {
var out []int
z.ForEachMachine(func(p *Machine) bool {
if p.IsGateway() {
out = append(out, p.ID)
-145
View File
@@ -1,145 +0,0 @@
package rings
import "net/netip"
// RingZeroPrefix represents the backbone that connects gateways
// of the different Ring 1 networks.
//
// The ring 0 network corresponds to what would be ring 2 for region_id 0.
// 10.0.0.0-10.0.255.255
func RingZeroPrefix(region RegionID, zone ZoneID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
default:
addr := unsafeRingZeroAddress(region, zone, 0)
cidr = netip.PrefixFrom(addr, RingZeroBits)
}
return cidr, err
}
// RingZeroAddress returns a Ring 0 address for a particular node.
//
// A ring 0 address looks like 10.0.(region_id << 4 + zone_id).(node_id)/20
func RingZeroAddress(region RegionID, zone ZoneID, node NodeID) (addr netip.Addr, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
case !node.ValidZero():
err = ErrOutOfRange(node, "node")
default:
addr = unsafeRingZeroAddress(region, zone, node)
}
return addr, err
}
// RingOnePrefix represents a (virtual) local network of a zone.
//
// Ring 1 is `10.(region_id).(zone_id << 4).(node_id)/20` network
// grouped under what would be Ring 2 for region_id 0.
// There are 12 bits worth of nodes but nodes under 255 are special
// as they also get a slot on Ring 0.
func RingOnePrefix(region RegionID, zone ZoneID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
default:
addr := unsafeRingOneAddress(region, zone, 0)
cidr = netip.PrefixFrom(addr, RingOneBits)
}
return cidr, err
}
// RingOneAddress returns a Ring 1 address for a particular node.
//
// A ring 1 address is `10.(region_id).(zone_id << 4).(node_id)/20`
// but the node_id can take up to 12 bits.
func RingOneAddress(region RegionID, zone ZoneID, node NodeID) (addr netip.Addr, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
case !node.Valid():
err = ErrOutOfRange(node, "node")
default:
addr = unsafeRingOneAddress(region, zone, node)
}
return addr, err
}
// RingTwoPrefix represents the services of a cluster
//
// Ring 2 subnets are of the form `10.(region_id).0.0/20`,
// using the address space that would belong to the ring 3
// region_id 0.
func RingTwoPrefix(region RegionID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
default:
addr := unsafeRingTwoAddress(region, 0)
cidr = netip.PrefixFrom(addr, RingTwoBits)
}
return cidr, err
}
// RingThreePrefix returns the subnet corresponding to
// the pods of a cluster.
//
// Ring 3 is a `10.(region_id << 4).0.0/12` network
func RingThreePrefix(region RegionID) (subnet netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
default:
addr := unsafeRingThreeAddress(region, 0)
subnet = netip.PrefixFrom(addr, RingThreeBits)
}
return subnet, err
}
func unsafeRingZeroAddress(region RegionID, zone ZoneID, node NodeID) netip.Addr {
r := uint(region)
z := uint(zone)
n := uint(node)
return AddrFrom4(10, 0, r<<4+z, n)
}
func unsafeRingOneAddress(region RegionID, zone ZoneID, node NodeID) netip.Addr {
r := uint(region)
z := uint(zone)
n := uint(node)
n1 := n >> 8
n0 := n >> 0
return AddrFrom4(10, r, z<<4+n1, n0)
}
func unsafeRingTwoAddress(region RegionID, n uint) netip.Addr {
r := uint(region)
n1 := n >> 8
n0 := n >> 0
return AddrFrom4(10, r, n1, n0)
}
func unsafeRingThreeAddress(region RegionID, n uint) netip.Addr {
r := uint(region)
n2 := n >> 16
n1 := n >> 8
n0 := n >> 0
return AddrFrom4(10, r<<4+n2, n1, n0)
}
-63
View File
@@ -1,63 +0,0 @@
package rings
import (
"fmt"
"net/netip"
"testing"
)
func TestRingZeroAddress(t *testing.T) {
RZNTest(t, "RingZeroAddress", RingZeroAddress, []RZNTestCase{
{1, 1, 50, MustParseAddr("10.0.17.50")},
{1, 2, 50, MustParseAddr("10.0.18.50")},
{2, 3, 1, MustParseAddr("10.0.35.1")},
{2, 3, 300, netip.Addr{}},
})
}
func TestRingOneAddress(t *testing.T) {
RZNTest(t, "RingOneAddress", RingOneAddress, []RZNTestCase{
{1, 1, 50, MustParseAddr("10.1.16.50")},
{1, 2, 50, MustParseAddr("10.1.32.50")},
{2, 3, 300, MustParseAddr("10.2.49.44")},
{1, 20, 50, netip.Addr{}},
})
}
type RZNTestCase struct {
region RegionID
zone ZoneID
node NodeID
addr netip.Addr
}
func RZNTest(t *testing.T,
fnName string, fn func(RegionID, ZoneID, NodeID) (netip.Addr, error),
cases []RZNTestCase) {
//
for i, tc := range cases {
s := fmt.Sprintf("%s(%v, %v, %v)", fnName,
tc.region,
tc.zone,
tc.node,
)
addr, err := fn(tc.region, tc.zone, tc.node)
switch {
case !tc.addr.IsValid():
// expect error
if err != nil {
t.Logf("[%v/%v]: %s → %s", i, len(cases), s, err)
} else {
t.Errorf("ERROR: [%v/%v]: %s → %s (expected %s)", i, len(cases), s, addr, "error")
}
case err != nil:
t.Errorf("ERROR: [%v/%v]: %s → %s (expected %s)", i, len(cases), s, err, tc.addr)
case addr.Compare(tc.addr) != 0:
t.Errorf("ERROR: [%v/%v]: %s → %s (expected %s)", i, len(cases), s, addr, tc.addr)
default:
t.Logf("[%v/%v]: %s → %s", i, len(cases), s, addr)
}
}
}
+42
View File
@@ -0,0 +1,42 @@
package rings
import "net/netip"
// RingOnePrefix represents a (virtual) local network of a zone.
//
// Ring 1 is `10.(region_id).(zone_id << 4).(node_id)/20` network
// grouped under what would be Ring 2 for region_id 0.
// There are 12 bits worth of nodes but nodes under 255 are special
// as they also get a slot on Ring 0.
func RingOnePrefix(region RegionID, zone ZoneID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
default:
addr := AddrFrom4(10, uint(region), uint(zone)<<4, 0)
cidr = netip.PrefixFrom(addr, RingOneBits)
}
return cidr, err
}
// RingOneAddress returns a Ring 1 address for a particular node.
//
// A ring 1 address is `10.(region_id).(zone_id << 4).(node_id)/20`
// but the node_id can take up to 12 bits.
func RingOneAddress(region RegionID, zone ZoneID, node NodeID) (addr netip.Addr, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
case !node.Valid():
err = ErrOutOfRange(node, "node")
default:
n1 := uint(node) / 8
n2 := uint(node) % 8
addr = AddrFrom4(10, uint(region), (uint(zone)<<4)+n1, n2)
}
return addr, err
}
+1 -1
View File
@@ -56,5 +56,5 @@ func (n NodeID) ValidZero() bool { return n > 0 && n <= NodeZeroMax }
// ErrOutOfRange is an error indicating the value of a field
// is out of range.
func ErrOutOfRange[T ~int | ~uint32](value T, field string) error {
return core.Wrap(syscall.EINVAL, "%s out of range (%v)", field, value)
return core.Wrap(syscall.EINVAL, "%v: %s out of range", value, field)
}
+18
View File
@@ -0,0 +1,18 @@
package rings
import "net/netip"
// RingThreePrefix returns the subnet corresponding to
// the pods of a cluster.
//
// Ring 3 is a `10.(region_id << 4).0.0/12` network
func RingThreePrefix(region RegionID) (subnet netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
default:
addr := AddrFrom4(10, uint(region)<<4, 0, 0)
subnet = netip.PrefixFrom(addr, RingThreeBits)
}
return subnet, err
}
+19
View File
@@ -0,0 +1,19 @@
package rings
import "net/netip"
// RingTwoPrefix represents the services of a cluster
//
// Ring 2 subnets are of the form `10.(region_id).0.0/20`,
// using the address space that would belong to the ring 3
// region_id 0.
func RingTwoPrefix(region RegionID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
default:
addr := AddrFrom4(10, uint(region), 0, 0)
cidr = netip.PrefixFrom(addr, RingTwoBits)
}
return cidr, err
}
+40
View File
@@ -0,0 +1,40 @@
package rings
import "net/netip"
// RingZeroPrefix represents the backbone that connects gateways
// of the different Ring 1 networks.
//
// The ring 0 network corresponds to what would be ring 2 for region_id 0.
// 10.0.0.0-10.0.255.255
func RingZeroPrefix(region RegionID, zone ZoneID) (cidr netip.Prefix, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
default:
addr := AddrFrom4(10, 0, uint(region)<<4+uint(zone), 0)
cidr = netip.PrefixFrom(addr, RingZeroBits)
}
return cidr, err
}
// RingZeroAddress returns a Ring 0 address for a particular node.
//
// A ring 0 address looks like 10.0.(region_id << 4 + zone_id).(node_id)/20
func RingZeroAddress(region RegionID, zone ZoneID, node NodeID) (addr netip.Addr, err error) {
switch {
case !region.Valid():
err = ErrOutOfRange(region, "region")
case !zone.Valid():
err = ErrOutOfRange(zone, "zone")
case !node.ValidZero():
err = ErrOutOfRange(node, "node")
default:
addr = AddrFrom4(10, 0, uint(region)<<4+uint(zone), uint(node))
}
return addr, err
}