github.com/scaleway/scaleway-cli@v1.11.1/pkg/api/helpers.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 api
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"sort"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/Sirupsen/logrus"
    17  	log "github.com/Sirupsen/logrus"
    18  	"github.com/docker/docker/pkg/namesgenerator"
    19  	"github.com/dustin/go-humanize"
    20  	"github.com/moul/anonuuid"
    21  	"github.com/scaleway/scaleway-cli/pkg/utils"
    22  )
    23  
    24  // ScalewayResolvedIdentifier represents a list of matching identifier for a specifier pattern
    25  type ScalewayResolvedIdentifier struct {
    26  	// Identifiers holds matching identifiers
    27  	Identifiers ScalewayResolverResults
    28  
    29  	// Needle is the criteria used to lookup identifiers
    30  	Needle string
    31  }
    32  
    33  // ScalewayImageInterface is an interface to multiple Scaleway items
    34  type ScalewayImageInterface struct {
    35  	CreationDate time.Time
    36  	Identifier   string
    37  	Name         string
    38  	Tag          string
    39  	VirtualSize  uint64
    40  	Public       bool
    41  	Type         string
    42  	Organization string
    43  	Archs        []string
    44  	Region       []string
    45  }
    46  
    47  // ResolveGateway tries to resolve a server public ip address, else returns the input string, i.e. IPv4, hostname
    48  func ResolveGateway(api *ScalewayAPI, gateway string) (string, error) {
    49  	if gateway == "" {
    50  		return "", nil
    51  	}
    52  
    53  	// Parses optional type prefix, i.e: "server:name" -> "name"
    54  	_, gateway = parseNeedle(gateway)
    55  
    56  	servers, err := api.ResolveServer(gateway)
    57  	if err != nil {
    58  		return "", err
    59  	}
    60  
    61  	if len(servers) == 0 {
    62  		return gateway, nil
    63  	}
    64  
    65  	if len(servers) > 1 {
    66  		return "", showResolverResults(gateway, servers)
    67  	}
    68  
    69  	// if len(servers) == 1 {
    70  	server, err := api.GetServer(servers[0].Identifier)
    71  	if err != nil {
    72  		return "", err
    73  	}
    74  	return server.PublicAddress.IP, nil
    75  }
    76  
    77  // CreateVolumeFromHumanSize creates a volume on the API with a human readable size
    78  func CreateVolumeFromHumanSize(api *ScalewayAPI, size string) (*string, error) {
    79  	bytes, err := humanize.ParseBytes(size)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	var newVolume ScalewayVolumeDefinition
    85  	newVolume.Name = size
    86  	newVolume.Size = bytes
    87  	newVolume.Type = "l_ssd"
    88  
    89  	volumeID, err := api.PostVolume(newVolume)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	return &volumeID, nil
    95  }
    96  
    97  // fillIdentifierCache fills the cache by fetching from the API
    98  func fillIdentifierCache(api *ScalewayAPI, identifierType int) {
    99  	log.Debugf("Filling the cache")
   100  	var wg sync.WaitGroup
   101  	wg.Add(5)
   102  	go func() {
   103  		if identifierType&(IdentifierUnknown|IdentifierServer) > 0 {
   104  			api.GetServers(true, 0)
   105  		}
   106  		wg.Done()
   107  	}()
   108  	go func() {
   109  		if identifierType&(IdentifierUnknown|IdentifierImage) > 0 {
   110  			api.GetImages()
   111  		}
   112  		wg.Done()
   113  	}()
   114  	go func() {
   115  		if identifierType&(IdentifierUnknown|IdentifierSnapshot) > 0 {
   116  			api.GetSnapshots()
   117  		}
   118  		wg.Done()
   119  	}()
   120  	go func() {
   121  		if identifierType&(IdentifierUnknown|IdentifierVolume) > 0 {
   122  			api.GetVolumes()
   123  		}
   124  		wg.Done()
   125  	}()
   126  	go func() {
   127  		if identifierType&(IdentifierUnknown|IdentifierBootscript) > 0 {
   128  			api.GetBootscripts()
   129  		}
   130  		wg.Done()
   131  	}()
   132  	wg.Wait()
   133  }
   134  
   135  // GetIdentifier returns a an identifier if the resolved needles only match one element, else, it exists the program
   136  func GetIdentifier(api *ScalewayAPI, needle string) (*ScalewayResolverResult, error) {
   137  	idents, err := ResolveIdentifier(api, needle)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	if len(idents) == 1 {
   143  		return &idents[0], nil
   144  	}
   145  	if len(idents) == 0 {
   146  		return nil, fmt.Errorf("No such identifier: %s", needle)
   147  	}
   148  
   149  	sort.Sort(idents)
   150  	for _, identifier := range idents {
   151  		// FIXME: also print the name
   152  		fmt.Fprintf(os.Stderr, "- %s\n", identifier.Identifier)
   153  	}
   154  	return nil, fmt.Errorf("Too many candidates for %s (%d)", needle, len(idents))
   155  }
   156  
   157  // ResolveIdentifier resolves needle provided by the user
   158  func ResolveIdentifier(api *ScalewayAPI, needle string) (ScalewayResolverResults, error) {
   159  	idents, err := api.Cache.LookUpIdentifiers(needle)
   160  	if err != nil {
   161  		return idents, err
   162  	}
   163  	if len(idents) > 0 {
   164  		return idents, nil
   165  	}
   166  
   167  	identifierType, _ := parseNeedle(needle)
   168  	fillIdentifierCache(api, identifierType)
   169  
   170  	return api.Cache.LookUpIdentifiers(needle)
   171  }
   172  
   173  // ResolveIdentifiers resolves needles provided by the user
   174  func ResolveIdentifiers(api *ScalewayAPI, needles []string, out chan ScalewayResolvedIdentifier) {
   175  	// first attempt, only lookup from the cache
   176  	var unresolved []string
   177  	for _, needle := range needles {
   178  		idents, err := api.Cache.LookUpIdentifiers(needle)
   179  		if err != nil {
   180  			api.Logger.Fatalf("%s", err)
   181  		}
   182  		if len(idents) == 0 {
   183  			unresolved = append(unresolved, needle)
   184  		} else {
   185  			out <- ScalewayResolvedIdentifier{
   186  				Identifiers: idents,
   187  				Needle:      needle,
   188  			}
   189  		}
   190  	}
   191  	// fill the cache by fetching from the API and resolve missing identifiers
   192  	if len(unresolved) > 0 {
   193  		// compute identifierType:
   194  		//   if identifierType is the same for every unresolved needle,
   195  		//   we use it directly, else, we choose IdentifierUnknown to
   196  		//   fulfill every types of cache
   197  		identifierType, _ := parseNeedle(unresolved[0])
   198  		for _, needle := range unresolved {
   199  			newIdentifierType, _ := parseNeedle(needle)
   200  			if identifierType != newIdentifierType {
   201  				identifierType = IdentifierUnknown
   202  				break
   203  			}
   204  		}
   205  
   206  		// fill all the cache
   207  		fillIdentifierCache(api, identifierType)
   208  
   209  		// lookup again in the cache
   210  		for _, needle := range unresolved {
   211  			idents, err := api.Cache.LookUpIdentifiers(needle)
   212  			if err != nil {
   213  				api.Logger.Fatalf("%s", err)
   214  			}
   215  			out <- ScalewayResolvedIdentifier{
   216  				Identifiers: idents,
   217  				Needle:      needle,
   218  			}
   219  		}
   220  	}
   221  
   222  	close(out)
   223  }
   224  
   225  // InspectIdentifierResult is returned by `InspectIdentifiers` and contains the inspected `Object` with its `Type`
   226  type InspectIdentifierResult struct {
   227  	Type   int
   228  	Object interface{}
   229  }
   230  
   231  // InspectIdentifiers inspects identifiers concurrently
   232  func InspectIdentifiers(api *ScalewayAPI, ci chan ScalewayResolvedIdentifier, cj chan InspectIdentifierResult, arch string) {
   233  	var wg sync.WaitGroup
   234  	for {
   235  		idents, ok := <-ci
   236  		if !ok {
   237  			break
   238  		}
   239  		idents.Identifiers = FilterImagesByArch(idents.Identifiers, arch)
   240  		idents.Identifiers = FilterImagesByRegion(idents.Identifiers, api.Region)
   241  		if len(idents.Identifiers) != 1 {
   242  			if len(idents.Identifiers) == 0 {
   243  				log.Errorf("Unable to resolve identifier %s", idents.Needle)
   244  			} else {
   245  				logrus.Fatal(showResolverResults(idents.Needle, idents.Identifiers))
   246  			}
   247  		} else {
   248  			ident := idents.Identifiers[0]
   249  			wg.Add(1)
   250  			go func() {
   251  				var obj interface{}
   252  				var err error
   253  
   254  				switch ident.Type {
   255  				case IdentifierServer:
   256  					obj, err = api.GetServer(ident.Identifier)
   257  				case IdentifierImage:
   258  					obj, err = api.GetImage(ident.Identifier)
   259  				case IdentifierSnapshot:
   260  					obj, err = api.GetSnapshot(ident.Identifier)
   261  				case IdentifierVolume:
   262  					obj, err = api.GetVolume(ident.Identifier)
   263  				case IdentifierBootscript:
   264  					obj, err = api.GetBootscript(ident.Identifier)
   265  				}
   266  				if err == nil && obj != nil {
   267  					cj <- InspectIdentifierResult{
   268  						Type:   ident.Type,
   269  						Object: obj,
   270  					}
   271  				}
   272  				wg.Done()
   273  			}()
   274  		}
   275  	}
   276  	wg.Wait()
   277  	close(cj)
   278  }
   279  
   280  // ConfigCreateServer represents the options sent to CreateServer and defining a server
   281  type ConfigCreateServer struct {
   282  	ImageName         string
   283  	Name              string
   284  	Bootscript        string
   285  	Env               string
   286  	AdditionalVolumes string
   287  	IP                string
   288  	CommercialType    string
   289  	DynamicIPRequired bool
   290  	EnableIPV6        bool
   291  }
   292  
   293  // CreateServer creates a server using API based on typical server fields
   294  func CreateServer(api *ScalewayAPI, c *ConfigCreateServer) (string, error) {
   295  	commercialType := os.Getenv("SCW_COMMERCIAL_TYPE")
   296  	if commercialType == "" {
   297  		commercialType = c.CommercialType
   298  	}
   299  	if len(commercialType) < 2 {
   300  		return "", errors.New("Invalid commercial type")
   301  	}
   302  
   303  	if c.Name == "" {
   304  		c.Name = strings.Replace(namesgenerator.GetRandomName(0), "_", "-", -1)
   305  	}
   306  
   307  	var server ScalewayServerDefinition
   308  
   309  	server.CommercialType = commercialType
   310  	server.Volumes = make(map[string]string)
   311  	server.DynamicIPRequired = &c.DynamicIPRequired
   312  	server.EnableIPV6 = c.EnableIPV6
   313  	if commercialType == "" {
   314  		return "", errors.New("You need to specify a commercial-type")
   315  	}
   316  	if c.IP != "" {
   317  		if anonuuid.IsUUID(c.IP) == nil {
   318  			server.PublicIP = c.IP
   319  		} else {
   320  			ips, err := api.GetIPS()
   321  			if err != nil {
   322  				return "", err
   323  			}
   324  			for _, ip := range ips.IPS {
   325  				if ip.Address == c.IP {
   326  					server.PublicIP = ip.ID
   327  					break
   328  				}
   329  			}
   330  			if server.PublicIP == "" {
   331  				return "", fmt.Errorf("IP address %v not found", c.IP)
   332  			}
   333  		}
   334  	}
   335  	server.Tags = []string{}
   336  	if c.Env != "" {
   337  		server.Tags = strings.Split(c.Env, " ")
   338  	}
   339  	switch c.CommercialType {
   340  	case "VC1M":
   341  		if c.AdditionalVolumes == "" {
   342  			c.AdditionalVolumes = "50G"
   343  			log.Debugf("This server needs a least 50G")
   344  		}
   345  	case "VC1L":
   346  		if c.AdditionalVolumes == "" {
   347  			c.AdditionalVolumes = "150G"
   348  			log.Debugf("This server needs a least 150G")
   349  		}
   350  	}
   351  	if c.AdditionalVolumes != "" {
   352  		volumes := strings.Split(c.AdditionalVolumes, " ")
   353  		for i := range volumes {
   354  			volumeID, err := CreateVolumeFromHumanSize(api, volumes[i])
   355  			if err != nil {
   356  				return "", err
   357  			}
   358  
   359  			volumeIDx := fmt.Sprintf("%d", i+1)
   360  			server.Volumes[volumeIDx] = *volumeID
   361  		}
   362  	}
   363  	arch := os.Getenv("SCW_TARGET_ARCH")
   364  	if arch == "" {
   365  		server.CommercialType = strings.ToUpper(server.CommercialType)
   366  		switch server.CommercialType[:2] {
   367  		case "C1":
   368  			arch = "arm"
   369  		case "C2", "VC":
   370  			arch = "x86_64"
   371  		default:
   372  			return "", fmt.Errorf("%s wrong commercial type", server.CommercialType)
   373  		}
   374  	}
   375  	imageIdentifier := &ScalewayImageIdentifier{
   376  		Arch: arch,
   377  	}
   378  	server.Name = c.Name
   379  	inheritingVolume := false
   380  	_, err := humanize.ParseBytes(c.ImageName)
   381  	if err == nil {
   382  		// Create a new root volume
   383  		volumeID, errCreateVol := CreateVolumeFromHumanSize(api, c.ImageName)
   384  		if errCreateVol != nil {
   385  			return "", errCreateVol
   386  		}
   387  		server.Volumes["0"] = *volumeID
   388  	} else {
   389  		// Use an existing image
   390  		inheritingVolume = true
   391  		if anonuuid.IsUUID(c.ImageName) == nil {
   392  			server.Image = &c.ImageName
   393  		} else {
   394  			imageIdentifier, err = api.GetImageID(c.ImageName, arch)
   395  			if err != nil {
   396  				return "", err
   397  			}
   398  			if imageIdentifier.Identifier != "" {
   399  				server.Image = &imageIdentifier.Identifier
   400  			} else {
   401  				snapshotID, errGetSnapID := api.GetSnapshotID(c.ImageName)
   402  				if errGetSnapID != nil {
   403  					return "", errGetSnapID
   404  				}
   405  				snapshot, errGetSnap := api.GetSnapshot(snapshotID)
   406  				if errGetSnap != nil {
   407  					return "", errGetSnap
   408  				}
   409  				if snapshot.BaseVolume.Identifier == "" {
   410  					return "", fmt.Errorf("snapshot %v does not have base volume", snapshot.Name)
   411  				}
   412  				server.Volumes["0"] = snapshot.BaseVolume.Identifier
   413  			}
   414  		}
   415  	}
   416  
   417  	if c.Bootscript != "" {
   418  		bootscript := ""
   419  
   420  		if anonuuid.IsUUID(c.Bootscript) == nil {
   421  			bootscript = c.Bootscript
   422  		} else {
   423  			var errGetBootScript error
   424  
   425  			bootscript, errGetBootScript = api.GetBootscriptID(c.Bootscript, imageIdentifier.Arch)
   426  			if errGetBootScript != nil {
   427  				return "", errGetBootScript
   428  			}
   429  		}
   430  		server.Bootscript = &bootscript
   431  	}
   432  	serverID, err := api.PostServer(server)
   433  	if err != nil {
   434  		return "", err
   435  	}
   436  
   437  	// For inherited volumes, we prefix the name with server hostname
   438  	if inheritingVolume {
   439  		createdServer, err := api.GetServer(serverID)
   440  		if err != nil {
   441  			return "", err
   442  		}
   443  		currentVolume := createdServer.Volumes["0"]
   444  
   445  		var volumePayload ScalewayVolumePutDefinition
   446  		newName := fmt.Sprintf("%s-%s", createdServer.Hostname, currentVolume.Name)
   447  		volumePayload.Name = &newName
   448  		volumePayload.CreationDate = &currentVolume.CreationDate
   449  		volumePayload.Organization = &currentVolume.Organization
   450  		volumePayload.Server.Identifier = &currentVolume.Server.Identifier
   451  		volumePayload.Server.Name = &currentVolume.Server.Name
   452  		volumePayload.Identifier = &currentVolume.Identifier
   453  		volumePayload.Size = &currentVolume.Size
   454  		volumePayload.ModificationDate = &currentVolume.ModificationDate
   455  		volumePayload.ExportURI = &currentVolume.ExportURI
   456  		volumePayload.VolumeType = &currentVolume.VolumeType
   457  
   458  		err = api.PutVolume(currentVolume.Identifier, volumePayload)
   459  		if err != nil {
   460  			return "", err
   461  		}
   462  	}
   463  
   464  	return serverID, nil
   465  }
   466  
   467  // WaitForServerState asks API in a loop until a server matches a wanted state
   468  func WaitForServerState(api *ScalewayAPI, serverID string, targetState string) (*ScalewayServer, error) {
   469  	var server *ScalewayServer
   470  	var err error
   471  
   472  	var currentState string
   473  
   474  	for {
   475  		server, err = api.GetServer(serverID)
   476  		if err != nil {
   477  			return nil, err
   478  		}
   479  		if currentState != server.State {
   480  			log.Infof("Server changed state to '%s'", server.State)
   481  			currentState = server.State
   482  		}
   483  		if server.State == targetState {
   484  			break
   485  		}
   486  		time.Sleep(1 * time.Second)
   487  	}
   488  
   489  	return server, nil
   490  }
   491  
   492  // WaitForServerReady wait for a server state to be running, then wait for the SSH port to be available
   493  func WaitForServerReady(api *ScalewayAPI, serverID, gateway string) (*ScalewayServer, error) {
   494  	promise := make(chan bool)
   495  	var server *ScalewayServer
   496  	var err error
   497  	var currentState string
   498  
   499  	go func() {
   500  		defer close(promise)
   501  
   502  		for {
   503  			server, err = api.GetServer(serverID)
   504  			if err != nil {
   505  				promise <- false
   506  				return
   507  			}
   508  			if currentState != server.State {
   509  				log.Infof("Server changed state to '%s'", server.State)
   510  				currentState = server.State
   511  			}
   512  			if server.State == "running" {
   513  				break
   514  			}
   515  			if server.State == "stopped" {
   516  				err = fmt.Errorf("The server has been stopped")
   517  				promise <- false
   518  				return
   519  			}
   520  			time.Sleep(1 * time.Second)
   521  		}
   522  
   523  		if gateway == "" {
   524  			dest := fmt.Sprintf("%s:22", server.PublicAddress.IP)
   525  			log.Debugf("Waiting for server SSH port %s", dest)
   526  			err = utils.WaitForTCPPortOpen(dest)
   527  			if err != nil {
   528  				promise <- false
   529  				return
   530  			}
   531  		} else {
   532  			dest := fmt.Sprintf("%s:22", gateway)
   533  			log.Debugf("Waiting for server SSH port %s", dest)
   534  			err = utils.WaitForTCPPortOpen(dest)
   535  			if err != nil {
   536  				promise <- false
   537  				return
   538  			}
   539  			log.Debugf("Check for SSH port through the gateway: %s", server.PrivateIP)
   540  			timeout := time.Tick(120 * time.Second)
   541  			for {
   542  				select {
   543  				case <-timeout:
   544  					err = fmt.Errorf("Timeout: unable to ping %s", server.PrivateIP)
   545  					goto OUT
   546  				default:
   547  					if utils.SSHExec("", server.PrivateIP, "root", 22, []string{
   548  						"nc",
   549  						"-z",
   550  						"-w",
   551  						"1",
   552  						server.PrivateIP,
   553  						"22",
   554  					}, false, gateway) == nil {
   555  						goto OUT
   556  					}
   557  					time.Sleep(2 * time.Second)
   558  				}
   559  			}
   560  		OUT:
   561  			if err != nil {
   562  				logrus.Info(err)
   563  				err = nil
   564  			}
   565  		}
   566  		promise <- true
   567  	}()
   568  
   569  	loop := 0
   570  	for {
   571  		select {
   572  		case done := <-promise:
   573  			utils.LogQuiet("\r \r")
   574  			if !done {
   575  				return nil, err
   576  			}
   577  			return server, nil
   578  		case <-time.After(time.Millisecond * 100):
   579  			utils.LogQuiet(fmt.Sprintf("\r%c\r", "-\\|/"[loop%4]))
   580  			loop = loop + 1
   581  			if loop == 5 {
   582  				loop = 0
   583  			}
   584  		}
   585  	}
   586  }
   587  
   588  // WaitForServerStopped wait for a server state to be stopped
   589  func WaitForServerStopped(api *ScalewayAPI, serverID string) (*ScalewayServer, error) {
   590  	server, err := WaitForServerState(api, serverID, "stopped")
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  	return server, nil
   595  }
   596  
   597  // ByCreationDate sorts images by CreationDate field
   598  type ByCreationDate []ScalewayImageInterface
   599  
   600  func (a ByCreationDate) Len() int           { return len(a) }
   601  func (a ByCreationDate) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   602  func (a ByCreationDate) Less(i, j int) bool { return a[j].CreationDate.Before(a[i].CreationDate) }
   603  
   604  // StartServer start a server based on its needle, can optionaly block while server is booting
   605  func StartServer(api *ScalewayAPI, needle string, wait bool) error {
   606  	server, err := api.GetServerID(needle)
   607  	if err != nil {
   608  		return err
   609  	}
   610  
   611  	if err = api.PostServerAction(server, "poweron"); err != nil {
   612  		return err
   613  	}
   614  
   615  	if wait {
   616  		_, err = WaitForServerReady(api, server, "")
   617  		if err != nil {
   618  			return fmt.Errorf("failed to wait for server %s to be ready, %v", needle, err)
   619  		}
   620  	}
   621  	return nil
   622  }
   623  
   624  // StartServerOnce wraps StartServer for golang channel
   625  func StartServerOnce(api *ScalewayAPI, needle string, wait bool, successChan chan string, errChan chan error) {
   626  	err := StartServer(api, needle, wait)
   627  
   628  	if err != nil {
   629  		errChan <- err
   630  		return
   631  	}
   632  	successChan <- needle
   633  }
   634  
   635  // DeleteServerForce tries to delete a server using multiple ways
   636  func (a *ScalewayAPI) DeleteServerForce(serverID string) error {
   637  	// FIXME: also delete attached volumes and ip address
   638  	// FIXME: call delete and stop -t in parallel to speed up process
   639  	err := a.DeleteServer(serverID)
   640  	if err == nil {
   641  		logrus.Infof("Server '%s' successfully deleted", serverID)
   642  		return nil
   643  	}
   644  
   645  	err = a.PostServerAction(serverID, "terminate")
   646  	if err == nil {
   647  		logrus.Infof("Server '%s' successfully terminated", serverID)
   648  		return nil
   649  	}
   650  
   651  	// FIXME: retry in a loop until timeout or Control+C
   652  	logrus.Errorf("Failed to delete server %s", serverID)
   653  	logrus.Errorf("Try to run 'scw rm -f %s' later", serverID)
   654  	return err
   655  }
   656  
   657  // GetSSHFingerprintFromServer returns an array which containts ssh-host-fingerprints
   658  func (a *ScalewayAPI) GetSSHFingerprintFromServer(serverID string) []string {
   659  	ret := []string{}
   660  
   661  	if value, err := a.GetUserdata(serverID, "ssh-host-fingerprints", false); err == nil {
   662  		PublicKeys := strings.Split(string(*value), "\n")
   663  		for i := range PublicKeys {
   664  			if fingerprint, err := utils.SSHGetFingerprint([]byte(PublicKeys[i])); err == nil {
   665  				ret = append(ret, fingerprint)
   666  			}
   667  		}
   668  	}
   669  	return ret
   670  }