github.com/scaleway/scaleway-cli@v1.11.1/pkg/commands/run.go (about)

     1  // Copyright (C) 2015 Scaleway. All rights reserved.
     2  // Use of this source code is governed by a MIT-style
     3  // license that can be found in the LICENSE.md file.
     4  
     5  package commands
     6  
     7  import (
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/Sirupsen/logrus"
    16  	"github.com/scaleway/scaleway-cli/pkg/api"
    17  	"github.com/scaleway/scaleway-cli/pkg/config"
    18  	"github.com/scaleway/scaleway-cli/pkg/utils"
    19  )
    20  
    21  // RunArgs are flags for the `Run` function
    22  type RunArgs struct {
    23  	Bootscript     string
    24  	Command        []string
    25  	Gateway        string
    26  	Image          string
    27  	Name           string
    28  	IP             string
    29  	Tags           []string
    30  	Volumes        []string
    31  	Userdata       string
    32  	CommercialType string
    33  	State          string
    34  	SSHUser        string
    35  	Timeout        int64
    36  	SSHPort        int
    37  	AutoRemove     bool
    38  	TmpSSHKey      bool
    39  	ShowBoot       bool
    40  	Detach         bool
    41  	Attach         bool
    42  	IPV6           bool
    43  }
    44  
    45  // AddSSHKeyToTags adds the ssh key in the tags
    46  func AddSSHKeyToTags(ctx CommandContext, tags *[]string, image string) error {
    47  	home, err := config.GetHomeDir()
    48  	if err != nil {
    49  		return fmt.Errorf("unable to find your home %v", err)
    50  	}
    51  	idRsa := filepath.Join(home, ".ssh", "id_rsa")
    52  	if _, errStat := os.Stat(idRsa); errStat != nil {
    53  		if os.IsNotExist(errStat) {
    54  			logrus.Warnln("Unable to find your ~/.ssh/id_rsa")
    55  			logrus.Warnln("Run 'ssh-keygen -t rsa'")
    56  			return nil
    57  		}
    58  	}
    59  	idRsa = strings.Join([]string{idRsa, ".pub"}, "")
    60  	data, err := ioutil.ReadFile(idRsa)
    61  	if err != nil {
    62  		return fmt.Errorf("failed to read %v", err)
    63  	}
    64  	data[7] = '_'
    65  	for i := range data {
    66  		if data[i] == ' ' {
    67  			data = data[:i]
    68  			break
    69  		}
    70  	}
    71  	*tags = append(*tags, strings.Join([]string{"AUTHORIZED_KEY", string(data[:])}, "="))
    72  	return nil
    73  }
    74  
    75  func addUserData(ctx CommandContext, userdatas []string, serverID string) {
    76  	for i := range userdatas {
    77  		keyValue := strings.Split(userdatas[i], "=")
    78  		if len(keyValue) != 2 {
    79  			logrus.Warn("Bad format: ", userdatas[i])
    80  			continue
    81  		}
    82  		var data []byte
    83  		var err error
    84  
    85  		// Set userdata
    86  		if keyValue[1][0] == '@' {
    87  			data, err = ioutil.ReadFile(keyValue[1][1:])
    88  			if err != nil {
    89  				logrus.Warn("ReadFile: ", err)
    90  				continue
    91  			}
    92  		} else {
    93  			data = []byte(keyValue[1])
    94  		}
    95  		if err = ctx.API.PatchUserdata(serverID, keyValue[0], data, false); err != nil {
    96  			logrus.Warn("PatchUserdata: ", err)
    97  			continue
    98  		}
    99  	}
   100  }
   101  
   102  func runShowBoot(ctx CommandContext, args RunArgs, serverID, region string, closeTimeout chan struct{}, timeoutExit chan struct{}) error {
   103  	// Attach to server serial
   104  	logrus.Info("Attaching to server console ...")
   105  	gottycli, done, err := utils.AttachToSerial(serverID, ctx.API.Token, ctx.API.ResolveTTYUrl())
   106  	if err != nil {
   107  		close(closeTimeout)
   108  		return fmt.Errorf("cannot attach to server serial: %v", err)
   109  	}
   110  	utils.Quiet(true)
   111  	notif, gateway, err := waitSSHConnection(ctx, args, serverID)
   112  	if err != nil {
   113  		close(closeTimeout)
   114  		gottycli.ExitLoop()
   115  		<-done
   116  		return err
   117  	}
   118  	select {
   119  	case <-timeoutExit:
   120  		gottycli.ExitLoop()
   121  		<-done
   122  		utils.Quiet(false)
   123  		return fmt.Errorf("Operation timed out")
   124  	case sshConnection := <-notif:
   125  		close(closeTimeout)
   126  		gottycli.ExitLoop()
   127  		<-done
   128  		utils.Quiet(false)
   129  		if sshConnection.err != nil {
   130  			return sshConnection.err
   131  		}
   132  		if fingerprints := ctx.API.GetSSHFingerprintFromServer(serverID); len(fingerprints) > 0 {
   133  			for i := range fingerprints {
   134  				fmt.Fprintf(ctx.Stdout, "%s\n", fingerprints[i])
   135  			}
   136  		}
   137  		server := sshConnection.server
   138  		logrus.Info("Connecting to server ...")
   139  		if err = utils.SSHExec(server.PublicAddress.IP, server.PrivateIP, args.SSHUser, args.SSHPort, []string{}, false, gateway); err != nil {
   140  			return fmt.Errorf("Connection to server failed: %v", err)
   141  		}
   142  	}
   143  	return nil
   144  }
   145  
   146  // Run is the handler for 'scw run'
   147  func Run(ctx CommandContext, args RunArgs) error {
   148  	if args.Gateway == "" {
   149  		args.Gateway = ctx.Getenv("SCW_GATEWAY")
   150  	}
   151  
   152  	if args.TmpSSHKey {
   153  		err := AddSSHKeyToTags(ctx, &args.Tags, args.Image)
   154  		if err != nil {
   155  			return err
   156  		}
   157  	}
   158  	env := strings.Join(args.Tags, " ")
   159  	volume := strings.Join(args.Volumes, " ")
   160  
   161  	// create IMAGE
   162  	logrus.Info("Server creation ...")
   163  	config := api.ConfigCreateServer{
   164  		ImageName:         args.Image,
   165  		Name:              args.Name,
   166  		Bootscript:        args.Bootscript,
   167  		Env:               env,
   168  		AdditionalVolumes: volume,
   169  		DynamicIPRequired: false,
   170  		IP:                args.IP,
   171  		CommercialType:    args.CommercialType,
   172  		EnableIPV6:        args.IPV6,
   173  	}
   174  	if args.IP == "dynamic" || (args.IP == "" && args.Gateway == "") {
   175  		config.DynamicIPRequired = true
   176  		config.IP = ""
   177  	} else if args.IP == "none" || args.IP == "no" || (args.IP == "" && args.Gateway != "") {
   178  		config.IP = ""
   179  	}
   180  	serverID, err := api.CreateServer(ctx.API, &config)
   181  	if err != nil {
   182  		return fmt.Errorf("failed to create server: %v", err)
   183  	}
   184  	logrus.Infof("Server created: %s", serverID)
   185  	logrus.Debugf("PublicDNS %s", serverID+api.URLPublicDNS)
   186  	logrus.Debugf("PrivateDNS %s", serverID+api.URLPrivateDNS)
   187  
   188  	if args.AutoRemove {
   189  		defer ctx.API.DeleteServerForce(serverID)
   190  	}
   191  
   192  	// start SERVER
   193  	logrus.Info("Server start requested ...")
   194  	if err = api.StartServer(ctx.API, serverID, false); err != nil {
   195  		return fmt.Errorf("failed to start server %s: %v", serverID, err)
   196  	}
   197  	logrus.Info("Server is starting, this may take up to a minute ...")
   198  
   199  	if args.Userdata != "" {
   200  		addUserData(ctx, strings.Split(args.Userdata, " "), serverID)
   201  	}
   202  	// Sync cache on disk
   203  	ctx.API.Sync()
   204  
   205  	if args.Detach {
   206  		fmt.Fprintln(ctx.Stdout, serverID)
   207  		return nil
   208  	}
   209  
   210  	closeTimeout := make(chan struct{})
   211  	timeoutExit := make(chan struct{})
   212  
   213  	if args.Timeout > 0 {
   214  		go func() {
   215  			select {
   216  			case <-time.After(time.Duration(args.Timeout) * time.Second):
   217  				close(timeoutExit)
   218  			case <-closeTimeout:
   219  				break
   220  			}
   221  		}()
   222  	}
   223  	if args.State != "" {
   224  		go func() {
   225  			for {
   226  				server, err := ctx.API.GetServer(serverID)
   227  				if err != nil {
   228  					logrus.Errorf("%s", err)
   229  					return
   230  				}
   231  				if server.StateDetail == "kernel-started" {
   232  					err = ctx.API.PatchServer(serverID, api.ScalewayServerPatchDefinition{
   233  						StateDetail: &args.State,
   234  					})
   235  					if err != nil {
   236  						logrus.Errorf("%s", err)
   237  					}
   238  					return
   239  				}
   240  				time.Sleep(1 * time.Second)
   241  			}
   242  		}()
   243  	}
   244  	if args.ShowBoot {
   245  		return runShowBoot(ctx, args, serverID, ctx.API.Region, closeTimeout, timeoutExit)
   246  	} else if args.Attach {
   247  		// Attach to server serial
   248  		logrus.Info("Attaching to server console ...")
   249  		gottycli, done, err := utils.AttachToSerial(serverID, ctx.API.Token, ctx.API.ResolveTTYUrl())
   250  		close(closeTimeout)
   251  		if err != nil {
   252  			return fmt.Errorf("cannot attach to server serial: %v", err)
   253  		}
   254  		<-done
   255  		gottycli.Close()
   256  	} else {
   257  		notif, gateway, err := waitSSHConnection(ctx, args, serverID)
   258  		if err != nil {
   259  			close(closeTimeout)
   260  			return err
   261  		}
   262  		select {
   263  		case <-timeoutExit:
   264  			return fmt.Errorf("Operation timed out")
   265  		case sshConnection := <-notif:
   266  			close(closeTimeout)
   267  			if sshConnection.err != nil {
   268  				return sshConnection.err
   269  			}
   270  			if fingerprints := ctx.API.GetSSHFingerprintFromServer(serverID); len(fingerprints) > 0 {
   271  				for i := range fingerprints {
   272  					fmt.Fprintf(ctx.Stdout, "%s\n", fingerprints[i])
   273  				}
   274  			}
   275  			server := sshConnection.server
   276  			// exec -w SERVER COMMAND ARGS...
   277  			if len(args.Command) < 1 {
   278  				logrus.Info("Connecting to server ...")
   279  				if err = utils.SSHExec(server.PublicAddress.IP, server.PrivateIP, args.SSHUser, args.SSHPort, []string{}, false, gateway); err != nil {
   280  					return fmt.Errorf("Connection to server failed: %v", err)
   281  				}
   282  			} else {
   283  				logrus.Infof("Executing command: %s ...", args.Command)
   284  				if err = utils.SSHExec(server.PublicAddress.IP, server.PrivateIP, args.SSHUser, args.SSHPort, args.Command, false, gateway); err != nil {
   285  					return fmt.Errorf("command execution failed: %v", err)
   286  				}
   287  				logrus.Info("Command successfully executed")
   288  			}
   289  		}
   290  	}
   291  	return nil
   292  }
   293  
   294  type notifSSHConnection struct {
   295  	server *api.ScalewayServer
   296  	err    error
   297  }
   298  
   299  func waitSSHConnection(ctx CommandContext, args RunArgs, serverID string) (chan notifSSHConnection, string, error) {
   300  	notif := make(chan notifSSHConnection)
   301  	// Resolve gateway
   302  	gateway, err := api.ResolveGateway(ctx.API, args.Gateway)
   303  	if err != nil {
   304  		return nil, "", fmt.Errorf("cannot resolve Gateway '%s': %v", args.Gateway, err)
   305  	}
   306  
   307  	// waiting for server to be ready
   308  	logrus.Debug("Waiting for server to be ready")
   309  	// We wait for 30 seconds, which is the minimal amount of time needed by a server to boot
   310  	go func() {
   311  		server, err := api.WaitForServerReady(ctx.API, serverID, gateway)
   312  		if err != nil {
   313  			notif <- notifSSHConnection{
   314  				err: fmt.Errorf("cannot get access to server %s: %v", serverID, err),
   315  			}
   316  			return
   317  		}
   318  		logrus.Debugf("SSH server is available: %s:22", server.PublicAddress.IP)
   319  		logrus.Info("Server is ready !")
   320  		notif <- notifSSHConnection{
   321  			server: server,
   322  		}
   323  	}()
   324  	return notif, gateway, nil
   325  }