// 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 } */