From 024f643724a85e9d8132845255e134cec27d4051 Mon Sep 17 00:00:00 2001 From: Ian Eyberg Date: Wed, 8 Mar 2023 08:14:14 -0800 Subject: [PATCH] initial IBM cloud (#1439) * initial ibm cloud support * . --- provider/ibm/ibm.go | 88 +++++++++++ provider/ibm/ibm_image.go | 248 ++++++++++++++++++++++++++++++ provider/ibm/ibm_instance.go | 271 +++++++++++++++++++++++++++++++++ provider/ibm/ibm_networking.go | 154 +++++++++++++++++++ provider/ibm/ibm_store.go | 65 ++++++++ provider/ibm/ibm_volume.go | 29 ++++ provider/provider.go | 4 + 7 files changed, 859 insertions(+) create mode 100644 provider/ibm/ibm.go create mode 100644 provider/ibm/ibm_image.go create mode 100644 provider/ibm/ibm_instance.go create mode 100644 provider/ibm/ibm_networking.go create mode 100644 provider/ibm/ibm_store.go create mode 100644 provider/ibm/ibm_volume.go diff --git a/provider/ibm/ibm.go b/provider/ibm/ibm.go new file mode 100644 index 00000000..adb2390c --- /dev/null +++ b/provider/ibm/ibm.go @@ -0,0 +1,88 @@ +package ibm + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + + "github.com/nanovms/ops/lepton" + "github.com/nanovms/ops/types" +) + +// ProviderName of the cloud platform provider +const ProviderName = "ibm" + +// IBM Provider to interact with IBM infrastructure +type IBM struct { + Storage *Objects + token string + iam string +} + +// NewProvider IBM +func NewProvider() *IBM { + return &IBM{} +} + +// Initialize provider +func (v *IBM) Initialize(config *types.ProviderConfig) error { + v.token = os.Getenv("TOKEN") + if v.token == "" { + return fmt.Errorf("TOKEN is not set") + } + + v.setIAMToken() + return nil +} + +// Token is the return type for a new IAM token. +type Token struct { + AccessToken string `json:"access_token"` +} + +func (v *IBM) setIAMToken() { + + uri := "https://iam.cloud.ibm.com/oidc/token" + + data := url.Values{} + data.Set("apikey", v.token) + data.Set("response_type", "cloud_iam") + data.Set("grant_type", "urn:ibm:params:oauth:grant-type:apikey") + + client := &http.Client{} + r, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode())) + if err != nil { + fmt.Println(err) + } + + r.Header.Add("Content-Type", "application/x-www-form-urlencoded") + r.Header.Add("Accept", "application/json") + + res, err := client.Do(r) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + it := &Token{} + err = json.Unmarshal(body, &it) + if err != nil { + fmt.Println(err) + } + + v.iam = it.AccessToken +} + +// GetStorage returns storage interface for cloud provider +func (v *IBM) GetStorage() lepton.Storage { + return v.Storage +} diff --git a/provider/ibm/ibm_image.go b/provider/ibm/ibm_image.go new file mode 100644 index 00000000..ec9b7ad3 --- /dev/null +++ b/provider/ibm/ibm_image.go @@ -0,0 +1,248 @@ +package ibm + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + + "github.com/dustin/go-humanize" + "github.com/nanovms/ops/lepton" + "github.com/nanovms/ops/log" + "github.com/nanovms/ops/types" + "github.com/olekukonko/tablewriter" +) + +// BuildImage to be upload on IBM +func (v *IBM) BuildImage(ctx *lepton.Context) (string, error) { + c := ctx.Config() + err := lepton.BuildImage(*c) + if err != nil { + return "", err + } + + return v.CustomizeImage(ctx) +} + +// BuildImageWithPackage to upload on IBM. +func (v *IBM) BuildImageWithPackage(ctx *lepton.Context, pkgpath string) (string, error) { + c := ctx.Config() + err := lepton.BuildImageFromPackage(pkgpath, *c) + if err != nil { + return "", err + } + return v.CustomizeImage(ctx) +} + +func (v *IBM) destroyImage(snapshotid string) { +} + +// CreateImage - Creates image on IBM using nanos images +func (v *IBM) CreateImage(ctx *lepton.Context, imagePath string) error { + // also worth gzipping + + icow := imagePath + ".qcow2" + + args := []string{ + "convert", "-f", "raw", "-O", "qcow2", imagePath, icow, + } + + cmd := exec.Command("qemu-img", args...) + out, err := cmd.Output() + if err != nil { + fmt.Println(err) + fmt.Println(out) + } + + store := &Objects{ + token: v.iam, + } + + v.Storage = store + err = v.Storage.CopyToBucket(ctx.Config(), icow) + if err != nil { + return err + } + + imgName := ctx.Config().CloudConfig.ImageName + + v.createImage(ctx, icow, imgName) + return nil +} + +func (v *IBM) createImage(ctx *lepton.Context, icow string, imgName string) { + baseName := filepath.Base(icow) + + c := ctx.Config() + zone := c.CloudConfig.Zone + + region := extractRegionFromZone(zone) + + bucket := c.CloudConfig.BucketName + + uri := "https://" + region + ".iaas.cloud.ibm.com/v1/images?version=2023-02-16&generation=2" + + rgroup := v.getDefaultResourceGroup() + + j := `{ + "name": "` + imgName + `", + "operating_system": { + "name": "ubuntu-18-04-amd64" + }, + "file": { + "href": "cos://` + region + `/` + bucket + `/` + baseName + `" + }, + "resource_group": { + "id": "` + rgroup + `" + } + }` + + reqBody := []byte(j) + + client := &http.Client{} + req, err := http.NewRequest("POST", uri, bytes.NewBuffer(reqBody)) + if err != nil { + fmt.Println(err) + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+v.iam) + req.Header.Add("Accept", "application/json") + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + fmt.Println(string(body)) +} + +// ImageListResponse is the set of instances available from IBM in an +// images list call. +type ImageListResponse struct { + Images []Image `json:"images"` +} + +// Image represents a given IBM image configuration. +type Image struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` +} + +// GetImages return all images on IBM +// needs tags added +func (v *IBM) GetImages(ctx *lepton.Context) ([]lepton.CloudImage, error) { + client := &http.Client{} + + c := ctx.Config() + zone := c.CloudConfig.Zone + + region := extractRegionFromZone(zone) + + uri := "https://" + region + ".iaas.cloud.ibm.com/v1/images?version=2023-02-28&generation=2&visibility=private" + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + fmt.Println(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+v.iam) + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + ilr := &ImageListResponse{} + err = json.Unmarshal(body, &ilr) + if err != nil { + fmt.Println(err) + } + + var images []lepton.CloudImage + + for _, img := range ilr.Images { + images = append(images, lepton.CloudImage{ + ID: img.ID, + Name: img.Name, + Status: img.Status, + Path: "", + }) + } + + return images, nil + +} + +// ListImages lists images on IBM +func (v *IBM) ListImages(ctx *lepton.Context) error { + images, err := v.GetImages(ctx) + if err != nil { + fmt.Println(err) + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"ID", "Name", "Date created", "Size", "Status"}) + table.SetHeaderColor( + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}) + table.SetRowLine(true) + + for _, image := range images { + var row []string + row = append(row, image.ID) + row = append(row, image.Name) + row = append(row, "") + row = append(row, humanize.Bytes(uint64(image.Size))) + row = append(row, image.Status) + table.Append(row) + } + + table.Render() + + return nil +} + +// DeleteImage deletes image from v +func (v *IBM) DeleteImage(ctx *lepton.Context, snapshotID string) error { + return nil +} + +// SyncImage syncs image from provider to another provider +func (v *IBM) SyncImage(config *types.Config, target lepton.Provider, image string) error { + log.Warn("not yet implemented") + return nil +} + +// ResizeImage is not supported on IBM. +func (v *IBM) ResizeImage(ctx *lepton.Context, imagename string, hbytes string) error { + return fmt.Errorf("operation not supported") +} + +// CustomizeImage returns image path with adaptations needed by cloud provider +func (v *IBM) CustomizeImage(ctx *lepton.Context) (string, error) { + imagePath := ctx.Config().RunConfig.ImageName + return imagePath, nil +} diff --git a/provider/ibm/ibm_instance.go b/provider/ibm/ibm_instance.go new file mode 100644 index 00000000..f23bf3fe --- /dev/null +++ b/provider/ibm/ibm_instance.go @@ -0,0 +1,271 @@ +package ibm + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/nanovms/ops/lepton" + + "github.com/olekukonko/tablewriter" +) + +func extractRegionFromZone(zone string) string { + s := strings.Split(zone, "-") + return s[0] + "-" + s[1] +} + +// CreateInstance - Creates instance on IBM Cloud Platform +func (v *IBM) CreateInstance(ctx *lepton.Context) error { + c := ctx.Config() + zone := c.CloudConfig.Zone + + region := extractRegionFromZone(zone) + + imgs, err := v.GetImages(ctx) + if err != nil { + fmt.Println(err) + } + + config := ctx.Config() + + imgID := "" + for i := 0; i < len(imgs); i++ { + if imgs[i].Name == config.CloudConfig.ImageName { + imgID = imgs[i].ID + } + } + if imgID == "" { + return errors.New("can't find image") + } + + uri := "https://" + region + ".iaas.cloud.ibm.com/v1/instances?version=2023-02-28&generation=2" + + vpcID := v.getDefaultVPC(region) + subnetID := v.getDefaultSubnet(region) + + t := time.Now().Unix() + st := strconv.FormatInt(t, 10) + instName := config.CloudConfig.ImageName + "-" + st + + stuff := `{ + "boot_volume_attachment": { + "volume": { + "name": "my-boot-volume", + "profile": { + "name": "general-purpose" + } + } + }, + "image": { + "id": "` + imgID + `" + }, + "name": "` + instName + `", + "primary_network_interface": { + "name": "my-network-interface", + "subnet": { + "id": "` + subnetID + `" + } + }, + "profile": { + "name": "bx2-2x8" + }, + "vpc": { + "id": "` + vpcID + `" + }, + "zone": { + "name": "` + zone + `" + } +}` + + reqBody := []byte(stuff) + + client := &http.Client{} + req, err := http.NewRequest("POST", uri, bytes.NewBuffer(reqBody)) + if err != nil { + fmt.Println(err) + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+v.iam) + req.Header.Add("Accept", "application/json") + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + fmt.Println(string(body)) + } + + return nil +} + +// GetInstanceByName returns instance with given name +func (v *IBM) GetInstanceByName(ctx *lepton.Context, name string) (*lepton.CloudInstance, error) { + return nil, nil +} + +// InstancesListResponse is the set of instances available from IBM in an +// images list call. +type InstancesListResponse struct { + Instances []Instance `json:"instance"` +} + +// Instance represents a virtual server instance. +type Instance struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` +} + +// GetInstances return all instances on IBM +func (v *IBM) GetInstances(ctx *lepton.Context) ([]lepton.CloudInstance, error) { + c := ctx.Config() + zone := c.CloudConfig.Zone + + region := extractRegionFromZone(zone) + + uri := "https://" + region + ".iaas.cloud.ibm.com/v1/instances?version=2023-02-28&generation=2" + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + fmt.Println(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+v.iam) + + client := &http.Client{} + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + ilr := &InstancesListResponse{} + err = json.Unmarshal(body, &ilr) + if err != nil { + fmt.Println(err) + } + + var cloudInstances []lepton.CloudInstance + + for _, instance := range ilr.Instances { + cloudInstances = append(cloudInstances, lepton.CloudInstance{ + ID: instance.ID, + Status: instance.Status, + Created: instance.CreatedAt, + PublicIps: []string{""}, + Image: "", + }) + } + + return cloudInstances, nil + +} + +// ListInstances lists instances on v +func (v *IBM) ListInstances(ctx *lepton.Context) error { + instances, err := v.GetInstances(ctx) + if err != nil { + fmt.Println(err) + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"ID", "MainIP", "Status", "ImageID"}) + table.SetHeaderColor( + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}) + table.SetRowLine(true) + + for _, instance := range instances { + var row []string + row = append(row, instance.ID) + row = append(row, instance.PublicIps[0]) + row = append(row, instance.Status) + row = append(row, instance.Image) /// Os) + table.Append(row) + } + + table.Render() + + return nil +} + +// DeleteInstance deletes instance from IBM +func (v *IBM) DeleteInstance(ctx *lepton.Context, instanceID string) error { + c := ctx.Config() + zone := c.CloudConfig.Zone + + region := extractRegionFromZone(zone) + + uri := "https://" + region + ".iaas.cloud.ibm.com/v1/instances/$instance_id?version=2023-02-28&generation=2" + + client := &http.Client{} + req, err := http.NewRequest("DELETE", uri, nil) + if err != nil { + fmt.Println(err) + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", v.iam) + req.Header.Add("Accept", "application/json") + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + fmt.Println(string(body)) + + return nil +} + +// StartInstance starts an instance in IBM +func (v *IBM) StartInstance(ctx *lepton.Context, instanceID string) error { + return nil +} + +// StopInstance halts instance from v +func (v *IBM) StopInstance(ctx *lepton.Context, instanceID string) error { + // POST https://api.IBM.com/v4/IBM/instances/{IBMId}/shutdown + return nil +} + +// PrintInstanceLogs writes instance logs to console +func (v *IBM) PrintInstanceLogs(ctx *lepton.Context, instancename string, watch bool) error { + return nil +} + +// GetInstanceLogs gets instance related logs +// https://cloud.ibm.com/docs/vpc?topic=vpc-vsi_is_connecting_console&interface=api +func (v *IBM) GetInstanceLogs(ctx *lepton.Context, instancename string) (string, error) { + return "", nil +} diff --git a/provider/ibm/ibm_networking.go b/provider/ibm/ibm_networking.go new file mode 100644 index 00000000..0c3fd88c --- /dev/null +++ b/provider/ibm/ibm_networking.go @@ -0,0 +1,154 @@ +package ibm + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +// VPCListResponse is the response type for the vpc list endpoint. +type VPCListResponse struct { + VPCs []VPC `json:"vpcs"` +} + +// VPC represents a single vpc. +type VPC struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// SubnetResponse is the response type for the subnet list endpoint. +type SubnetResponse struct { + Subnets []Subnet `json:"subnets"` +} + +// Subnet represents a single subnet. +type Subnet struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// SubnetListResponse is the response type for the subnet list endpoint. +type SubnetListResponse struct { + Subnets []Subnet `json:"subnets"` +} + +// ResourceGroupResponse is the response type for the resource group list endpoint. +type ResourceGroupResponse struct { + ResourceGroups []ResourceGroup `json:"resources"` +} + +// ResourceGroup represents a single resource group. +type ResourceGroup struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func (v *IBM) getDefaultResourceGroup() string { + client := &http.Client{} + + uri := "https://resource-controller.cloud.ibm.com/v2/resource_groups" + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + fmt.Println(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+v.iam) + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + ilr := &ResourceGroupResponse{} + err = json.Unmarshal(body, &ilr) + if err != nil { + fmt.Println(err) + } + + rid := "" + for i := 0; i < len(ilr.ResourceGroups); i++ { + if ilr.ResourceGroups[i].Name == "Default" { + return ilr.ResourceGroups[i].ID + } + } + + return rid +} + +func (v *IBM) getDefaultVPC(region string) string { + client := &http.Client{} + + uri := "https://" + region + ".iaas.cloud.ibm.com/v1/vpcs?version=2023-02-28&generation=2" + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + fmt.Println(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+v.iam) + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + ilr := &VPCListResponse{} + err = json.Unmarshal(body, &ilr) + if err != nil { + fmt.Println(err) + } + + //hack + return ilr.VPCs[0].ID +} + +func (v *IBM) getDefaultSubnet(region string) string { + client := &http.Client{} + + uri := "https://" + region + ".iaas.cloud.ibm.com/v1/subnets?version=2023-02-28&generation=2" + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + fmt.Println(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+v.iam) + + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + ilr := &SubnetListResponse{} + err = json.Unmarshal(body, &ilr) + if err != nil { + fmt.Println(err) + } + + //hack + return ilr.Subnets[0].ID +} diff --git a/provider/ibm/ibm_store.go b/provider/ibm/ibm_store.go new file mode 100644 index 00000000..dfa749be --- /dev/null +++ b/provider/ibm/ibm_store.go @@ -0,0 +1,65 @@ +package ibm + +import ( + "bufio" + "fmt" + "github.com/nanovms/ops/types" + "io" + "net/http" + "os" + "path/filepath" +) + +// Objects represent storage specific information for cloud object +// storage. +type Objects struct { + token string +} + +// CopyToBucket copies archive to bucket +func (s *Objects) CopyToBucket(config *types.Config, archPath string) error { + zone := config.CloudConfig.Zone + + region := extractRegionFromZone(zone) + + bucket := config.CloudConfig.BucketName + + baseName := filepath.Base(archPath) + uri := "https://s3." + region + ".cloud-object-storage.appdomain.cloud" + "/" + bucket + "/" + baseName + + f, err := os.Open(archPath) + if err != nil { + fmt.Println(err) + } + + reader := bufio.NewReader(f) + + client := &http.Client{} + r, err := http.NewRequest(http.MethodPut, uri, reader) + if err != nil { + fmt.Println(err) + } + r.Header.Set("Content-Type", "application/octet-stream") + + r.Header.Set("Authorization", "Bearer "+s.token) + + res, err := client.Do(r) + if err != nil { + fmt.Println(err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + } + + fmt.Println(string(body)) + + return nil +} + +// DeleteFromBucket deletes key from config's bucket +func (s *Objects) DeleteFromBucket(config *types.Config, key string) error { + return nil +} diff --git a/provider/ibm/ibm_volume.go b/provider/ibm/ibm_volume.go new file mode 100644 index 00000000..e4599410 --- /dev/null +++ b/provider/ibm/ibm_volume.go @@ -0,0 +1,29 @@ +package ibm + +import "github.com/nanovms/ops/lepton" + +// CreateVolume is a stub to satisfy VolumeService interface +func (v *IBM) CreateVolume(ctx *lepton.Context, name, data, provider string) (lepton.NanosVolume, error) { + var vol lepton.NanosVolume + return vol, nil +} + +// GetAllVolumes is a stub to satisfy VolumeService interface +func (v *IBM) GetAllVolumes(ctx *lepton.Context) (*[]lepton.NanosVolume, error) { + return nil, nil +} + +// DeleteVolume is a stub to satisfy VolumeService interface +func (v *IBM) DeleteVolume(ctx *lepton.Context, name string) error { + return nil +} + +// AttachVolume is a stub to satisfy VolumeService interface +func (v *IBM) AttachVolume(ctx *lepton.Context, image, name string, attachID int) error { + return nil +} + +// DetachVolume is a stub to satisfy VolumeService interface +func (v *IBM) DetachVolume(ctx *lepton.Context, image, name string) error { + return nil +} diff --git a/provider/provider.go b/provider/provider.go index e0c6e304..5ae16f60 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -11,6 +11,7 @@ import ( "github.com/nanovms/ops/provider/digitalocean" "github.com/nanovms/ops/provider/gcp" "github.com/nanovms/ops/provider/hyperv" + "github.com/nanovms/ops/provider/ibm" "github.com/nanovms/ops/provider/linode" "github.com/nanovms/ops/provider/oci" "github.com/nanovms/ops/provider/onprem" @@ -43,6 +44,9 @@ func CloudProvider(providerName string, c *types.ProviderConfig) (lepton.Provide case hyperv.ProviderName: p = hyperv.NewProvider() + case ibm.ProviderName: + p = ibm.NewProvider() + case linode.ProviderName: p = linode.NewProvider()