// Copyright (C) 2020 Marius Schellenberger package pve import ( "context" "errors" "fmt" "strconv" "strings" ) var ErrServerNotFound = errors.New("server not found") type Server struct { ID string `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("%s_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 string `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 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, 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 string // 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 == "" { 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"] = 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 } */