pve-go/server.go
2020-06-01 12:18:20 +02:00

424 lines
9.4 KiB
Go

// Copyright (C) 2020 Marius Schellenberger
package pve
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
)
type Server struct {
ID string `json:"vmid"`
Name string `json:"name"`
Node string
Status ServerStatus `json:"status"`
Resources Resources
NetDevices []*NetworkDevice
IPConfig []*IPConfig
Nameserver Nameserver
SearchDomain string
UserData *UserData
}
func (s *Server) body() (httpbody, error) {
body := httpbody{
"memory": strconv.FormatInt(s.Resources.Memory, 10),
"cores": strconv.FormatInt(s.Resources.Cores, 10),
"name": s.Name,
"nameserver": s.Nameserver.String(),
"searchdomain": s.SearchDomain,
}
userdata, err := s.UserData.String()
body["ciuserdata"] = userdata
for i, n := range s.NetDevices {
body["net"+strconv.Itoa(i)] = n.String()
}
for i, ip := range s.IPConfig {
body["ipconfig"+strconv.Itoa(i)] = ip.String()
}
return body, err
}
func (s *Server) InstanceID() string {
return s.String()
}
func (s *Server) String() string {
return NewURL(s.Node, s.ID)
}
type Resources struct {
Memory int64
Cores int64
BootDisk string
BootDiskSize string
}
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(node, id string) (s *Server, err error) {
s = &Server{ID: id, Node: node}
for k := range sc {
switch k {
case "name":
if v, ok := sc[k].(string); ok {
s.Name = v
}
case "ciuserdata":
if v, ok := sc[k].(string); ok {
s.UserData, err = parseUserData(v)
}
case "memory":
if v, ok := sc[k].(float64); ok {
s.Resources.Memory = int64(v)
}
case "cores":
if v, ok := sc[k].(float64); ok {
s.Resources.Cores = int64(v)
}
case "bootdisk":
if v, ok := sc[k].(string); ok {
s.Resources.BootDisk = v
if val, ok := sc[v].(string); ok {
for _, o := range strings.Split(val, ",") {
if strings.HasPrefix(o, "size=") {
s.Resources.BootDiskSize = o[5:]
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 string, 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, node, id string) (cfg serverConfig, err error) {
cfg = make(serverConfig)
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%s/config", node, id), nil)
if err != nil {
return
}
_, err = c.client.Do(req, &cfg)
return
}
func (c *ServerClient) getServer(ctx context.Context, node, id string) (s *Server, cfg serverConfig, err error) {
cfg, err = c.getConfig(ctx, node, id)
if err != nil {
return
}
s, err = cfg.Server(node, id)
return
}
func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err error) {
node, id, err := ParseURL(url)
if err != nil {
return
}
s, cfg, err := c.getServer(ctx, node, id)
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
}
type ServerTemplateOpts struct {
Name string
TemplateURL string
Pool string
TargetStorage string
TargetNode string
}
func (o ServerTemplateOpts) Validate() error {
if o.Name == "" {
return errors.New("missing name")
}
if o.TemplateURL == "" {
return errors.New("missing template url")
}
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, err error) {
err = opts.Validate()
if err != nil {
return
}
tempnode, id, err := ParseURL(opts.TemplateURL)
if err != nil {
return
}
nextid, err := c.NextID(ctx)
if err != nil {
return
}
body := opts.body()
body["newid"] = nextid
if tempnode != opts.TargetNode {
body["target"] = opts.TargetNode
}
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%s/clone", tempnode, id), 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, s *Server) (t *Task, err error) {
return c.setStatus(ctx, s, "start")
}
func (c *ServerClient) Reboot(ctx context.Context, s *Server) (t *Task, err error) {
return c.setStatus(ctx, s, "reboot")
}
func (c *ServerClient) Shutdown(ctx context.Context, s *Server) (t *Task, err error) {
return c.setStatus(ctx, s, "shutdown")
}
func (c *ServerClient) Reset(ctx context.Context, s *Server) (t *Task, err error) {
return c.setStatus(ctx, s, "reset")
}
func (c *ServerClient) Suspend(ctx context.Context, s *Server) (t *Task, err error) {
return c.setStatus(ctx, s, "suspent")
}
func (c *ServerClient) Resume(ctx context.Context, s *Server) (t *Task, err error) {
return c.setStatus(ctx, s, "resume")
}
func (c *ServerClient) Stop(ctx context.Context, s *Server) (t *Task, err error) {
return c.setStatus(ctx, s, "stop")
}
func (c *ServerClient) setStatus(ctx context.Context, s *Server, status string) (t *Task, err error) {
req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%s/status/%s", s.Node, s.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.Node, s.ID)
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, err error) {
b, err = n.body()
if err != nil {
return
}
curbody, err := cur.body()
if err != nil {
return
}
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.BootDisk,
"size": s.Resources.BootDiskSize,
}
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) (err error) {
cur, _, err := c.getServer(ctx, s.Node, s.ID)
if err != nil {
return
}
body, err := diffBody(cur, s)
if err != nil {
return
}
var (
cfg error
resize error
)
if len(body) > 0 {
cfg = c.updateConfig(ctx, s, body)
}
if cur.Resources.BootDiskSize != s.Resources.BootDiskSize {
resize = c.resizeDisk(ctx, s)
}
if cfg != nil && resize != nil {
return fmt.Errorf("config: %s resize: %s", cfg, resize)
} else if cfg != nil {
return fmt.Errorf("config: %s", cfg)
} else if resize != nil {
return fmt.Errorf("resize: %s", resize)
}
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)
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
}
*/