From 6de6e8001f9654944231dc55c7714447403666eb Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 15:17:50 +0200 Subject: [PATCH 01/47] test --- helper.go | 5 ++++- server.go | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/helper.go b/helper.go index 878c82d..bd130d9 100644 --- a/helper.go +++ b/helper.go @@ -5,6 +5,7 @@ package pve import ( "encoding/json" "errors" + "fmt" "io" "net/http" "net/url" @@ -41,5 +42,7 @@ func (b httpbody) Reader() io.Reader { for k, v := range b { data.Set(k, v) } - return strings.NewReader(data.Encode()) + enc := data.Encode() + fmt.Println("--- encoded:", enc) + return strings.NewReader(enc) } diff --git a/server.go b/server.go index c1a1590..c165bd3 100644 --- a/server.go +++ b/server.go @@ -32,6 +32,7 @@ func (s *Server) body() (httpbody, error) { "searchdomain": s.SearchDomain, } userdata, err := s.UserData.String() + fmt.Println("--- plain:", userdata) body["ciuserdata"] = userdata for i, n := range s.NetDevices { body["net"+strconv.Itoa(i)] = n.String() From 12816a532cb64f03324785e0df93a7c4568e919f Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 15:40:52 +0200 Subject: [PATCH 02/47] test --- helper.go | 4 ++-- server.go | 2 +- userdata.go | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/helper.go b/helper.go index bd130d9..c184315 100644 --- a/helper.go +++ b/helper.go @@ -5,7 +5,7 @@ package pve import ( "encoding/json" "errors" - "fmt" + //"fmt" "io" "net/http" "net/url" @@ -43,6 +43,6 @@ func (b httpbody) Reader() io.Reader { data.Set(k, v) } enc := data.Encode() - fmt.Println("--- encoded:", enc) + //fmt.Println("--- encoded:", enc) return strings.NewReader(enc) } diff --git a/server.go b/server.go index c165bd3..dbf5e65 100644 --- a/server.go +++ b/server.go @@ -32,7 +32,7 @@ func (s *Server) body() (httpbody, error) { "searchdomain": s.SearchDomain, } userdata, err := s.UserData.String() - fmt.Println("--- plain:", userdata) + //fmt.Println("--- plain:", userdata) body["ciuserdata"] = userdata for i, n := range s.NetDevices { body["net"+strconv.Itoa(i)] = n.String() diff --git a/userdata.go b/userdata.go index 3b2eb24..ecd8e41 100644 --- a/userdata.go +++ b/userdata.go @@ -26,6 +26,9 @@ func UnmarshalUserData(b []byte) (u *UserData, err error) { return } func MarshalUserData(u *UserData) ([]byte, error) { + if u == nil { + return + } return yaml.Marshal(u) } From e4271a2eba660245c6cf16434372d95ccaa4fdc6 Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 15:42:12 +0200 Subject: [PATCH 03/47] test --- userdata.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/userdata.go b/userdata.go index ecd8e41..59a082b 100644 --- a/userdata.go +++ b/userdata.go @@ -27,7 +27,7 @@ func UnmarshalUserData(b []byte) (u *UserData, err error) { } func MarshalUserData(u *UserData) ([]byte, error) { if u == nil { - return + return nil, nil } return yaml.Marshal(u) } From 3c97fbfe7c0824807cf642c8c556b2ef378fac58 Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 15:55:18 +0200 Subject: [PATCH 04/47] added body dumping --- client.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client.go b/client.go index 0075c1e..9c9710e 100644 --- a/client.go +++ b/client.go @@ -188,6 +188,13 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re var HTTPErr = errors.New("http error") func (c *Client) Do(r *http.Request, v interface{}) (resp *http.Response, err error) { + if c.debugWriter != nil { + dumpReq, err := httputil.DumpRequestOut(r, true) + if err != nil { + return &http.Response{}, err + } + fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq) + } resp, err = c.httpClient.Do(r) if err != nil { return @@ -201,12 +208,6 @@ func (c *Client) Do(r *http.Request, v interface{}) (resp *http.Response, err er resp.Body = ioutil.NopCloser(bytes.NewReader(body)) if c.debugWriter != nil { - dumpReq, err := httputil.DumpRequest(r, true) - if err != nil { - return resp, err - } - fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq) - dumpResp, err := httputil.DumpResponse(resp, true) if err != nil { return resp, err From 9ed72b6a4857ad07288761035a83c84eee717328 Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 16:08:04 +0200 Subject: [PATCH 05/47] fixed encoding --- userdata.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/userdata.go b/userdata.go index 59a082b..aea4d39 100644 --- a/userdata.go +++ b/userdata.go @@ -52,7 +52,7 @@ func (u *UserData) String() (s string, err error) { if err != nil { return } - s = base64.StdEncoding.EncodeToString(b) + s = url.QueryEscape(base64.StdEncoding.EncodeToString(b)) return } From 3789178234ca63319184a072f05c98e9627734e1 Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 16:11:25 +0200 Subject: [PATCH 06/47] removed changes --- helper.go | 5 +---- server.go | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/helper.go b/helper.go index c184315..878c82d 100644 --- a/helper.go +++ b/helper.go @@ -5,7 +5,6 @@ package pve import ( "encoding/json" "errors" - //"fmt" "io" "net/http" "net/url" @@ -42,7 +41,5 @@ func (b httpbody) Reader() io.Reader { for k, v := range b { data.Set(k, v) } - enc := data.Encode() - //fmt.Println("--- encoded:", enc) - return strings.NewReader(enc) + return strings.NewReader(data.Encode()) } diff --git a/server.go b/server.go index dbf5e65..c1a1590 100644 --- a/server.go +++ b/server.go @@ -32,7 +32,6 @@ func (s *Server) body() (httpbody, error) { "searchdomain": s.SearchDomain, } userdata, err := s.UserData.String() - //fmt.Println("--- plain:", userdata) body["ciuserdata"] = userdata for i, n := range s.NetDevices { body["net"+strconv.Itoa(i)] = n.String() From 163f11c3979a75324224185ff1131a3bc58cf1ab Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 16:35:35 +0200 Subject: [PATCH 07/47] make configs settable --- ipconfig.go | 16 ++++++++++++++++ network.go | 16 ++++++++++++++++ server.go | 8 ++++++++ 3 files changed, 40 insertions(+) diff --git a/ipconfig.go b/ipconfig.go index da6a2a8..6459faf 100644 --- a/ipconfig.go +++ b/ipconfig.go @@ -8,6 +8,22 @@ import ( type IPConfigs []*IPConfig +func (c *IPConfigs) Set(index int, ipc *IPConfig) { + l := len(*c) + if index >= l { + n := make(IPConfigs, index+1) + copy(n, *c) + n[index] = ipc + *c = n + return + } + if l == 0 { + *c = append(*c, ipc) + return + } + (*c)[index] = ipc +} + func (c IPConfigs) Get(index int) *IPConfig { if len(c)-1 >= index { return c[index] diff --git a/network.go b/network.go index 1e55ece..1ec084c 100644 --- a/network.go +++ b/network.go @@ -9,6 +9,22 @@ import ( type NetworkDevices []*NetworkDevice +func (c *NetworkDevices) Set(index int, d *NetworkDevice) { + l := len(*c) + if index >= l { + n := make(NetworkDevices, index+1) + copy(n, *c) + n[index] = d + *c = n + return + } + if l == 0 { + *c = append(*c, d) + return + } + (*c)[index] = d +} + func (c NetworkDevices) Get(index int) *NetworkDevice { if len(c)-1 >= index { return c[index] diff --git a/server.go b/server.go index c1a1590..c8423fc 100644 --- a/server.go +++ b/server.go @@ -34,9 +34,17 @@ func (s *Server) body() (httpbody, error) { userdata, err := s.UserData.String() body["ciuserdata"] = userdata for i, n := range s.NetDevices { + if n == nil { + body["net"+strconv.Itoa(i)] = "" + continue + } body["net"+strconv.Itoa(i)] = n.String() } for i, ip := range s.IPConfig { + if ip == nil { + body["ipconfig"+strconv.Itoa(i)] = "" + continue + } body["ipconfig"+strconv.Itoa(i)] = ip.String() } return body, err From fb1d8d26473723bfd09283b76e816bf1716f4c99 Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 16:37:50 +0200 Subject: [PATCH 08/47] added pointer receiver --- ipconfig.go | 6 +++--- network.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ipconfig.go b/ipconfig.go index 6459faf..6e77195 100644 --- a/ipconfig.go +++ b/ipconfig.go @@ -24,9 +24,9 @@ func (c *IPConfigs) Set(index int, ipc *IPConfig) { (*c)[index] = ipc } -func (c IPConfigs) Get(index int) *IPConfig { - if len(c)-1 >= index { - return c[index] +func (c *IPConfigs) Get(index int) *IPConfig { + if len(*c)-1 >= index { + return (*c)[index] } return nil } diff --git a/network.go b/network.go index 1ec084c..0828578 100644 --- a/network.go +++ b/network.go @@ -25,9 +25,9 @@ func (c *NetworkDevices) Set(index int, d *NetworkDevice) { (*c)[index] = d } -func (c NetworkDevices) Get(index int) *NetworkDevice { - if len(c)-1 >= index { - return c[index] +func (c *NetworkDevices) Get(index int) *NetworkDevice { + if len(*c)-1 >= index { + return (*c)[index] } return nil } From 20ac152a2b7dbcf9d16734df025bde483cdcc432 Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 16:55:54 +0200 Subject: [PATCH 09/47] fixed generated cloud config --- userdata.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/userdata.go b/userdata.go index aea4d39..9fad585 100644 --- a/userdata.go +++ b/userdata.go @@ -8,6 +8,8 @@ import ( "net/url" ) +var cloudConfig = []byte("#cloud-config\n") + func parseUserData(s string) (u *UserData, err error) { s, err = url.QueryUnescape(s) if err != nil { @@ -29,7 +31,11 @@ func MarshalUserData(u *UserData) ([]byte, error) { if u == nil { return nil, nil } - return yaml.Marshal(u) + b, err := yaml.Marshal(u) + if err == nil { + b = append(cloudConfig, b...) + } + return b, err } type UserData struct { From 7f405ea6e5bbe0946ec38f7f45de0c6d34778848 Mon Sep 17 00:00:00 2001 From: ston1th Date: Mon, 1 Jun 2020 22:32:18 +0200 Subject: [PATCH 10/47] added empty check --- server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server.go b/server.go index c8423fc..301beaf 100644 --- a/server.go +++ b/server.go @@ -78,6 +78,9 @@ func (n Nameserver) String() string { type serverConfig map[string]interface{} func (sc serverConfig) Server(node, id string) (s *Server, err error) { + if len(sc) == 0 { + return + } s = &Server{ID: id, Node: node} for k := range sc { From 6d4e4c31f33043d073140de15545336897504017 Mon Sep 17 00:00:00 2001 From: ston1th Date: Tue, 2 Jun 2020 00:29:10 +0200 Subject: [PATCH 11/47] added failsafe --- node.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/node.go b/node.go index 9da1138..70ac72c 100644 --- a/node.go +++ b/node.go @@ -8,7 +8,11 @@ import ( "fmt" ) -var ErrServerNotFound = errors.New("server not found") +var ( + ErrServerNotFound = errors.New("server not found") + ErrNodesOffline = errors.New("one or more nodes are offline") + ErrNodesNotSearched = errors.New("one or more nodes could not be searched") +) type NodeStatus string @@ -59,12 +63,16 @@ func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *Server if err != nil { return } + nodeOffline := false + nodeNotSearched := false for _, node := range nl { if node.Status != NodeStatusOnline { + nodeOffline = true continue } sl, err := c.ListServers(ctx, node) if err != nil { + nodeNotSearched = true continue } for _, s := range sl { @@ -76,6 +84,12 @@ func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *Server } } } + if nodeOffline { + return nil, ErrNodesOffline + } + if nodeNotSearched { + return nil, ErrNodesNotSearched + } return nil, ErrServerNotFound } From 249d7c6844e5d25e40ff12959e3a1f7b05a4bca1 Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 3 Jun 2020 23:10:22 +0200 Subject: [PATCH 12/47] better pool and changed schema --- node.go | 5 +---- pool.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ scheme.go | 6 +++--- server.go | 45 +++++++++++++++++++++++++++++++-------------- 4 files changed, 82 insertions(+), 21 deletions(-) diff --git a/node.go b/node.go index 70ac72c..7863d90 100644 --- a/node.go +++ b/node.go @@ -9,7 +9,6 @@ import ( ) var ( - ErrServerNotFound = errors.New("server not found") ErrNodesOffline = errors.New("one or more nodes are offline") ErrNodesNotSearched = errors.New("one or more nodes could not be searched") ) @@ -29,8 +28,6 @@ type Node struct { type NodeList []*Node -type ServerList []*Server - type NodeClient struct { client *Client } @@ -106,5 +103,5 @@ func (c *NodeClient) FindServerByURL(ctx context.Context, url string) (s *Server if err != nil { return } - return c.FindServer(ctx, "", id) + return c.FindServerByID(ctx, id) } diff --git a/pool.go b/pool.go index e675d1d..f42d636 100644 --- a/pool.go +++ b/pool.go @@ -72,3 +72,50 @@ func (c *PoolClient) Delete(ctx context.Context, name string) (err error) { _, err = c.client.Do(req, nil) return } + +type pool struct { + members ServerList `json:"members"` +} + +func (c *PoolClient) List(ctx context.Context, name string) (sl ServerList, err error) { + var p pool + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/pools/%s", name), nil) + if err != nil { + return + } + _, err = c.client.Do(req, &p) + sl = p.members + return +} + +func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id string) (s *Server, err error) { + sl, err := c.List(ctx, poolname) + if err != nil { + return + } + for _, srv := range sl { + if id != "" && srv.ID == id { + return srv, nil + } + if servername != "" && srv.Name == servername { + return srv, nil + } + } + return nil, ErrServerNotFound +} + +func (c *PoolClient) FindServerByName(ctx context.Context, poolname, servername string) (s *Server, err error) { + return c.FindServer(ctx, poolname, servername, "") +} + +func (c *PoolClient) FindServerByID(ctx context.Context, poolname, id string) (s *Server, err error) { + return c.FindServer(ctx, poolname, "", id) +} + +func (c *PoolClient) FindServerByURL(ctx context.Context, url string) (s *Server, err error) { + pool, id, err := ParseURL(url) + if err != nil { + return + } + return c.FindServerByID(ctx, pool, id) +} diff --git a/scheme.go b/scheme.go index 57d19fc..2f5b2a9 100644 --- a/scheme.go +++ b/scheme.go @@ -13,7 +13,7 @@ const PVEScheme = "pve" var ErrInvalidPVEURL = errors.New("invalid pve url scheme") -func ParseURL(s string) (node, id string, err error) { +func ParseURL(s string) (pool, id string, err error) { u, err := url.Parse(s) if err != nil { return @@ -25,6 +25,6 @@ func ParseURL(s string) (node, id string, err error) { return u.Host, strings.TrimPrefix(u.Path, "/"), nil } -func NewURL(node, id string) string { - return fmt.Sprintf("%s://%s/%s", PVEScheme, node, id) +func NewURL(pool, id string) string { + return fmt.Sprintf("%s://%s/%s", PVEScheme, pool, id) } diff --git a/server.go b/server.go index 301beaf..77e33d4 100644 --- a/server.go +++ b/server.go @@ -10,6 +10,10 @@ import ( "strings" ) +var ErrServerNotFound = errors.New("server not found") + +type ServerList []*Server + type Server struct { ID string `json:"vmid"` Name string `json:"name"` @@ -180,11 +184,26 @@ func (c *ServerClient) getServer(ctx context.Context, node, id string) (s *Serve } func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err error) { - node, id, err := ParseURL(url) + pool, id, err := ParseURL(url) if err != nil { return } - s, cfg, err := c.getServer(ctx, node, id) + var srv *Server + if pool != "" { + srv, err = c.client.Pool.FindServerByID(ctx, pool, id) + if err == ErrServerNotFound { + return + } + } + srv, err = c.client.Node.FindServerByID(ctx, id) + if err != nil { + return + } + if srv == nil { + err = ErrServerNotFound + return + } + s, cfg, err := c.getServer(ctx, srv.Node, srv.ID) if err != nil { return } @@ -200,7 +219,8 @@ func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err type ServerTemplateOpts struct { Name string - TemplateURL string + TemplateID string + TemplateNode string Pool string TargetStorage string TargetNode string @@ -210,8 +230,11 @@ func (o ServerTemplateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } - if o.TemplateURL == "" { - return errors.New("missing template url") + if o.TemplateID == "" { + return errors.New("missing template id") + } + if o.TemplateNode == "" { + return errors.New("missing template node") } return nil } @@ -233,23 +256,17 @@ func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts ServerTempla 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 opts.TargetNode != "" && tempnode != opts.TargetNode { + if opts.TargetNode != "" && opts.TemplateNode != opts.TargetNode { body["target"] = opts.TargetNode - url = NewURL(opts.TargetNode, nextid) - } else { - url = NewURL(tempnode, nextid) } - req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%s/clone", tempnode, id), body.Reader()) + url = NewURL(opts.Pool, 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 } From 0994baf5b08ed7b4c718042cc924eef2858fc683 Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 3 Jun 2020 23:45:54 +0200 Subject: [PATCH 13/47] extended URL scheme --- node.go | 7 ++++++- pool.go | 7 ++++++- scheme.go | 22 +++++++++++++++++----- server.go | 21 ++++++++++----------- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/node.go b/node.go index 7863d90..f58bc1e 100644 --- a/node.go +++ b/node.go @@ -11,6 +11,7 @@ import ( var ( ErrNodesOffline = errors.New("one or more nodes are offline") ErrNodesNotSearched = errors.New("one or more nodes could not be searched") + ErrEmptyID = errors.New("id is empty") ) type NodeStatus string @@ -95,11 +96,15 @@ func (c *NodeClient) FindServerByName(ctx context.Context, name string) (s *Serv } func (c *NodeClient) FindServerByID(ctx context.Context, id string) (s *Server, err error) { + if id == "" { + err = ErrEmptyID + return + } return c.FindServer(ctx, "", id) } func (c *NodeClient) FindServerByURL(ctx context.Context, url string) (s *Server, err error) { - _, id, err := ParseURL(url) + _, _, id, err := ParseURL(url) if err != nil { return } diff --git a/pool.go b/pool.go index f42d636..fbd0680 100644 --- a/pool.go +++ b/pool.go @@ -12,6 +12,7 @@ import ( var ( ErrPoolExists = errors.New("pool already exists") ErrPoolNotExists = errors.New("pool does not exist") + ErrEmptyPoolName = errors.New("pool name is empty") ) func MakePoolName(namespace, name string) string { @@ -78,6 +79,10 @@ type pool struct { } func (c *PoolClient) List(ctx context.Context, name string) (sl ServerList, err error) { + if name == "" { + err = ErrEmptyPoolName + return + } var p pool req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/pools/%s", name), nil) if err != nil { @@ -113,7 +118,7 @@ func (c *PoolClient) FindServerByID(ctx context.Context, poolname, id string) (s } func (c *PoolClient) FindServerByURL(ctx context.Context, url string) (s *Server, err error) { - pool, id, err := ParseURL(url) + pool, _, id, err := ParseURL(url) if err != nil { return } diff --git a/scheme.go b/scheme.go index 2f5b2a9..e980787 100644 --- a/scheme.go +++ b/scheme.go @@ -11,9 +11,12 @@ import ( const PVEScheme = "pve" -var ErrInvalidPVEURL = errors.New("invalid pve url scheme") +var ( + ErrInvalidPVEURL = errors.New("invalid pve url scheme") + ErrNoID = errors.New("missing id in url") +) -func ParseURL(s string) (pool, id string, err error) { +func ParseURL(s string) (pool, node, id string, err error) { u, err := url.Parse(s) if err != nil { return @@ -22,9 +25,18 @@ func ParseURL(s string) (pool, id string, err error) { err = ErrInvalidPVEURL return } - return u.Host, strings.TrimPrefix(u.Path, "/"), nil + a := strings.Split(u.Path, "/") + if len(a) != 3 { + err = ErrInvalidPVEURL + return + } + if a[2] == "" { + err = ErrNoID + return + } + return u.Host, a[1], a[2], nil } -func NewURL(pool, id string) string { - return fmt.Sprintf("%s://%s/%s", PVEScheme, pool, id) +func NewURL(pool, node, id string) string { + return fmt.Sprintf("%s://%s/%s/%s", PVEScheme, pool, node, id) } diff --git a/server.go b/server.go index 77e33d4..23f9516 100644 --- a/server.go +++ b/server.go @@ -184,26 +184,25 @@ func (c *ServerClient) getServer(ctx context.Context, node, id string) (s *Serve } func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err error) { - pool, id, err := ParseURL(url) + pool, node, id, err := ParseURL(url) if err != nil { return } var srv *Server - if pool != "" { + if node == "" && pool != "" { srv, err = c.client.Pool.FindServerByID(ctx, pool, id) if err == ErrServerNotFound { return } + node = srv.Node + } else if node == "" && pool == "" { + srv, err = c.client.Node.FindServerByID(ctx, id) + if err != nil { + return + } + node = srv.Node } - srv, err = c.client.Node.FindServerByID(ctx, id) - if err != nil { - return - } - if srv == nil { - err = ErrServerNotFound - return - } - s, cfg, err := c.getServer(ctx, srv.Node, srv.ID) + s, cfg, err := c.getServer(ctx, node, id) if err != nil { return } From 07f48d43695992bf99f40a32405a840dcbf70695 Mon Sep 17 00:00:00 2001 From: ston1th Date: Thu, 4 Jun 2020 20:24:46 +0200 Subject: [PATCH 14/47] some fixes --- server.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server.go b/server.go index 23f9516..c17a66f 100644 --- a/server.go +++ b/server.go @@ -55,11 +55,11 @@ func (s *Server) body() (httpbody, error) { } func (s *Server) InstanceID() string { - return s.String() + return NewURL("", s.Node, s.ID) } -func (s *Server) String() string { - return NewURL(s.Node, s.ID) +func (s *Server) K8sID(pool string) string { + return NewURL(pool, "", s.ID) } type Resources struct { @@ -166,7 +166,7 @@ func (c *ServerClient) NextID(ctx context.Context) (id string, err error) { 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) + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%s/config?current=1", node, id), nil) if err != nil { return } @@ -261,10 +261,12 @@ func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts ServerTempla } 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, nextid) + 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 From 59ad9d8ec5e2461001a7490ec96dda84ce574f14 Mon Sep 17 00:00:00 2001 From: ston1th Date: Thu, 4 Jun 2020 21:37:23 +0200 Subject: [PATCH 15/47] added pool finder --- pool.go | 53 +++++++++++++++++++++++++++++++++++++++++++---------- scheme.go | 8 ++++++++ server.go | 5 +++-- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/pool.go b/pool.go index fbd0680..2c72834 100644 --- a/pool.go +++ b/pool.go @@ -74,11 +74,31 @@ func (c *PoolClient) Delete(ctx context.Context, name string) (err error) { return } +type pools struct { + poolid string `json:"poolid"` +} + +func (c *PoolClient) ListPools(ctx context.Context) (list []string, err error) { + var ps []pools + req, err := c.client.NewRequest(ctx, "GET", "/pools", nil) + if err != nil { + return + } + _, err = c.client.Do(req, &ps) + if err != nil { + return + } + for _, p := range ps { + list = append(list, p.poolid) + } + return +} + type pool struct { members ServerList `json:"members"` } -func (c *PoolClient) List(ctx context.Context, name string) (sl ServerList, err error) { +func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerList, err error) { if name == "" { err = ErrEmptyPoolName return @@ -94,16 +114,29 @@ func (c *PoolClient) List(ctx context.Context, name string) (sl ServerList, err } func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id string) (s *Server, err error) { - sl, err := c.List(ctx, poolname) - if err != nil { - return - } - for _, srv := range sl { - if id != "" && srv.ID == id { - return srv, nil + var pools []string + if poolname != "" { + pools = append(pools, poolname) + } else { + pools, err = c.ListPools(ctx) + if err != nil { + return } - if servername != "" && srv.Name == servername { - return srv, nil + } + for _, pool := range pools { + sl, err := c.ListMembers(ctx, pool) + if err != nil { + return + } + for _, srv := range sl { + if id != "" && srv.ID == id { + srv.Pool = pool + return srv, nil + } + if servername != "" && srv.Name == servername { + srv.Pool = pool + return srv, nil + } } } return nil, ErrServerNotFound diff --git a/scheme.go b/scheme.go index e980787..a9f03e5 100644 --- a/scheme.go +++ b/scheme.go @@ -40,3 +40,11 @@ func ParseURL(s string) (pool, node, id string, err error) { func NewURL(pool, node, id string) string { return fmt.Sprintf("%s://%s/%s/%s", PVEScheme, pool, node, id) } + +func K8sURL(url string) (string, error) { + pool, _, id, err := ParseURL(url) + if err != nil { + return "", nil + } + return NewURL(pool, "", id), nil +} diff --git a/server.go b/server.go index c17a66f..dff583e 100644 --- a/server.go +++ b/server.go @@ -18,6 +18,7 @@ type Server struct { ID string `json:"vmid"` Name string `json:"name"` Node string + Pool string Status ServerStatus `json:"status"` Resources Resources NetDevices NetworkDevices @@ -58,8 +59,8 @@ func (s *Server) InstanceID() string { return NewURL("", s.Node, s.ID) } -func (s *Server) K8sID(pool string) string { - return NewURL(pool, "", s.ID) +func (s *Server) K8sID() string { + return NewURL(s.Pool, "", s.ID) } type Resources struct { From 0dead7e42330ca753ae0b6a6c4639bcd9c7f2b98 Mon Sep 17 00:00:00 2001 From: ston1th Date: Thu, 4 Jun 2020 21:38:24 +0200 Subject: [PATCH 16/47] fixed typo --- pool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pool.go b/pool.go index 2c72834..cb7b50d 100644 --- a/pool.go +++ b/pool.go @@ -126,7 +126,7 @@ func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id st for _, pool := range pools { sl, err := c.ListMembers(ctx, pool) if err != nil { - return + return nil, err } for _, srv := range sl { if id != "" && srv.ID == id { From 4c7e511a72be79bf40d235d02a122d6f28f9c922 Mon Sep 17 00:00:00 2001 From: ston1th Date: Thu, 4 Jun 2020 22:40:38 +0200 Subject: [PATCH 17/47] fixed pool search --- .gitignore | 1 + pool.go | 28 ++++++++++++++++++++++++---- server.go | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa6c60a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cmd diff --git a/pool.go b/pool.go index cb7b50d..3f3b809 100644 --- a/pool.go +++ b/pool.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/http" + "strconv" ) var ( @@ -75,7 +76,7 @@ func (c *PoolClient) Delete(ctx context.Context, name string) (err error) { } type pools struct { - poolid string `json:"poolid"` + PoolID string `json:"poolid"` } func (c *PoolClient) ListPools(ctx context.Context) (list []string, err error) { @@ -89,13 +90,32 @@ func (c *PoolClient) ListPools(ctx context.Context) (list []string, err error) { return } for _, p := range ps { - list = append(list, p.poolid) + list = append(list, p.PoolID) + } + return +} + +type poolMember struct { + ID int `json:"vmid"` + Name string `json:"name"` + Node string `json:"node"` +} + +type poolMembers []poolMember + +func (pms poolMembers) ServerList() (sl ServerList) { + for _, pm := range pms { + sl = append(sl, &Server{ + ID: strconv.Itoa(pm.ID), + Name: pm.Name, + Node: pm.Node, + }) } return } type pool struct { - members ServerList `json:"members"` + Members poolMembers `json:"members"` } func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerList, err error) { @@ -109,7 +129,7 @@ func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerLis return } _, err = c.client.Do(req, &p) - sl = p.members + sl = p.Members.ServerList() return } diff --git a/server.go b/server.go index dff583e..fb42968 100644 --- a/server.go +++ b/server.go @@ -192,7 +192,7 @@ func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err var srv *Server if node == "" && pool != "" { srv, err = c.client.Pool.FindServerByID(ctx, pool, id) - if err == ErrServerNotFound { + if err != nil { return } node = srv.Node From 4edb93b58105b500bf464f625446cfd6f415b59d Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 10 Jun 2020 00:35:20 +0200 Subject: [PATCH 18/47] added initial node scheduling --- node.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/node.go b/node.go index f58bc1e..e6b05a5 100644 --- a/node.go +++ b/node.go @@ -23,16 +23,137 @@ const ( ) type Node struct { - Name string `json:"node"` - Status NodeStatus `json:"status"` + Name string `json:"node"` + Status NodeStatus `json:"status"` + CPU float64 `json:"cpu"` + MaxCPU float64 `json:"maxcpu"` + Mem int64 `json:"mem"` + MaxMem int64 `json:"maxmem"` + Disk int64 `json:"disk"` + MaxDisk int64 `json:"maxdisk"` + ServerList ServerList `json:"-"` +} + +func (n *Node) MemFreePercent() float64 { + return float64(n.Mem) / float64(n.MaxMem) * 100 } type NodeList []*Node +func (nl NodeList) sortByFreeMem() { + sort.Sort(memSorter(nl)) +} + +func (nl *NodeList) remove(i int) { + (*nl)[i] = (*nl)[len(*nl)-1] + *nl = (*nl)[:len(*nl)-1] +} + +func (nl *NodeList) Filter(filters ...NodeFilter) { + for _, f := range filters { + f(nl) + } +} + +func (nl *NodeList) GetServerList(ctx context.Context, c *NodeClient) (err error) { + for _, n := range *nl { + n.ServerList, err = c.ListServers(ctx, n) + if err != nil { + return + } + } +} + +type memSorter NodeList + +func (m memSorter) Len() int { return len(m) } +func (m memSorter) Less(i, j int) bool { return m[i].MemFreePercent() < m[j].MemFreePercent() } +func (m memSorter) Swap(i, j int) { m[i], m[j] = m[j], m[i] } + +type NodeFilter func(*NodeList) + +func FilterOnlineNode() NodeFilter { + return func(nl *NodeList) { + for i, n := range *nl { + if n.Status != NodeStatusOnline { + nl.remove(i) + } + } + } +} + +func FilterNodeName(name string) NodeFilter { + return func(nl *NodeList) { + for i, n := range *nl { + if n.Name == name { + nl.remove(i) + return + } + } + } +} + +func FilterMaxCores(cores int64) NodeFilter { + return func(nl *NodeList) { + for i, n := range *nl { + if cores > n.MaxCPU { + nl.remove(i) + } + } + } +} + +const ( + mib = 1024 * 1024 + gib = mib * 1024 +) + +func FilterFreeMem(mem int64) NodeFilter { + return func(nl *NodeList) { + for i, n := range *nl { + if (mem * gib) >= n.MaxMem-n.Mem { + nl.remove(i) + } + } + } +} + +// Useful? only local disk? +func FilterFreeDisk(disk int64) NodeFilter { + return func(nl *NodeList) { + for i, n := range *nl { + if (disk * gib) >= n.MaxDisk-n.Disk { + nl.remove(i) + } + } + } +} + +func FilterVMAntiAffinity(url string) NodeFilter { + return func(nl *NodeList) { + _, _, id, err := ParseURL(url) + if err != nil { + return + } + for i, n := range *nl { + for _, s := range n.ServerList { + if s.ID == id { + nl.remove(i) + return + } + } + } + } +} + type NodeClient struct { client *Client } +func (c *NodeClient) Schedule(ctx context.Context) (n *Node, err error) { + return +} + func (c *NodeClient) List(ctx context.Context) (nl NodeList, err error) { req, err := c.client.NewRequest(ctx, "GET", "/nodes", nil) if err != nil { From 4158f7f9464d7af86610c4558bf7d50c999fefbc Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 10 Jun 2020 01:22:35 +0200 Subject: [PATCH 19/47] added first scheduler --- node.go | 52 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/node.go b/node.go index e6b05a5..7a8f89e 100644 --- a/node.go +++ b/node.go @@ -12,6 +12,7 @@ var ( ErrNodesOffline = errors.New("one or more nodes are offline") ErrNodesNotSearched = errors.New("one or more nodes could not be searched") ErrEmptyID = errors.New("id is empty") + ErrUnschedulable = errors.New("no schedulable node found") ) type NodeStatus string @@ -31,17 +32,25 @@ type Node struct { MaxMem int64 `json:"maxmem"` Disk int64 `json:"disk"` MaxDisk int64 `json:"maxdisk"` - ServerList ServerList `json:"-"` + serverList ServerList `json:"-"` } -func (n *Node) MemFreePercent() float64 { +func (n *Node) memFreePercent() float64 { return float64(n.Mem) / float64(n.MaxMem) * 100 } +func (n *Node) memUsedPercent() float64 { + return float64(n.MaxMem-n.Mem) / float64(n.MaxMem) * 100 +} + +func (n *Node) weight() int { + return int(n.memUsedPercent()) + (len(n.serverList) * 10) +} + type NodeList []*Node -func (nl NodeList) sortByFreeMem() { - sort.Sort(memSorter(nl)) +func (nl NodeList) sortByWeight() { + sort.Sort(weightSorter(nl)) } func (nl *NodeList) remove(i int) { @@ -49,26 +58,28 @@ func (nl *NodeList) remove(i int) { *nl = (*nl)[:len(*nl)-1] } -func (nl *NodeList) Filter(filters ...NodeFilter) { +func (nl *NodeList) filter(filters ...NodeFilter) { for _, f := range filters { f(nl) } } -func (nl *NodeList) GetServerList(ctx context.Context, c *NodeClient) (err error) { +func (nl *NodeList) getServerList(ctx context.Context, c *NodeClient) (err error) { for _, n := range *nl { - n.ServerList, err = c.ListServers(ctx, n) + n.serverList, err = c.ListServers(ctx, n) if err != nil { return } } } -type memSorter NodeList +type weightSorter NodeList -func (m memSorter) Len() int { return len(m) } -func (m memSorter) Less(i, j int) bool { return m[i].MemFreePercent() < m[j].MemFreePercent() } -func (m memSorter) Swap(i, j int) { m[i], m[j] = m[j], m[i] } +func (s weightSorter) Len() int { return len(s) } +func (s weightSorter) Less(i, j int) bool { + return s.weight() < s[j].weight() +} +func (s weightSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } type NodeFilter func(*NodeList) @@ -136,7 +147,7 @@ func FilterVMAntiAffinity(url string) NodeFilter { return } for i, n := range *nl { - for _, s := range n.ServerList { + for _, s := range n.serverList { if s.ID == id { nl.remove(i) return @@ -150,7 +161,22 @@ type NodeClient struct { client *Client } -func (c *NodeClient) Schedule(ctx context.Context) (n *Node, err error) { +func (c *NodeClient) Schedule(ctx context.Context, filters ...NodeFilter) (n *Node, err error) { + nl, err := c.List(ctx) + if err != nil { + return + } + err = nl.getServerList(ctx, c) + if err != nil { + return + } + nl.filter(filters) + nl.sortByWeight() + if len(nl) == 0 { + err = ErrUnschedulable + return + } + n = nl[0] return } From 349afa60e135842672942bfa5e9a6ecff08b5938 Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 10 Jun 2020 01:25:17 +0200 Subject: [PATCH 20/47] fixed typos --- node.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node.go b/node.go index 7a8f89e..9e24db0 100644 --- a/node.go +++ b/node.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "sort" ) var ( @@ -71,13 +72,14 @@ func (nl *NodeList) getServerList(ctx context.Context, c *NodeClient) (err error return } } + return } type weightSorter NodeList func (s weightSorter) Len() int { return len(s) } func (s weightSorter) Less(i, j int) bool { - return s.weight() < s[j].weight() + return s[i].weight() < s[j].weight() } func (s weightSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } @@ -107,7 +109,7 @@ func FilterNodeName(name string) NodeFilter { func FilterMaxCores(cores int64) NodeFilter { return func(nl *NodeList) { for i, n := range *nl { - if cores > n.MaxCPU { + if cores > int64(n.MaxCPU) { nl.remove(i) } } @@ -170,7 +172,7 @@ func (c *NodeClient) Schedule(ctx context.Context, filters ...NodeFilter) (n *No if err != nil { return } - nl.filter(filters) + nl.filter(filters...) nl.sortByWeight() if len(nl) == 0 { err = ErrUnschedulable From 861ca4edf7794ee80a1e62ca5cc46de2793108a5 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 11:58:48 +0100 Subject: [PATCH 21/47] added snippet upload --- README.md | 2 +- client.go | 10 +++-- server.go | 116 ++++++++++++++++++++++++++++++--------------------- snippet.diff | 36 ++++++++++++++++ snippet.go | 103 +++++++++++++++++++++++++++++++++++++++++++++ task.go | 7 ++++ userdata.go | 82 +++++++++++++++++++++++++----------- 7 files changed, 278 insertions(+), 78 deletions(-) create mode 100644 snippet.diff create mode 100644 snippet.go diff --git a/README.md b/README.md index 43178b3..899bf47 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 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 diff --git a/client.go b/client.go index 9c9710e..21e863e 100644 --- a/client.go +++ b/client.go @@ -46,10 +46,11 @@ type Client struct { debugWriter io.Writer insecure bool - Server ServerClient - Task TaskClient - Pool PoolClient - Node NodeClient + Server ServerClient + Task TaskClient + Pool PoolClient + Node NodeClient + Snippet SnippetClient } type session struct { @@ -133,6 +134,7 @@ func NewClient(options ...ClientOption) *Client { client.Task = TaskClient{client: client} client.Pool = PoolClient{client: client} client.Node = NodeClient{client: client} + client.Snippet = SnippetClient{client: client} return client } diff --git a/server.go b/server.go index fb42968..beedb94 100644 --- a/server.go +++ b/server.go @@ -15,44 +15,56 @@ var ErrServerNotFound = errors.New("server not found") type ServerList []*Server 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 + 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) body() (httpbody, error) { - body := httpbody{ +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": 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 + if s.UserData != nil || s.cicustom { + b["cicustom"] = s.UserDataSnippet(s.UserDataStorage) + } + // TODO userdata, err := s.UserData.String() for i, n := range s.NetDevices { if n == nil { - body["net"+strconv.Itoa(i)] = "" + b["net"+strconv.Itoa(i)] = "" continue } - body["net"+strconv.Itoa(i)] = n.String() + b["net"+strconv.Itoa(i)] = n.String() } for i, ip := range s.IPConfig { if ip == nil { - body["ipconfig"+strconv.Itoa(i)] = "" + b["ipconfig"+strconv.Itoa(i)] = "" continue } - body["ipconfig"+strconv.Itoa(i)] = ip.String() + b["ipconfig"+strconv.Itoa(i)] = ip.String() } - return body, err + return } 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 { s.Name = v } - case "ciuserdata": - if v, ok := sc[k].(string); ok { - s.UserData, err = parseUserData(v) + case "cicustom": + if _, ok := sc[k].(string); ok { + s.cicustom = true } + // TODO + // 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) @@ -349,15 +366,9 @@ func (c *ServerClient) GetStatus(ctx context.Context, s *Server, lock *string) ( 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 - } +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) @@ -388,31 +399,40 @@ func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) { 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) if err != nil { return } - body, err := diffBody(cur, s) - 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)) + } } - var ( - cfg error - resize error - ) 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 { - 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) - } else if cfg != nil { - return fmt.Errorf("config: %s", cfg) - } else if resize != nil { - return fmt.Errorf("resize: %s", resize) + if len(e) > 0 { + err = fmt.Errorf("%s", strings.Join(e, "")) + } + if t == nil { + dummy := DummyTask + t = &dummy } return } diff --git a/snippet.diff b/snippet.diff new file mode 100644 index 0000000..c9e0080 --- /dev/null +++ b/snippet.diff @@ -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) = @_; diff --git a/snippet.go b/snippet.go new file mode 100644 index 0000000..be30c47 --- /dev/null +++ b/snippet.go @@ -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 +} diff --git a/task.go b/task.go index 89bfa14..3f1d00d 100644 --- a/task.go +++ b/task.go @@ -16,8 +16,12 @@ const ( TaskExitStatusOK = "OK" TaskStatusRunning = "running" TaskStatusStopped = "stopped" + + DummyTaskID = "dummy" ) +var DummyTask = Task{ID: DummyTaskID} + type Task struct { ID string `json:"upid"` 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) func (c *TaskClient) Wait(ctx context.Context, t *Task, f OnTaskChange) error { + if t.ID == DummyTaskID { + return nil + } if f != nil && t.Status != "" { f(t) } diff --git a/userdata.go b/userdata.go index 9fad585..1bf2dbb 100644 --- a/userdata.go +++ b/userdata.go @@ -3,40 +3,51 @@ package pve import ( - "encoding/base64" + //"encoding/base64" + //"net/url" "gopkg.in/yaml.v2" - "net/url" ) var cloudConfig = []byte("#cloud-config\n") -func parseUserData(s string) (u *UserData, err error) { - s, err = url.QueryUnescape(s) - if err != nil { - return - } - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return - } - return UnmarshalUserData(b) -} +const ( + minBytes = 87 + pad = "########################################################################\n" +) + +//TODO +//func parseUserData(s string) (u *UserData, err error) { +// s, err = url.QueryUnescape(s) +// if err != nil { +// return +// } +// b, err := base64.StdEncoding.DecodeString(s) +// if err != nil { +// return +// } +// return UnmarshalUserData(b) +//} func UnmarshalUserData(b []byte) (u *UserData, err error) { u = new(UserData) err = yaml.Unmarshal(b, u) return } -func MarshalUserData(u *UserData) ([]byte, error) { - if u == nil { - return nil, nil - } - b, err := yaml.Marshal(u) - if err == nil { - b = append(cloudConfig, b...) - } - return b, err -} + +//TODO +//func MarshalUserData(u *UserData) ([]byte, error) { +// if u == nil { +// return nil, nil +// } +// 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 b, err +//} type UserData struct { Hostname string `yaml:"hostname"` @@ -53,12 +64,33 @@ type UserData struct { 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) { - b, err := MarshalUserData(u) + b, err := u.Bytes() if err != nil { 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 } From 3d68cef8ba4d94823f8e491e3290e08cf6e92573 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 18:26:14 +0100 Subject: [PATCH 22/47] added resources --- server.go | 57 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/server.go b/server.go index beedb94..3e37662 100644 --- a/server.go +++ b/server.go @@ -40,8 +40,8 @@ func (s *Server) UserDataSnippet(storage string) string { func (s *Server) body() (b httpbody) { b = httpbody{ - "memory": strconv.FormatInt(s.Resources.Memory, 10), - "cores": strconv.FormatInt(s.Resources.Cores, 10), + "memory": s.Resources.Memory.String(), + "cores": s.Resources.Cores.String(), "name": s.Name, "nameserver": s.Nameserver.String(), "searchdomain": s.SearchDomain, @@ -76,10 +76,31 @@ func (s *Server) K8sID() string { } type Resources struct { - Memory int64 - Cores int64 - BootDisk string - BootDiskSize string + 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"` + Size uint64 `json:"size"` +} + +func (d Disk) String() string { + return strconv.FormatUint(d.Size, 10) + "G" } type Nameserver []string @@ -117,19 +138,27 @@ func (sc serverConfig) Server(node, id string) (s *Server, err error) { // } case "memory": if v, ok := sc[k].(float64); ok { - s.Resources.Memory = int64(v) + s.Resources.Memory = Memory(uint64(v) / 1024) } case "cores": if v, ok := sc[k].(float64); ok { - s.Resources.Cores = int64(v) + s.Resources.Cores = Cores(uint64(v)) } case "bootdisk": if v, ok := sc[k].(string); ok { - s.Resources.BootDisk = v + s.Resources.Disk.Name = v if val, ok := sc[v].(string); ok { - for _, o := range strings.Split(val, ",") { + 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=") { - s.Resources.BootDiskSize = o[5:] + size, _ := strconv.ParseUint(o[5:], 10, 64) + s.Resources.Disk.Size = size break } } @@ -388,8 +417,8 @@ func (c *ServerClient) updateConfig(ctx context.Context, s *Server, body httpbod func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) { body := httpbody{ - "disk": s.Resources.BootDisk, - "size": s.Resources.BootDiskSize, + "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 { @@ -421,7 +450,7 @@ func (c *ServerClient) Update(ctx context.Context, s *Server) (t *Task, err erro e = append(e, fmt.Sprintf("config: %s", cfg)) } } - if cur.Resources.BootDiskSize != s.Resources.BootDiskSize { + 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)) From aee5ce47a2b0ed358eb52dd946f873c942efe8f9 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 18:44:16 +0100 Subject: [PATCH 23/47] fixed types --- node.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/node.go b/node.go index 9e24db0..cef05b8 100644 --- a/node.go +++ b/node.go @@ -29,10 +29,10 @@ type Node struct { Status NodeStatus `json:"status"` CPU float64 `json:"cpu"` MaxCPU float64 `json:"maxcpu"` - Mem int64 `json:"mem"` - MaxMem int64 `json:"maxmem"` - Disk int64 `json:"disk"` - MaxDisk int64 `json:"maxdisk"` + Mem uint64 `json:"mem"` + MaxMem uint64 `json:"maxmem"` + Disk uint64 `json:"disk"` + MaxDisk uint64 `json:"maxdisk"` serverList ServerList `json:"-"` } @@ -106,10 +106,10 @@ func FilterNodeName(name string) NodeFilter { } } -func FilterMaxCores(cores int64) NodeFilter { +func FilterMaxCores(cores uint64) NodeFilter { return func(nl *NodeList) { for i, n := range *nl { - if cores > int64(n.MaxCPU) { + if cores > uint64(n.MaxCPU) { nl.remove(i) } } @@ -121,7 +121,7 @@ const ( gib = mib * 1024 ) -func FilterFreeMem(mem int64) NodeFilter { +func FilterFreeMem(mem uint64) NodeFilter { return func(nl *NodeList) { for i, n := range *nl { if (mem * gib) >= n.MaxMem-n.Mem { @@ -132,7 +132,7 @@ func FilterFreeMem(mem int64) NodeFilter { } // Useful? only local disk? -func FilterFreeDisk(disk int64) NodeFilter { +func FilterFreeDisk(disk uint64) NodeFilter { return func(nl *NodeList) { for i, n := range *nl { if (disk * gib) >= n.MaxDisk-n.Disk { From aa780c9d7e2bb582086cc80f0df9b602ed68055b Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 18:54:57 +0100 Subject: [PATCH 24/47] make diskname optional --- server.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server.go b/server.go index 3e37662..5f0c17a 100644 --- a/server.go +++ b/server.go @@ -94,9 +94,9 @@ func (m Memory) String() string { } type Disk struct { - Storage string `json:"storage"` - Name string `json:"name"` - Size uint64 `json:"size"` + Storage string `json:"storage"` + Name *string `json:"name,omitempty"` + Size uint64 `json:"size"` } func (d Disk) String() string { @@ -146,7 +146,7 @@ func (sc serverConfig) Server(node, id string) (s *Server, err error) { } case "bootdisk": if v, ok := sc[k].(string); ok { - s.Resources.Disk.Name = v + s.Resources.Disk.Name = &v if val, ok := sc[v].(string); ok { vals := strings.Split(val, ",") if len(vals) > 0 { @@ -416,8 +416,12 @@ func (c *ServerClient) updateConfig(ctx context.Context, s *Server, body httpbod } func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) { + diskName := "" + if s.Resources.Disk.Name != nil { + diskName = s.Resources.Disk.Name + } body := httpbody{ - "disk": s.Resources.Disk.Name, + "disk": diskName, "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()) From 6ef2b18c66ce13496ca26851e125de4acd96b599 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 18:56:31 +0100 Subject: [PATCH 25/47] make diskname optional --- server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.go b/server.go index 5f0c17a..7eda406 100644 --- a/server.go +++ b/server.go @@ -418,7 +418,7 @@ func (c *ServerClient) updateConfig(ctx context.Context, s *Server, body httpbod func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) { diskName := "" if s.Resources.Disk.Name != nil { - diskName = s.Resources.Disk.Name + diskName = *s.Resources.Disk.Name } body := httpbody{ "disk": diskName, From b4a538849ae3afc3ae9430e77e38c0956614e5b7 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 19:05:18 +0100 Subject: [PATCH 26/47] non pointer disk name --- server.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/server.go b/server.go index 7eda406..3fba1f8 100644 --- a/server.go +++ b/server.go @@ -94,9 +94,9 @@ func (m Memory) String() string { } type Disk struct { - Storage string `json:"storage"` - Name *string `json:"name,omitempty"` - Size uint64 `json:"size"` + Storage string `json:"storage"` + Name string `json:"name,omitempty"` + Size uint64 `json:"size"` } func (d Disk) String() string { @@ -146,7 +146,7 @@ func (sc serverConfig) Server(node, id string) (s *Server, err error) { } case "bootdisk": if v, ok := sc[k].(string); ok { - s.Resources.Disk.Name = &v + s.Resources.Disk.Name = v if val, ok := sc[v].(string); ok { vals := strings.Split(val, ",") if len(vals) > 0 { @@ -416,12 +416,8 @@ func (c *ServerClient) updateConfig(ctx context.Context, s *Server, body httpbod } func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) { - diskName := "" - if s.Resources.Disk.Name != nil { - diskName = *s.Resources.Disk.Name - } body := httpbody{ - "disk": diskName, + "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()) From 78911eca5697c1b790e19d916a7021abfaa61d85 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 21:29:19 +0100 Subject: [PATCH 27/47] added server ref --- node.go | 28 ++++++------- pool.go | 14 +++---- scheme.go | 13 ++++++ server.go | 122 +++++++++++++++++++++++++++++++++--------------------- 4 files changed, 108 insertions(+), 69 deletions(-) diff --git a/node.go b/node.go index cef05b8..7c78e76 100644 --- a/node.go +++ b/node.go @@ -25,15 +25,15 @@ const ( ) type Node struct { - Name string `json:"node"` - Status NodeStatus `json:"status"` - CPU float64 `json:"cpu"` - MaxCPU float64 `json:"maxcpu"` - Mem uint64 `json:"mem"` - MaxMem uint64 `json:"maxmem"` - Disk uint64 `json:"disk"` - MaxDisk uint64 `json:"maxdisk"` - serverList ServerList `json:"-"` + Name string `json:"node"` + Status NodeStatus `json:"status"` + CPU float64 `json:"cpu"` + MaxCPU float64 `json:"maxcpu"` + Mem uint64 `json:"mem"` + MaxMem uint64 `json:"maxmem"` + Disk uint64 `json:"disk"` + MaxDisk uint64 `json:"maxdisk"` + serverList ServerRefList `json:"-"` } func (n *Node) memFreePercent() float64 { @@ -191,7 +191,7 @@ func (c *NodeClient) List(ctx context.Context) (nl NodeList, err error) { return } -func (c *NodeClient) ListServers(ctx context.Context, n *Node) (sl ServerList, err error) { +func (c *NodeClient) ListServers(ctx context.Context, n *Node) (sl ServerRefList, err error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu", n.Name), nil) if err != nil { return @@ -205,7 +205,7 @@ func (c *NodeClient) ListServers(ctx context.Context, n *Node) (sl ServerList, e return } -func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *Server, err error) { +func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *ServerRef, err error) { nl, err := c.List(ctx) if err != nil { return @@ -240,11 +240,11 @@ func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *Server return nil, ErrServerNotFound } -func (c *NodeClient) FindServerByName(ctx context.Context, name string) (s *Server, err error) { +func (c *NodeClient) FindServerByName(ctx context.Context, name string) (s *ServerRef, err error) { return c.FindServer(ctx, name, "") } -func (c *NodeClient) FindServerByID(ctx context.Context, id string) (s *Server, err error) { +func (c *NodeClient) FindServerByID(ctx context.Context, id string) (s *ServerRef, err error) { if id == "" { err = ErrEmptyID return @@ -252,7 +252,7 @@ func (c *NodeClient) FindServerByID(ctx context.Context, id string) (s *Server, return c.FindServer(ctx, "", id) } -func (c *NodeClient) FindServerByURL(ctx context.Context, url string) (s *Server, err error) { +func (c *NodeClient) FindServerByURL(ctx context.Context, url string) (s *ServerRef, err error) { _, _, id, err := ParseURL(url) if err != nil { return diff --git a/pool.go b/pool.go index 3f3b809..57dffed 100644 --- a/pool.go +++ b/pool.go @@ -103,9 +103,9 @@ type poolMember struct { type poolMembers []poolMember -func (pms poolMembers) ServerList() (sl ServerList) { +func (pms poolMembers) ServerList() (sl ServerRefList) { for _, pm := range pms { - sl = append(sl, &Server{ + sl = append(sl, &ServerRef{ ID: strconv.Itoa(pm.ID), Name: pm.Name, Node: pm.Node, @@ -118,7 +118,7 @@ type pool struct { Members poolMembers `json:"members"` } -func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerList, err error) { +func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerRefList, err error) { if name == "" { err = ErrEmptyPoolName return @@ -133,7 +133,7 @@ func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerLis return } -func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id string) (s *Server, err error) { +func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id string) (s *ServerRef, err error) { var pools []string if poolname != "" { pools = append(pools, poolname) @@ -162,15 +162,15 @@ func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id st return nil, ErrServerNotFound } -func (c *PoolClient) FindServerByName(ctx context.Context, poolname, servername string) (s *Server, err error) { +func (c *PoolClient) FindServerByName(ctx context.Context, poolname, servername string) (s *ServerRef, err error) { return c.FindServer(ctx, poolname, servername, "") } -func (c *PoolClient) FindServerByID(ctx context.Context, poolname, id string) (s *Server, err error) { +func (c *PoolClient) FindServerByID(ctx context.Context, poolname, id string) (s *ServerRef, err error) { return c.FindServer(ctx, poolname, "", id) } -func (c *PoolClient) FindServerByURL(ctx context.Context, url string) (s *Server, err error) { +func (c *PoolClient) FindServerByURL(ctx context.Context, url string) (s *ServerRef, err error) { pool, _, id, err := ParseURL(url) if err != nil { return diff --git a/scheme.go b/scheme.go index a9f03e5..6ae3b89 100644 --- a/scheme.go +++ b/scheme.go @@ -37,6 +37,19 @@ func ParseURL(s string) (pool, node, id string, err error) { return u.Host, a[1], a[2], nil } +func ServerRefFromURL(s string) (ref *ServerRef, err error) { + pool, node, id, err := ParseURL(s) + if err != nil { + return + } + ref = &ServerRef{ + ID: id, + Node: node, + Pool: pool, + } + return +} + func NewURL(pool, node, id string) string { return fmt.Sprintf("%s://%s/%s/%s", PVEScheme, pool, node, id) } diff --git a/server.go b/server.go index 3fba1f8..a82d801 100644 --- a/server.go +++ b/server.go @@ -12,8 +12,6 @@ import ( var ErrServerNotFound = errors.New("server not found") -type ServerList []*Server - type Server struct { ID string `json:"vmid"` Name string `json:"name"` @@ -75,6 +73,32 @@ 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"` @@ -115,11 +139,11 @@ func (n Nameserver) String() string { type serverConfig map[string]interface{} -func (sc serverConfig) Server(node, id string) (s *Server, err error) { +func (sc serverConfig) Server(ref *ServerRef) (s *Server, err error) { if len(sc) == 0 { return } - s = &Server{ID: id, Node: node} + s = &Server{ID: ref.ID, Node: ref.Node} for k := range sc { switch k { @@ -211,9 +235,9 @@ func (c *ServerClient) NextID(ctx context.Context) (id string, err error) { return } -func (c *ServerClient) getConfig(ctx context.Context, node, id string) (cfg serverConfig, err error) { +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", node, id), nil) + 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 } @@ -221,35 +245,17 @@ func (c *ServerClient) getConfig(ctx context.Context, node, id string) (cfg serv return } -func (c *ServerClient) getServer(ctx context.Context, node, id string) (s *Server, cfg serverConfig, err error) { - cfg, err = c.getConfig(ctx, node, id) +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(node, id) + s, err = cfg.Server(ref) return } -func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err error) { - pool, node, id, err := ParseURL(url) - if err != nil { - return - } - var srv *Server - if node == "" && pool != "" { - srv, err = c.client.Pool.FindServerByID(ctx, pool, id) - if err != nil { - return - } - node = srv.Node - } else if node == "" && pool == "" { - srv, err = c.client.Node.FindServerByID(ctx, id) - if err != nil { - return - } - node = srv.Node - } - s, cfg, err := c.getServer(ctx, node, id) +func (c *ServerClient) GetByRef(ctx context.Context, ref *ServerRef) (s *Server, err error) { + s, cfg, err := c.getServer(ctx, ref) if err != nil { return } @@ -263,6 +269,26 @@ func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err 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 @@ -324,36 +350,36 @@ func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts ServerTempla return } -func (c *ServerClient) Start(ctx context.Context, s *Server) (t *Task, err error) { - return c.setStatus(ctx, s, "start") +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, s *Server) (t *Task, err error) { - return c.setStatus(ctx, s, "reboot") +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, s *Server) (t *Task, err error) { - return c.setStatus(ctx, s, "shutdown") +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, s *Server) (t *Task, err error) { - return c.setStatus(ctx, s, "reset") +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, s *Server) (t *Task, err error) { - return c.setStatus(ctx, s, "suspent") +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, s *Server) (t *Task, err error) { - return c.setStatus(ctx, s, "resume") +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, s *Server) (t *Task, err error) { - return c.setStatus(ctx, s, "stop") +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, 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) +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 } @@ -370,7 +396,7 @@ type statusObj struct { 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) + cfg, err := c.getConfig(ctx, s.Ref()) if err != nil { return err } @@ -429,7 +455,7 @@ func (c *ServerClient) resizeDisk(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.Ref()) if err != nil { return } @@ -468,7 +494,7 @@ func (c *ServerClient) Update(ctx context.Context, s *Server) (t *Task, err erro 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) + task, err := c.Stop(ctx, s.Ref()) if err != nil { return nil, err } From 3c9a99682ebb47542050f09eb85cbf310e7f2c48 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 25 Oct 2020 22:09:19 +0100 Subject: [PATCH 28/47] added search for template node --- server.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/server.go b/server.go index a82d801..466f1a9 100644 --- a/server.go +++ b/server.go @@ -290,15 +290,16 @@ func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err } type ServerTemplateOpts struct { - Name string - TemplateID string + Name string + TemplateID string + // TemplateNode is optional TemplateNode string Pool string TargetStorage string TargetNode string } -func (o ServerTemplateOpts) Validate() error { +func (o ServerTemplateOpts) Validate(ctx context.Context, c *Client) error { if o.Name == "" { return errors.New("missing name") } @@ -306,7 +307,14 @@ func (o ServerTemplateOpts) Validate() error { return errors.New("missing template id") } if o.TemplateNode == "" { - return errors.New("missing template node") + 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 } @@ -324,7 +332,7 @@ func (o ServerTemplateOpts) body() httpbody { } func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts ServerTemplateOpts) (t *Task, url string, err error) { - err = opts.Validate() + err = opts.Validate(ctx, c.client) if err != nil { return } From 93d99e29cc728100ec8eb290e2063330a5396e0d Mon Sep 17 00:00:00 2001 From: ston1th Date: Fri, 30 Oct 2020 19:32:47 +0100 Subject: [PATCH 29/47] fixed validation --- server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.go b/server.go index 466f1a9..d925d9d 100644 --- a/server.go +++ b/server.go @@ -299,7 +299,7 @@ type ServerTemplateOpts struct { TargetNode string } -func (o ServerTemplateOpts) Validate(ctx context.Context, c *Client) error { +func (o *ServerTemplateOpts) Validate(ctx context.Context, c *Client) error { if o.Name == "" { return errors.New("missing name") } From 271c91eecbeb562724612a179fc3f04b81faf6dd Mon Sep 17 00:00:00 2001 From: ston1th Date: Sat, 31 Oct 2020 12:36:57 +0100 Subject: [PATCH 30/47] load options from env --- client.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/client.go b/client.go index 21e863e..0357e39 100644 --- a/client.go +++ b/client.go @@ -13,6 +13,8 @@ import ( "net" "net/http" "net/http/httputil" + "os" + "strconv" "strings" "sync" "time" @@ -107,6 +109,40 @@ func WithCredentials(username, password string) ClientOption { } } +var ( + ErrMissingAPIEnv = errors.New("missing environment variable PVE_API") + ErrMissingUserEnv = errors.New("missing environment variable PVE_USER") + ErrMissingPasswordEnv = errors.New("missing environment variable PVE_PASSWORD") +) + +func ClientOptionsFromEnv() (options []ClientOption, err error) { + api := os.Getenv("PVE_API") + if api == "" { + err = ErrMissingAPIEnv + //return + } + user := os.Getenv("PVE_USER") + if user == "" { + err = ErrMissingUserEnv + //return + } + + pass := os.Getenv("PVE_PASSWORD") + if pass == "" { + err = ErrMissingPasswordEnv + //return + } + + insecure, _ := strconv.ParseBool(os.Getenv("PVE_INSECURE")) + + options = []ClientOption{ + WithEndpoint(api), + WithCredentials(user, pass), + WithInsecureClient(insecure), + } + return +} + func NewClient(options ...ClientOption) *Client { client := &Client{} /* From 928086aea9c1aec930d750947bcecc09859c7a28 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sat, 31 Oct 2020 21:54:59 +0100 Subject: [PATCH 31/47] added config --- client.go | 80 +++++++++++++++++++++++++++++++++++++++++++------------ scheme.go | 7 +++-- 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/client.go b/client.go index 0357e39..ed9c4d4 100644 --- a/client.go +++ b/client.go @@ -38,10 +38,10 @@ func ExponentialBackoff(b float64, d time.Duration) BackoffFunc { type Client struct { sync.Mutex - endpoint string - username string - password string - session session + endpoints []string + username string + password string + session session //pollInterval time.Duration //backoffFunc BackoffFunc httpClient *http.Client @@ -66,7 +66,15 @@ type ClientOption func(*Client) func WithEndpoint(endpoint string) ClientOption { return func(client *Client) { - client.endpoint = strings.TrimRight(endpoint, "/") + client.endpoints = append(client.endpoints, strings.TrimRight(endpoint, "/")) + } +} + +func WithEndpoints(endpoints []string) ClientOption { + return func(client *Client) { + for _, e := range endpoints { + client.endpoints = append(client.endpoints, strings.TrimRight(e, "/")) + } } } @@ -110,33 +118,65 @@ func WithCredentials(username, password string) ClientOption { } var ( - ErrMissingAPIEnv = errors.New("missing environment variable PVE_API") - ErrMissingUserEnv = errors.New("missing environment variable PVE_USER") - ErrMissingPasswordEnv = errors.New("missing environment variable PVE_PASSWORD") + ErrMissingEndpointsEnv = errors.New("missing environment variable PVE_ENDPOINTS") + ErrMissingUserEnv = errors.New("missing environment variable PVE_USER") + ErrMissingPasswordEnv = errors.New("missing environment variable PVE_PASSWORD") + + ErrMissingEndpoints = errors.New("missing Endpoints in config") + ErrMissingUser = errors.New("missing User in config") + ErrMissingPassword = errors.New("missing Password in config") ) +type Config struct { + Endpoints []string `json:"endpoints"` + User string `json:"user"` + Password string `json:"password"` + Insecure bool `json:"insecure"` +} + +func ClientOptionsFromConfig(cfg Config) (options []ClientOption, err error) { + if len(cfg.Endpoints) == 0 { + err = ErrMissingEndpoints + return + } + if cfg.User == "" { + err = ErrMissingUser + return + } + if cfg.Password == "" { + err = ErrMissingPassword + return + } + options = []ClientOption{ + WithEndpoints(cfg.Endpoints), + WithCredentials(cfg.User, cfg.Password), + WithInsecureClient(cfg.Insecure), + } + return +} + func ClientOptionsFromEnv() (options []ClientOption, err error) { - api := os.Getenv("PVE_API") - if api == "" { - err = ErrMissingAPIEnv - //return + endpoints := os.Getenv("PVE_ENDPOINTS") + if endpoints == "" { + err = ErrMissingEndpointsEnv + return } user := os.Getenv("PVE_USER") if user == "" { err = ErrMissingUserEnv - //return + return } pass := os.Getenv("PVE_PASSWORD") if pass == "" { err = ErrMissingPasswordEnv - //return + return } insecure, _ := strconv.ParseBool(os.Getenv("PVE_INSECURE")) options = []ClientOption{ - WithEndpoint(api), + WithEndpoints(strings.Split(endpoints, ",")), WithCredentials(user, pass), WithInsecureClient(insecure), } @@ -174,6 +214,12 @@ func NewClient(options ...ClientOption) *Client { return client } +func (c *Client) getEndpoint() string { + if len(c.endpoints) == 0 { + return "" + } + return c.endpoints[0] +} func (c *Client) Auth() (err error) { if !time.Now().After(c.session.Time) { @@ -184,7 +230,7 @@ func (c *Client) Auth() (err error) { if !time.Now().After(c.session.Time) { return } - u := c.endpoint + "/access/ticket" + u := c.getEndpoint() + "/access/ticket" body := httpbody{"username": c.username, "password": c.password} r, err := http.NewRequest("POST", u, body.Reader()) if err != nil { @@ -210,7 +256,7 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re if err != nil { return nil, err } - url := c.endpoint + path + url := c.getEndpoint() + path req, err := http.NewRequest(method, url, body) if err != nil { return nil, err diff --git a/scheme.go b/scheme.go index 6ae3b89..1efec5f 100644 --- a/scheme.go +++ b/scheme.go @@ -9,7 +9,10 @@ import ( "strings" ) -const PVEScheme = "pve" +const ( + PVEScheme = "pve" + PVESchemeURL = PVEScheme + "://" +) var ( ErrInvalidPVEURL = errors.New("invalid pve url scheme") @@ -51,7 +54,7 @@ func ServerRefFromURL(s string) (ref *ServerRef, err error) { } func NewURL(pool, node, id string) string { - return fmt.Sprintf("%s://%s/%s/%s", PVEScheme, pool, node, id) + return fmt.Sprintf("%s%s/%s/%s", PVESchemeURL, pool, node, id) } func K8sURL(url string) (string, error) { From eb9546c9aff31ffaa4a6deb262cd9ddd38040783 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 1 Nov 2020 20:09:16 +0100 Subject: [PATCH 32/47] added clout init image docs --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ helper.go | 23 +++++++++++++++++++++++ server.go | 6 ------ snippet.go | 38 ++++++++++---------------------------- 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 899bf47..7812340 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,54 @@ 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 + +## Cloud-Init Image + +How to prepare a cloud-init image. + +Download the latest version of the base image to one of your proxmox nodes: + +``` +curl -sSL https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img >/tmp/bionic-server-cloudimg-amd64.img +``` + +Create a cloud-init config to bootstrap the template: +``` +cat <<'EOF'> /var/lib/vz/snippets/9000_bootstrap +#cloud-config +package_update: true +package_upgrade: true +ntp: + enabled: true + servers: ["pool.ntp.org"] +ssh_deletekeys: true +ssh_genkeytypes: ["ed25519", "rsa"] +runcmd: +- "apt-get -y clean" +- "apt-get -y autoremove --purge" +- "bash -c 'rm /home/ubuntu/.ssh/authorized_keys; exit 0'" +- "bash -c 'find /var/log -type f | while read f; do echo -ne >$f; done; exit 0'" +- "bash -c 'rm -rf /var/lib/cloud/*; exit 0'" +power_state: + mode: poweroff +EOF +``` + +Create the template VM: +``` +qm destroy 9000 +qm create 9000 --memory 2048 --net0 virtio,bridge=vmbr0 +qm importdisk 9000 /tmp/bionic-server-cloudimg-amd64.img local +qm set 9000 --scsihw virtio-scsi-pci --scsi0 local:9000/vm-9000-disk-0.raw +qm set 9000 --ide0 local:cloudinit +qm set 9000 --boot c --bootdisk scsi0 +qm set 9000 --serial0 socket --vga serial0 +qm set 9000 --cicustom "user=local:snippets/9000_bootstrap" +qm set 9000 --ipconfig0 ip=dhcp,ip6=dhcp + +qm start 9000 +sleep 10;while :; do qm status 9000|grep -q stopped && break; done +qm set 9000 --cicustom "" +qm set 9000 --ipconfig0 "" +qm template 9000 +``` diff --git a/helper.go b/helper.go index 878c82d..01628ed 100644 --- a/helper.go +++ b/helper.go @@ -3,6 +3,7 @@ package pve import ( + "bytes" "encoding/json" "errors" "io" @@ -43,3 +44,25 @@ func (b httpbody) Reader() io.Reader { } return strings.NewReader(data.Encode()) } + +type unreadBuffer struct { + b *bytes.Buffer + save []byte +} + +func newUnreadBuffer() (ub *unreadBuffer, buf *bytes.Buffer) { + buf = new(bytes.Buffer) + ub = &unreadBuffer{b: buf} + return +} + +func (u *unreadBuffer) Save() { + u.save = u.b.Bytes() +} + +func (u *unreadBuffer) Reset(b **bytes.Buffer) { + if u.save != nil && b != nil { + u.b = bytes.NewBuffer(u.save) + *b = u.b + } +} diff --git a/server.go b/server.go index d925d9d..28f4e1d 100644 --- a/server.go +++ b/server.go @@ -47,7 +47,6 @@ func (s *Server) body() (b httpbody) { if s.UserData != nil || s.cicustom { b["cicustom"] = s.UserDataSnippet(s.UserDataStorage) } - // TODO userdata, err := s.UserData.String() for i, n := range s.NetDevices { if n == nil { b["net"+strconv.Itoa(i)] = "" @@ -155,11 +154,6 @@ func (sc serverConfig) Server(ref *ServerRef) (s *Server, err error) { if _, ok := sc[k].(string); ok { s.cicustom = true } - // TODO - // 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 = Memory(uint64(v) / 1024) diff --git a/snippet.go b/snippet.go index be30c47..abb9a01 100644 --- a/snippet.go +++ b/snippet.go @@ -3,7 +3,6 @@ package pve import ( - "bytes" "context" "errors" "fmt" @@ -18,16 +17,6 @@ var ( 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 } @@ -45,38 +34,32 @@ func (c *SnippetClient) Create(ctx context.Context, node, storage, name string, err = ErrNoSnippetName return } - var b bytes.Buffer - w := multipart.NewWriter(&b) - err = w.WriteField("content", "snippets") + ub, b := newUnreadBuffer() + m := multipart.NewWriter(b) + err = m.WriteField("content", "snippets") if err != nil { return } - fw, err := w.CreateFormFile("filename", name) + fw, err := m.CreateFormFile("filename", name) if err != nil { return } if _, err = fw.Write(data); err != nil { return } - w.Close() - cb := CopyBuffer{&b} - ct := w.FormDataContentType() + m.Close() + ub.Save() + ct := m.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, err = c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/storage/%s/upload", node, storage), b) req.Header.Set("Content-Type", ct) resp, err = c.client.Do(req, &taskid) - if err != nil { + if err != nil || resp.StatusCode != http.StatusOK { + ub.Reset(&b) time.Sleep(time.Millisecond * 500) - // TODO log error - continue - } - if resp.StatusCode != http.StatusOK { - time.Sleep(time.Millisecond * 500) - // TODO log error continue } break @@ -90,7 +73,6 @@ func SnippetVolume(storage, name string) string { } 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 { From 29b1c2b3e54dc932c1fce621c39ecdaa2958b20b Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 1 Nov 2020 21:06:01 +0100 Subject: [PATCH 33/47] added more cleanups --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 7812340..fe75ac6 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,17 @@ qm set 9000 --cicustom "" qm set 9000 --ipconfig0 "" qm template 9000 ``` + +### More Image Cleanup + +``` +apt -y purge apport apport-symptoms bash-completion byobu htop landscape-common lxcfs lxd lxd-client motd-news-config nano ntfs-3g os-prober ufw +apt -y autoremove +``` + +Even more cleanup: + +``` +apt -y purge lshw lsof ltrace man-db manpages mdadm mtr-tiny screen sosreport strace tcpdump tmux usbutils whiptail +apt -y autoremove +``` From c8c868b1d5337b97cfcb76438a0c4c50fe117b79 Mon Sep 17 00:00:00 2001 From: ston1th Date: Tue, 3 Nov 2020 00:45:39 +0100 Subject: [PATCH 34/47] added new patch --- snippet.diff | 36 ------------------------------------ snippets.patch | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 36 deletions(-) delete mode 100644 snippet.diff create mode 100644 snippets.patch diff --git a/snippet.diff b/snippet.diff deleted file mode 100644 index c9e0080..0000000 --- a/snippet.diff +++ /dev/null @@ -1,36 +0,0 @@ -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) = @_; diff --git a/snippets.patch b/snippets.patch new file mode 100644 index 0000000..57c4113 --- /dev/null +++ b/snippets.patch @@ -0,0 +1,29 @@ +--- PVE/Storage.pm.old 2020-11-02 19:09:57.318839122 +0100 ++++ PVE/Storage.pm 2020-11-02 19:10:52.910426246 +0100 +@@ -363,6 +363,15 @@ + 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) = @_; + +--- PVE/API2/Storage/Status.pm.old 2020-11-02 19:09:03.207241634 +0100 ++++ PVE/API2/Storage/Status.pm 2020-11-02 19:09:26.631067308 +0100 +@@ -424,6 +424,8 @@ + raise_param_exc({ filename => "missing '.tar.gz' or '.tar.xz' extension" }); + } + $path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage}); ++ } elsif ($content eq 'snippets') { ++ $path = PVE::Storage::get_snippet_dir($cfg, $param->{storage}); + } else { + raise_param_exc({ content => "upload content type '$content' not allowed" }); + } From a14b1ca025d4f474dc0d2d37e83ae65bb0ed7b29 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sat, 10 Apr 2021 17:59:37 +0200 Subject: [PATCH 35/47] addes error status --- serverstatus.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/serverstatus.go b/serverstatus.go index b6f5d8e..723423f 100644 --- a/serverstatus.go +++ b/serverstatus.go @@ -21,6 +21,9 @@ const ( ServerStatusSespending ServerStatus = "suspending" ServerStatusSespended ServerStatus = "suspended" ServerStatusResuming ServerStatus = "resuming" + + ServerStatusError ServerStatus = "error" + ServerStatusScheduleError ServerStatus = "schedule-error" ) func ServerStatusFromLock(status, lock string) ServerStatus { @@ -61,7 +64,6 @@ func ServerStatusFromTask(t *Task) ServerStatus { if t.Exitstatus == TaskExitStatusOK { return ServerStatusSespended } - return ServerStatusUnknown case "qmresume": if t.Status == TaskStatusRunning { return ServerStatusResuming @@ -69,7 +71,6 @@ func ServerStatusFromTask(t *Task) ServerStatus { if t.Exitstatus == TaskExitStatusOK { return ServerStatusRunning } - return ServerStatusUnknown case "qmclone", "qmcreate": if t.Status == TaskStatusRunning { return ServerStatusInitializing @@ -77,7 +78,6 @@ func ServerStatusFromTask(t *Task) ServerStatus { if t.Exitstatus == TaskExitStatusOK { return ServerStatusStopped } - return ServerStatusUnknown case "qmreboot", "qmreset": if t.Status == TaskStatusRunning { return ServerStatusRebooting @@ -85,7 +85,6 @@ func ServerStatusFromTask(t *Task) ServerStatus { if t.Exitstatus == TaskExitStatusOK { return ServerStatusRunning } - return ServerStatusUnknown case "qmstart": if t.Status == TaskStatusRunning { return ServerStatusStarting @@ -101,12 +100,10 @@ func ServerStatusFromTask(t *Task) ServerStatus { if t.Exitstatus == TaskExitStatusOK { return ServerStatusStopped } - return ServerStatusUnknown case "qmdestroy": if t.Status == TaskStatusRunning { return ServerStatusDeleting } - return ServerStatusUnknown } return ServerStatusUnknown } From 04bbde74ad96644ad5cfff22c8c10a5f01b5c8eb Mon Sep 17 00:00:00 2001 From: ston1th Date: Sat, 17 Apr 2021 19:56:27 +0200 Subject: [PATCH 36/47] added mtu --- ipconfig.go | 6 ------ network.go | 10 ++++++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ipconfig.go b/ipconfig.go index 6e77195..19d4be4 100644 --- a/ipconfig.go +++ b/ipconfig.go @@ -36,7 +36,6 @@ type IPConfig struct { IPv4Gateway string IPv6CIDR string IPv6Gateway string - MTU string } func (c *IPConfig) String() string { @@ -53,9 +52,6 @@ func (c *IPConfig) String() string { str = append(str, "gw6="+c.IPv6Gateway) } } - if c.MTU != "" { - str = append(str, "mtu="+c.MTU) - } return strings.Join(str, ",") } @@ -79,8 +75,6 @@ func parseIPConfig(s string) (c *IPConfig) { c.IPv6CIDR = v[1] case "gw6": c.IPv6Gateway = v[1] - case "mtu": - c.MTU = v[1] } } return diff --git a/network.go b/network.go index 0828578..7c550cf 100644 --- a/network.go +++ b/network.go @@ -36,10 +36,15 @@ type NetworkDevice struct { Type string MACAddress string Bridge string + MTU string } func (n *NetworkDevice) String() string { - return fmt.Sprintf("%s=%s,bridge=%s", n.Type, n.MACAddress, n.Bridge) + var mtu string + if n.MTU != "" { + mtu = "," + n.MTU + } + return fmt.Sprintf("%s=%s,bridge=%s%s", n.Type, n.MACAddress, n.Bridge, mtu) } func parseNetworkDevice(s string) (n *NetworkDevice) { cfg := strings.Split(s, ",") @@ -57,7 +62,8 @@ func parseNetworkDevice(s string) (n *NetworkDevice) { for _, o := range cfg { if strings.HasPrefix(o, "bridge=") { n.Bridge = o[7:] - break + } else if strings.HasPrefix(o, "mtu=") { + n.MTU = o[4:] } } return From e8575df628bb8abcdd91e78965f939fba416e331 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 18 Apr 2021 12:39:35 +0200 Subject: [PATCH 37/47] added docs and new patch --- docs/proxmox_hcloud.txt | 177 ++++++++++++++++++ .../snippets_6.3-3.patch | 12 +- 2 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 docs/proxmox_hcloud.txt rename snippets.patch => patches/snippets_6.3-3.patch (72%) diff --git a/docs/proxmox_hcloud.txt b/docs/proxmox_hcloud.txt new file mode 100644 index 0000000..239c78a --- /dev/null +++ b/docs/proxmox_hcloud.txt @@ -0,0 +1,177 @@ +echo "deb http://download.proxmox.com/debian/pve buster pve-no-subscription" > /etc/apt/sources.list.d/pve-install-repo.list +curl -sSL http://download.proxmox.com/debian/proxmox-ve-release-6.x.gpg | apt-key add - + +cat <>/etc/network/interfaces +auto eth0 +iface eth0 inet dhcp + dns-nameservers 1.1.1.1 8.8.8.8 + +EOF +rm /etc/network/interfaces.d/50-cloud-init +echo "alias ll='ls -lah'" >>/root/.bashrc +cat </root/.vimrc +syntax on +set mouse= +set ttymouse= +EOF +iface=enp7s0 +ip=$(ip a show $iface | grep "inet " | cut -d" " -f6 | cut -d"/" -f1) +sed -i "s/127.0.1.1/$ip/" /etc/hosts +cat <>/etc/network/interfaces +auto $iface +iface $iface inet static + mtu 1450 + +auto vmbr0 +iface vmbr0 inet static + address $ip/32 + bridge-ports $iface + bridge-stp off + bridge-fd 0 + mtu 1450 + post-up ip route add 172.16.0.1/32 dev vmbr0 + post-up ip route add 172.16.0.0/24 via 172.16.0.1 +EOF + +apt update && apt purge cloud-init -y && apt install ifupdown2 -y && systemctl restart networking + +# install ifupdown2 ? + +DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y -o DPkg::options::="--force-confold"; apt autoremove -y; DEBIAN_FRONTEND=noninteractive apt install --assume-yes -o DPkg::options::="--force-confdef" -y proxmox-ve vim isc-dhcp-server libpve-network-perl; +systemctl disable hc-net-ifup@ +reboot +apt remove -y os-prober linux-image-amd64 'linux-image-4.19*'; apt autoremove -y; + +cat <>/etc/network/interfaces + +auto vmbr1 +iface vmbr1 inet static + address 10.0.0.254/24 + bridge-ports none + bridge-stp off + bridge-fd 0 + post-up echo 1 > /proc/sys/net/ipv4/ip_forward + post-up iptables -t nat -A POSTROUTING -s '10.0.0.0/24' -o eth0 -j MASQUERADE + post-down iptables -t nat -F +EOF +systemctl restart networking + +cat </etc/default/isc-dhcp-server +INTERFACESv4="vmbr1" +INTERFACESv6="" +EOF +cat </etc/dhcp/dhcpd.conf +default-lease-time 600; +max-lease-time 7200; + +subnet 10.0.0.0 netmask 255.255.255.0 { + range 10.0.0.1 10.0.0.253; + option domain-name-servers 1.1.1.1, 8.8.8.8; + option routers 10.0.0.254; +} +EOF +systemctl restart isc-dhcp-server + +# controllers +# id: router +# asn: 65534 +# peers: 172.16.0.2,172.16.0.3,172.16.0.4 +# gateway-nodes: pmx01, pmx02 + +# zones +# id: evpn +# tag: 10000 +# controller: router +# mtu 1400 + +# vnets +# name: vnet2 +# zone: evpn +# tag: 11000 +# anycast: 10.0.0.1/24 + +# /etc/frr/frr.conf + bgp listen range 172.16.0.0/24 + +pvecm add 172.16.0.2 + +# vxlan zone +# id: vxlan +# peer: 172.16.0.2,172.16.0.3,172.16.0.4 +# mut: 1400 + +# vnets +# name: vnet1 +# zone: vxlan +# tag: 100000 + +fallocate -l 10G /var/osd.img +losetup -l -P /dev/loop1 /var/osd.img +wipefs -a /dev/loop1 +lsblk +sed -i "s/'mpath'/'mpath', 'loop'/g" /usr/lib/python3/dist-packages/ceph_volume/util/disk.py + +/usr/share/perl5/PVE/Diskmanage.pm +get_disks +$dev !~ m/^loop\d+$/; + +get_sysdir_info +my $data = {}; +if ($sysdir =~ /loop\d+/) { +$data->{size} = 1024; +$data->{rotational} = 1; +$data->{vendor} = 'unknown'; +$data->{model} = 'unknown'; +return $data; +} +systemctl restart pvedaemon + + +cat <<'EOF'> /var/lib/vz/snippets/9000_bootstrap +#cloud-config +package_update: true +package_upgrade: true +ntp: + enabled: true + servers: ["pool.ntp.org"] +ssh_deletekeys: true +ssh_genkeytypes: ["ed25519", "rsa"] +runcmd: +- "apt-get -y clean" +- "apt-get -y autoremove --purge" +- "bash -c 'rm /home/ubuntu/.ssh/authorized_keys; exit 0'" +- "bash -c 'find /var/log -type f | while read f; do echo -ne >$f; done; exit 0'" +- "bash -c 'rm -rf /var/lib/cloud/*; exit 0'" +power_state: + mode: poweroff +EOF + + +curl -sSL https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img >/tmp/bionic-server-cloudimg-amd64.img + +qm destroy 9000 +qm create 9000 --memory 512 --net0 virtio,bridge=vmbr1 +qm importdisk 9000 /tmp/bionic-server-cloudimg-amd64.img local +qm set 9000 --scsihw virtio-scsi-pci --scsi0 local:9000/vm-9000-disk-0.raw +qm set 9000 --ide0 local:cloudinit +qm set 9000 --boot c --bootdisk scsi0 +qm set 9000 --serial0 socket --vga serial0 +qm set 9000 --cicustom "user=local:snippets/9000_bootstrap" +qm set 9000 --ipconfig0 ip=dhcp,ip6=dhcp +qm set 9000 --kvm 0 +qm set 9000 --balloon 0 +qm set 9000 --cpu kvm64 + +qm start 9000 +sleep 10;while :; do qm status 9000|grep -q stopped && break; done +qm set 9000 --cicustom "" +qm set 9000 --ipconfig0 "" +qm template 9000 + + +# iptables +iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 222 -j DNAT --to 10.0.0.11:22 +iptables -t nat -A POSTROUTING -s '10.0.0.0/24' -o eth0 -j MASQUERADE + +# ceph +https://www.netways.de/blog/2018/11/14/ceph-mimic-using-loop-devices-as-osd/ diff --git a/snippets.patch b/patches/snippets_6.3-3.patch similarity index 72% rename from snippets.patch rename to patches/snippets_6.3-3.patch index 57c4113..96af6ee 100644 --- a/snippets.patch +++ b/patches/snippets_6.3-3.patch @@ -1,6 +1,6 @@ ---- PVE/Storage.pm.old 2020-11-02 19:09:57.318839122 +0100 -+++ PVE/Storage.pm 2020-11-02 19:10:52.910426246 +0100 -@@ -363,6 +363,15 @@ +--- PVE/Storage.pm.old 2021-04-18 12:35:48.201033594 +0200 ++++ PVE/Storage.pm 2021-04-09 23:28:07.554075001 +0200 +@@ -420,6 +420,15 @@ return $plugin->get_subdir($scfg, 'iso'); } @@ -15,9 +15,9 @@ + sub get_vztmpl_dir { my ($cfg, $storeid) = @_; - ---- PVE/API2/Storage/Status.pm.old 2020-11-02 19:09:03.207241634 +0100 -+++ PVE/API2/Storage/Status.pm 2020-11-02 19:09:26.631067308 +0100 + +--- PVE/API2/Storage/Status.pm.old 2021-04-18 12:36:10.512862744 +0200 ++++ PVE/API2/Storage/Status.pm 2021-04-09 23:28:46.429777635 +0200 @@ -424,6 +424,8 @@ raise_param_exc({ filename => "missing '.tar.gz' or '.tar.xz' extension" }); } From 0d329a2758879c63e50738ffca2f7d7185c7672f Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 11 Aug 2021 22:52:11 +0200 Subject: [PATCH 38/47] added digitalocean cluster --- docs/dump.sh | 34 ++++++++ docs/proxmox_do.txt | 206 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 docs/dump.sh create mode 100644 docs/proxmox_do.txt diff --git a/docs/dump.sh b/docs/dump.sh new file mode 100644 index 0000000..9d88f9d --- /dev/null +++ b/docs/dump.sh @@ -0,0 +1,34 @@ +#!/bin/bash +#=================================================================================== +# +# FILE: dump.sh +# USAGE: dump.sh [-i interface] [tcpdump-parameters] +# DESCRIPTION: tcpdump on any interface and add the prefix [Interace:xy] in front of the dump data. +# OPTIONS: same as tcpdump +# REQUIREMENTS: tcpdump, sed, ifconfig, kill, awk, grep, posix regex matching +# BUGS: --- +# FIXED: - In 1.0 The parameter -w would not work without -i parameter as multiple tcpdumps are started. +# - In 1.1 VLAN's would not be shown if a single interface was dumped. +# NOTES: --- +# - 1.2 git initial +# AUTHOR: Sebastian Haas +# COMPANY: pharma mall +# VERSION: 1.2 +# CREATED: 16.09.2014 +# REVISION: 22.09.2014 +# +#=================================================================================== + +# When this exits, exit all background processes: +trap 'kill $(jobs -p) &> /dev/null && sleep 0.2 && echo ' EXIT +# Create one tcpdump output per interface and add an identifier to the beginning of each line: +if [[ $@ =~ -i[[:space:]]?[^[:space:]]+ ]]; then + tcpdump -l $@ | sed 's/^/[Interface:'"${BASH_REMATCH[0]:2}"'] /' & +else + for interface in $(ifconfig | grep '^[a-z0-9]' | awk '{print $1}') + do + tcpdump -l -i $interface -nn $@ | sed 's/^/[Interface:'"$interface"'] /' & + done +fi +# wait .. until CTRL+C +wait diff --git a/docs/proxmox_do.txt b/docs/proxmox_do.txt new file mode 100644 index 0000000..85dfe98 --- /dev/null +++ b/docs/proxmox_do.txt @@ -0,0 +1,206 @@ +apt update && apt install -y curl tcpdump +echo "deb http://download.proxmox.com/debian/pve buster pve-no-subscription" > /etc/apt/sources.list.d/pve-install-repo.list +curl -sSL http://download.proxmox.com/debian/proxmox-ve-release-6.x.gpg | apt-key add - + +rm /etc/network/interfaces.d/50-cloud-init +echo "alias ll='ls -lah'" >>/root/.bashrc +cat </root/.vimrc +syntax on +set mouse= +set ttymouse= +EOF +# source /etc/network/interfaces.d/* +sed -i -e '24,29d' /etc/network/interfaces +sed -i '1isource /etc/network/interfaces.d/*' /etc/network/interfaces +iface=eth1 +ip=$(ip a show $iface | grep "inet " | cut -d" " -f6 | cut -d"/" -f1) +hw=$(ip a show $iface | grep "link/" | cut -d" " -f6) +sed -i "s/127.0.1.1/$ip/" /etc/hosts +cat <>/etc/network/interfaces +auto $iface +iface $iface inet static + hwaddress $hw + +auto vmbr0 +iface vmbr0 inet static + address $ip/24 + bridge-ports $iface + bridge-stp off + bridge-fd 0 +EOF + +apt update && apt purge cloud-init -y && apt install ifupdown2 -y && systemctl restart networking + +# install ifupdown2 ? + +DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y -o DPkg::options::="--force-confold"; apt autoremove -y; DEBIAN_FRONTEND=noninteractive apt install --assume-yes -o DPkg::options::="--force-confdef" -y proxmox-ve vim isc-dhcp-server libpve-network-perl; +#systemctl disable hc-net-ifup@ +reboot +apt remove -y os-prober linux-image-amd64 'linux-image-4.19*'; apt autoremove -y; + +cat <>/etc/network/interfaces + +auto vmbr1 +iface vmbr1 inet static + address 10.0.0.254/24 + bridge-ports none + bridge-stp off + bridge-fd 0 + post-up echo 1 > /proc/sys/net/ipv4/ip_forward + post-up iptables -t nat -A POSTROUTING -s '10.0.0.0/24' -o eth0 -j MASQUERADE + post-down iptables -t nat -F +EOF +systemctl restart networking + +pvecm create pmx -link0 172.16.0.2 + +cat </etc/default/isc-dhcp-server +INTERFACESv4="vmbr1" +INTERFACESv6="" +EOF +cat </etc/dhcp/dhcpd.conf +default-lease-time 600; +max-lease-time 7200; + +subnet 10.0.0.0 netmask 255.255.255.0 { + range 10.0.0.1 10.0.0.253; + option domain-name-servers 1.1.1.1, 8.8.8.8; + option routers 10.0.0.254; +} +EOF +systemctl restart isc-dhcp-server + +# controllers +# id: evpn +# asn: 65000 +# peers: 172.16.0.2,172.16.0.3,172.16.0.4 + +# zones +# id: evpn +# tag: 10000 +# exit-nodes: pmx01, pmx02 +# controller: evpn +# mtu 1400 + +# vnets +# name: vnet1 +# zone: evpn +# tag: 11000 +# gateway: 192.168.1.1 + +# name: vnet2 +# zone: evpn +# tag: 12000 +# gateway: 192.168.2.1 + +cd /var/lib/vz/template/iso +curl -sSLO https://dl-cdn.alpinelinux.org/alpine/v3.14/releases/x86_64/alpine-virt-3.14.1-x86_64.iso + +ip link set up dev eth0 +ip link set mtu 1450 dev eth0 +ip a add 192.168.1.2/24 dev eth0 +ip route add default via 192.168.1.1 + +ip a add 192.168.2.2/24 dev eth0 +ip route add default via 192.168.2.1 + +rm /etc/apt/sources.list.d/pve-enterprise.list +sed -i 's/buster\/updates/bullseye-security/g;s/buster/bullseye/g' /etc/apt/sources.list +sed -i -e 's/buster/bullseye/g' /etc/apt/sources.list.d/pve-install-repo.list + +apt update && apt dist-upgrade + + +cd /tmp +curl -sSLO https://mutulin1.odiso.net/frr_7.5.1-2+pve_amd64.deb +curl -sSLO https://mutulin1.odiso.net/frr-pythontools_7.5.1-2+pve_all.deb +dpkg -i frr_7.5.1-2+pve_amd64.deb +dpkg -i frr-pythontools_7.5.1-2+pve_all.deb + +ip route add table vrf_evpn default dev eth0 via 164.90.160. + +# /etc/frr/frr.conf + bgp listen range 172.16.0.0/24 + +pvecm add 172.16.0.2 + +# vxlan zone +# id: vxlan +# peer: 172.16.0.2,172.16.0.3,172.16.0.4 +# mut: 1400 + +# vnets +# name: vnet1 +# zone: vxlan +# tag: 100000 + +fallocate -l 10G /var/osd.img +losetup -l -P /dev/loop1 /var/osd.img +wipefs -a /dev/loop1 +lsblk +sed -i "s/'mpath'/'mpath', 'loop'/g" /usr/lib/python3/dist-packages/ceph_volume/util/disk.py + +/usr/share/perl5/PVE/Diskmanage.pm +get_disks +$dev !~ m/^loop\d+$/; + +get_sysdir_info +my $data = {}; +if ($sysdir =~ /loop\d+/) { +$data->{size} = 1024; +$data->{rotational} = 1; +$data->{vendor} = 'unknown'; +$data->{model} = 'unknown'; +return $data; +} +systemctl restart pvedaemon + + +cat <<'EOF'> /var/lib/vz/snippets/9000_bootstrap +#cloud-config +package_update: true +package_upgrade: true +ntp: + enabled: true + servers: ["pool.ntp.org"] +ssh_deletekeys: true +ssh_genkeytypes: ["ed25519", "rsa"] +runcmd: +- "apt-get -y clean" +- "apt-get -y autoremove --purge" +- "bash -c 'rm /home/ubuntu/.ssh/authorized_keys; exit 0'" +- "bash -c 'find /var/log -type f | while read f; do echo -ne >$f; done; exit 0'" +- "bash -c 'rm -rf /var/lib/cloud/*; exit 0'" +power_state: + mode: poweroff +EOF + + +curl -sSL https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img >/tmp/bionic-server-cloudimg-amd64.img + +qm destroy 9000 +qm create 9000 --memory 512 --net0 virtio,bridge=vmbr1 +qm importdisk 9000 /tmp/bionic-server-cloudimg-amd64.img local +qm set 9000 --scsihw virtio-scsi-pci --scsi0 local:9000/vm-9000-disk-0.raw +qm set 9000 --ide0 local:cloudinit +qm set 9000 --boot c --bootdisk scsi0 +qm set 9000 --serial0 socket --vga serial0 +qm set 9000 --cicustom "user=local:snippets/9000_bootstrap" +qm set 9000 --ipconfig0 ip=dhcp,ip6=dhcp +qm set 9000 --kvm 0 +qm set 9000 --balloon 0 +qm set 9000 --cpu kvm64 + +qm start 9000 +sleep 10;while :; do qm status 9000|grep -q stopped && break; done +qm set 9000 --cicustom "" +qm set 9000 --ipconfig0 "" +qm template 9000 + + +# iptables +iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 222 -j DNAT --to 10.0.0.11:22 +iptables -t nat -A POSTROUTING -s '10.0.0.0/24' -o eth0 -j MASQUERADE + +# ceph +https://www.netways.de/blog/2018/11/14/ceph-mimic-using-loop-devices-as-osd/ From 41dda70d6b3a8a338a81491d49bc32bb71c5b80d Mon Sep 17 00:00:00 2001 From: ston1th Date: Sat, 18 Sep 2021 21:07:42 +0200 Subject: [PATCH 39/47] added minimal image --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index fe75ac6..def9db7 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,45 @@ Even more cleanup: apt -y purge lshw lsof ltrace man-db manpages mdadm mtr-tiny screen sosreport strace tcpdump tmux usbutils whiptail apt -y autoremove ``` + +## Minimal Cloud Image + +``` +curl -sSL https://cloud-images.ubuntu.com/minimal/releases/bionic/release/ubuntu-18.04-minimal-cloudimg-amd64.img >/tmp/ubuntu-18.04-minimal-cloudimg-amd64.img + +cat <<'EOF'> /var/lib/vz/snippets/9001_bootstrap +#cloud-config +package_update: true +package_upgrade: true +ntp: + enabled: true + servers: ["pool.ntp.org"] +ssh_deletekeys: true +ssh_genkeytypes: ["ed25519", "rsa"] +runcmd: +- "apt-get -y purge apport htop lxcfs lxd lxd-client motd-news-config mdadm os-prober sosreport" +- "apt-get -y autoremove --purge" +- "apt-get -y clean" +- "bash -c 'rm /home/ubuntu/.ssh/authorized_keys; exit 0'" +- "bash -c 'find /var/log -type f | while read f; do echo -ne >$f; done; exit 0'" +- "bash -c 'rm -rf /var/lib/cloud/*; exit 0'" +power_state: + mode: poweroff +EOF + +qm destroy 9001 +qm create 9001 --memory 2048 --net0 virtio,bridge=vmbr0 +qm importdisk 9001 /tmp/ubuntu-18.04-minimal-cloudimg-amd64.img local +qm set 9001 --scsihw virtio-scsi-pci --scsi0 local:9001/vm-9001-disk-0.raw +qm set 9001 --ide0 local:cloudinit +qm set 9001 --boot c --bootdisk scsi0 +qm set 9001 --serial0 socket --vga serial0 +qm set 9001 --cicustom "user=local:snippets/9001_bootstrap" +qm set 9001 --ipconfig0 ip=dhcp,ip6=dhcp + +qm start 9001 +sleep 10;while :; do qm status 9001|grep -q stopped && break; done +qm set 9001 --cicustom "" +qm set 9001 --ipconfig0 "" +qm template 9001 +``` From 6c04e86a8c3c66e54ec6c2bbda38b1b1aef39cd4 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sat, 16 Oct 2021 17:55:46 +0200 Subject: [PATCH 40/47] added more default client options --- client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index ed9c4d4..f941bc3 100644 --- a/client.go +++ b/client.go @@ -198,8 +198,14 @@ func NewClient(options ...ClientOption) *Client { if client.httpClient == nil { client.httpClient = &http.Client{Transport: &http.Transport{ DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, TLSClientConfig: &tls.Config{ InsecureSkipVerify: client.insecure, }, From ffb356182e08631f072235af219aeea8dc73f6e2 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 7 Nov 2021 11:57:25 +0100 Subject: [PATCH 41/47] linter fixes --- client.go | 11 +++++------ server.go | 4 ++-- task.go | 2 +- userdata.go | 2 -- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index f941bc3..2e0802a 100644 --- a/client.go +++ b/client.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "net/http/httputil" @@ -37,7 +36,7 @@ func ExponentialBackoff(b float64, d time.Duration) BackoffFunc { */ type Client struct { - sync.Mutex + mu sync.Mutex endpoints []string username string password string @@ -231,8 +230,8 @@ func (c *Client) Auth() (err error) { if !time.Now().After(c.session.Time) { return } - c.Lock() - defer c.Unlock() + c.mu.Lock() + defer c.mu.Unlock() if !time.Now().After(c.session.Time) { return } @@ -289,13 +288,13 @@ func (c *Client) Do(r *http.Request, v interface{}) (resp *http.Response, err er if err != nil { return } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { resp.Body.Close() return } resp.Body.Close() - resp.Body = ioutil.NopCloser(bytes.NewReader(body)) + resp.Body = io.NopCloser(bytes.NewReader(body)) if c.debugWriter != nil { dumpResp, err := httputil.DumpResponse(resp, true) diff --git a/server.go b/server.go index 28f4e1d..87fcd61 100644 --- a/server.go +++ b/server.go @@ -313,7 +313,7 @@ func (o *ServerTemplateOpts) Validate(ctx context.Context, c *Client) error { return nil } -func (o ServerTemplateOpts) body() httpbody { +func (o *ServerTemplateOpts) body() httpbody { body := httpbody{"name": o.Name} if o.TargetStorage != "" { body["storage"] = o.TargetStorage @@ -325,7 +325,7 @@ func (o ServerTemplateOpts) body() httpbody { return body } -func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts ServerTemplateOpts) (t *Task, url string, err error) { +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 diff --git a/task.go b/task.go index 3f1d00d..5cd007a 100644 --- a/task.go +++ b/task.go @@ -97,7 +97,7 @@ func (c *TaskClient) Wait(ctx context.Context, t *Task, f OnTaskChange) error { } } time.Sleep(TaskStatusCheckInterval * time.Second) - waited = waited + TaskStatusCheckInterval + waited += TaskStatusCheckInterval } return errors.New("task wait timeout for: " + t.ID) } diff --git a/userdata.go b/userdata.go index 1bf2dbb..7e96a59 100644 --- a/userdata.go +++ b/userdata.go @@ -3,8 +3,6 @@ package pve import ( - //"encoding/base64" - //"net/url" "gopkg.in/yaml.v2" ) From 7ff2a92118d918342dfb7932ece367f07eddb1fe Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 28 Nov 2021 11:50:56 +0100 Subject: [PATCH 42/47] migrate to integer ID --- node.go | 10 +++++----- pool.go | 11 +++++------ scheme.go | 14 ++++++++++---- server.go | 14 +++++++------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/node.go b/node.go index 7c78e76..8490464 100644 --- a/node.go +++ b/node.go @@ -205,7 +205,7 @@ func (c *NodeClient) ListServers(ctx context.Context, n *Node) (sl ServerRefList return } -func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *ServerRef, err error) { +func (c *NodeClient) FindServer(ctx context.Context, name string, id int) (s *ServerRef, err error) { nl, err := c.List(ctx) if err != nil { return @@ -223,7 +223,7 @@ func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *Server continue } for _, s := range sl { - if id != "" && s.ID == id { + if id != -1 && s.ID == id { return s, nil } if name != "" && s.Name == name { @@ -241,11 +241,11 @@ func (c *NodeClient) FindServer(ctx context.Context, name, id string) (s *Server } func (c *NodeClient) FindServerByName(ctx context.Context, name string) (s *ServerRef, err error) { - return c.FindServer(ctx, name, "") + return c.FindServer(ctx, name, -1) } -func (c *NodeClient) FindServerByID(ctx context.Context, id string) (s *ServerRef, err error) { - if id == "" { +func (c *NodeClient) FindServerByID(ctx context.Context, id int) (s *ServerRef, err error) { + if id == -1 { err = ErrEmptyID return } diff --git a/pool.go b/pool.go index 57dffed..228c948 100644 --- a/pool.go +++ b/pool.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "net/http" - "strconv" ) var ( @@ -106,7 +105,7 @@ type poolMembers []poolMember func (pms poolMembers) ServerList() (sl ServerRefList) { for _, pm := range pms { sl = append(sl, &ServerRef{ - ID: strconv.Itoa(pm.ID), + ID: pm.ID, Name: pm.Name, Node: pm.Node, }) @@ -133,7 +132,7 @@ func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerRef return } -func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id string) (s *ServerRef, err error) { +func (c *PoolClient) FindServer(ctx context.Context, poolname, servername string, id int) (s *ServerRef, err error) { var pools []string if poolname != "" { pools = append(pools, poolname) @@ -149,7 +148,7 @@ func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id st return nil, err } for _, srv := range sl { - if id != "" && srv.ID == id { + if id != -1 && srv.ID == id { srv.Pool = pool return srv, nil } @@ -163,10 +162,10 @@ func (c *PoolClient) FindServer(ctx context.Context, poolname, servername, id st } func (c *PoolClient) FindServerByName(ctx context.Context, poolname, servername string) (s *ServerRef, err error) { - return c.FindServer(ctx, poolname, servername, "") + return c.FindServer(ctx, poolname, servername, -1) } -func (c *PoolClient) FindServerByID(ctx context.Context, poolname, id string) (s *ServerRef, err error) { +func (c *PoolClient) FindServerByID(ctx context.Context, poolname string, id int) (s *ServerRef, err error) { return c.FindServer(ctx, poolname, "", id) } diff --git a/scheme.go b/scheme.go index 1efec5f..9e8062d 100644 --- a/scheme.go +++ b/scheme.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/url" + "strconv" "strings" ) @@ -17,9 +18,10 @@ const ( var ( ErrInvalidPVEURL = errors.New("invalid pve url scheme") ErrNoID = errors.New("missing id in url") + ErrParsingID = errors.New("error parsing id in url") ) -func ParseURL(s string) (pool, node, id string, err error) { +func ParseURL(s string) (pool, node string, id int, err error) { u, err := url.Parse(s) if err != nil { return @@ -37,7 +39,11 @@ func ParseURL(s string) (pool, node, id string, err error) { err = ErrNoID return } - return u.Host, a[1], a[2], nil + id, err = strconv.Atoi(a[2]) + if err != nil { + err = ErrParsingID + } + return u.Host, a[1], id, nil } func ServerRefFromURL(s string) (ref *ServerRef, err error) { @@ -53,8 +59,8 @@ func ServerRefFromURL(s string) (ref *ServerRef, err error) { return } -func NewURL(pool, node, id string) string { - return fmt.Sprintf("%s%s/%s/%s", PVESchemeURL, pool, node, id) +func NewURL(pool, node string, id int) string { + return fmt.Sprintf("%s%s/%s/%d", PVESchemeURL, pool, node, id) } func K8sURL(url string) (string, error) { diff --git a/server.go b/server.go index 87fcd61..fb6079e 100644 --- a/server.go +++ b/server.go @@ -13,7 +13,7 @@ import ( var ErrServerNotFound = errors.New("server not found") type Server struct { - ID string `json:"vmid"` + ID int `json:"vmid"` Name string `json:"name"` Node string Pool string @@ -29,7 +29,7 @@ type Server struct { } func (s *Server) UserDataSnippetName() string { - return fmt.Sprintf("%s_userdata", s.ID) + return fmt.Sprintf("%d_userdata", s.ID) } func (s *Server) UserDataSnippet(storage string) string { @@ -84,7 +84,7 @@ func (s *Server) Ref() *ServerRef { type ServerRefList []*ServerRef type ServerRef struct { - ID string `json:"vmid"` + ID int `json:"vmid"` Name string `json:"name"` Node string Pool string @@ -220,7 +220,7 @@ type ServerClient struct { client *Client } -func (c *ServerClient) NextID(ctx context.Context) (id string, err error) { +func (c *ServerClient) NextID(ctx context.Context) (id int, err error) { req, err := c.client.NewRequest(ctx, "GET", "/cluster/nextid", nil) if err != nil { return @@ -285,7 +285,7 @@ func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err type ServerTemplateOpts struct { Name string - TemplateID string + TemplateID int // TemplateNode is optional TemplateNode string Pool string @@ -297,7 +297,7 @@ func (o *ServerTemplateOpts) Validate(ctx context.Context, c *Client) error { if o.Name == "" { return errors.New("missing name") } - if o.TemplateID == "" { + if o.TemplateID <= 0 { return errors.New("missing template id") } if o.TemplateNode == "" { @@ -335,7 +335,7 @@ func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts *ServerTempl return } body := opts.body() - body["newid"] = nextid + body["newid"] = strconv.Itoa(nextid) node := opts.TemplateNode if opts.TargetNode != "" && opts.TemplateNode != opts.TargetNode { node = opts.TargetNode From 066afb15b42189bfa04360c52a08f534542e25ae Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 28 Nov 2021 12:38:37 +0100 Subject: [PATCH 43/47] more int fixes --- server.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/server.go b/server.go index fb6079e..77117be 100644 --- a/server.go +++ b/server.go @@ -221,17 +221,22 @@ type ServerClient struct { } func (c *ServerClient) NextID(ctx context.Context) (id int, err error) { + id = -1 req, err := c.client.NewRequest(ctx, "GET", "/cluster/nextid", nil) if err != nil { return } - _, err = c.client.Do(req, &id) - return + var sid string + _, err = c.client.Do(req, &sid) + if err != nil { + return + } + return strconv.Atoi(sid) } 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) + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%d/config?current=1", ref.Node, ref.ID), nil) if err != nil { return } @@ -342,7 +347,7 @@ func (c *ServerClient) CreateFromTemplate(ctx context.Context, opts *ServerTempl 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()) + req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%d/clone", opts.TemplateNode, opts.TemplateID), body.Reader()) if err != nil { return } @@ -381,7 +386,7 @@ func (c *ServerClient) Stop(ctx context.Context, ref *ServerRef) (t *Task, err e } 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) + req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%d/status/%s", ref.Node, ref.ID, status), nil) if err != nil { return } @@ -409,7 +414,7 @@ func (c *ServerClient) GetStatus(ctx context.Context, s *Server, lock *string) ( 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) + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%d/status/current", s.Node, s.ID), nil) if err != nil { s.Status = ServerStatusUnknown return @@ -435,7 +440,7 @@ func diffBody(cur, n *Server) (b httpbody) { } 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()) + req, err := c.client.NewRequest(ctx, "PUT", fmt.Sprintf("/nodes/%s/qemu/%d/config", s.Node, s.ID), body.Reader()) if err != nil { return err } @@ -448,7 +453,7 @@ func (c *ServerClient) resizeDisk(ctx context.Context, s *Server) (err error) { "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()) + req, err := c.client.NewRequest(ctx, "PUT", fmt.Sprintf("/nodes/%s/qemu/%d/resize", s.Node, s.ID), body.Reader()) if err != nil { return err } @@ -505,7 +510,7 @@ func (c *ServerClient) Delete(ctx context.Context, s *Server, f OnTaskChange) (t return nil, err } } - req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/nodes/%s/qemu/%s", s.Node, s.ID), nil) + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/nodes/%s/qemu/%d", s.Node, s.ID), nil) if err != nil { return } From d703fdea5f8d7ed7b1fa6b285eb8457358843069 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 28 Nov 2021 16:59:44 +0100 Subject: [PATCH 44/47] updated patches --- patches/snippets_7.0-11.patch | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 patches/snippets_7.0-11.patch diff --git a/patches/snippets_7.0-11.patch b/patches/snippets_7.0-11.patch new file mode 100644 index 0000000..f23e284 --- /dev/null +++ b/patches/snippets_7.0-11.patch @@ -0,0 +1,60 @@ +--- Storage.pm.old 2021-11-28 12:58:53.026308987 +0100 ++++ Storage.pm 2021-11-28 12:59:14.394144995 +0100 +@@ -427,6 +427,15 @@ + return $plugin->get_subdir($scfg, 'vztmpl'); + } + ++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_backup_dir { + my ($cfg, $storeid) = @_; + + +--- API2/Storage/Config.pm.old 2021-11-28 14:09:07.861745249 +0100 ++++ API2/Storage/Config.pm 2021-11-28 14:00:09.133897607 +0100 +@@ -19,7 +19,7 @@ + + use base qw(PVE::RESTHandler); + +-my @ctypes = qw(images vztmpl iso backup); ++my @ctypes = qw(images vztmpl iso backup snippets); + + my $storage_type_enum = PVE::Storage::Plugin->lookup_types(); + + +--- API2/Storage/Status.pm.old 2021-11-28 13:51:17.609996827 +0100 ++++ API2/Storage/Status.pm 2021-11-28 13:51:02.190115817 +0100 +@@ -381,7 +381,7 @@ + content => { + description => "Content type.", + type => 'string', format => 'pve-storage-content', +- enum => ['iso', 'vztmpl'], ++ enum => ['iso', 'vztmpl', 'snippets'], + }, + filename => { + description => "The name of the file to create. Caution: This will be normalized!", +@@ -433,6 +433,8 @@ + raise_param_exc({ filename => "wrong file extension" }); + } + $path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage}); ++ } elsif ($content eq 'snippets') { ++ $path = PVE::Storage::get_snippet_dir($cfg, $param->{storage}); + } else { + raise_param_exc({ content => "upload content type '$content' not allowed" }); + } +@@ -534,7 +536,7 @@ + content => { + description => "Content type.", # TODO: could be optional & detected in most cases + type => 'string', format => 'pve-storage-content', +- enum => ['iso', 'vztmpl'], ++ enum => ['iso', 'vztmpl', 'snippets'], + }, + filename => { + description => "The name of the file to create. Caution: This will be normalized!", From 78b40626e68a627eb5082cce2d4df47900efe024 Mon Sep 17 00:00:00 2001 From: ston1th Date: Fri, 10 Dec 2021 20:33:20 +0100 Subject: [PATCH 45/47] added better id handling --- node.go | 35 ++++++++++++++++++++++------------- pool.go | 27 +++++++++++++++++---------- scheme.go | 13 ++++++++++++- server.go | 35 ++++++++++++++++++++++++++--------- 4 files changed, 77 insertions(+), 33 deletions(-) diff --git a/node.go b/node.go index 8490464..2251970 100644 --- a/node.go +++ b/node.go @@ -10,10 +10,10 @@ import ( ) var ( - ErrNodesOffline = errors.New("one or more nodes are offline") - ErrNodesNotSearched = errors.New("one or more nodes could not be searched") - ErrEmptyID = errors.New("id is empty") - ErrUnschedulable = errors.New("no schedulable node found") + ErrNodesOffline = errors.New("one or more nodes are offline") + ErrNodesNotSearched = errors.New("one or more nodes could not be searched") + ErrUnschedulable = errors.New("no schedulable node found") + ErrTooManyServersFound = errors.New("too many servers found with the same name") ) type NodeStatus string @@ -205,13 +205,16 @@ func (c *NodeClient) ListServers(ctx context.Context, n *Node) (sl ServerRefList return } -func (c *NodeClient) FindServer(ctx context.Context, name string, id int) (s *ServerRef, err error) { +func (c *NodeClient) FindServer(ctx context.Context, name string, id int) (ref *ServerRef, err error) { + valid := ValidateID(id) == nil nl, err := c.List(ctx) if err != nil { return } - nodeOffline := false - nodeNotSearched := false + var ( + nodeOffline bool + nodeNotSearched bool + ) for _, node := range nl { if node.Status != NodeStatusOnline { nodeOffline = true @@ -223,11 +226,14 @@ func (c *NodeClient) FindServer(ctx context.Context, name string, id int) (s *Se continue } for _, s := range sl { - if id != -1 && s.ID == id { + if valid && s.ID == id { return s, nil } if name != "" && s.Name == name { - return s, nil + if ref != nil { + return nil, ErrTooManyServersFound + } + ref = s } } } @@ -237,16 +243,19 @@ func (c *NodeClient) FindServer(ctx context.Context, name string, id int) (s *Se if nodeNotSearched { return nil, ErrNodesNotSearched } - return nil, ErrServerNotFound + if ref == nil { + err = ErrServerNotFound + } + return } func (c *NodeClient) FindServerByName(ctx context.Context, name string) (s *ServerRef, err error) { - return c.FindServer(ctx, name, -1) + return c.FindServer(ctx, name, InvalidID) } func (c *NodeClient) FindServerByID(ctx context.Context, id int) (s *ServerRef, err error) { - if id == -1 { - err = ErrEmptyID + err = ValidateID(id) + if err != nil { return } return c.FindServer(ctx, "", id) diff --git a/pool.go b/pool.go index 228c948..9bcad91 100644 --- a/pool.go +++ b/pool.go @@ -132,7 +132,8 @@ func (c *PoolClient) ListMembers(ctx context.Context, name string) (sl ServerRef return } -func (c *PoolClient) FindServer(ctx context.Context, poolname, servername string, id int) (s *ServerRef, err error) { +func (c *PoolClient) FindServer(ctx context.Context, poolname, servername string, id int) (ref *ServerRef, err error) { + valid := ValidateID(id) == nil var pools []string if poolname != "" { pools = append(pools, poolname) @@ -147,22 +148,28 @@ func (c *PoolClient) FindServer(ctx context.Context, poolname, servername string if err != nil { return nil, err } - for _, srv := range sl { - if id != -1 && srv.ID == id { - srv.Pool = pool - return srv, nil + for _, s := range sl { + if valid && s.ID == id { + s.Pool = pool + return s, nil } - if servername != "" && srv.Name == servername { - srv.Pool = pool - return srv, nil + if servername != "" && s.Name == servername { + if ref != nil { + return nil, ErrTooManyServersFound + } + s.Pool = pool + ref = s } } } - return nil, ErrServerNotFound + if ref == nil { + err = ErrServerNotFound + } + return } func (c *PoolClient) FindServerByName(ctx context.Context, poolname, servername string) (s *ServerRef, err error) { - return c.FindServer(ctx, poolname, servername, -1) + return c.FindServer(ctx, poolname, servername, InvalidID) } func (c *PoolClient) FindServerByID(ctx context.Context, poolname string, id int) (s *ServerRef, err error) { diff --git a/scheme.go b/scheme.go index 9e8062d..2926628 100644 --- a/scheme.go +++ b/scheme.go @@ -13,14 +13,25 @@ import ( const ( PVEScheme = "pve" PVESchemeURL = PVEScheme + "://" + + MinID = 100 + InvalidID = 0 ) var ( ErrInvalidPVEURL = errors.New("invalid pve url scheme") ErrNoID = errors.New("missing id in url") + ErrInvalidID = errors.New("invalid id") ErrParsingID = errors.New("error parsing id in url") ) +func ValidateID(id int) error { + if id < MinID { + return ErrInvalidID + } + return nil +} + func ParseURL(s string) (pool, node string, id int, err error) { u, err := url.Parse(s) if err != nil { @@ -43,7 +54,7 @@ func ParseURL(s string) (pool, node string, id int, err error) { if err != nil { err = ErrParsingID } - return u.Host, a[1], id, nil + return u.Host, a[1], id, ValidateID(id) } func ServerRefFromURL(s string) (ref *ServerRef, err error) { diff --git a/server.go b/server.go index 77117be..d7bc902 100644 --- a/server.go +++ b/server.go @@ -84,10 +84,11 @@ func (s *Server) Ref() *ServerRef { type ServerRefList []*ServerRef type ServerRef struct { - ID int `json:"vmid"` - Name string `json:"name"` - Node string - Pool string + ID int `json:"vmid"` + Name string `json:"name"` + Template int `json:"template,omitempty"` + Node string `json:"node,omitempty"` + Pool string `json:"pool,omitempty"` } func (ref *ServerRef) InstanceID() string { @@ -98,6 +99,10 @@ func (ref *ServerRef) K8sID() string { return NewURL(ref.Pool, "", ref.ID) } +func (ref *ServerRef) IsTemplate() bool { + return ref.Template == 1 +} + type Resources struct { Cores Cores `json:"cores"` Memory Memory `json:"memory"` @@ -221,7 +226,7 @@ type ServerClient struct { } func (c *ServerClient) NextID(ctx context.Context) (id int, err error) { - id = -1 + id = InvalidID req, err := c.client.NewRequest(ctx, "GET", "/cluster/nextid", nil) if err != nil { return @@ -289,8 +294,9 @@ func (c *ServerClient) GetByURL(ctx context.Context, url string) (s *Server, err } type ServerTemplateOpts struct { - Name string - TemplateID int + Name string + TemplateID int + TemplateName string // TemplateNode is optional TemplateNode string Pool string @@ -302,8 +308,19 @@ func (o *ServerTemplateOpts) Validate(ctx context.Context, c *Client) error { if o.Name == "" { return errors.New("missing name") } - if o.TemplateID <= 0 { - return errors.New("missing template id") + if ValidateID(o.TemplateID) != nil { + if o.TemplateName == "" { + return errors.New("missing or invalid template id") + } + ref, err := c.Node.FindServerByName(ctx, o.TemplateName) + if err != nil { + return fmt.Errorf("template id not found: %w", err) + } + if !ref.IsTemplate() { + return errors.New("server is not a vm template") + } + o.TemplateID = ref.ID + o.TemplateNode = ref.Node } if o.TemplateNode == "" { ref, err := c.Node.FindServerByID(ctx, o.TemplateID) From c168edb271a57801e7ff6d10e6787820359ca601 Mon Sep 17 00:00:00 2001 From: ston1th Date: Fri, 10 Dec 2021 23:56:09 +0100 Subject: [PATCH 46/47] added ubuntu 20.04 --- README.md | 143 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index def9db7..675586a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,92 @@ The design is based on the Hetzner Clound API implementation: https://github.com How to prepare a cloud-init image. +### Ubuntu 20.04 + +Download the latest version of the base image to one of your proxmox nodes: + +``` +curl -sSL https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img >/tmp/focal-server-cloudimg-amd64.img +``` + +Create a cloud-init config to bootstrap the template: +``` +cat <<'EOF'> /var/lib/vz/snippets/9002_bootstrap +#cloud-config +package_update: true +package_upgrade: true +ntp: + enabled: true + servers: ["pool.ntp.org"] +ssh_deletekeys: true +ssh_genkeytypes: ["ed25519", "rsa"] +runcmd: +- "apt-get -y clean" +- "apt-get -y autoremove --purge" +- "bash -c 'rm /home/ubuntu/.ssh/authorized_keys; exit 0'" +- "bash -c 'find /var/log -type f | while read f; do echo -ne >$f; done; exit 0'" +- "bash -c 'rm -rf /var/lib/cloud/* /var/tmp/* /tmp/* /tmp/.*-unix; exit 0'" +power_state: + mode: poweroff +EOF +``` + +Create the template VM: +``` +qm stop 9002; qm destroy 9002 +qm create 9002 --name ubuntu-2004 --memory 2048 --net0 virtio,bridge=vmbr0 +qm importdisk 9002 /tmp/focal-server-cloudimg-amd64.img local +qm set 9002 --scsihw virtio-scsi-pci --scsi0 local:9002/vm-9002-disk-0.raw +qm set 9002 --ide0 local:cloudinit +qm set 9002 --boot c --bootdisk scsi0 +qm set 9002 --serial0 socket --vga serial0 +qm set 9002 --cicustom "user=local:snippets/9002_bootstrap" +qm set 9002 --ipconfig0 ip=dhcp,ip6=dhcp + +qm start 9002 +sleep 10;while :; do qm status 9002|grep -q stopped && break; done +qm set 9002 --cicustom "" +qm set 9002 --ipconfig0 "" +qm template 9002 +``` + +#### More Image Cleanup + +``` +mkdir /etc/systemd/system/motd-news.service.d +cat </etc/systemd/system/motd-news.service.d/override.conf +[Service] +ExecStart=/bin/true +EOF + +mkdir /etc/systemd/timesyncd.conf.d +cat </etc/systemd/timesyncd.conf.d/ntp.conf +[Time] +NTP=pool.ntp.org +EOF + +apt -y purge alsa-topology-conf alsa-ucm-conf apport \ + apport-symptoms at bash-completion bolt byobu eatmydata command-not-found \ + eject fonts-ubuntu-console htop install-info landscape-common lxd-agent-loader \ + motd-news-config nano ntfs-3g pastebinit plymouth plymouth-theme-ubuntu-text \ + popularity-contest policykit-1 python3-apport os-prober snapd \ + sound-theme-freedesktop ubuntu-advantage-tools ufw + +# optional +# apt -y purge accountsservice multipath-tools packagekit udisks2 unattended-upgrades + +apt -y autoremove +``` + +Even more cleanup: + +``` +apt -y purge lshw lsof ltrace man-db manpages mdadm mtr-tiny screen sosreport strace tcpdump tmux usbutils whiptail +apt -y autoremove +``` + +### Ubuntu 18.04 + Download the latest version of the base image to one of your proxmox nodes: ``` @@ -38,8 +124,8 @@ EOF Create the template VM: ``` -qm destroy 9000 -qm create 9000 --memory 2048 --net0 virtio,bridge=vmbr0 +qm stop 9000; qm destroy 9000 +qm create 9000 --name ubuntu-1804 --memory 2048 --net0 virtio,bridge=vmbr0 qm importdisk 9000 /tmp/bionic-server-cloudimg-amd64.img local qm set 9000 --scsihw virtio-scsi-pci --scsi0 local:9000/vm-9000-disk-0.raw qm set 9000 --ide0 local:cloudinit @@ -55,7 +141,7 @@ qm set 9000 --ipconfig0 "" qm template 9000 ``` -### More Image Cleanup +#### More Image Cleanup ``` apt -y purge apport apport-symptoms bash-completion byobu htop landscape-common lxcfs lxd lxd-client motd-news-config nano ntfs-3g os-prober ufw @@ -71,6 +157,44 @@ apt -y autoremove ## Minimal Cloud Image +### Ubuntu 20.04 + +``` +curl -sSL https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img >/tmp/focal-server-cloudimg-amd64.img + +cat <<'EOF'> /var/lib/vz/snippets/9002_bootstrap +#cloud-config +package_update: true +package_upgrade: true +ssh_deletekeys: true +ssh_genkeytypes: ["ed25519", "rsa"] +runcmd: +- "curl -sSL https://git.giftfish.de/ston1th/cleanup/raw/branch/master/ubuntu_2004_full.sh >/tmp/cleanup.sh" +- | + bash -c '[ "$(sha256sum /tmp/cleanup.sh|cut -d" " -f1)" = "fe2827d03ebba6e058ed7b9e56116eef78a45606f2e5f2956b2b2f495f042724" ] && sh /tmp/cleanup.sh || echo "error: cleanup script hash does not match" >&2' +power_state: + mode: poweroff +EOF + +qm stop 9002; qm destroy 9002 +qm create 9002 --name ubuntu-2004 --memory 2048 --net0 virtio,bridge=vmbr0 +qm importdisk 9002 /tmp/focal-server-cloudimg-amd64.img local +qm set 9002 --scsihw virtio-scsi-pci --scsi0 local:9002/vm-9002-disk-0.raw +qm set 9002 --ide0 local:cloudinit +qm set 9002 --boot c --bootdisk scsi0 +qm set 9002 --serial0 socket --vga serial0 +qm set 9002 --cicustom "user=local:snippets/9002_bootstrap" +qm set 9002 --ipconfig0 ip=dhcp,ip6=dhcp + +qm start 9002 +sleep 10;while :; do qm status 9002|grep -q stopped && break; done +qm set 9002 --cicustom "" +qm set 9002 --ipconfig0 "" +qm template 9002 +``` + +### Ubuntu 18.04 + ``` curl -sSL https://cloud-images.ubuntu.com/minimal/releases/bionic/release/ubuntu-18.04-minimal-cloudimg-amd64.img >/tmp/ubuntu-18.04-minimal-cloudimg-amd64.img @@ -84,18 +208,15 @@ ntp: ssh_deletekeys: true ssh_genkeytypes: ["ed25519", "rsa"] runcmd: -- "apt-get -y purge apport htop lxcfs lxd lxd-client motd-news-config mdadm os-prober sosreport" -- "apt-get -y autoremove --purge" -- "apt-get -y clean" -- "bash -c 'rm /home/ubuntu/.ssh/authorized_keys; exit 0'" -- "bash -c 'find /var/log -type f | while read f; do echo -ne >$f; done; exit 0'" -- "bash -c 'rm -rf /var/lib/cloud/*; exit 0'" +- "curl -sSL https://git.giftfish.de/ston1th/cleanup/raw/branch/master/ubuntu_1804.sh >/tmp/cleanup.sh" +- | + bash -c '[ "$(sha256sum /tmp/cleanup.sh|cut -d" " -f1)" = "ecb2ae04cdd6b91fdc56a5db2015f401e474196b8eaaaf8e3aa5dc4b5e4c087d" ] && sh /tmp/cleanup.sh || echo "error: cleanup script hash does not match" >&2' power_state: mode: poweroff EOF -qm destroy 9001 -qm create 9001 --memory 2048 --net0 virtio,bridge=vmbr0 +qm stop 9001; qm destroy 9001 +qm create 9001 --name ubuntu-1804-min --memory 2048 --net0 virtio,bridge=vmbr0 qm importdisk 9001 /tmp/ubuntu-18.04-minimal-cloudimg-amd64.img local qm set 9001 --scsihw virtio-scsi-pci --scsi0 local:9001/vm-9001-disk-0.raw qm set 9001 --ide0 local:cloudinit From a7ec208373d3e7f56450aa7ed3b0081693ae7629 Mon Sep 17 00:00:00 2001 From: ston1th Date: Sat, 11 Dec 2021 19:01:05 +0100 Subject: [PATCH 47/47] added minimal image --- MINIMAL.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ TODO | 1 + 2 files changed, 58 insertions(+) create mode 100644 MINIMAL.md create mode 100644 TODO diff --git a/MINIMAL.md b/MINIMAL.md new file mode 100644 index 0000000..0fa563d --- /dev/null +++ b/MINIMAL.md @@ -0,0 +1,57 @@ +# Minimal Cloud Images + +## Cloud-Init Image + +How to prepare a cloud-init image. + +### Ubuntu 20.04 + +Download the latest version of the base image to one of your proxmox nodes: + +``` +curl -sSL https://cloud-images.ubuntu.com/minimal/releases/focal/release/ubuntu-20.04-minimal-cloudimg-amd64.img >/tmp/ubuntu-20.04-minimal-cloudimg-amd64.img +``` + +Create a cloud-init config to bootstrap the template: +``` +cat <<'EOF'> /var/lib/vz/snippets/9003_bootstrap +#cloud-config +package_update: true +package_upgrade: true +ntp: + enabled: true + servers: ["pool.ntp.org"] + ntp_client: systemd-timesyncd +ssh_deletekeys: true +ssh_genkeytypes: ["ed25519", "rsa"] +bootcmd: +- | + sed -i 's/ENABLED=1/ENABLED=0/' /etc/default/motd-news + systemctl disable motd-news.service motd-news.timer --now +runcmd: +- | + curl -sSL https://git.giftfish.de/ston1th/cleanup/raw/branch/master/ubuntu_2004_min.sh >/tmp/cleanup.sh + [ "$(sha256sum /tmp/cleanup.sh|cut -d" " -f1)" = "9fbd939eb8d1ef6157a61bb984a5045339ce361596b8a99a8847280cc785b726" ] && sh /tmp/cleanup.sh || echo "error: cleanup script hash does not match" >&2 +power_state: + mode: poweroff +EOF +``` + +Create the template VM: +``` +qm stop 9003; qm destroy 9003 +qm create 9003 --name ubuntu-2004-min --memory 2048 --net0 virtio,bridge=vmbr0 +qm importdisk 9003 /tmp/ubuntu-20.04-minimal-cloudimg-amd64.img local +qm set 9003 --scsihw virtio-scsi-pci --scsi0 local:9003/vm-9003-disk-0.raw +qm set 9003 --ide0 local:cloudinit +qm set 9003 --boot c --bootdisk scsi0 +qm set 9003 --serial0 socket --vga serial0 +qm set 9003 --cicustom "user=local:snippets/9003_bootstrap" +qm set 9003 --ipconfig0 ip=dhcp,ip6=dhcp + +qm start 9003 +sleep 10;while :; do qm status 9003|grep -q stopped && break; done +qm set 9003 --cicustom "" +qm set 9003 --ipconfig0 "" +qm template 9003 +``` diff --git a/TODO b/TODO new file mode 100644 index 0000000..e667792 --- /dev/null +++ b/TODO @@ -0,0 +1 @@ +remove 18.04 fat cloud images