added snippet upload
This commit is contained in:
parent
349afa60e1
commit
861ca4edf7
7 changed files with 281 additions and 81 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
94
server.go
94
server.go
|
|
@ -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
36
snippet.diff
Normal 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
103
snippet.go
Normal 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
|
||||||
|
}
|
||||||
7
task.go
7
task.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
userdata.go
82
userdata.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue