diff --git a/README.md b/README.md index 03637156e..5005514fd 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Currently supported platforms are: - [Amazon Web Services](docs/platform-aws.md) - [Google Cloud](docs/platform-gcp.md) - [Microsoft Azure](docs/platform-azure.md) + - [OpenStack](docs/platform-openstack.md) - [packet.net](docs/platform-packet.md) diff --git a/docs/platform-openstack.md b/docs/platform-openstack.md new file mode 100644 index 000000000..303ff8606 --- /dev/null +++ b/docs/platform-openstack.md @@ -0,0 +1,53 @@ +# LinuxKit with OpenStack + +LinuxKit interacts with OpenStack through its native APIs and requires access +to both an OpenStack Keystone server for authentication and a OpenStack Glance +server in order to host the LinuxKit images. + +Supported (Tested) Versions: + +- OpenStack Ocata Release +- Keystone v3 API +- Glance v2 API + +##Push + +### Image types supported: +- **ami** (Amazon Machine image) +- **vhd** (Hyper-V) +- **vhdx** (Hyper-V) +- **vmdk** (VMware Disk) +- **raw** (Raw disk image) +- **qcow2** (Qemu disk image) +- **iso** (ISO9660 compatible CD-ROM image) + +A compatible/supported image needs to have the correct extension (must match +one from above) in order to be supported by the `linuxkit push openstack` +command. The `openstack` backend will use the filename extension to determine +the image type, and use the filename as a label for the new image. + +The `openstack` backend also supports OpenStack projects to provide +multi-tenancy support when uploading images. + +### Usage + +The `openstack` backend uses the password authentication method in order to +retrieve a token that can be used to interact with the various components of +OpenStack. The URLs for the Keystone/Glance server components need to have +the ports detailed as below. + +``` +./linuxkit push openstack \ +-keystoneAddr=http://keystone.com:5000 \ +-username=admin \ +-password=XXXXXXXXXXX \ +-project=linuxkit \ +-glanceAddr=http://glance.com:9292 \ +./linuxkit.iso +``` + +### Execution Flow +1. Log in to OpenStack (Keystone) +2. Retrieve the OpenStack Key from the response header +3. Create a "queued" image on the glance server and return the new image ID +4. Use the new image ID and upload the LinuxKit image under this new ID diff --git a/src/cmd/linuxkit/push.go b/src/cmd/linuxkit/push.go index 78cf59675..8d40939b4 100644 --- a/src/cmd/linuxkit/push.go +++ b/src/cmd/linuxkit/push.go @@ -17,6 +17,7 @@ func pushUsage() { fmt.Printf(" aws\n") fmt.Printf(" azure\n") fmt.Printf(" gcp\n") + fmt.Printf(" openstack\n") fmt.Printf(" vcenter\n") fmt.Printf("\n") fmt.Printf("'options' are the backend specific options.\n") @@ -38,6 +39,8 @@ func push(args []string) { pushAzure(args[1:]) case "gcp": pushGcp(args[1:]) + case "openstack": + pushOpenstack(args[1:]) case "vcenter": pushVCenter(args[1:]) case "help", "-h", "-help", "--help": diff --git a/src/cmd/linuxkit/push_openstack.go b/src/cmd/linuxkit/push_openstack.go new file mode 100644 index 000000000..a0f403166 --- /dev/null +++ b/src/cmd/linuxkit/push_openstack.go @@ -0,0 +1,250 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" + + log "github.com/Sirupsen/logrus" +) + +// KeyStoneV3 for OpenStack login +type KeyStoneV3 struct { + Auth struct { + Identity struct { + Methods []string `json:"methods"` + Password struct { + User struct { + Domain struct { + Name string `json:"name"` + } `json:"domain"` + Name string `json:"name"` + Password string `json:"password"` + } `json:"user"` + } `json:"password"` + } `json:"identity"` + Scope struct { + Project struct { + Domain struct { + Name string `json:"name"` + } `json:"domain"` + Name string `json:"name"` + } `json:"project"` + } `json:"scope"` + } `json:"auth"` +} + +// GlanceV2Image - the struct for uploading of images +type GlanceV2Image struct { + ContainerFormat string `json:"container_format"` + DiskFormat string `json:"disk_format"` + Name string `json:"name"` +} + +// GlanceV2ImageResponse - the struct for uploading of images +type GlanceV2ImageResponse struct { + Status string `json:"status"` + Name string `json:"name"` + Tags []interface{} `json:"tags"` + ContainerFormat string `json:"container_format"` + CreatedAt time.Time `json:"created_at"` + Size interface{} `json:"size"` + DiskFormat string `json:"disk_format"` + UpdatedAt time.Time `json:"updated_at"` + Visibility string `json:"visibility"` + Locations []interface{} `json:"locations"` + Self string `json:"self"` + MinDisk int `json:"min_disk"` + Protected bool `json:"protected"` + ID string `json:"id"` + File string `json:"file"` + Checksum interface{} `json:"checksum"` + Owner string `json:"owner"` + VirtualSize interface{} `json:"virtual_size"` + MinRAM int `json:"min_ram"` + Schema string `json:"schema"` +} + +// Process the run arguments and execute run +func pushOpenstack(args []string) { + flags := flag.NewFlagSet("openstack", flag.ExitOnError) + invoked := filepath.Base(os.Args[0]) + flags.Usage = func() { + fmt.Printf("USAGE: %s push openstack [options] path\n\n", invoked) + fmt.Printf("'path' is the full path to an image that will be uploaded to an OpenStack Image store (glance)\n") + fmt.Printf("Options:\n\n") + flags.PrintDefaults() + } + usernameFlag := flags.String("username", "", "Username with permissions to upload image") + passwordFlag := flags.String("password", "", "Password for the Username") + userDomainFlag := flags.String("userDomain", "Default", "") + + projectName := flags.String("project", "", "Name of the Project to be used") + projectDomain := flags.String("projectDomain", "Default", "") + + keystoneAddress := flags.String("keystoneAddr", "", "The hostname/address of the keystone server to AUTH against, including port(5000)") + glanceAddress := flags.String("glanceAddr", "", "The hostname/address of the glance server, including port(9292)") + + imageName := flags.String("imageName", "", "A Unique name for the image, if blank the filename will be used") + + if err := flags.Parse(args); err != nil { + log.Fatal("Unable to parse args") + } + + remArgs := flags.Args() + if len(remArgs) == 0 { + fmt.Printf("Please specify the path to the image to push\n") + flags.Usage() + os.Exit(1) + } + filePath := remArgs[0] + // Check that the file both exists, and can be read + checkFile(filePath) + + var data KeyStoneV3 + // Defaulting to password login, other login methods may be added later + data.Auth.Identity.Methods = append(data.Auth.Identity.Methods, "password") + + data.Auth.Identity.Password.User.Name = *usernameFlag + data.Auth.Identity.Password.User.Domain.Name = *userDomainFlag + data.Auth.Identity.Password.User.Password = *passwordFlag + data.Auth.Scope.Project.Domain.Name = *projectDomain + data.Auth.Scope.Project.Name = *projectName + + payloadBytes, err := json.Marshal(data) + if err != nil { + log.Fatalf("Error building JSON: %v", err) + } + body := bytes.NewReader(payloadBytes) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/v3/auth/tokens", *keystoneAddress), body) + if err != nil { + log.Fatalf("Error Creating HTTP Request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("Return Code: %s Error:%v", resp.Status, err) + } + defer resp.Body.Close() + if resp.StatusCode == 201 { // OK + // Find the OpenStack KeyStone Token + openstackToken := resp.Header.Get("x-subject-token") + + log.Debugf("Token => %s", openstackToken) + + if openstackToken == "" { + log.Fatalln("Error: Can't locate OpenStack Token in Headers") + } + // Create a new Image (which will be left in a queued state) + imageID := createOpenStackImage(filePath, *imageName, *glanceAddress, openstackToken) + // Take the returned ImageID and upload our image to the created ID + uploadOpenStackImage(filePath, *glanceAddress, openstackToken, imageID) + } else { + message, _ := ioutil.ReadAll(resp.Body) + log.Fatalf("Error authenticating with OpenStack, Error Details:\n%s", string(message)) + } +} + +func createOpenStackImage(filePath string, name string, glanceAddress string, token string) string { + // Currently supported image formats that are both supported by LinuxKit and OpenStack Glance V2 + formats := []string{"ami", "vhd", "vhdx", "vmdk", "raw", "qcow2", "iso"} + + // Find extension of the filename and remove the leading stop + fileExtension := strings.Replace(path.Ext(filePath), ".", "", -1) + fileName := strings.TrimSuffix(path.Base(filePath), filepath.Ext(filePath)) + // Check for Supported extension + var supportedExtension bool + supportedExtension = false + for i := 0; i < len(formats); i++ { + if strings.ContainsAny(fileExtension, formats[i]) { + supportedExtension = true + } + } + + if supportedExtension == false { + log.Fatalf("Extension [%s] is not supported", fileExtension) + } + + var image GlanceV2Image + image.ContainerFormat = "bare" + image.DiskFormat = fileExtension + if name == "" { + image.Name = fileName + } else { + image.Name = name + } + + payloadBytes, err := json.Marshal(image) + if err != nil { + log.Fatalf("Error building JSON: %v", err) + } + + body := bytes.NewReader(payloadBytes) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/v2/images", glanceAddress), body) + if err != nil { + log.Fatalf("Error Creating HTTP Request:%v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Auth-Token", token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("Return Code: %s Error:%v", resp.Status, err) + } + defer resp.Body.Close() + var responseJSON GlanceV2ImageResponse + if resp.StatusCode == 201 { // OK + bodyBytes, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + log.Fatalf("%v", readErr) + } + + err = json.Unmarshal(bodyBytes, &responseJSON) + if err != nil { + log.Fatalf("%v", err) + } + log.Debugf("New Image ID=> %s", responseJSON.ID) + + } else { + message, _ := ioutil.ReadAll(resp.Body) + log.Fatalf("Error creating new Image [%s], Error: %s", filePath, string(message)) + } + return responseJSON.ID +} + +func uploadOpenStackImage(filePath string, glanceAddress string, token string, imageID string) { + + f, err := os.Open(filePath) + if err != nil { + panic(err) + } + defer f.Close() + + req, err := http.NewRequest("PUT", fmt.Sprintf("%s/v2/images/%s/file", glanceAddress, imageID), f) + if err != nil { + log.Fatalf("Error Creating HTTP Request\n%v", err) + } + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("X-Auth-Token", token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("Return Code: %sError:%v", resp.Status, err) + } + defer resp.Body.Close() + if resp.StatusCode == 204 { // OK + log.Infof("Succesfully uploaded [%s]", filePath) + } else { + message, _ := ioutil.ReadAll(resp.Body) + log.Fatalf("Error uploading [%s] Error:%s", filePath, string(message)) + } +}