Skip to content

Commit

Permalink
Introduce allocation roles. (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit91 authored Aug 18, 2021
1 parent c281a9e commit c556d60
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 154 deletions.
17 changes: 12 additions & 5 deletions cmd/metal-api/internal/datastore/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ type MachineSearchQuery struct {
Tags []string `json:"tags" optional:"true"`

// allocation
AllocationName *string `json:"allocation_name" optional:"true"`
AllocationProject *string `json:"allocation_project" optional:"true"`
AllocationImageID *string `json:"allocation_image_id" optional:"true"`
AllocationHostname *string `json:"allocation_hostname" optional:"true"`
AllocationSucceeded *bool `json:"allocation_succeeded" optional:"true"`
AllocationName *string `json:"allocation_name" optional:"true"`
AllocationProject *string `json:"allocation_project" optional:"true"`
AllocationImageID *string `json:"allocation_image_id" optional:"true"`
AllocationHostname *string `json:"allocation_hostname" optional:"true"`
AllocationRole *metal.Role `json:"allocation_role" optional:"true"`
AllocationSucceeded *bool `json:"allocation_succeeded" optional:"true"`

// network
NetworkIDs []string `json:"network_ids" optional:"true"`
Expand Down Expand Up @@ -140,6 +141,12 @@ func (p *MachineSearchQuery) generateTerm(rs *RethinkStore) *r.Term {
})
}

if p.AllocationRole != nil {
q = q.Filter(func(row r.Term) r.Term {
return row.Field("allocation").Field("role").Eq(*p.AllocationRole)
})
}

if p.AllocationSucceeded != nil {
q = q.Filter(func(row r.Term) r.Term {
return row.Field("allocation").Field("succeeded").Eq(*p.AllocationSucceeded)
Expand Down
52 changes: 52 additions & 0 deletions cmd/metal-api/internal/datastore/migrations/03_machine_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package migrations

import (
r "gopkg.in/rethinkdb/rethinkdb-go.v6"

"github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore"
"github.com/metal-stack/metal-api/cmd/metal-api/internal/metal"
)

func init() {
datastore.MustRegisterMigration(datastore.Migration{
Name: "introduction of machine roles (#24)",
Version: 3,
Up: func(db *r.Term, session r.QueryExecutor, rs *datastore.RethinkStore) error {
ms, err := rs.ListMachines()
if err != nil {
return err
}

for i := range ms {
old := ms[i]
if old.Allocation == nil {
continue
}

n := old

if isFirewall(n.Allocation.MachineNetworks) {
n.Allocation.Role = metal.RoleFirewall
} else {
n.Allocation.Role = metal.RoleMachine
}

err = rs.UpdateMachine(&old, &n)
if err != nil {
return err
}
}
return nil
},
})
}

func isFirewall(nws []*metal.MachineNetwork) bool {
// only firewalls are part of the underlay network, so that is a unique and sufficient indicator
for _, n := range nws {
if n.Underlay {
return true
}
}
return false
}
45 changes: 29 additions & 16 deletions cmd/metal-api/internal/metal/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,34 @@ import (
// A MState is an enum which indicates the state of a machine
type MState string

// The enums for the machine states.
// Role describes the role of a machine.
type Role string

const (
// AvailableState describes a machine state where a machine is available for an allocation
AvailableState MState = ""
ReservedState MState = "RESERVED"
LockedState MState = "LOCKED"
// ReservedState describes a machine state where a machine is not being considered for random allocation
ReservedState MState = "RESERVED"
// LockedState describes a machine state where a machine cannot be deleted or allocated anymore
LockedState MState = "LOCKED"
)

var (
// RoleMachine is a role that indicates the allocated machine acts as a machine
RoleMachine Role = "machine"
// RoleFirewall is a role that indicates the allocated machine acts as a firewall
RoleFirewall Role = "firewall"
)

// AllStates contains all possible values of a machine state
var AllStates = []MState{AvailableState, ReservedState, LockedState}
var (
// AllStates contains all possible values of a machine state
AllStates = []MState{AvailableState, ReservedState, LockedState}
// AllRoles contains all possible values of a role
AllRoles = map[Role]bool{
RoleMachine: true,
RoleFirewall: true,
}
)

// A MachineState describes the state of a machine. If the Value is AvailableState,
// the machine will be available for allocation. In all other cases the allocation
Expand Down Expand Up @@ -94,21 +113,14 @@ type Machine struct {
type Machines []Machine

// IsFirewall returns true if this machine is a firewall machine.
func (m *Machine) IsFirewall(iMap ImageMap) bool {
func (m *Machine) IsFirewall() bool {
if m.Allocation == nil {
return false
}
image, ok := iMap[m.Allocation.ImageID]
if !ok {
return false
}
if !image.HasFeature(ImageFeatureFirewall) {
return false
if m.Allocation.Role == RoleFirewall {
return true
}
if len(m.Allocation.MachineNetworks) <= 1 {
return false
}
return true
return false
}

// A MachineAllocation stores the data which are only present for allocated machines.
Expand All @@ -128,6 +140,7 @@ type MachineAllocation struct {
Succeeded bool `rethinkdb:"succeeded" json:"succeeded"`
Reinstall bool `rethinkdb:"reinstall" json:"reinstall"`
MachineSetup *MachineSetup `rethinkdb:"setup" json:"setup"`
Role Role `rethinkdb:"role" json:"role"`
}

// A MachineSetup stores the data used for machine reinstallations.
Expand Down
45 changes: 9 additions & 36 deletions cmd/metal-api/internal/service/firewall-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,7 @@ func (r firewallResource) findFirewall(request *restful.Request, response *restf
return
}

imgs, err := r.ds.ListImages()
if checkError(request, response, utils.CurrentFuncName(), err) {
return
}

if !fw.IsFirewall(imgs.ByID()) {
if !fw.IsFirewall() {
sendError(utils.Logger(request), response, utils.CurrentFuncName(), httperrors.NotFound(errors.New("machine is not a firewall")))
return
}
Expand All @@ -146,25 +141,14 @@ func (r firewallResource) findFirewalls(request *restful.Request, response *rest
return
}

var possibleFws metal.Machines
err = r.ds.SearchMachines(&requestPayload, &possibleFws)
if checkError(request, response, utils.CurrentFuncName(), err) {
return
}
requestPayload.AllocationRole = &metal.RoleFirewall

imgs, err := r.ds.ListImages()
var fws metal.Machines
err = r.ds.SearchMachines(&requestPayload, &fws)
if checkError(request, response, utils.CurrentFuncName(), err) {
return
}

fws := metal.Machines{}
imageMap := imgs.ByID()
for i := range possibleFws {
if possibleFws[i].IsFirewall(imageMap) {
fws = append(fws, possibleFws[i])
}
}

err = response.WriteHeaderAndEntity(http.StatusOK, makeFirewallResponseList(fws, r.ds, utils.Logger(request).Sugar()))
if err != nil {
zapup.MustRootLogger().Error("Failed to send response", zap.Error(err))
Expand All @@ -173,25 +157,14 @@ func (r firewallResource) findFirewalls(request *restful.Request, response *rest
}

func (r firewallResource) listFirewalls(request *restful.Request, response *restful.Response) {
possibleFws, err := r.ds.ListMachines()
if checkError(request, response, utils.CurrentFuncName(), err) {
return
}

// potentially a little unefficient because images are also retrieved for creating the machine list response later
imgs, err := r.ds.ListImages()
var fws metal.Machines
err := r.ds.SearchMachines(&datastore.MachineSearchQuery{
AllocationRole: &metal.RoleFirewall,
}, &fws)
if checkError(request, response, utils.CurrentFuncName(), err) {
return
}

var fws metal.Machines
imageMap := imgs.ByID()
for i := range possibleFws {
if possibleFws[i].IsFirewall(imageMap) {
fws = append(fws, possibleFws[i])
}
}

err = response.WriteHeaderAndEntity(http.StatusOK, makeFirewallResponseList(fws, r.ds, utils.Logger(request).Sugar()))
if err != nil {
zapup.MustRootLogger().Error("Failed to send response", zap.Error(err))
Expand Down Expand Up @@ -273,7 +246,7 @@ func (r firewallResource) allocateFirewall(request *restful.Request, response *r
Networks: requestPayload.Networks,
IPs: requestPayload.IPs,
HA: ha,
IsFirewall: true,
Role: metal.RoleFirewall,
}

m, err := allocateMachine(utils.Logger(request).Sugar(), r.ds, r.ipamer, &spec, r.mdc, r.actor, r.grpcServer)
Expand Down
37 changes: 13 additions & 24 deletions cmd/metal-api/internal/service/machine-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ type machineAllocationSpec struct {
Networks v1.MachineAllocationNetworks
IPs []string
HA bool
IsFirewall bool
Role metal.Role
}

// allocationNetwork is intermediate struct to create machine networks from regular networks during machine allocation
Expand Down Expand Up @@ -972,7 +972,7 @@ func (r machineResource) allocateMachine(request *restful.Request, response *res
Networks: requestPayload.Networks,
IPs: requestPayload.IPs,
HA: false,
IsFirewall: false,
Role: metal.RoleMachine,
}

m, err := allocateMachine(utils.Logger(request).Sugar(), r.ds, r.ipamer, &spec, r.mdc, r.actor, r.grpcServer)
Expand Down Expand Up @@ -1004,22 +1004,11 @@ func allocateMachine(logger *zap.SugaredLogger, ds *datastore.RethinkStore, ipam
mq := p.GetProject().GetQuotas().GetMachine()
maxMachines := mq.GetQuota().GetValue()
var actualMachines metal.Machines
err := ds.SearchMachines(&datastore.MachineSearchQuery{AllocationProject: &projectID}, &actualMachines)
err := ds.SearchMachines(&datastore.MachineSearchQuery{AllocationProject: &projectID, AllocationRole: &metal.RoleFirewall}, &actualMachines)
if err != nil {
return nil, err
}
machineCount := int32(-1)
imageMap, err := ds.ListImages()
if err != nil {
return nil, err
}
for _, m := range actualMachines {
if m.IsFirewall(imageMap.ByID()) {
continue
}
machineCount++
}
if machineCount >= maxMachines {
if len(actualMachines) >= int(maxMachines) {
return nil, fmt.Errorf("project quota for machines reached max:%d", maxMachines)
}
}
Expand Down Expand Up @@ -1057,6 +1046,7 @@ func allocateMachine(logger *zap.SugaredLogger, ds *datastore.RethinkStore, ipam
UserData: allocationSpec.UserData,
SSHPubKeys: allocationSpec.SSHPubKeys,
MachineNetworks: []*metal.MachineNetwork{},
Role: allocationSpec.Role,
}
rollbackOnError := func(err error) error {
if err != nil {
Expand Down Expand Up @@ -1160,6 +1150,10 @@ func validateAllocationSpec(allocationSpec *machineAllocationSpec) error {
return errors.New("creator should be specified")
}

if !metal.AllRoles[allocationSpec.Role] {
return fmt.Errorf("role does not exist: %s", allocationSpec.Role)
}

for _, ip := range allocationSpec.IPs {
if net.ParseIP(ip) == nil {
return fmt.Errorf("%q is not a valid IP address", ip)
Expand All @@ -1174,7 +1168,7 @@ func validateAllocationSpec(allocationSpec *machineAllocationSpec) error {
}

// A firewall must have either IP or Network with auto IP acquire specified.
if allocationSpec.IsFirewall {
if allocationSpec.Role == metal.RoleFirewall {
if len(allocationSpec.IPs) == 0 && allocationSpec.autoNetworkN() == 0 {
return errors.New("when no ip is given at least one auto acquire network must be specified")
}
Expand Down Expand Up @@ -1276,7 +1270,7 @@ func gatherNetworks(ds *datastore.RethinkStore, allocationSpec *machineAllocatio
}

var underlayNetwork *allocationNetwork
if allocationSpec.IsFirewall {
if allocationSpec.Role == metal.RoleFirewall {
underlayNetwork, err = gatherUnderlayNetwork(ds, partition)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1388,7 +1382,7 @@ func gatherNetworksFromSpec(ds *datastore.RethinkStore, allocationSpec *machineA
primaryPrivateNetwork.networkType = metal.PrivatePrimaryShared
}

if !allocationSpec.IsFirewall && len(privateNetworks) > 1 {
if allocationSpec.Role == metal.RoleMachine && len(privateNetworks) > 1 {
return nil, errors.New("machines are not allowed to be placed into multiple private networks")
}

Expand Down Expand Up @@ -1613,12 +1607,7 @@ func (r machineResource) finalizeAllocation(request *restful.Request, response *
}

vrf := ""
imgs, err := r.ds.ListImages()
if checkError(request, response, utils.CurrentFuncName(), err) {
return
}

if m.IsFirewall(imgs.ByID()) {
if m.IsFirewall() {
// if a machine has multiple networks, it serves as firewall, so it can not be enslaved into the tenant vrf
vrf = "default"
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func TestMachineAllocationIntegrationFullCycle(t *testing.T) {
require.NotNil(allocatedMachine.Allocation.Image)
assert.Equal(machine.ImageID, allocatedMachine.Allocation.Image.ID)
assert.Equal(machine.ProjectID, allocatedMachine.Allocation.Project)
assert.Equal(string(metal.RoleMachine), allocatedMachine.Allocation.Role)
assert.Len(allocatedMachine.Allocation.MachineNetworks, 1)
assert.Equal(allocatedMachine.Allocation.MachineNetworks[0].NetworkType, metal.PrivatePrimaryUnshared.String())
assert.NotEmpty(allocatedMachine.Allocation.MachineNetworks[0].Vrf)
Expand Down
Loading

0 comments on commit c556d60

Please sign in to comment.