diff --git a/cmd/cmd_instance.go b/cmd/cmd_instance.go index 6532363d..5f9a9eee 100644 --- a/cmd/cmd_instance.go +++ b/cmd/cmd_instance.go @@ -96,6 +96,8 @@ func instanceCreateCommandHandler(cmd *cobra.Command, args []string) { if err != nil { exitWithError(err.Error()) } + + fmt.Printf("%s instance '%s' created...\n", c.CloudConfig.Platform, c.RunConfig.InstanceName) } func instanceListCommand() *cobra.Command { diff --git a/go.mod b/go.mod index a1e2f17b..6a8988d0 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/docker/distribution v2.8.0+incompatible github.com/docker/docker v20.10.7+incompatible github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.0 github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/go-errors/errors v1.0.1 diff --git a/lepton/helpers.go b/lepton/helpers.go index 9cdfc928..4d0e61ba 100644 --- a/lepton/helpers.go +++ b/lepton/helpers.go @@ -4,6 +4,8 @@ import ( "fmt" "math" "sort" + "strconv" + "strings" "time" ) @@ -16,6 +18,18 @@ const ( Month = 30 * Day Year = 12 * Month LongTime = 37 * Year + + KB = 1000 + MB = 1000 * KB + GB = 1000 * MB + TB = 1000 * GB + PB = 1000 * TB + + KiB = 1024 + MiB = 1024 * KiB + GiB = 1024 * MiB + TiB = 1024 * GiB + PiB = 1024 * TiB ) // Time2Human formats a time into a relative string. @@ -45,6 +59,8 @@ type RelTimeMagnitude struct { DivBy time.Duration } +type unitMap map[byte]int64 + var defaultMagnitudes = []RelTimeMagnitude{ {time.Second, "now", time.Second}, {2 * time.Second, "1 second %s", 1}, @@ -65,6 +81,13 @@ var defaultMagnitudes = []RelTimeMagnitude{ {math.MaxInt64, "a long while %s", 1}, } +var ( + decimalMap = unitMap{'k': KB, 'm': MB, 'g': GB, 't': TB, 'p': PB} + binaryMap = unitMap{'k': KiB, 'm': MiB, 'g': GiB, 't': TiB, 'p': PiB} + decimapAbbrs = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} +) + // RelTime formats a time into a relative string. // // It takes two times and two labels. In addition to the generic time @@ -132,3 +155,77 @@ func Bytes2Human(b int64) string { return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } + +// RAMInBytes parses a human-readable string representing an amount of RAM +// in bytes, kibibytes, mebibytes, gibibytes, or tebibytes and +// returns the number of bytes, or -1 if the string is unparseable. +// Units are case-insensitive, and the 'b' suffix is optional. +func RAMInBytes(size string) (int64, error) { + return parseSize(size, binaryMap) +} + +// Parses the human-readable size string into the amount it represents. +func parseSize(sizeStr string, uMap unitMap) (int64, error) { + // TODO: rewrite to use strings.Cut if there's a space + // once Go < 1.18 is deprecated. + sep := strings.LastIndexAny(sizeStr, "01234567890. ") + if sep == -1 { + // There should be at least a digit. + return -1, fmt.Errorf("invalid size: '%s'", sizeStr) + } + var num, sfx string + if sizeStr[sep] != ' ' { + num = sizeStr[:sep+1] + sfx = sizeStr[sep+1:] + } else { + // Omit the space separator. + num = sizeStr[:sep] + sfx = sizeStr[sep+1:] + } + + size, err := strconv.ParseFloat(num, 64) + if err != nil { + return -1, err + } + // Backward compatibility: reject negative sizes. + if size < 0 { + return -1, fmt.Errorf("invalid size: '%s'", sizeStr) + } + + if len(sfx) == 0 { + return int64(size), nil + } + + // Process the suffix. + + if len(sfx) > 3 { // Too long. + goto badSuffix + } + sfx = strings.ToLower(sfx) + // Trivial case: b suffix. + if sfx[0] == 'b' { + if len(sfx) > 1 { // no extra characters allowed after b. + goto badSuffix + } + return int64(size), nil + } + // A suffix from the map. + if mul, ok := uMap[sfx[0]]; ok { + size *= float64(mul) + } else { + goto badSuffix + } + + // The suffix may have extra "b" or "ib" (e.g. KiB or MB). + switch { + case len(sfx) == 2 && sfx[1] != 'b': + goto badSuffix + case len(sfx) == 3 && sfx[1:] != "ib": + goto badSuffix + } + + return int64(size), nil + +badSuffix: + return -1, fmt.Errorf("invalid suffix: '%s'", sfx) +} diff --git a/proxmox/proxmox_checks.go b/proxmox/proxmox_checks.go index 82418405..266478fc 100644 --- a/proxmox/proxmox_checks.go +++ b/proxmox/proxmox_checks.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" ) type pData struct { @@ -22,12 +23,23 @@ type sData struct { Active int `json:"active"` Enabled int `json:"enabled"` Storage string `json:"storage"` + Content string `json:"content"` } type sDataArr struct { Data []sData `json:"data"` } +type bData struct { + Active int `json:"active"` + Type string `json:"type"` + Iface string `json:"iface"` +} + +type bDataArr struct { + Data []bData `json:"data"` +} + // CheckInit return custom error on {"data": null} or {"data": []} result come from ProxMox API /api2/json/pools func (p *ProxMox) CheckInit() error { @@ -75,7 +87,7 @@ func (p *ProxMox) CheckInit() error { } // CheckStorage return error when not found configured storage or any storages via ProxMox API -func (p *ProxMox) CheckStorage(storage string) error { +func (p *ProxMox) CheckStorage(storage string, stype string) error { var err error @@ -87,6 +99,8 @@ func (p *ProxMox) CheckStorage(storage string) error { enb := errors.New("storage is disabled: " + storage) ect := errors.New("storage is not active: " + storage) ecs := errors.New("not found storage: " + storage) + eim := errors.New("storage is not configured for containing disk images: " + storage) + eis := errors.New("storage is not configured for containing iso images: " + storage) req, err := http.NewRequest("GET", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/storage", &b) if err != nil { @@ -137,6 +151,94 @@ func (p *ProxMox) CheckStorage(storage string) error { return enb } + if stype == "images" { + + if !strings.Contains(v.Content, stype) { + return eim + } + + } else if stype == "iso" { + + if !strings.Contains(v.Content, stype) { + return eis + } + + } else { + return errors.New("unknown type of storage") + } + + return nil + + } + } + + return ecs + +} + +// CheckBridge return error when not found configured bridge any network interfaces via ProxMox API +func (p *ProxMox) CheckBridge(bridge string) error { + + var err error + + var brs bDataArr + + var b bytes.Buffer + + edk := errors.New("no any bridges is configured") + ebr := errors.New("is not a bridge: " + bridge) + ect := errors.New("bridge is not active: " + bridge) + ecs := errors.New("not found bridge: " + bridge) + + req, err := http.NewRequest("GET", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/network", &b) + if err != nil { + return err + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + + req.Header.Add("Authorization", "PVEAPIToken="+p.tokenID+"="+p.secret) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(body, &brs) + if err != nil { + return err + } + + if err == nil && brs.Data == nil { + return edk + } + + debug := false + if debug { + fmt.Println(string(body)) + } + + for _, v := range brs.Data { + + if v.Iface == bridge { + + if v.Active != 1 { + return ect + } + + if v.Type != "bridge" { + return ebr + } + return nil } @@ -168,7 +270,7 @@ func (p *ProxMox) CheckResult(body []byte) error { } // CheckResultType return error or custom error based on type of check, when {"data": null} or {"data": []} result come from ProxMox API -func (p *ProxMox) CheckResultType(body []byte, rtype string) error { +func (p *ProxMox) CheckResultType(body []byte, rtype string, rname string) error { var err error @@ -177,17 +279,17 @@ func (p *ProxMox) CheckResultType(body []byte, rtype string) error { switch rtype { case "createimage": - ecs = errors.New("can not create disk image in 'local' storage, " + edef) + ecs = errors.New("can not create disk image in " + rname + " storage, " + edef) case "listimages": - ecs = errors.New("no disk images found in 'local' storage or " + edef) + ecs = errors.New("no disk images found in " + rname + " storage or " + edef) case "createinstance": - ecs = errors.New("can not create machine instance with disk image from 'local' storage, " + edef) + ecs = errors.New("can not create machine instance with disk image from " + rname + " image/storage, " + edef) case "getnextid": ecs = errors.New("can not get next id, " + edef) case "movdisk": - ecs = errors.New("can not move iso to raw disk on 'local-lvm' storage, " + edef) + ecs = errors.New("can not move iso to raw disk on " + rname + " storage, " + edef) case "addvirtiodisk": - ecs = errors.New("can not add virtio disk from 'local' storage, " + edef) + ecs = errors.New("can not add virtio disk from " + rname + " storage, " + edef) case "bootorderset": ecs = errors.New("can not set boot order, " + edef) } diff --git a/proxmox/proxmox_image.go b/proxmox/proxmox_image.go index 6e76f083..0435efb5 100644 --- a/proxmox/proxmox_image.go +++ b/proxmox/proxmox_image.go @@ -65,9 +65,15 @@ func (p *ProxMox) CreateImage(ctx *lepton.Context, imagePath string) error { c := ctx.Config() - imageName := c.CloudConfig.ImageName + imageName := c.ProxmoxConfig.ImageName - err = p.CheckStorage("local") + isoStorageName := c.ProxmoxConfig.IsoStorageName + + if isoStorageName == "" { + isoStorageName = "local" + } + + err = p.CheckStorage(isoStorageName, "iso") if err != nil { return err } @@ -105,7 +111,7 @@ func (p *ProxMox) CreateImage(ctx *lepton.Context, imagePath string) error { w.Close() - req, err := http.NewRequest("POST", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/storage/local/upload", &b) + req, err := http.NewRequest("POST", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/storage/"+isoStorageName+"/upload", &b) if err != nil { fmt.Println(err) return err @@ -131,7 +137,7 @@ func (p *ProxMox) CreateImage(ctx *lepton.Context, imagePath string) error { return err } - err = p.CheckResultType(body, "createimage") + err = p.CheckResultType(body, "createimage", isoStorageName) if err != nil { return err } @@ -163,7 +169,22 @@ type ImageInfo struct { // ListImages lists images on ProxMox func (p *ProxMox) ListImages(ctx *lepton.Context) error { - req, err := http.NewRequest("GET", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/storage/local/content", nil) + var err error + + c := ctx.Config() + + isoStorageName := c.ProxmoxConfig.StorageName + + if isoStorageName == "" { + isoStorageName = "local" + } + + err = p.CheckStorage(isoStorageName, "iso") + if err != nil { + return err + } + + req, err := http.NewRequest("GET", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/storage/"+isoStorageName+"/content", nil) if err != nil { fmt.Println(err) return err @@ -187,7 +208,7 @@ func (p *ProxMox) ListImages(ctx *lepton.Context) error { return err } - err = p.CheckResultType(body, "listimages") + err = p.CheckResultType(body, "listimages", isoStorageName) if err != nil { return err } diff --git a/proxmox/proxmox_instance.go b/proxmox/proxmox_instance.go index edca4fcc..a247008a 100644 --- a/proxmox/proxmox_instance.go +++ b/proxmox/proxmox_instance.go @@ -43,7 +43,7 @@ func (p *ProxMox) getNextID() string { fmt.Println(err) } - err = p.CheckResultType(body, "getnextid") + err = p.CheckResultType(body, "getnextid", "") if err != nil { return "" } @@ -56,16 +56,126 @@ func (p *ProxMox) getNextID() string { // CreateInstance - Creates instance on Proxmox. func (p *ProxMox) CreateInstance(ctx *lepton.Context) error { + + var err error + config := ctx.Config() nextid := p.getNextID() - imageName := config.CloudConfig.ImageName + instanceName := config.RunConfig.InstanceName + + imageName := config.ProxmoxConfig.ImageName + + archType := config.ProxmoxConfig.Arch + machineType := config.ProxmoxConfig.Machine + socketsNum := config.ProxmoxConfig.Sockets + coresNum := config.ProxmoxConfig.Cores + numaStr := strconv.FormatBool(config.ProxmoxConfig.Numa) + memoryHmn := config.ProxmoxConfig.Memory + + storageName := config.ProxmoxConfig.StorageName + isoStorageName := config.ProxmoxConfig.IsoStorageName + bridgeName := config.ProxmoxConfig.BridgeName + bridgeName0 := config.ProxmoxConfig.BridgeName0 + onbootStr := strconv.FormatBool(config.ProxmoxConfig.Onboot) + protectionStr := strconv.FormatBool(config.ProxmoxConfig.Protection) + + // Check ProxMox configuration options + + if archType == "" { + archType = "x86_64" + } + + if socketsNum == 0 { + socketsNum = 1 + } + + socketsStr := strconv.FormatInt(int64(socketsNum), 10) + + if coresNum == 0 { + coresNum = 1 + } + + coresStr := strconv.FormatInt(int64(coresNum), 10) + + if numaStr == "true" { + numaStr = "1" + } else { + numaStr = "0" + } + + if memoryHmn == "" { + memoryHmn = "512M" + } + + memoryInt, err := lepton.RAMInBytes(memoryHmn) + if err != nil { + return err + } + + memoryInt = memoryInt / 1024 / 1024 + + memoryStr := strconv.FormatInt(memoryInt, 10) + + if storageName == "" { + storageName = "local-lvm" + } + + if isoStorageName == "" { + isoStorageName = "local" + } + + if bridgeName0 == "" { + if bridgeName != "" { + bridgeName0 = bridgeName + } else { + bridgeName0 = "vmbr0" + } + } + + err = p.CheckStorage(storageName, "images") + if err != nil { + return err + } + + err = p.CheckStorage(isoStorageName, "iso") + if err != nil { + return err + } + + err = p.CheckBridge(bridgeName0) + if err != nil { + return err + } + + if onbootStr == "true" { + onbootStr = "1" + } else { + onbootStr = "0" + } + + if protectionStr == "true" { + protectionStr = "1" + } else { + protectionStr = "0" + } data := url.Values{} data.Set("vmid", nextid) - data.Set("name", imageName) - data.Set("net0", "model=virtio,bridge=vmbr0") + data.Set("name", instanceName) + // Not work correctly through ProxMox API (Uses auto detecting by ProxMox) + // data.Set("arch", archType) + if machineType != "" { + data.Set("machine", machineType) + } + data.Set("sockets", socketsStr) + data.Set("cores", coresStr) + data.Set("numa", numaStr) + data.Set("memory", memoryStr) + data.Set("net0", "model=virtio,bridge="+bridgeName0) + data.Set("onboot", onbootStr) + data.Set("protection", protectionStr) req, err := http.NewRequest("POST", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/qemu", bytes.NewBufferString(data.Encode())) if err != nil { @@ -91,37 +201,35 @@ func (p *ProxMox) CreateInstance(ctx *lepton.Context) error { return err } - err = p.CheckResultType(body, "createinstance") + debug := false + if debug { + fmt.Println(string(body)) + } + + err = p.CheckResultType(body, "createinstance", "file="+isoStorageName+":iso/"+imageName+".iso") if err != nil { return err } - err = p.addVirtioDisk(ctx, nextid, imageName) + err = p.addVirtioDisk(ctx, nextid, imageName, isoStorageName) if err != nil { return err } - err = p.movDisk(ctx, nextid, imageName) + err = p.movDisk(ctx, nextid, imageName, storageName) return err } -func (p *ProxMox) movDisk(ctx *lepton.Context, vmid string, imageName string) error { - - var err error +func (p *ProxMox) movDisk(ctx *lepton.Context, vmid string, imageName string, storageName string) error { data := url.Values{} data.Set("disk", "virtio0") data.Set("node", p.nodeNAME) data.Set("format", "raw") - data.Set("storage", "local-lvm") + data.Set("storage", storageName) data.Set("vmid", vmid) - err = p.CheckStorage("local-lvm") - if err != nil { - return err - } - req, err := http.NewRequest("POST", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/qemu/"+vmid+"/move_disk", bytes.NewBufferString(data.Encode())) if err != nil { fmt.Println(err) @@ -146,7 +254,7 @@ func (p *ProxMox) movDisk(ctx *lepton.Context, vmid string, imageName string) er return err } - err = p.CheckResultType(body, "movdisk") + err = p.CheckResultType(body, "movdisk", storageName) if err != nil { return err } @@ -159,11 +267,12 @@ func (p *ProxMox) movDisk(ctx *lepton.Context, vmid string, imageName string) er return nil } -func (p *ProxMox) addVirtioDisk(ctx *lepton.Context, vmid string, imageName string) error { +func (p *ProxMox) addVirtioDisk(ctx *lepton.Context, vmid string, imageName string, isoStorageName string) error { + data := url.Values{} // attach disk - data.Set("virtio0", "file=local:iso/"+imageName+".iso") + data.Set("virtio0", "file="+isoStorageName+":iso/"+imageName+".iso") req, err := http.NewRequest("POST", p.apiURL+"/api2/json/nodes/"+p.nodeNAME+"/qemu/"+vmid+"/config", bytes.NewBufferString(data.Encode())) if err != nil { @@ -189,7 +298,7 @@ func (p *ProxMox) addVirtioDisk(ctx *lepton.Context, vmid string, imageName stri return err } - err = p.CheckResultType(body, "addvirtiodisk") + err = p.CheckResultType(body, "addvirtiodisk", isoStorageName) if err != nil { return err } @@ -216,7 +325,7 @@ func (p *ProxMox) addVirtioDisk(ctx *lepton.Context, vmid string, imageName stri return err } - err = p.CheckResultType(body, "bootorderset") + err = p.CheckResultType(body, "bootorderset", "") if err != nil { return err } diff --git a/types/config.go b/types/config.go index d66c9d62..de2f7557 100644 --- a/types/config.go +++ b/types/config.go @@ -80,6 +80,9 @@ type Config struct { // attach/detach. ProgramPath string + // ProxmoxConfig configures various attributes about the ProxMox provider. + ProxmoxConfig ProxmoxConfig + // RebootOnExit defines whether the image should automatically reboot // if an error/failure occurs. RebootOnExit bool @@ -118,6 +121,7 @@ type Config struct { // ProviderConfig give provider details type ProviderConfig struct { + // BucketName specifies the bucket to store the ops built image artifacts. BucketName string `cloud:"bucketname"` @@ -174,6 +178,52 @@ type ProviderConfig struct { Zone string `cloud:"zone"` } +// ProxmoxConfig give provider details +type ProxmoxConfig struct { + + // Arch specifies the type of CPU architecture + Arch string `cloud:"arch"` + + // Cores of CPU + Cores uint `cloud:"cores"` + + // Machine specifies the type of machine + Machine string `cloud:"machine"` + + // Memory + Memory string `cloud:"memory"` + + // Numa + Numa bool `cloud:"numa"` + + // BridgeName specifies the name of first bridge interface + BridgeName string `cloud:"bridgename"` + + // BridgeName0 (alias for BridgeName) specifies the name of first bridge interface + BridgeName0 string `cloud:"bridgename0"` + + // BridgeName1 (secondary interface) specifies the name of first bridge interface (Not used yet) + BridgeName1 string `cloud:"bridgename1"` + + // ImageName + ImageName string `cloud:"imagename"` + + // IsoStorageName is used for upload intermediate iso images via ProxMox API + IsoStorageName string `cloud:"isostoragename"` + + // Onboot is used to define automatic startup option for instance (Used only for ProxMox yet) + Onboot bool `cloud:"onboot"` + + // Protection is used to define vm/image protection for instance (Used only for ProxMox yet) + Protection bool `cloud:"protection"` + + // Sockets of CPUs + Sockets uint `cloud:"sockets"` + + // StorageName is used for create bootable raw image for instance via ProxMox API from iso image + StorageName string `cloud:"storagename"` +} + // Tag is used as property on creating instances type Tag struct { // Key