diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index 8a2f9387b..f496ca151 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -48,6 +48,7 @@ func (s *Server) planApply() { // which may be partial or if there was an error func (s *Server) evaluatePlan(plan *structs.Plan) (*structs.PlanResult, error) { defer metrics.MeasureSince([]string{"nomad", "plan", "evaluate"}, time.Now()) + // Snapshot the state so that we have a consistent view of the world snap, err := s.fsm.State().Snapshot() if err != nil { @@ -61,30 +62,13 @@ func (s *Server) evaluatePlan(plan *structs.Plan) (*structs.PlanResult, error) { } // Check each allocation to see if it should be allowed - for nodeID, allocList := range plan.NodeAllocation { - // Get the node itself - node, err := snap.GetNodeByID(nodeID) + for nodeID := range plan.NodeAllocation { + // Evaluate the plan for this node + fit, err := evaluateNodePlan(snap, plan, nodeID) if err != nil { - return nil, fmt.Errorf("failed to get node '%s': %v", node, err) + return nil, err } - - // Get the existing allocations - existingAlloc, err := snap.AllocsByNode(nodeID) - if err != nil { - return nil, fmt.Errorf("failed to get existing allocations for '%s': %v", node, err) - } - - // Determine the proposed allocation by first removing allocations - // that are planned evictions and adding the new allocations. - proposed := existingAlloc - evictions := plan.NodeEvict[nodeID] - if len(evictions) > 0 { - proposed = structs.RemoveAllocs(existingAlloc, evictions) - } - proposed = append(proposed, allocList...) - - // Determine if everything fits - if !AllocationsFit(node, proposed) { + if !fit { // Scheduler must have stale data, RefreshIndex should force // the latest view of allocations and nodes allocIndex, err := snap.GetIndex("allocs") @@ -108,12 +92,8 @@ func (s *Server) evaluatePlan(plan *structs.Plan) (*structs.PlanResult, error) { } // Add this to the plan result - if len(evictions) > 0 { - result.NodeEvict[nodeID] = evictions - } - if len(allocList) > 0 { - result.NodeAllocation[nodeID] = allocList - } + result.NodeEvict[nodeID] = plan.NodeEvict[nodeID] + result.NodeAllocation[nodeID] = plan.NodeAllocation[nodeID] } return result, nil } @@ -132,3 +112,35 @@ func (s *Server) applyPlan(result *structs.PlanResult) (uint64, error) { _, index, err := s.raftApply(structs.AllocUpdateRequestType, &req) return index, err } + +// evaluateNodePlan is used to evalute the plan for a single node, +// returning if the plan is valid or if an error is encountered +func evaluateNodePlan(snap *StateSnapshot, plan *structs.Plan, nodeID string) (bool, error) { + // Get the node itself + node, err := snap.GetNodeByID(nodeID) + if err != nil { + return false, fmt.Errorf("failed to get node '%s': %v", node, err) + } + + // If the node does not exist or is not ready for schduling it is not fit + if node == nil || node.Status != structs.NodeStatusReady { + return false, nil + } + + // Get the existing allocations + existingAlloc, err := snap.AllocsByNode(nodeID) + if err != nil { + return false, fmt.Errorf("failed to get existing allocations for '%s': %v", node, err) + } + + // Determine the proposed allocation by first removing allocations + // that are planned evictions and adding the new allocations. + proposed := existingAlloc + if evict := plan.NodeEvict[nodeID]; len(evict) > 0 { + proposed = structs.RemoveAllocs(existingAlloc, evict) + } + proposed = append(proposed, plan.NodeAllocation[nodeID]...) + + // Check if these allocations fit + return structs.AllocsFit(node, proposed) +} diff --git a/nomad/plan_apply_test.go b/nomad/plan_apply_test.go index 911ce2e5d..ead617ff2 100644 --- a/nomad/plan_apply_test.go +++ b/nomad/plan_apply_test.go @@ -1,7 +1,131 @@ package nomad -import "testing" +import ( + "testing" -func TestPlanApply(t *testing.T) { - // TODO + "github.com/hashicorp/nomad/nomad/structs" +) + +func TestPlanApply_EvalNodePlan_Simple(t *testing.T) { + state := testStateStore(t) + node := mockNode() + state.RegisterNode(1000, node) + snap, _ := state.Snapshot() + + alloc := mockAlloc() + plan := &structs.Plan{ + NodeAllocation: map[string][]*structs.Allocation{ + node.ID: []*structs.Allocation{alloc}, + }, + } + + fit, err := evaluateNodePlan(snap, plan, node.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if !fit { + t.Fatalf("bad") + } +} + +func TestPlanApply_EvalNodePlan_NodeNotReady(t *testing.T) { + state := testStateStore(t) + node := mockNode() + node.Status = structs.NodeStatusInit + state.RegisterNode(1000, node) + snap, _ := state.Snapshot() + + alloc := mockAlloc() + plan := &structs.Plan{ + NodeAllocation: map[string][]*structs.Allocation{ + node.ID: []*structs.Allocation{alloc}, + }, + } + + fit, err := evaluateNodePlan(snap, plan, node.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if fit { + t.Fatalf("bad") + } +} + +func TestPlanApply_EvalNodePlan_NodeNotExist(t *testing.T) { + state := testStateStore(t) + snap, _ := state.Snapshot() + + nodeID := "foo" + alloc := mockAlloc() + plan := &structs.Plan{ + NodeAllocation: map[string][]*structs.Allocation{ + nodeID: []*structs.Allocation{alloc}, + }, + } + + fit, err := evaluateNodePlan(snap, plan, nodeID) + if err != nil { + t.Fatalf("err: %v", err) + } + if fit { + t.Fatalf("bad") + } +} + +func TestPlanApply_EvalNodePlan_NodeFull(t *testing.T) { + alloc := mockAlloc() + state := testStateStore(t) + node := mockNode() + alloc.NodeID = node.ID + node.Resources = alloc.Resources + node.Reserved = nil + state.RegisterNode(1000, node) + state.UpdateAllocations(1001, nil, + []*structs.Allocation{alloc}) + snap, _ := state.Snapshot() + + plan := &structs.Plan{ + NodeAllocation: map[string][]*structs.Allocation{ + node.ID: []*structs.Allocation{alloc}, + }, + } + + fit, err := evaluateNodePlan(snap, plan, node.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if fit { + t.Fatalf("bad") + } +} + +func TestPlanApply_EvalNodePlan_NodeFull_Evict(t *testing.T) { + alloc := mockAlloc() + state := testStateStore(t) + node := mockNode() + alloc.NodeID = node.ID + node.Resources = alloc.Resources + node.Reserved = nil + state.RegisterNode(1000, node) + state.UpdateAllocations(1001, nil, + []*structs.Allocation{alloc}) + snap, _ := state.Snapshot() + + alloc2 := mockAlloc() + plan := &structs.Plan{ + NodeEvict: map[string][]string{ + node.ID: []string{alloc.ID}, + }, + NodeAllocation: map[string][]*structs.Allocation{ + node.ID: []*structs.Allocation{alloc2}, + }, + } + + fit, err := evaluateNodePlan(snap, plan, node.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if !fit { + t.Fatalf("bad") + } } diff --git a/nomad/state_store_test.go b/nomad/state_store_test.go index afe8f6478..e43e7ecb2 100644 --- a/nomad/state_store_test.go +++ b/nomad/state_store_test.go @@ -58,7 +58,7 @@ func mockNode() *structs.Node { "pci-dss": "true", }, NodeClass: "linux-medium-pci", - Status: structs.NodeStatusInit, + Status: structs.NodeStatusReady, } return node } @@ -125,6 +125,20 @@ func mockAlloc() *structs.Allocation { alloc := &structs.Allocation{ ID: generateUUID(), NodeID: "foo", + Resources: &structs.Resources{ + CPU: 1.0, + MemoryMB: 1024, + DiskMB: 1024, + IOPS: 10, + Networks: []*structs.NetworkResource{ + &structs.NetworkResource{ + Public: true, + CIDR: "192.168.0.100/32", + ReservedPorts: []int{12345}, + MBits: 100, + }, + }, + }, } return alloc }