Add Scaleway support for linuxkit command line tool

Signed-off-by: Patrik Cyvoct <patrik@ptrk.io>
This commit is contained in:
Patrik Cyvoct 2018-06-14 11:07:22 +02:00
parent f8d399490e
commit a6783261f3
No known key found for this signature in database
GPG Key ID: D9105E724B0143B2
5 changed files with 727 additions and 0 deletions

View File

@ -19,6 +19,7 @@ func pushUsage() {
fmt.Printf(" gcp\n")
fmt.Printf(" openstack\n")
fmt.Printf(" packet\n")
fmt.Printf(" scaleway\n")
fmt.Printf(" vcenter\n")
fmt.Printf("\n")
fmt.Printf("'options' are the backend specific options.\n")
@ -44,6 +45,8 @@ func push(args []string) {
pushOpenstack(args[1:])
case "packet":
pushPacket(args[1:])
case "scaleway":
pushScaleway(args[1:])
case "vcenter":
pushVCenter(args[1:])
case "help", "-h", "-help", "--help":

View File

@ -0,0 +1,104 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
)
func pushScaleway(args []string) {
flags := flag.NewFlagSet("scaleway", flag.ExitOnError)
invoked := filepath.Base(os.Args[0])
flags.Usage = func() {
fmt.Printf("USAGE: %s push scaleway [options] path\n\n", invoked)
fmt.Printf("'path' is the full path to an EFI ISO image. It will be copied to a new Scaleway instance in order to create a Scaeway image out of it.\n")
fmt.Printf("Options:\n\n")
flags.PrintDefaults()
}
nameFlag := flags.String("img-name", "", "Overrides the name used to identify the image name in Scaleway's images. Defaults to the base of 'path' with the '.iso' suffix removed")
tokenFlag := flags.String("token", "", "Token to connet to Scaleway API")
sshKeyFlag := flags.String("ssh-key", os.Getenv("HOME")+"/.ssh/id_rsa", "SSH key file")
instanceIDFlag := flags.String("instance-id", "", "Instance ID of a running Scaleway instance, with a second volume.")
deviceNameFlag := flags.String("device-name", "/dev/vdb", "Device name on which the image will be copied")
regionFlag := flags.String("region", defaultScalewayRegion, "Select scaleway region")
noCleanFlag := flags.Bool("no-clean", false, "Do not remove temporary instance and volumes")
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)
}
path := remArgs[0]
name := getStringValue(scalewayNameVar, *nameFlag, "")
token := getStringValue(tokenVar, *tokenFlag, "")
sshKeyFile := getStringValue(sshKeyVar, *sshKeyFlag, "")
instanceID := getStringValue(instanceIDVar, *instanceIDFlag, "")
deviceName := getStringValue(deviceNameVar, *deviceNameFlag, "")
region := getStringValue(regionVar, *regionFlag, defaultScalewayRegion)
const suffix = ".iso"
if name == "" {
name = strings.TrimSuffix(path, suffix)
name = filepath.Base(name)
}
client, err := NewScalewayClient(token, region)
if err != nil {
log.Fatalf("Unable to connect to Scaleway: %v", err)
}
// if no instanceID is provided, we create the instance
if instanceID == "" {
instanceID, err = client.CreateInstance()
if err != nil {
log.Fatalf("Error creating a Scaleway instance: %v", err)
}
err = client.BootInstanceAndWait(instanceID)
if err != nil {
log.Fatalf("Error booting instance: %v", err)
}
}
volumeID, err := client.GetSecondVolumeID(instanceID)
if err != nil {
log.Fatalf("Error retrieving second volume ID: %v", err)
}
err = client.CopyImageToInstance(instanceID, path, sshKeyFile)
if err != nil {
log.Fatalf("Error copying ISO file to Scaleway's instance: %v", err)
}
err = client.WriteImageToVolume(instanceID, deviceName)
if err != nil {
log.Fatalf("Error writing ISO file to additional volume: %v", err)
}
err = client.TerminateInstance(instanceID)
if err != nil {
log.Fatalf("Error terminating Scaleway's instance: %v", err)
}
err = client.CreateScalewayImage(instanceID, volumeID, name)
if err != nil {
log.Fatalf("Error creating Scaleway image: %v", err)
}
if !*noCleanFlag {
err = client.DeleteInstanceAndVolumes(instanceID)
if err != nil {
log.Fatalf("Error deleting Scaleway instance and volumes: %v")
}
}
}

View File

@ -25,6 +25,7 @@ func runUsage() {
fmt.Printf(" openstack\n")
fmt.Printf(" packet\n")
fmt.Printf(" qemu [linux]\n")
fmt.Printf(" scaleway\n")
fmt.Printf(" vbox\n")
fmt.Printf(" vcenter\n")
fmt.Printf(" vmware\n")
@ -62,6 +63,8 @@ func run(args []string) {
runPacket(args[1:])
case "qemu":
runQemu(args[1:])
case "scaleway":
runScaleway(args[1:])
case "vmware":
runVMware(args[1:])
case "vbox":

View File

@ -0,0 +1,94 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
)
const (
defaultScalewayInstanceType = "VC1S"
defaultScalewayRegion = "par1"
scalewayNameVar = "SCW_IMAGE_NAME" // non-standard
tokenVar = "SCW_TOKEN" // non-standard
sshKeyVar = "SCW_SSH_KEY_FILE" // non-standard
instanceIDVar = "SCW_INSTANCE_ID" // non-standard
deviceNameVar = "SCW_DEVICE_NAME" // non-standard
regionVar = "SCW_TARGET_REGION"
instanceTypeVar = "SCW_RUN_TYPE" // non-standard
)
func runScaleway(args []string) {
flags := flag.NewFlagSet("scaleway", flag.ExitOnError)
invoked := filepath.Base(os.Args[0])
flags.Usage = func() {
fmt.Printf("USAGE: %s run scaleway [options] [name]\n\n", invoked)
fmt.Printf("'name' is the name of a Scaleway image that has alread \n")
fmt.Printf("been uploaded using 'linuxkit push'\n\n")
fmt.Printf("Options:\n\n")
flags.PrintDefaults()
}
instanceTypeFlag := flags.String("instance-type", defaultScalewayInstanceType, "Scaleway instance type")
instanceNameFlag := flags.String("instance-name", "linuxkit", "Name of the create instance, default to the image name")
tokenFlag := flags.String("token", "", "Token to connect to Scaleway API")
regionFlag := flags.String("region", defaultScalewayRegion, "Select Scaleway region")
cleanFlag := flags.Bool("clean", false, "Remove instance")
noAttachFlag := flags.Bool("no-attach", false, "Don't attach to serial port, you will have to connect to instance manually")
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 name of the image to boot\n")
flags.Usage()
os.Exit(1)
}
name := remArgs[0]
instanceType := getStringValue(instanceTypeVar, *instanceTypeFlag, defaultScalewayInstanceType)
instanceName := getStringValue("", *instanceNameFlag, name)
token := getStringValue(tokenVar, *tokenFlag, "")
region := getStringValue(regionVar, *regionFlag, defaultScalewayRegion)
client, err := NewScalewayClient(token, region)
if err != nil {
log.Fatalf("Unable to connect to Scaleway: %v", err)
}
instanceID, err := client.CreateLinuxkitInstance(instanceName, name, instanceType)
if err != nil {
log.Fatalf("Unable to create Scaleway instance: %v", err)
}
err = client.BootInstance(instanceID)
if err != nil {
log.Fatalf("Unable to boot Scaleway instance: %v", err)
}
if !*noAttachFlag {
err = client.ConnectSerialPort(instanceID)
if err != nil {
log.Fatalf("Unable to connect to serial port: %v", err)
}
}
if *cleanFlag {
err = client.TerminateInstance(instanceID)
if err != nil {
log.Fatalf("Unable to stop instance: %v", err)
}
err = client.DeleteInstanceAndVolumes(instanceID)
if err != nil {
log.Fatalf("Unable to delete instance: %v", err)
}
}
}

View File

@ -0,0 +1,523 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"
"time"
gotty "github.com/moul/gotty-client"
scw "github.com/scaleway/go-scaleway"
"github.com/scaleway/go-scaleway/logger"
"github.com/scaleway/go-scaleway/types"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
// ScalewayClient contains state required for communication with Scaleway as well as the instance
type ScalewayClient struct {
api *scw.ScalewayAPI
fileName string
region string
sshConfig *ssh.ClientConfig
}
// ScalewayConfig contains required field to read scaleway config file
type ScalewayConfig struct {
Organization string `json:"organization"`
Token string `json:"token"`
Version string `json:"version"`
}
// NewScalewayClient creates a new scaleway client
func NewScalewayClient(token, region string) (*ScalewayClient, error) {
log.Debugf("Connecting to Scaleway")
organization := ""
if token == "" {
log.Debugf("Using .scwrc file to get token")
homeDir := os.Getenv("HOME")
if homeDir == "" {
homeDir = os.Getenv("USERPROFILE") // Windows support
}
if homeDir == "" {
return nil, fmt.Errorf("Home directory not found")
}
swrcPath := filepath.Join(homeDir, ".scwrc")
file, err := ioutil.ReadFile(swrcPath)
if err != nil {
return nil, fmt.Errorf("Error reading Scaleway config file: %v", err)
}
var scalewayConfig ScalewayConfig
err = json.Unmarshal(file, &scalewayConfig)
if err != nil {
return nil, fmt.Errorf("Error during unmarshal of Scaleway config file: %v", err)
}
token = scalewayConfig.Token
organization = scalewayConfig.Organization
}
api, err := scw.NewScalewayAPI(organization, token, "", region)
if err != nil {
return nil, err
}
l := logger.NewDisableLogger()
api.Logger = l
if organization == "" {
organisations, err := api.GetOrganization()
if err != nil {
return nil, err
}
api.Organization = organisations.Organizations[0].ID
}
client := &ScalewayClient{
api: api,
fileName: "",
region: region,
}
return client, nil
}
// CreateInstance create an instance with one additional volume
func (s *ScalewayClient) CreateInstance() (string, error) {
// get the Ubuntu Xenial image id
image, err := s.api.GetImageID("Ubuntu Xenial", "x86_64") // TODO fix arch and use from args
if err != nil {
return "", err
}
imageID := image.Identifier
var serverDefinition types.ScalewayServerDefinition
serverDefinition.Name = "linuxkit-builder"
serverDefinition.Image = &imageID
serverDefinition.CommercialType = "VC1M" // TODO use args?
// creation of second volume
var volumeDefinition types.ScalewayVolumeDefinition
volumeDefinition.Name = "linuxkit-builder-volume"
volumeDefinition.Size = 50000000000 // FIX remove hardcoded value
volumeDefinition.Type = "l_ssd"
log.Debugf("Creating volume on Scaleway")
volumeID, err := s.api.PostVolume(volumeDefinition)
if err != nil {
return "", err
}
serverDefinition.Volumes = make(map[string]string)
serverDefinition.Volumes["1"] = volumeID
serverID, err := s.api.PostServer(serverDefinition)
if err != nil {
return "", err
}
log.Debugf("Created server %s on Scaleway", serverID)
return serverID, nil
}
// GetSecondVolumeID returns the ID of the second volume of the server
func (s *ScalewayClient) GetSecondVolumeID(instanceID string) (string, error) {
server, err := s.api.GetServer(instanceID)
if err != nil {
return "", err
}
secondVolume, ok := server.Volumes["1"]
if !ok {
return "", errors.New("No second volume found")
}
return secondVolume.Identifier, nil
}
// BootInstanceAndWait boots and wait for instance to be booted
func (s *ScalewayClient) BootInstanceAndWait(instanceID string) error {
err := s.api.PostServerAction(instanceID, "poweron")
if err != nil {
return err
}
log.Debugf("Waiting for server %s to be started", instanceID)
// code taken from scaleway-cli, could need some changes
promise := make(chan bool)
var server *types.ScalewayServer
var currentState string
go func() {
defer close(promise)
for {
server, err = s.api.GetServer(instanceID)
if err != nil {
promise <- false
return
}
if currentState != server.State {
currentState = server.State
}
if server.State == "running" {
break
}
if server.State == "stopped" {
promise <- false
return
}
time.Sleep(1 * time.Second)
}
ip := server.PublicAddress.IP
if ip == "" && server.EnableIPV6 {
ip = fmt.Sprintf("[%s]", server.IPV6.Address)
}
dest := fmt.Sprintf("%s:22", ip)
for {
conn, err := net.Dial("tcp", dest)
if err == nil {
defer conn.Close()
break
} else {
time.Sleep(1 * time.Second)
}
}
promise <- true
}()
loop := 0
for {
select {
case done := <-promise:
if !done {
return err
}
log.Debugf("Server %s started", instanceID)
return nil
case <-time.After(time.Millisecond * 100):
loop = loop + 1
if loop == 5 {
loop = 0
}
}
}
}
// getSSHAuth is uses to get the ssh.Signer needed to connect via SSH
func getSSHAuth(sshKeyPath string) (ssh.Signer, error) {
f, err := os.Open(sshKeyPath)
if err != nil {
return nil, err
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(buf)
if err != nil {
return nil, err
}
return signer, err
}
// CopyImageToInstance copies the image to the instance via ssh
func (s *ScalewayClient) CopyImageToInstance(instanceID, path, sshKeyPath string) error {
_, base := filepath.Split(path)
s.fileName = base
server, err := s.api.GetServer(instanceID)
if err != nil {
return err
}
signer, err := getSSHAuth(sshKeyPath)
if err != nil {
return err
}
s.sshConfig = &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO validate server before?
}
client, err := ssh.Dial("tcp", server.PublicAddress.IP+":22", s.sshConfig) // TODO remove hardocoded port?
if err != nil {
return err
}
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// code taken from bramvdbogaerde/go-scp
contentBytes, err := ioutil.ReadAll(f)
if err != nil {
return err
}
bytesReader := bytes.NewReader(contentBytes)
log.Infof("Starting to upload %s on server", base)
go func() {
w, err := session.StdinPipe()
if err != nil {
return
}
defer w.Close()
fmt.Fprintln(w, "C0600", int64(len(contentBytes)), base)
io.Copy(w, bytesReader)
fmt.Fprintln(w, "\x00")
}()
session.Run("/usr/bin/scp -t /root/") // TODO remove hardcoded remote path?
return err
}
// WriteImageToVolume does a dd command on the remote instance via ssh
func (s *ScalewayClient) WriteImageToVolume(instanceID, deviceName string) error {
server, err := s.api.GetServer(instanceID)
if err != nil {
return err
}
client, err := ssh.Dial("tcp", server.PublicAddress.IP+":22", s.sshConfig) // TODO remove hardcoded port + use the same dial as before?
if err != nil {
return err
}
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var ddPathBuf bytes.Buffer
session.Stdout = &ddPathBuf
err = session.Run("which dd") // get the right path
if err != nil {
return err
}
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
ddCommand := strings.Trim(ddPathBuf.String(), " \n")
command := fmt.Sprintf("%s if=%s of=%s", ddCommand, s.fileName, deviceName)
log.Infof("Starting writing iso to disk")
err = session.Run(command)
if err != nil {
return err
}
log.Infof("ISO image written to disk")
return nil
}
// TerminateInstance terminates the instance and wait for termination
func (s *ScalewayClient) TerminateInstance(instanceID string) error {
server, err := s.api.GetServer(instanceID)
if err != nil {
return err
}
log.Debugf("Shutting down server %s", instanceID)
err = s.api.PostServerAction(server.Identifier, "poweroff")
if err != nil {
return err
}
// code taken from scaleway-cli
time.Sleep(10 * time.Second)
var currentState string
log.Debugf("Waiting for server to shutdown")
for {
server, err = s.api.GetServer(instanceID)
if err != nil {
return err
}
if currentState != server.State {
currentState = server.State
}
if server.State == "stopped" {
break
}
time.Sleep(1 * time.Second)
}
return nil
}
// CreateScalewayImage creates the image and delete old image and snapshot if same name
func (s *ScalewayClient) CreateScalewayImage(instanceID, volumeID, name string) error {
oldImage, err := s.api.GetImageID(name, "x86_64")
if err == nil {
err = s.api.DeleteImage(oldImage.Identifier)
if err != nil {
return err
}
}
oldSnapshot, err := s.api.GetSnapshotID(name)
if err == nil {
err := s.api.DeleteSnapshot(oldSnapshot)
if err != nil {
return err
}
}
snapshotID, err := s.api.PostSnapshot(volumeID, name)
if err != nil {
return err
}
imageID, err := s.api.PostImage(snapshotID, name, "", "x86_64") // TODO remove hardcoded arch
if err != nil {
return err
}
log.Infof("Image %s with ID %s created", name, imageID)
return nil
}
// DeleteInstanceAndVolumes deletes the instance and the volumes attached
func (s *ScalewayClient) DeleteInstanceAndVolumes(instanceID string) error {
server, err := s.api.GetServer(instanceID)
if err != nil {
return err
}
err = s.api.DeleteServer(instanceID)
if err != nil {
return err
}
for _, volume := range server.Volumes {
err = s.api.DeleteVolume(volume.Identifier)
if err != nil {
return err
}
}
log.Infof("Server %s deleted", instanceID)
return nil
}
// CreateLinuxkitInstance creates an instance with the given linuxkit image
func (s *ScalewayClient) CreateLinuxkitInstance(instanceName, imageName, instanceType string) (string, error) {
// get the image ID
image, err := s.api.GetImageID(imageName, "x86_64") // TODO fix arch and use from args
if err != nil {
return "", err
}
imageID := image.Identifier
var serverDefinition types.ScalewayServerDefinition
serverDefinition.Name = instanceName
serverDefinition.Image = &imageID
serverDefinition.CommercialType = instanceType
serverDefinition.BootType = "local"
log.Debugf("Creating volume on Scaleway")
log.Debugf("Creating server %s on Scaleway", serverDefinition.Name)
serverID, err := s.api.PostServer(serverDefinition)
if err != nil {
return "", err
}
return serverID, nil
}
// BootInstance boots the specified instance, and don't wait
func (s *ScalewayClient) BootInstance(instanceID string) error {
err := s.api.PostServerAction(instanceID, "poweron")
if err != nil {
return err
}
return nil
}
// ConnectSerialPort connects to the serial port of the instance
func (s *ScalewayClient) ConnectSerialPort(instanceID string) error {
var gottyURL string
switch s.region {
case "par1":
gottyURL = "https://tty-par1.scaleway.com/v2/"
case "ams1":
gottyURL = "https://tty-ams1.scaleway.com/"
default:
return errors.New("Instance have no region")
}
fullURL := fmt.Sprintf("%s?arg=%s&arg=%s", gottyURL, s.api.Token, instanceID)
log.Debugf("Connection to ", fullURL)
gottyClient, err := gotty.NewClient(fullURL)
if err != nil {
return err
}
gottyClient.SkipTLSVerify = true
gottyClient.UseProxyFromEnv = true
err = gottyClient.Connect()
if err != nil {
return err
}
done := make(chan bool)
fmt.Println("You are connected, type 'Ctrl+q' to quit.")
go func() {
err = gottyClient.Loop()
if err != nil {
fmt.Printf("ERROR: " + err.Error())
}
//gottyClient.Close()
done <- true
}()
<-done
return nil
}