github.com/coreos/mantle@v0.13.0/platform/api/openstack/api.go (about) 1 // Copyright 2018 Red Hat 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package openstack 16 17 import ( 18 "fmt" 19 "os" 20 "strings" 21 "time" 22 23 "github.com/coreos/pkg/capnslog" 24 "github.com/gophercloud/gophercloud" 25 "github.com/gophercloud/gophercloud/openstack" 26 "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" 27 "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" 28 "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" 29 computeImages "github.com/gophercloud/gophercloud/openstack/compute/v2/images" 30 "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" 31 "github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata" 32 "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" 33 "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" 34 "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" 35 "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" 36 "github.com/gophercloud/gophercloud/pagination" 37 38 "github.com/coreos/mantle/auth" 39 "github.com/coreos/mantle/platform" 40 "github.com/coreos/mantle/util" 41 ) 42 43 var ( 44 plog = capnslog.NewPackageLogger("github.com/coreos/mantle", "platform/api/openstack") 45 ) 46 47 type Options struct { 48 *platform.Options 49 50 // Config file. Defaults to $HOME/.config/openstack.json. 51 ConfigPath string 52 // Profile name 53 Profile string 54 55 // Region (e.g. "regionOne") 56 Region string 57 // Instance Flavor ID 58 Flavor string 59 // Image ID 60 Image string 61 // Network ID 62 Network string 63 // Domain ID 64 Domain string 65 // Floating IP Pool 66 FloatingIPPool string 67 } 68 69 type Server struct { 70 Server *servers.Server 71 FloatingIP *floatingips.FloatingIP 72 } 73 74 type API struct { 75 opts *Options 76 computeClient *gophercloud.ServiceClient 77 imageClient *gophercloud.ServiceClient 78 networkClient *gophercloud.ServiceClient 79 } 80 81 func New(opts *Options) (*API, error) { 82 profiles, err := auth.ReadOpenStackConfig(opts.ConfigPath) 83 if err != nil { 84 return nil, fmt.Errorf("couldn't read OpenStack config: %v", err) 85 } 86 87 if opts.Profile == "" { 88 opts.Profile = "default" 89 } 90 profile, ok := profiles[opts.Profile] 91 if !ok { 92 return nil, fmt.Errorf("no such profile %q", opts.Profile) 93 } 94 95 if opts.Domain == "" { 96 opts.Domain = profile.Domain 97 } 98 99 osOpts := gophercloud.AuthOptions{ 100 IdentityEndpoint: profile.AuthURL, 101 TenantID: profile.TenantID, 102 TenantName: profile.TenantName, 103 Username: profile.Username, 104 Password: profile.Password, 105 DomainID: opts.Domain, 106 } 107 108 provider, err := openstack.AuthenticatedClient(osOpts) 109 if err != nil { 110 return nil, fmt.Errorf("failed creating provider: %v", err) 111 } 112 113 if opts.Region == "" { 114 opts.Region = profile.Region 115 } 116 117 computeClient, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ 118 Name: "nova", 119 Region: opts.Region, 120 }) 121 if err != nil { 122 return nil, fmt.Errorf("failed to create compute client: %v", err) 123 } 124 125 imageClient, err := openstack.NewImageServiceV2(provider, gophercloud.EndpointOpts{ 126 Name: "glance", 127 Region: opts.Region, 128 }) 129 if err != nil { 130 return nil, fmt.Errorf("failed to create image client: %v", err) 131 } 132 133 networkClient, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ 134 Name: "neutron", 135 Region: opts.Region, 136 }) 137 138 a := &API{ 139 opts: opts, 140 computeClient: computeClient, 141 imageClient: imageClient, 142 networkClient: networkClient, 143 } 144 145 if a.opts.Flavor != "" { 146 tmp, err := a.resolveFlavor() 147 if err != nil { 148 return nil, fmt.Errorf("resolving flavor: %v", err) 149 } 150 a.opts.Flavor = tmp 151 } 152 153 if a.opts.Image != "" { 154 tmp, err := a.ResolveImage(a.opts.Image) 155 if err != nil { 156 return nil, fmt.Errorf("resolving image: %v", err) 157 } 158 a.opts.Image = tmp 159 } 160 161 if a.opts.Network != "" { 162 tmp, err := a.resolveNetwork() 163 if err != nil { 164 return nil, fmt.Errorf("resolving network: %v", err) 165 } 166 a.opts.Network = tmp 167 } 168 169 if a.opts.FloatingIPPool == "" { 170 a.opts.FloatingIPPool = profile.FloatingIPPool 171 } 172 173 return a, nil 174 } 175 176 func unwrapPages(pager pagination.Pager, allowEmpty bool) (pagination.Page, error) { 177 if pager.Err != nil { 178 return nil, fmt.Errorf("retrieving pager: %v", pager.Err) 179 } 180 181 pages, err := pager.AllPages() 182 if err != nil { 183 return nil, fmt.Errorf("retrieving pages: %v", err) 184 } 185 186 if !allowEmpty { 187 empty, err := pages.IsEmpty() 188 if err != nil { 189 return nil, fmt.Errorf("parsing pages: %v", err) 190 } 191 if empty { 192 return nil, fmt.Errorf("empty pager") 193 } 194 } 195 return pages, nil 196 } 197 198 func (a *API) resolveFlavor() (string, error) { 199 pager := flavors.ListDetail(a.computeClient, flavors.ListOpts{}) 200 201 pages, err := unwrapPages(pager, false) 202 if err != nil { 203 return "", fmt.Errorf("flavors: %v", err) 204 } 205 206 flavors, err := flavors.ExtractFlavors(pages) 207 if err != nil { 208 return "", fmt.Errorf("extracting flavors: %v", err) 209 } 210 211 for _, flavor := range flavors { 212 if flavor.ID == a.opts.Flavor || flavor.Name == a.opts.Flavor { 213 return flavor.ID, nil 214 } 215 } 216 217 return "", fmt.Errorf("specified flavor %q not found", a.opts.Flavor) 218 } 219 220 func (a *API) ResolveImage(img string) (string, error) { 221 pager := computeImages.ListDetail(a.computeClient, computeImages.ListOpts{}) 222 223 pages, err := unwrapPages(pager, false) 224 if err != nil { 225 return "", fmt.Errorf("images: %v", err) 226 } 227 228 images, err := computeImages.ExtractImages(pages) 229 if err != nil { 230 return "", fmt.Errorf("extracting images: %v", err) 231 } 232 233 for _, image := range images { 234 if image.ID == img || image.Name == img { 235 return image.ID, nil 236 } 237 } 238 239 return "", fmt.Errorf("specified image %q not found", img) 240 } 241 242 func (a *API) resolveNetwork() (string, error) { 243 networks, err := a.getNetworks() 244 if err != nil { 245 return "", err 246 } 247 248 for _, network := range networks { 249 if network.ID == a.opts.Network || network.Name == a.opts.Network { 250 return network.ID, nil 251 } 252 } 253 254 return "", fmt.Errorf("specified network %q not found", a.opts.Network) 255 } 256 257 func (a *API) PreflightCheck() error { 258 if err := servers.List(a.computeClient, servers.ListOpts{}).Err; err != nil { 259 return fmt.Errorf("listing servers: %v", err) 260 } 261 return nil 262 } 263 264 func (a *API) CreateServer(name, sshKeyID, userdata string) (*Server, error) { 265 networkID := a.opts.Network 266 if networkID == "" { 267 networks, err := a.getNetworks() 268 if err != nil { 269 return nil, fmt.Errorf("getting network: %v", err) 270 } 271 networkID = networks[0].ID 272 } 273 274 securityGroup, err := a.getSecurityGroup() 275 if err != nil { 276 return nil, fmt.Errorf("retrieving security group: %v", err) 277 } 278 279 server, err := servers.Create(a.computeClient, keypairs.CreateOptsExt{ 280 CreateOptsBuilder: servers.CreateOpts{ 281 Name: name, 282 FlavorRef: a.opts.Flavor, 283 ImageRef: a.opts.Image, 284 Metadata: map[string]string{ 285 "CreatedBy": "mantle", 286 }, 287 SecurityGroups: []string{securityGroup}, 288 Networks: []servers.Network{ 289 { 290 UUID: networkID, 291 }, 292 }, 293 UserData: []byte(userdata), 294 }, 295 KeyName: sshKeyID, 296 }).Extract() 297 if err != nil { 298 return nil, fmt.Errorf("creating server: %v", err) 299 } 300 301 serverID := server.ID 302 303 err = util.WaitUntilReady(5*time.Minute, 10*time.Second, func() (bool, error) { 304 var err error 305 server, err = servers.Get(a.computeClient, serverID).Extract() 306 if err != nil { 307 return false, err 308 } 309 return server.Status == "ACTIVE", nil 310 }) 311 if err != nil { 312 a.DeleteServer(serverID) 313 return nil, fmt.Errorf("waiting for instance to run: %v", err) 314 } 315 316 var floatingip *floatingips.FloatingIP 317 if a.opts.FloatingIPPool != "" { 318 floatingip, err = a.createFloatingIP() 319 if err != nil { 320 a.DeleteServer(serverID) 321 return nil, fmt.Errorf("creating floating ip: %v", err) 322 } 323 err = floatingips.AssociateInstance(a.computeClient, serverID, floatingips.AssociateOpts{ 324 FloatingIP: floatingip.IP, 325 }).ExtractErr() 326 if err != nil { 327 a.DeleteServer(serverID) 328 // Explicitly delete the floating ip as DeleteServer only deletes floating IPs that are 329 // associated with servers 330 a.deleteFloatingIP(floatingip.ID) 331 return nil, fmt.Errorf("associating floating ip: %v", err) 332 } 333 334 server, err = servers.Get(a.computeClient, serverID).Extract() 335 if err != nil { 336 a.DeleteServer(serverID) 337 return nil, fmt.Errorf("retrieving server info: %v", err) 338 } 339 } 340 341 return &Server{ 342 Server: server, 343 FloatingIP: floatingip, 344 }, nil 345 } 346 347 func (a *API) getNetworks() ([]networks.Network, error) { 348 pager := networks.List(a.networkClient, networks.ListOpts{}) 349 350 pages, err := unwrapPages(pager, false) 351 if err != nil { 352 return nil, fmt.Errorf("networks: %v", err) 353 } 354 355 networks, err := networks.ExtractNetworks(pages) 356 if err != nil { 357 return nil, fmt.Errorf("extracting networks: %v", err) 358 } 359 return networks, nil 360 } 361 362 func (a *API) getSecurityGroup() (string, error) { 363 id, err := groups.IDFromName(a.networkClient, "kola") 364 if err != nil { 365 if _, ok := err.(gophercloud.ErrResourceNotFound); ok { 366 return a.createSecurityGroup() 367 } 368 return "", fmt.Errorf("finding security group: %v", err) 369 } 370 return id, nil 371 } 372 373 func (a *API) createSecurityGroup() (string, error) { 374 securityGroup, err := groups.Create(a.networkClient, groups.CreateOpts{ 375 Name: "kola", 376 }).Extract() 377 if err != nil { 378 return "", fmt.Errorf("creating security group: %v", err) 379 } 380 381 ruleSet := []struct { 382 Direction rules.RuleDirection 383 EtherType rules.RuleEtherType 384 Protocol rules.RuleProtocol 385 PortRangeMin int 386 PortRangeMax int 387 RemoteGroupID string 388 RemoteIPPrefix string 389 }{ 390 { 391 Direction: rules.DirIngress, 392 EtherType: rules.EtherType4, 393 RemoteGroupID: securityGroup.ID, 394 }, 395 { 396 Direction: rules.DirIngress, 397 EtherType: rules.EtherType4, 398 Protocol: rules.ProtocolTCP, 399 PortRangeMin: 22, 400 PortRangeMax: 22, 401 RemoteIPPrefix: "0.0.0.0/0", 402 }, 403 { 404 Direction: rules.DirIngress, 405 EtherType: rules.EtherType6, 406 RemoteGroupID: securityGroup.ID, 407 }, 408 { 409 Direction: rules.DirIngress, 410 EtherType: rules.EtherType4, 411 Protocol: rules.ProtocolTCP, 412 PortRangeMin: 2379, 413 PortRangeMax: 2380, 414 RemoteIPPrefix: "0.0.0.0/0", 415 }, 416 } 417 418 for _, rule := range ruleSet { 419 _, err = rules.Create(a.networkClient, rules.CreateOpts{ 420 Direction: rule.Direction, 421 EtherType: rule.EtherType, 422 SecGroupID: securityGroup.ID, 423 PortRangeMax: rule.PortRangeMax, 424 PortRangeMin: rule.PortRangeMin, 425 Protocol: rule.Protocol, 426 RemoteGroupID: rule.RemoteGroupID, 427 RemoteIPPrefix: rule.RemoteIPPrefix, 428 }).Extract() 429 if err != nil { 430 a.deleteSecurityGroup(securityGroup.ID) 431 return "", fmt.Errorf("adding security rule: %v", err) 432 } 433 } 434 435 return securityGroup.ID, nil 436 } 437 438 func (a *API) deleteSecurityGroup(id string) error { 439 return groups.Delete(a.networkClient, id).ExtractErr() 440 } 441 442 func (a *API) createFloatingIP() (*floatingips.FloatingIP, error) { 443 return floatingips.Create(a.computeClient, floatingips.CreateOpts{ 444 Pool: a.opts.FloatingIPPool, 445 }).Extract() 446 } 447 448 func (a *API) disassociateFloatingIP(serverID, id string) error { 449 return floatingips.DisassociateInstance(a.computeClient, serverID, floatingips.DisassociateOpts{ 450 FloatingIP: id, 451 }).ExtractErr() 452 } 453 454 func (a *API) deleteFloatingIP(id string) error { 455 return floatingips.Delete(a.computeClient, id).ExtractErr() 456 } 457 458 func (a *API) findFloatingIP(serverID string) (*floatingips.FloatingIP, error) { 459 pager := floatingips.List(a.computeClient) 460 461 pages, err := unwrapPages(pager, true) 462 if err != nil { 463 return nil, fmt.Errorf("floating ips: %v", err) 464 } 465 466 floatingiplist, err := floatingips.ExtractFloatingIPs(pages) 467 if err != nil { 468 return nil, fmt.Errorf("extracting floating ips: %v", err) 469 } 470 471 for _, floatingip := range floatingiplist { 472 if floatingip.InstanceID == serverID { 473 return &floatingip, nil 474 } 475 } 476 477 return nil, nil 478 } 479 480 // Deletes the server, and disassociates & deletes any floating IP associated with the given server. 481 func (a *API) DeleteServer(id string) error { 482 fip, err := a.findFloatingIP(id) 483 if err != nil { 484 return err 485 } 486 if fip != nil { 487 if err := a.disassociateFloatingIP(id, fip.IP); err != nil { 488 return fmt.Errorf("couldn't disassociate floating ip %s from server %s: %v", fip.ID, id, err) 489 } 490 if err := a.deleteFloatingIP(fip.ID); err != nil { 491 // if the deletion of this floating IP fails then mantle cannot detect the floating IP was tied to the 492 // server anymore. as such warn and continue deleting the server. 493 plog.Warningf("couldn't delete floating ip %s: %v", fip.ID, err) 494 } 495 } 496 497 if err := servers.Delete(a.computeClient, id).ExtractErr(); err != nil { 498 return fmt.Errorf("deleting server: %v: %v", id, err) 499 } 500 501 return nil 502 } 503 504 func (a *API) GetConsoleOutput(id string) (string, error) { 505 return servers.ShowConsoleOutput(a.computeClient, id, servers.ShowConsoleOutputOpts{}).Extract() 506 } 507 508 func (a *API) UploadImage(name, path string) (string, error) { 509 image, err := images.Create(a.imageClient, images.CreateOpts{ 510 Name: name, 511 ContainerFormat: "bare", 512 DiskFormat: "qcow2", 513 Tags: []string{"mantle"}, 514 }).Extract() 515 if err != nil { 516 return "", fmt.Errorf("creating image: %v", err) 517 } 518 519 data, err := os.Open(path) 520 if err != nil { 521 a.DeleteImage(image.ID) 522 return "", fmt.Errorf("opening image file: %v", err) 523 } 524 defer data.Close() 525 526 err = imagedata.Upload(a.imageClient, image.ID, data).ExtractErr() 527 if err != nil { 528 a.DeleteImage(image.ID) 529 return "", fmt.Errorf("uploading image data: %v", err) 530 } 531 532 return image.ID, nil 533 } 534 535 func (a *API) DeleteImage(imageID string) error { 536 return images.Delete(a.imageClient, imageID).ExtractErr() 537 } 538 539 func (a *API) AddKey(name, key string) error { 540 _, err := keypairs.Create(a.computeClient, keypairs.CreateOpts{ 541 Name: name, 542 PublicKey: key, 543 }).Extract() 544 return err 545 } 546 547 func (a *API) DeleteKey(name string) error { 548 return keypairs.Delete(a.computeClient, name).ExtractErr() 549 } 550 551 func (a *API) listServersWithMetadata(metadata map[string]string) ([]servers.Server, error) { 552 pager := servers.List(a.computeClient, servers.ListOpts{}) 553 554 pages, err := unwrapPages(pager, true) 555 if err != nil { 556 return nil, fmt.Errorf("servers: %v", err) 557 } 558 559 allServers, err := servers.ExtractServers(pages) 560 if err != nil { 561 return nil, fmt.Errorf("extracting servers: %v", err) 562 } 563 var retServers []servers.Server 564 for _, server := range allServers { 565 isMatch := true 566 for key, val := range metadata { 567 if value, ok := server.Metadata[key]; !ok || val != value { 568 isMatch = false 569 break 570 } 571 } 572 if isMatch { 573 retServers = append(retServers, server) 574 } 575 } 576 return retServers, nil 577 } 578 579 func (a *API) GC(gracePeriod time.Duration) error { 580 threshold := time.Now().Add(-gracePeriod) 581 582 servers, err := a.listServersWithMetadata(map[string]string{ 583 "CreatedBy": "mantle", 584 }) 585 if err != nil { 586 return err 587 } 588 for _, server := range servers { 589 if strings.Contains(server.Status, "DELETED") || server.Created.After(threshold) { 590 continue 591 } 592 593 if err := a.DeleteServer(server.ID); err != nil { 594 return fmt.Errorf("couldn't delete server %s: %v", server.ID, err) 595 } 596 } 597 return nil 598 }