534 lines
12 KiB
Go
534 lines
12 KiB
Go
// Copyright (C) 2020 Marius Schellenberger
|
|
|
|
package pve
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var ErrServerNotFound = errors.New("server not found")
|
|
|
|
type Server struct {
|
|
ID int `json:"vmid"`
|
|
Name string `json:"name"`
|
|
Node string
|
|
Pool string
|
|
Status ServerStatus `json:"status"`
|
|
Resources Resources
|
|
NetDevices NetworkDevices
|
|
IPConfig IPConfigs
|
|
Nameserver Nameserver
|
|
SearchDomain string
|
|
UserData *UserData
|
|
UserDataStorage string
|
|
cicustom bool
|
|
}
|
|
|
|
func (s *Server) UserDataSnippetName() string {
|
|
return fmt.Sprintf("%d_userdata", s.ID)
|
|
}
|
|
|
|
func (s *Server) UserDataSnippet(storage string) string {
|
|
return fmt.Sprintf("user=%s", SnippetVolume(storage, s.UserDataSnippetName()))
|
|
}
|
|
|
|
func (s *Server) body() (b httpbody) {
|
|
b = httpbody{
|
|
"memory": s.Resources.Memory.String(),
|
|
"cores": s.Resources.Cores.String(),
|
|
"name": s.Name,
|
|
"nameserver": s.Nameserver.String(),
|
|
"searchdomain": s.SearchDomain,
|
|
}
|
|
if s.UserData != nil || s.cicustom {
|
|
b["cicustom"] = s.UserDataSnippet(s.UserDataStorage)
|
|
}
|
|
for i, n := range s.NetDevices {
|
|
if n == nil {
|
|
b["net"+strconv.Itoa(i)] = ""
|
|
continue
|
|
}
|
|
b["net"+strconv.Itoa(i)] = n.String()
|
|
}
|
|
for i, ip := range s.IPConfig {
|
|
if ip == nil {
|
|
b["ipconfig"+strconv.Itoa(i)] = ""
|
|
continue
|
|
}
|
|
b["ipconfig"+strconv.Itoa(i)] = ip.String()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *Server) InstanceID() string {
|
|
return NewURL("", s.Node, s.ID)
|
|
}
|
|
|
|
func (s *Server) K8sID() string {
|
|
return NewURL(s.Pool, "", s.ID)
|
|
}
|
|
|
|
func (s *Server) Ref() *ServerRef {
|
|
return &ServerRef{
|
|
ID: s.ID,
|
|
Node: s.Node,
|
|
Name: s.Name,
|
|
Pool: s.Pool,
|
|
}
|
|
}
|
|
|
|
type ServerRefList []*ServerRef
|
|
|
|
type ServerRef struct {
|
|
ID int `json:"vmid"`
|
|
Name string `json:"name"`
|
|
Node string
|
|
Pool string
|
|
}
|
|
|
|
func (ref *ServerRef) InstanceID() string {
|
|
return NewURL("", ref.Node, ref.ID)
|
|
}
|
|
|
|
func (ref *ServerRef) K8sID() string {
|
|
return NewURL(ref.Pool, "", ref.ID)
|
|
}
|
|
|
|
type Resources struct {
|
|
Cores Cores `json:"cores"`
|
|
Memory Memory `json:"memory"`
|
|
Disk Disk `json:"disk"`
|
|
}
|
|
|
|
type Cores uint64
|
|
|
|
func (c Cores) String() string {
|
|
return strconv.FormatUint(uint64(c), 10)
|
|
}
|
|
|
|
type Memory uint64
|
|
|
|
func (m Memory) String() string {
|
|
return strconv.FormatUint(uint64(m)*1024, 10)
|
|
}
|
|
|
|
type Disk struct {
|
|
Storage string `json:"storage"`
|
|
Name string `json:"name,omitempty"`
|
|
Size uint64 `json:"size"`
|
|
}
|
|
|
|
func (d Disk) String() string {
|
|
return strconv.FormatUint(d.Size, 10) + "G"
|
|
}
|
|
|
|
type Nameserver []string
|
|
|
|
func parseNameserver(s string) Nameserver {
|
|
return strings.Split(s, " ")
|
|
}
|
|
|
|
func (n Nameserver) String() string {
|
|
return strings.Join(n, " ")
|
|
}
|
|
|
|
type serverConfig map[string]interface{}
|
|
|
|
func (sc serverConfig) Server(ref *ServerRef) (s *Server, err error) {
|
|
if len(sc) == 0 {
|
|
return
|
|
}
|
|
s = &Server{ID: ref.ID, Node: ref.Node}
|
|
|
|
for k := range sc {
|
|
switch k {
|
|
case "name":
|
|
if v, ok := sc[k].(string); ok {
|
|
s.Name = v
|
|
}
|
|
case "cicustom":
|
|
if _, ok := sc[k].(string); ok {
|
|
s.cicustom = true
|
|
}
|
|
case "memory":
|
|
if v, ok := sc[k].(float64); ok {
|
|
s.Resources.Memory = Memory(uint64(v) / 1024)
|
|
}
|
|
case "cores":
|
|
if v, ok := sc[k].(float64); ok {
|
|
s.Resources.Cores = Cores(uint64(v))
|
|
}
|
|
case "bootdisk":
|
|
if v, ok := sc[k].(string); ok {
|
|
s.Resources.Disk.Name = v
|
|
if val, ok := sc[v].(string); ok {
|
|
vals := strings.Split(val, ",")
|
|
if len(vals) > 0 {
|
|
storage := strings.Split(vals[0], ":")
|
|
if len(storage) > 0 {
|
|
s.Resources.Disk.Storage = storage[0]
|
|
}
|
|
}
|
|
for _, o := range vals {
|
|
if strings.HasPrefix(o, "size=") {
|
|
size, _ := strconv.ParseUint(o[5:], 10, 64)
|
|
s.Resources.Disk.Size = size
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case "nameserver":
|
|
if v, ok := sc[k].(string); ok {
|
|
s.Nameserver = parseNameserver(v)
|
|
}
|
|
case "searchdomain":
|
|
if v, ok := sc[k].(string); ok {
|
|
s.SearchDomain = v
|
|
}
|
|
default:
|
|
switch {
|
|
case strings.HasPrefix(k, "net"):
|
|
if v, ok := sc[k].(string); ok {
|
|
n := parseNetworkDevice(v)
|
|
if n != nil {
|
|
s.NetDevices = append(s.NetDevices, n)
|
|
}
|
|
}
|
|
case strings.HasPrefix(k, "ipconfig"):
|
|
if v, ok := sc[k].(string); ok {
|
|
ip := parseIPConfig(v)
|
|
if ip != nil {
|
|
s.IPConfig = append(s.IPConfig, ip)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
if s.Resources.Cores == 0 {
|
|
s.Resources.Cores = 1
|
|
}
|
|
return
|
|
}
|
|
|
|
type ServerClient struct {
|
|
client *Client
|
|
}
|
|
|
|
func (c *ServerClient) NextID(ctx context.Context) (id int, err error) {
|
|
req, err := c.client.NewRequest(ctx, "GET", "/cluster/nextid", nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = c.client.Do(req, &id)
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) getConfig(ctx context.Context, ref *ServerRef) (cfg serverConfig, err error) {
|
|
cfg = make(serverConfig)
|
|
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%s/config?current=1", ref.Node, ref.ID), nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = c.client.Do(req, &cfg)
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) getServer(ctx context.Context, ref *ServerRef) (s *Server, cfg serverConfig, err error) {
|
|
cfg, err = c.getConfig(ctx, ref)
|
|
if err != nil {
|
|
return
|
|
}
|
|
s, err = cfg.Server(ref)
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) GetByRef(ctx context.Context, ref *ServerRef) (s *Server, err error) {
|
|
s, cfg, err := c.getServer(ctx, ref)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if s != nil {
|
|
var lock string
|
|
if v, ok := cfg["lock"].(string); ok {
|
|
lock = v
|
|
}
|
|
err = c.GetStatus(ctx, s, &lock)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err error) {
|
|
ref, err := ServerRefFromURL(url)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if ref.Node == "" && ref.Pool != "" {
|
|
ref, err = c.client.Pool.FindServerByID(ctx, ref.Pool, ref.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if ref.Node == "" && ref.Pool == "" {
|
|
ref, err = c.client.Node.FindServerByID(ctx, ref.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
s, err = c.GetByRef(ctx, ref)
|
|
return
|
|
}
|
|
|
|
type ServerTemplateOpts struct {
|
|
Name string
|
|
TemplateID int
|
|
// TemplateNode is optional
|
|
TemplateNode string
|
|
Pool string
|
|
TargetStorage string
|
|
TargetNode string
|
|
}
|
|
|
|
func (o *ServerTemplateOpts) Validate(ctx context.Context, c *Client) error {
|
|
if o.Name == "" {
|
|
return errors.New("missing name")
|
|
}
|
|
if o.TemplateID <= 0 {
|
|
return errors.New("missing template id")
|
|
}
|
|
if o.TemplateNode == "" {
|
|
ref, err := c.Node.FindServerByID(ctx, o.TemplateID)
|
|
if err != nil {
|
|
return fmt.Errorf("template node not found err: %w", err)
|
|
}
|
|
if ref.Node == "" {
|
|
return errors.New("missing template node")
|
|
}
|
|
o.TemplateNode = ref.Node
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *ServerTemplateOpts) body() httpbody {
|
|
body := httpbody{"name": o.Name}
|
|
if o.TargetStorage != "" {
|
|
body["storage"] = o.TargetStorage
|
|
body["full"] = "1"
|
|
}
|
|
if o.Pool != "" {
|
|
body["pool"] = o.Pool
|
|
}
|
|
return body
|
|
}
|
|
|
|
func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts *ServerTemplateOpts) (t *Task, url string, err error) {
|
|
err = opts.Validate(ctx, c.client)
|
|
if err != nil {
|
|
return
|
|
}
|
|
nextid, err := c.NextID(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
body := opts.body()
|
|
body["newid"] = strconv.Itoa(nextid)
|
|
node := opts.TemplateNode
|
|
if opts.TargetNode != "" && opts.TemplateNode != opts.TargetNode {
|
|
node = opts.TargetNode
|
|
body["target"] = opts.TargetNode
|
|
}
|
|
url = NewURL(opts.Pool, node, nextid)
|
|
req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%s/clone", opts.TemplateNode, opts.TemplateID), body.Reader())
|
|
if err != nil {
|
|
return
|
|
}
|
|
var taskid string
|
|
_, err = c.client.Do(req, &taskid)
|
|
t = c.client.Task.MustGet(ctx, taskid)
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) Start(ctx context.Context, ref *ServerRef) (t *Task, err error) {
|
|
return c.setStatus(ctx, ref, "start")
|
|
}
|
|
|
|
func (c *ServerClient) Reboot(ctx context.Context, ref *ServerRef) (t *Task, err error) {
|
|
return c.setStatus(ctx, ref, "reboot")
|
|
}
|
|
|
|
func (c *ServerClient) Shutdown(ctx context.Context, ref *ServerRef) (t *Task, err error) {
|
|
return c.setStatus(ctx, ref, "shutdown")
|
|
}
|
|
|
|
func (c *ServerClient) Reset(ctx context.Context, ref *ServerRef) (t *Task, err error) {
|
|
return c.setStatus(ctx, ref, "reset")
|
|
}
|
|
|
|
func (c *ServerClient) Suspend(ctx context.Context, ref *ServerRef) (t *Task, err error) {
|
|
return c.setStatus(ctx, ref, "suspent")
|
|
}
|
|
|
|
func (c *ServerClient) Resume(ctx context.Context, ref *ServerRef) (t *Task, err error) {
|
|
return c.setStatus(ctx, ref, "resume")
|
|
}
|
|
|
|
func (c *ServerClient) Stop(ctx context.Context, ref *ServerRef) (t *Task, err error) {
|
|
return c.setStatus(ctx, ref, "stop")
|
|
}
|
|
|
|
func (c *ServerClient) setStatus(ctx context.Context, ref *ServerRef, status string) (t *Task, err error) {
|
|
req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%s/status/%s", ref.Node, ref.ID, status), nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var taskid string
|
|
_, err = c.client.Do(req, &taskid)
|
|
t = c.client.Task.MustGet(ctx, taskid)
|
|
return
|
|
}
|
|
|
|
type statusObj struct {
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (c *ServerClient) GetStatus(ctx context.Context, s *Server, lock *string) (err error) {
|
|
var lockval string
|
|
if lock == nil {
|
|
cfg, err := c.getConfig(ctx, s.Ref())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v, ok := cfg["lock"].(string); ok {
|
|
lockval = v
|
|
}
|
|
} else {
|
|
lockval = *lock
|
|
}
|
|
var status statusObj
|
|
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%s/status/current", s.Node, s.ID), nil)
|
|
if err != nil {
|
|
s.Status = ServerStatusUnknown
|
|
return
|
|
}
|
|
_, err = c.client.Do(req, &status)
|
|
if err != nil {
|
|
s.Status = ServerStatusUnknown
|
|
return
|
|
}
|
|
s.Status = ServerStatusFromLock(status.Status, lockval)
|
|
return
|
|
}
|
|
|
|
func diffBody(cur, n *Server) (b httpbody) {
|
|
b = n.body()
|
|
curbody := cur.body()
|
|
for k, v := range b {
|
|
if curbody[k] == v {
|
|
delete(b, k)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) updateConfig(ctx context.Context, s *Server, body httpbody) (err error) {
|
|
req, err := c.client.NewRequest(ctx, "PUT", fmt.Sprintf("/nodes/%s/qemu/%s/config", s.Node, s.ID), body.Reader())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = c.client.Do(req, nil)
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) {
|
|
body := httpbody{
|
|
"disk": s.Resources.Disk.Name,
|
|
"size": s.Resources.Disk.String(),
|
|
}
|
|
req, err := c.client.NewRequest(ctx, "PUT", fmt.Sprintf("/nodes/%s/qemu/%s/resize", s.Node, s.ID), body.Reader())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = c.client.Do(req, nil)
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) Update(ctx context.Context, s *Server) (t *Task, err error) {
|
|
cur, _, err := c.getServer(ctx, s.Ref())
|
|
if err != nil {
|
|
return
|
|
}
|
|
body := diffBody(cur, s)
|
|
var e []string
|
|
if s.UserData != nil {
|
|
b, userdata := s.UserData.Bytes()
|
|
if userdata == nil {
|
|
t, userdata = c.client.Snippet.Create(ctx, s.Node, s.UserDataStorage, s.UserDataSnippetName(), b)
|
|
}
|
|
if userdata != nil {
|
|
e = append(e, fmt.Sprintf("userdata: %s", userdata))
|
|
}
|
|
}
|
|
if len(body) > 0 {
|
|
cfg := c.updateConfig(ctx, s, body)
|
|
if cfg != nil {
|
|
e = append(e, fmt.Sprintf("config: %s", cfg))
|
|
}
|
|
}
|
|
if cur.Resources.Disk.Size != s.Resources.Disk.Size {
|
|
resize := c.resizeDisk(ctx, s)
|
|
if resize != nil {
|
|
e = append(e, fmt.Sprintf("resize: %s", resize))
|
|
}
|
|
}
|
|
if len(e) > 0 {
|
|
err = fmt.Errorf("%s", strings.Join(e, ""))
|
|
}
|
|
if t == nil {
|
|
dummy := DummyTask
|
|
t = &dummy
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *ServerClient) Delete(ctx context.Context, s *Server, f OnTaskChange) (t *Task, err error) {
|
|
if s.Status == ServerStatusRunning || s.Status == ServerStatusUnknown {
|
|
task, err := c.Stop(ctx, s.Ref())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = c.client.Task.Wait(ctx, task, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/nodes/%s/qemu/%s", s.Node, s.ID), nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var taskid string
|
|
_, err = c.client.Do(req, &taskid)
|
|
t = c.client.Task.MustGet(ctx, taskid)
|
|
return
|
|
}
|
|
|
|
/* TODO
|
|
|
|
// ServerCreateOpts specifies options for creating a new server.
|
|
type ServerCreateOpts struct {
|
|
Server
|
|
TemplateID string
|
|
UserData string
|
|
}
|
|
|
|
// Validate checks if options are valid.
|
|
func (o ServerCreateOpts) Validate() error {
|
|
if o.Name == "" {
|
|
return errors.New("missing name")
|
|
}
|
|
return nil
|
|
}
|
|
*/
|