270 lines
5.3 KiB
Go
270 lines
5.3 KiB
Go
// Copyright (C) 2020 Marius Schellenberger
|
|
|
|
package pve
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
var (
|
|
ErrNodesOffline = errors.New("one or more nodes are offline")
|
|
ErrNodesNotSearched = errors.New("one or more nodes could not be searched")
|
|
ErrUnschedulable = errors.New("no schedulable node found")
|
|
ErrTooManyServersFound = errors.New("too many servers found with the same name")
|
|
)
|
|
|
|
type NodeStatus string
|
|
|
|
const (
|
|
NodeStatusOnline NodeStatus = "online"
|
|
NodeStatusOffline NodeStatus = "offline"
|
|
NodeStatusUnknown NodeStatus = "unknown"
|
|
)
|
|
|
|
type Node struct {
|
|
Name string `json:"node"`
|
|
Status NodeStatus `json:"status"`
|
|
CPU float64 `json:"cpu"`
|
|
MaxCPU float64 `json:"maxcpu"`
|
|
Mem uint64 `json:"mem"`
|
|
MaxMem uint64 `json:"maxmem"`
|
|
Disk uint64 `json:"disk"`
|
|
MaxDisk uint64 `json:"maxdisk"`
|
|
serverList ServerRefList `json:"-"`
|
|
}
|
|
|
|
func (n *Node) memFreePercent() float64 {
|
|
return float64(n.Mem) / float64(n.MaxMem) * 100
|
|
}
|
|
|
|
func (n *Node) memUsedPercent() float64 {
|
|
return float64(n.MaxMem-n.Mem) / float64(n.MaxMem) * 100
|
|
}
|
|
|
|
func (n *Node) weight() int {
|
|
return int(n.memUsedPercent()) + (len(n.serverList) * 10)
|
|
}
|
|
|
|
type NodeList []*Node
|
|
|
|
func (nl NodeList) sortByWeight() {
|
|
sort.Sort(weightSorter(nl))
|
|
}
|
|
|
|
func (nl *NodeList) remove(i int) {
|
|
(*nl)[i] = (*nl)[len(*nl)-1]
|
|
*nl = (*nl)[:len(*nl)-1]
|
|
}
|
|
|
|
func (nl *NodeList) filter(filters ...NodeFilter) {
|
|
for _, f := range filters {
|
|
f(nl)
|
|
}
|
|
}
|
|
|
|
func (nl *NodeList) getServerList(ctx context.Context, c *NodeClient) (err error) {
|
|
for _, n := range *nl {
|
|
n.serverList, err = c.ListServers(ctx, n)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
type weightSorter NodeList
|
|
|
|
func (s weightSorter) Len() int { return len(s) }
|
|
func (s weightSorter) Less(i, j int) bool {
|
|
return s[i].weight() < s[j].weight()
|
|
}
|
|
func (s weightSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
type NodeFilter func(*NodeList)
|
|
|
|
func FilterOnlineNode() NodeFilter {
|
|
return func(nl *NodeList) {
|
|
for i, n := range *nl {
|
|
if n.Status != NodeStatusOnline {
|
|
nl.remove(i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func FilterNodeName(name string) NodeFilter {
|
|
return func(nl *NodeList) {
|
|
for i, n := range *nl {
|
|
if n.Name == name {
|
|
nl.remove(i)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func FilterMaxCores(cores uint64) NodeFilter {
|
|
return func(nl *NodeList) {
|
|
for i, n := range *nl {
|
|
if cores > uint64(n.MaxCPU) {
|
|
nl.remove(i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
mib = 1024 * 1024
|
|
gib = mib * 1024
|
|
)
|
|
|
|
func FilterFreeMem(mem uint64) NodeFilter {
|
|
return func(nl *NodeList) {
|
|
for i, n := range *nl {
|
|
if (mem * gib) >= n.MaxMem-n.Mem {
|
|
nl.remove(i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Useful? only local disk?
|
|
func FilterFreeDisk(disk uint64) NodeFilter {
|
|
return func(nl *NodeList) {
|
|
for i, n := range *nl {
|
|
if (disk * gib) >= n.MaxDisk-n.Disk {
|
|
nl.remove(i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func FilterVMAntiAffinity(url string) NodeFilter {
|
|
return func(nl *NodeList) {
|
|
_, _, id, err := ParseURL(url)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for i, n := range *nl {
|
|
for _, s := range n.serverList {
|
|
if s.ID == id {
|
|
nl.remove(i)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type NodeClient struct {
|
|
client *Client
|
|
}
|
|
|
|
func (c *NodeClient) Schedule(ctx context.Context, filters ...NodeFilter) (n *Node, err error) {
|
|
nl, err := c.List(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = nl.getServerList(ctx, c)
|
|
if err != nil {
|
|
return
|
|
}
|
|
nl.filter(filters...)
|
|
nl.sortByWeight()
|
|
if len(nl) == 0 {
|
|
err = ErrUnschedulable
|
|
return
|
|
}
|
|
n = nl[0]
|
|
return
|
|
}
|
|
|
|
func (c *NodeClient) List(ctx context.Context) (nl NodeList, err error) {
|
|
req, err := c.client.NewRequest(ctx, "GET", "/nodes", nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = c.client.Do(req, &nl)
|
|
return
|
|
}
|
|
|
|
func (c *NodeClient) ListServers(ctx context.Context, n *Node) (sl ServerRefList, err error) {
|
|
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu", n.Name), nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = c.client.Do(req, &sl)
|
|
if err == nil {
|
|
for i := range sl {
|
|
sl[i].Node = n.Name
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *NodeClient) FindServer(ctx context.Context, name string, id int) (ref *ServerRef, err error) {
|
|
valid := ValidateID(id) == nil
|
|
nl, err := c.List(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var (
|
|
nodeOffline bool
|
|
nodeNotSearched bool
|
|
)
|
|
for _, node := range nl {
|
|
if node.Status != NodeStatusOnline {
|
|
nodeOffline = true
|
|
continue
|
|
}
|
|
sl, err := c.ListServers(ctx, node)
|
|
if err != nil {
|
|
nodeNotSearched = true
|
|
continue
|
|
}
|
|
for _, s := range sl {
|
|
if valid && s.ID == id {
|
|
return s, nil
|
|
}
|
|
if name != "" && s.Name == name {
|
|
if ref != nil {
|
|
return nil, ErrTooManyServersFound
|
|
}
|
|
ref = s
|
|
}
|
|
}
|
|
}
|
|
if nodeOffline {
|
|
return nil, ErrNodesOffline
|
|
}
|
|
if nodeNotSearched {
|
|
return nil, ErrNodesNotSearched
|
|
}
|
|
if ref == nil {
|
|
err = ErrServerNotFound
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *NodeClient) FindServerByName(ctx context.Context, name string) (s *ServerRef, err error) {
|
|
return c.FindServer(ctx, name, InvalidID)
|
|
}
|
|
|
|
func (c *NodeClient) FindServerByID(ctx context.Context, id int) (s *ServerRef, err error) {
|
|
err = ValidateID(id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return c.FindServer(ctx, "", id)
|
|
}
|
|
|
|
func (c *NodeClient) FindServerByURL(ctx context.Context, url string) (s *ServerRef, err error) {
|
|
_, _, id, err := ParseURL(url)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return c.FindServerByID(ctx, id)
|
|
}
|