added snippet upload

This commit is contained in:
ston1th 2020-10-25 11:58:48 +01:00
commit 861ca4edf7
7 changed files with 281 additions and 81 deletions

View file

@ -1,5 +1,5 @@
# pve-go # pve-go
PVE-Go is a go library for the Porxmox VE API. PVE-Go is a go library for the Proxmox VE API.
The design is based on the Hetzner Clound API implementation: https://github.com/hetznercloud/hcloud-go The design is based on the Hetzner Clound API implementation: https://github.com/hetznercloud/hcloud-go

View file

@ -50,6 +50,7 @@ type Client struct {
Task TaskClient Task TaskClient
Pool PoolClient Pool PoolClient
Node NodeClient Node NodeClient
Snippet SnippetClient
} }
type session struct { type session struct {
@ -133,6 +134,7 @@ func NewClient(options ...ClientOption) *Client {
client.Task = TaskClient{client: client} client.Task = TaskClient{client: client}
client.Pool = PoolClient{client: client} client.Pool = PoolClient{client: client}
client.Node = NodeClient{client: client} client.Node = NodeClient{client: client}
client.Snippet = SnippetClient{client: client}
return client return client
} }

View file

@ -26,33 +26,45 @@ type Server struct {
Nameserver Nameserver Nameserver Nameserver
SearchDomain string SearchDomain string
UserData *UserData UserData *UserData
UserDataStorage string
cicustom bool
} }
func (s *Server) body() (httpbody, error) { func (s *Server) UserDataSnippetName() string {
body := httpbody{ 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": strconv.FormatInt(s.Resources.Memory, 10), "memory": strconv.FormatInt(s.Resources.Memory, 10),
"cores": strconv.FormatInt(s.Resources.Cores, 10), "cores": strconv.FormatInt(s.Resources.Cores, 10),
"name": s.Name, "name": s.Name,
"nameserver": s.Nameserver.String(), "nameserver": s.Nameserver.String(),
"searchdomain": s.SearchDomain, "searchdomain": s.SearchDomain,
} }
userdata, err := s.UserData.String() if s.UserData != nil || s.cicustom {
body["ciuserdata"] = userdata b["cicustom"] = s.UserDataSnippet(s.UserDataStorage)
}
// TODO userdata, err := s.UserData.String()
for i, n := range s.NetDevices { for i, n := range s.NetDevices {
if n == nil { if n == nil {
body["net"+strconv.Itoa(i)] = "" b["net"+strconv.Itoa(i)] = ""
continue continue
} }
body["net"+strconv.Itoa(i)] = n.String() b["net"+strconv.Itoa(i)] = n.String()
} }
for i, ip := range s.IPConfig { for i, ip := range s.IPConfig {
if ip == nil { if ip == nil {
body["ipconfig"+strconv.Itoa(i)] = "" b["ipconfig"+strconv.Itoa(i)] = ""
continue continue
} }
body["ipconfig"+strconv.Itoa(i)] = ip.String() b["ipconfig"+strconv.Itoa(i)] = ip.String()
} }
return body, err return
} }
func (s *Server) InstanceID() string { func (s *Server) InstanceID() string {
@ -94,10 +106,15 @@ func (sc serverConfig) Server(node, id string) (s *Server, err error) {
if v, ok := sc[k].(string); ok { if v, ok := sc[k].(string); ok {
s.Name = v s.Name = v
} }
case "ciuserdata": case "cicustom":
if v, ok := sc[k].(string); ok { if _, ok := sc[k].(string); ok {
s.UserData, err = parseUserData(v) s.cicustom = true
} }
// TODO
// case "ciuserdata":
// if v, ok := sc[k].(string); ok {
// s.UserData, err = parseUserData(v)
// }
case "memory": case "memory":
if v, ok := sc[k].(float64); ok { if v, ok := sc[k].(float64); ok {
s.Resources.Memory = int64(v) s.Resources.Memory = int64(v)
@ -349,15 +366,9 @@ func (c *ServerClient) GetStatus(ctx context.Context, s *Server, lock *string) (
return return
} }
func diffBody(cur, n *Server) (b httpbody, err error) { func diffBody(cur, n *Server) (b httpbody) {
b, err = n.body() b = n.body()
if err != nil { curbody := cur.body()
return
}
curbody, err := cur.body()
if err != nil {
return
}
for k, v := range b { for k, v := range b {
if curbody[k] == v { if curbody[k] == v {
delete(b, k) delete(b, k)
@ -388,31 +399,40 @@ func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) {
return return
} }
func (c *ServerClient) Update(ctx context.Context, s *Server) (err error) { func (c *ServerClient) Update(ctx context.Context, s *Server) (t *Task, err error) {
cur, _, err := c.getServer(ctx, s.Node, s.ID) cur, _, err := c.getServer(ctx, s.Node, s.ID)
if err != nil { if err != nil {
return return
} }
body, err := diffBody(cur, s) body := diffBody(cur, s)
if err != nil { var e []string
return 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))
}
} }
var (
cfg error
resize error
)
if len(body) > 0 { if len(body) > 0 {
cfg = c.updateConfig(ctx, s, body) cfg := c.updateConfig(ctx, s, body)
if cfg != nil {
e = append(e, fmt.Sprintf("config: %s", cfg))
}
} }
if cur.Resources.BootDiskSize != s.Resources.BootDiskSize { if cur.Resources.BootDiskSize != s.Resources.BootDiskSize {
resize = c.resizeDisk(ctx, s) resize := c.resizeDisk(ctx, s)
if resize != nil {
e = append(e, fmt.Sprintf("resize: %s", resize))
} }
if cfg != nil && resize != nil { }
return fmt.Errorf("config: %s resize: %s", cfg, resize) if len(e) > 0 {
} else if cfg != nil { err = fmt.Errorf("%s", strings.Join(e, ""))
return fmt.Errorf("config: %s", cfg) }
} else if resize != nil { if t == nil {
return fmt.Errorf("resize: %s", resize) dummy := DummyTask
t = &dummy
} }
return return
} }

36
snippet.diff Normal file
View file

@ -0,0 +1,36 @@
diff --git a/PVE/API2/Storage/Status.pm b/PVE/API2/Storage/Status.pm
index 9a5a952..fd7ac5f 100644
--- a/PVE/API2/Storage/Status.pm
+++ b/PVE/API2/Storage/Status.pm
@@ -417,8 +417,10 @@ __PACKAGE__->register_method ({
raise_param_exc({ filename => "missing '.tar.gz' or '.tar.xz' extension" });
}
$path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
- } else {
- raise_param_exc({ content => "upload content type '$content' not allowed" });
+ } elsif ($content eq 'snippets') {
+ $path = PVE::Storage::get_snippet_dir($cfg, $param->{storage});
+ } else {
+ raise_param_exc({ content => "upload content type '$content' not allowed" });
}
die "storage '$param->{storage}' does not support '$content' content\n"
diff --git a/PVE/Storage.pm b/PVE/Storage.pm
index eb5e86f..bc0981b 100755
--- a/PVE/Storage.pm
+++ b/PVE/Storage.pm
@@ -340,6 +340,15 @@ sub get_iso_dir {
return $plugin->get_subdir($scfg, 'iso');
}
+sub get_snippet_dir {
+ my ($cfg, $storeid) = @_;
+
+ my $scfg = storage_config($cfg, $storeid);
+ my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+ return $plugin->get_subdir($scfg, 'snippets');
+}
+
sub get_vztmpl_dir {
my ($cfg, $storeid) = @_;

103
snippet.go Normal file
View file

@ -0,0 +1,103 @@
// Copyright (C) 2020 Marius Schellenberger
package pve
import (
"bytes"
"context"
"errors"
"fmt"
"mime/multipart"
"net/http"
"time"
)
var (
ErrNoSnippetNode = errors.New("missing snippet target node")
ErrNoSnippetStorage = errors.New("missing snippet target storage")
ErrNoSnippetName = errors.New("missing snippet name")
)
type CopyBuffer struct {
*bytes.Buffer
}
func (c *CopyBuffer) Copy() *bytes.Buffer {
buf := make([]byte, c.Len())
copy(buf, c.Bytes())
return bytes.NewBuffer(buf)
}
type SnippetClient struct {
client *Client
}
func (c *SnippetClient) Create(ctx context.Context, node, storage, name string, data []byte) (t *Task, err error) {
if node == "" {
err = ErrNoSnippetNode
return
}
if storage == "" {
err = ErrNoSnippetStorage
return
}
if name == "" {
err = ErrNoSnippetName
return
}
var b bytes.Buffer
w := multipart.NewWriter(&b)
err = w.WriteField("content", "snippets")
if err != nil {
return
}
fw, err := w.CreateFormFile("filename", name)
if err != nil {
return
}
if _, err = fw.Write(data); err != nil {
return
}
w.Close()
cb := CopyBuffer{&b}
ct := w.FormDataContentType()
var taskid string
var req *http.Request
var resp *http.Response
for i := 0; i < 3; i++ {
buf := cb.Copy()
req, err = c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/storage/%s/upload", node, storage), buf)
req.Header.Set("Content-Type", ct)
resp, err = c.client.Do(req, &taskid)
if err != nil {
time.Sleep(time.Millisecond * 500)
// TODO log error
continue
}
if resp.StatusCode != http.StatusOK {
time.Sleep(time.Millisecond * 500)
// TODO log error
continue
}
break
}
t = c.client.Task.MustGet(ctx, taskid)
return
}
func SnippetVolume(storage, name string) string {
return fmt.Sprintf("%s:snippets/%s", storage, name)
}
func (c *SnippetClient) Delete(ctx context.Context, node, storage, name string) (t *Task, err error) {
//volume := fmt.Sprintf("/%s:snippets/%s", storage, name)
volume := SnippetVolume(storage, name)
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/nodes/%s/storage/%s/content/%s", node, storage, volume), nil)
if err != nil {
return
}
var taskid string
_, err = c.client.Do(req, &taskid)
t = c.client.Task.MustGet(ctx, taskid)
return
}

View file

@ -16,8 +16,12 @@ const (
TaskExitStatusOK = "OK" TaskExitStatusOK = "OK"
TaskStatusRunning = "running" TaskStatusRunning = "running"
TaskStatusStopped = "stopped" TaskStatusStopped = "stopped"
DummyTaskID = "dummy"
) )
var DummyTask = Task{ID: DummyTaskID}
type Task struct { type Task struct {
ID string `json:"upid"` ID string `json:"upid"`
Type string `json:"type"` Type string `json:"type"`
@ -63,6 +67,9 @@ func (c *TaskClient) Get(ctx context.Context, taskid string) (t *Task, err error
type OnTaskChange func(t *Task) type OnTaskChange func(t *Task)
func (c *TaskClient) Wait(ctx context.Context, t *Task, f OnTaskChange) error { func (c *TaskClient) Wait(ctx context.Context, t *Task, f OnTaskChange) error {
if t.ID == DummyTaskID {
return nil
}
if f != nil && t.Status != "" { if f != nil && t.Status != "" {
f(t) f(t)
} }

View file

@ -3,40 +3,51 @@
package pve package pve
import ( import (
"encoding/base64" //"encoding/base64"
//"net/url"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"net/url"
) )
var cloudConfig = []byte("#cloud-config\n") var cloudConfig = []byte("#cloud-config\n")
func parseUserData(s string) (u *UserData, err error) { const (
s, err = url.QueryUnescape(s) minBytes = 87
if err != nil { pad = "########################################################################\n"
return )
}
b, err := base64.StdEncoding.DecodeString(s) //TODO
if err != nil { //func parseUserData(s string) (u *UserData, err error) {
return // s, err = url.QueryUnescape(s)
} // if err != nil {
return UnmarshalUserData(b) // return
} // }
// b, err := base64.StdEncoding.DecodeString(s)
// if err != nil {
// return
// }
// return UnmarshalUserData(b)
//}
func UnmarshalUserData(b []byte) (u *UserData, err error) { func UnmarshalUserData(b []byte) (u *UserData, err error) {
u = new(UserData) u = new(UserData)
err = yaml.Unmarshal(b, u) err = yaml.Unmarshal(b, u)
return return
} }
func MarshalUserData(u *UserData) ([]byte, error) {
if u == nil { //TODO
return nil, nil //func MarshalUserData(u *UserData) ([]byte, error) {
} // if u == nil {
b, err := yaml.Marshal(u) // return nil, nil
if err == nil { // }
b = append(cloudConfig, b...) // b, err := yaml.Marshal(u)
} // if err == nil {
return b, err // if len(b)+len(cloudConfig) < minBytes {
} // b = append([]byte(pad[len(b):]), b...)
// }
// b = append(cloudConfig, b...)
// }
// return b, err
//}
type UserData struct { type UserData struct {
Hostname string `yaml:"hostname"` Hostname string `yaml:"hostname"`
@ -53,12 +64,33 @@ type UserData struct {
WriteFiles []File `yaml:"write_files,omitempty"` WriteFiles []File `yaml:"write_files,omitempty"`
} }
// TODO
// func (u *UserData) String() (s string, err error) {
// b, err := MarshalUserData(u)
// if err != nil {
// return
// }
// s = url.QueryEscape(base64.StdEncoding.EncodeToString(b))
// return
// }
func (u *UserData) String() (s string, err error) { func (u *UserData) String() (s string, err error) {
b, err := MarshalUserData(u) b, err := u.Bytes()
if err != nil { if err != nil {
return return
} }
s = url.QueryEscape(base64.StdEncoding.EncodeToString(b)) s = string(b)
return
}
func (u *UserData) Bytes() (b []byte, err error) {
b, err = yaml.Marshal(u)
if err == nil {
if len(b)+len(cloudConfig) < minBytes {
b = append([]byte(pad[len(b):]), b...)
}
b = append(cloudConfig, b...)
}
return return
} }