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 = ¤tVolume.CreationDate 449 volumePayload.Organization = ¤tVolume.Organization 450 volumePayload.Server.Identifier = ¤tVolume.Server.Identifier 451 volumePayload.Server.Name = ¤tVolume.Server.Name 452 volumePayload.Identifier = ¤tVolume.Identifier 453 volumePayload.Size = ¤tVolume.Size 454 volumePayload.ModificationDate = ¤tVolume.ModificationDate 455 volumePayload.ExportURI = ¤tVolume.ExportURI 456 volumePayload.VolumeType = ¤tVolume.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 }