mirror of
https://github.com/coreos/mantle.git
synced 2026-02-06 12:45:00 +01:00
599 lines
15 KiB
Go
599 lines
15 KiB
Go
// Copyright 2018 Red Hat
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package openstack
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/coreos/pkg/capnslog"
|
|
"github.com/gophercloud/gophercloud"
|
|
"github.com/gophercloud/gophercloud/openstack"
|
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
|
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
|
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
|
|
computeImages "github.com/gophercloud/gophercloud/openstack/compute/v2/images"
|
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
|
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata"
|
|
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
|
|
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups"
|
|
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules"
|
|
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
|
|
"github.com/gophercloud/gophercloud/pagination"
|
|
|
|
"github.com/coreos/mantle/auth"
|
|
"github.com/coreos/mantle/platform"
|
|
"github.com/coreos/mantle/util"
|
|
)
|
|
|
|
var (
|
|
plog = capnslog.NewPackageLogger("github.com/coreos/mantle", "platform/api/openstack")
|
|
)
|
|
|
|
type Options struct {
|
|
*platform.Options
|
|
|
|
// Config file. Defaults to $HOME/.config/openstack.json.
|
|
ConfigPath string
|
|
// Profile name
|
|
Profile string
|
|
|
|
// Region (e.g. "regionOne")
|
|
Region string
|
|
// Instance Flavor ID
|
|
Flavor string
|
|
// Image ID
|
|
Image string
|
|
// Network ID
|
|
Network string
|
|
// Domain ID
|
|
Domain string
|
|
// Floating IP Pool
|
|
FloatingIPPool string
|
|
}
|
|
|
|
type Server struct {
|
|
Server *servers.Server
|
|
FloatingIP *floatingips.FloatingIP
|
|
}
|
|
|
|
type API struct {
|
|
opts *Options
|
|
computeClient *gophercloud.ServiceClient
|
|
imageClient *gophercloud.ServiceClient
|
|
networkClient *gophercloud.ServiceClient
|
|
}
|
|
|
|
func New(opts *Options) (*API, error) {
|
|
profiles, err := auth.ReadOpenStackConfig(opts.ConfigPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't read OpenStack config: %v", err)
|
|
}
|
|
|
|
if opts.Profile == "" {
|
|
opts.Profile = "default"
|
|
}
|
|
profile, ok := profiles[opts.Profile]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no such profile %q", opts.Profile)
|
|
}
|
|
|
|
if opts.Domain == "" {
|
|
opts.Domain = profile.Domain
|
|
}
|
|
|
|
osOpts := gophercloud.AuthOptions{
|
|
IdentityEndpoint: profile.AuthURL,
|
|
TenantID: profile.TenantID,
|
|
TenantName: profile.TenantName,
|
|
Username: profile.Username,
|
|
Password: profile.Password,
|
|
DomainID: opts.Domain,
|
|
}
|
|
|
|
provider, err := openstack.AuthenticatedClient(osOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed creating provider: %v", err)
|
|
}
|
|
|
|
if opts.Region == "" {
|
|
opts.Region = profile.Region
|
|
}
|
|
|
|
computeClient, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{
|
|
Name: "nova",
|
|
Region: opts.Region,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create compute client: %v", err)
|
|
}
|
|
|
|
imageClient, err := openstack.NewImageServiceV2(provider, gophercloud.EndpointOpts{
|
|
Name: "glance",
|
|
Region: opts.Region,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create image client: %v", err)
|
|
}
|
|
|
|
networkClient, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
|
|
Name: "neutron",
|
|
Region: opts.Region,
|
|
})
|
|
|
|
a := &API{
|
|
opts: opts,
|
|
computeClient: computeClient,
|
|
imageClient: imageClient,
|
|
networkClient: networkClient,
|
|
}
|
|
|
|
if a.opts.Flavor != "" {
|
|
tmp, err := a.resolveFlavor()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolving flavor: %v", err)
|
|
}
|
|
a.opts.Flavor = tmp
|
|
}
|
|
|
|
if a.opts.Image != "" {
|
|
tmp, err := a.ResolveImage(a.opts.Image)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolving image: %v", err)
|
|
}
|
|
a.opts.Image = tmp
|
|
}
|
|
|
|
if a.opts.Network != "" {
|
|
tmp, err := a.resolveNetwork()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolving network: %v", err)
|
|
}
|
|
a.opts.Network = tmp
|
|
}
|
|
|
|
if a.opts.FloatingIPPool == "" {
|
|
a.opts.FloatingIPPool = profile.FloatingIPPool
|
|
}
|
|
|
|
return a, nil
|
|
}
|
|
|
|
func unwrapPages(pager pagination.Pager, allowEmpty bool) (pagination.Page, error) {
|
|
if pager.Err != nil {
|
|
return nil, fmt.Errorf("retrieving pager: %v", pager.Err)
|
|
}
|
|
|
|
pages, err := pager.AllPages()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving pages: %v", err)
|
|
}
|
|
|
|
if !allowEmpty {
|
|
empty, err := pages.IsEmpty()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing pages: %v", err)
|
|
}
|
|
if empty {
|
|
return nil, fmt.Errorf("empty pager")
|
|
}
|
|
}
|
|
return pages, nil
|
|
}
|
|
|
|
func (a *API) resolveFlavor() (string, error) {
|
|
pager := flavors.ListDetail(a.computeClient, flavors.ListOpts{})
|
|
|
|
pages, err := unwrapPages(pager, false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("flavors: %v", err)
|
|
}
|
|
|
|
flavors, err := flavors.ExtractFlavors(pages)
|
|
if err != nil {
|
|
return "", fmt.Errorf("extracting flavors: %v", err)
|
|
}
|
|
|
|
for _, flavor := range flavors {
|
|
if flavor.ID == a.opts.Flavor || flavor.Name == a.opts.Flavor {
|
|
return flavor.ID, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("specified flavor %q not found", a.opts.Flavor)
|
|
}
|
|
|
|
func (a *API) ResolveImage(img string) (string, error) {
|
|
pager := computeImages.ListDetail(a.computeClient, computeImages.ListOpts{})
|
|
|
|
pages, err := unwrapPages(pager, false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("images: %v", err)
|
|
}
|
|
|
|
images, err := computeImages.ExtractImages(pages)
|
|
if err != nil {
|
|
return "", fmt.Errorf("extracting images: %v", err)
|
|
}
|
|
|
|
for _, image := range images {
|
|
if image.ID == img || image.Name == img {
|
|
return image.ID, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("specified image %q not found", img)
|
|
}
|
|
|
|
func (a *API) resolveNetwork() (string, error) {
|
|
networks, err := a.getNetworks()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, network := range networks {
|
|
if network.ID == a.opts.Network || network.Name == a.opts.Network {
|
|
return network.ID, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("specified network %q not found", a.opts.Network)
|
|
}
|
|
|
|
func (a *API) PreflightCheck() error {
|
|
if err := servers.List(a.computeClient, servers.ListOpts{}).Err; err != nil {
|
|
return fmt.Errorf("listing servers: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *API) CreateServer(name, sshKeyID, userdata string) (*Server, error) {
|
|
networkID := a.opts.Network
|
|
if networkID == "" {
|
|
networks, err := a.getNetworks()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting network: %v", err)
|
|
}
|
|
networkID = networks[0].ID
|
|
}
|
|
|
|
securityGroup, err := a.getSecurityGroup()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving security group: %v", err)
|
|
}
|
|
|
|
server, err := servers.Create(a.computeClient, keypairs.CreateOptsExt{
|
|
CreateOptsBuilder: servers.CreateOpts{
|
|
Name: name,
|
|
FlavorRef: a.opts.Flavor,
|
|
ImageRef: a.opts.Image,
|
|
Metadata: map[string]string{
|
|
"CreatedBy": "mantle",
|
|
},
|
|
SecurityGroups: []string{securityGroup},
|
|
Networks: []servers.Network{
|
|
{
|
|
UUID: networkID,
|
|
},
|
|
},
|
|
UserData: []byte(userdata),
|
|
},
|
|
KeyName: sshKeyID,
|
|
}).Extract()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating server: %v", err)
|
|
}
|
|
|
|
serverID := server.ID
|
|
|
|
err = util.WaitUntilReady(5*time.Minute, 10*time.Second, func() (bool, error) {
|
|
var err error
|
|
server, err = servers.Get(a.computeClient, serverID).Extract()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return server.Status == "ACTIVE", nil
|
|
})
|
|
if err != nil {
|
|
a.DeleteServer(serverID)
|
|
return nil, fmt.Errorf("waiting for instance to run: %v", err)
|
|
}
|
|
|
|
var floatingip *floatingips.FloatingIP
|
|
if a.opts.FloatingIPPool != "" {
|
|
floatingip, err = a.createFloatingIP()
|
|
if err != nil {
|
|
a.DeleteServer(serverID)
|
|
return nil, fmt.Errorf("creating floating ip: %v", err)
|
|
}
|
|
err = floatingips.AssociateInstance(a.computeClient, serverID, floatingips.AssociateOpts{
|
|
FloatingIP: floatingip.IP,
|
|
}).ExtractErr()
|
|
if err != nil {
|
|
a.DeleteServer(serverID)
|
|
// Explicitly delete the floating ip as DeleteServer only deletes floating IPs that are
|
|
// associated with servers
|
|
a.deleteFloatingIP(floatingip.ID)
|
|
return nil, fmt.Errorf("associating floating ip: %v", err)
|
|
}
|
|
|
|
server, err = servers.Get(a.computeClient, serverID).Extract()
|
|
if err != nil {
|
|
a.DeleteServer(serverID)
|
|
return nil, fmt.Errorf("retrieving server info: %v", err)
|
|
}
|
|
}
|
|
|
|
return &Server{
|
|
Server: server,
|
|
FloatingIP: floatingip,
|
|
}, nil
|
|
}
|
|
|
|
func (a *API) getNetworks() ([]networks.Network, error) {
|
|
pager := networks.List(a.networkClient, networks.ListOpts{})
|
|
|
|
pages, err := unwrapPages(pager, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("networks: %v", err)
|
|
}
|
|
|
|
networks, err := networks.ExtractNetworks(pages)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extracting networks: %v", err)
|
|
}
|
|
return networks, nil
|
|
}
|
|
|
|
func (a *API) getSecurityGroup() (string, error) {
|
|
id, err := groups.IDFromName(a.networkClient, "kola")
|
|
if err != nil {
|
|
if _, ok := err.(gophercloud.ErrResourceNotFound); ok {
|
|
return a.createSecurityGroup()
|
|
}
|
|
return "", fmt.Errorf("finding security group: %v", err)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func (a *API) createSecurityGroup() (string, error) {
|
|
securityGroup, err := groups.Create(a.networkClient, groups.CreateOpts{
|
|
Name: "kola",
|
|
}).Extract()
|
|
if err != nil {
|
|
return "", fmt.Errorf("creating security group: %v", err)
|
|
}
|
|
|
|
ruleSet := []struct {
|
|
Direction rules.RuleDirection
|
|
EtherType rules.RuleEtherType
|
|
Protocol rules.RuleProtocol
|
|
PortRangeMin int
|
|
PortRangeMax int
|
|
RemoteGroupID string
|
|
RemoteIPPrefix string
|
|
}{
|
|
{
|
|
Direction: rules.DirIngress,
|
|
EtherType: rules.EtherType4,
|
|
RemoteGroupID: securityGroup.ID,
|
|
},
|
|
{
|
|
Direction: rules.DirIngress,
|
|
EtherType: rules.EtherType4,
|
|
Protocol: rules.ProtocolTCP,
|
|
PortRangeMin: 22,
|
|
PortRangeMax: 22,
|
|
RemoteIPPrefix: "0.0.0.0/0",
|
|
},
|
|
{
|
|
Direction: rules.DirIngress,
|
|
EtherType: rules.EtherType6,
|
|
RemoteGroupID: securityGroup.ID,
|
|
},
|
|
{
|
|
Direction: rules.DirIngress,
|
|
EtherType: rules.EtherType4,
|
|
Protocol: rules.ProtocolTCP,
|
|
PortRangeMin: 2379,
|
|
PortRangeMax: 2380,
|
|
RemoteIPPrefix: "0.0.0.0/0",
|
|
},
|
|
}
|
|
|
|
for _, rule := range ruleSet {
|
|
_, err = rules.Create(a.networkClient, rules.CreateOpts{
|
|
Direction: rule.Direction,
|
|
EtherType: rule.EtherType,
|
|
SecGroupID: securityGroup.ID,
|
|
PortRangeMax: rule.PortRangeMax,
|
|
PortRangeMin: rule.PortRangeMin,
|
|
Protocol: rule.Protocol,
|
|
RemoteGroupID: rule.RemoteGroupID,
|
|
RemoteIPPrefix: rule.RemoteIPPrefix,
|
|
}).Extract()
|
|
if err != nil {
|
|
a.deleteSecurityGroup(securityGroup.ID)
|
|
return "", fmt.Errorf("adding security rule: %v", err)
|
|
}
|
|
}
|
|
|
|
return securityGroup.ID, nil
|
|
}
|
|
|
|
func (a *API) deleteSecurityGroup(id string) error {
|
|
return groups.Delete(a.networkClient, id).ExtractErr()
|
|
}
|
|
|
|
func (a *API) createFloatingIP() (*floatingips.FloatingIP, error) {
|
|
return floatingips.Create(a.computeClient, floatingips.CreateOpts{
|
|
Pool: a.opts.FloatingIPPool,
|
|
}).Extract()
|
|
}
|
|
|
|
func (a *API) disassociateFloatingIP(serverID, id string) error {
|
|
return floatingips.DisassociateInstance(a.computeClient, serverID, floatingips.DisassociateOpts{
|
|
FloatingIP: id,
|
|
}).ExtractErr()
|
|
}
|
|
|
|
func (a *API) deleteFloatingIP(id string) error {
|
|
return floatingips.Delete(a.computeClient, id).ExtractErr()
|
|
}
|
|
|
|
func (a *API) findFloatingIP(serverID string) (*floatingips.FloatingIP, error) {
|
|
pager := floatingips.List(a.computeClient)
|
|
|
|
pages, err := unwrapPages(pager, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("floating ips: %v", err)
|
|
}
|
|
|
|
floatingiplist, err := floatingips.ExtractFloatingIPs(pages)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extracting floating ips: %v", err)
|
|
}
|
|
|
|
for _, floatingip := range floatingiplist {
|
|
if floatingip.InstanceID == serverID {
|
|
return &floatingip, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// Deletes the server, and disassociates & deletes any floating IP associated with the given server.
|
|
func (a *API) DeleteServer(id string) error {
|
|
fip, err := a.findFloatingIP(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fip != nil {
|
|
if err := a.disassociateFloatingIP(id, fip.IP); err != nil {
|
|
return fmt.Errorf("couldn't disassociate floating ip %s from server %s: %v", fip.ID, id, err)
|
|
}
|
|
if err := a.deleteFloatingIP(fip.ID); err != nil {
|
|
// if the deletion of this floating IP fails then mantle cannot detect the floating IP was tied to the
|
|
// server anymore. as such warn and continue deleting the server.
|
|
plog.Warningf("couldn't delete floating ip %s: %v", fip.ID, err)
|
|
}
|
|
}
|
|
|
|
if err := servers.Delete(a.computeClient, id).ExtractErr(); err != nil {
|
|
return fmt.Errorf("deleting server: %v: %v", id, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *API) GetConsoleOutput(id string) (string, error) {
|
|
return servers.ShowConsoleOutput(a.computeClient, id, servers.ShowConsoleOutputOpts{}).Extract()
|
|
}
|
|
|
|
func (a *API) UploadImage(name, path string) (string, error) {
|
|
image, err := images.Create(a.imageClient, images.CreateOpts{
|
|
Name: name,
|
|
ContainerFormat: "bare",
|
|
DiskFormat: "qcow2",
|
|
Tags: []string{"mantle"},
|
|
}).Extract()
|
|
if err != nil {
|
|
return "", fmt.Errorf("creating image: %v", err)
|
|
}
|
|
|
|
data, err := os.Open(path)
|
|
if err != nil {
|
|
a.DeleteImage(image.ID)
|
|
return "", fmt.Errorf("opening image file: %v", err)
|
|
}
|
|
defer data.Close()
|
|
|
|
err = imagedata.Upload(a.imageClient, image.ID, data).ExtractErr()
|
|
if err != nil {
|
|
a.DeleteImage(image.ID)
|
|
return "", fmt.Errorf("uploading image data: %v", err)
|
|
}
|
|
|
|
return image.ID, nil
|
|
}
|
|
|
|
func (a *API) DeleteImage(imageID string) error {
|
|
return images.Delete(a.imageClient, imageID).ExtractErr()
|
|
}
|
|
|
|
func (a *API) AddKey(name, key string) error {
|
|
_, err := keypairs.Create(a.computeClient, keypairs.CreateOpts{
|
|
Name: name,
|
|
PublicKey: key,
|
|
}).Extract()
|
|
return err
|
|
}
|
|
|
|
func (a *API) DeleteKey(name string) error {
|
|
return keypairs.Delete(a.computeClient, name).ExtractErr()
|
|
}
|
|
|
|
func (a *API) listServersWithMetadata(metadata map[string]string) ([]servers.Server, error) {
|
|
pager := servers.List(a.computeClient, servers.ListOpts{})
|
|
|
|
pages, err := unwrapPages(pager, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("servers: %v", err)
|
|
}
|
|
|
|
allServers, err := servers.ExtractServers(pages)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extracting servers: %v", err)
|
|
}
|
|
var retServers []servers.Server
|
|
for _, server := range allServers {
|
|
isMatch := true
|
|
for key, val := range metadata {
|
|
if value, ok := server.Metadata[key]; !ok || val != value {
|
|
isMatch = false
|
|
break
|
|
}
|
|
}
|
|
if isMatch {
|
|
retServers = append(retServers, server)
|
|
}
|
|
}
|
|
return retServers, nil
|
|
}
|
|
|
|
func (a *API) GC(gracePeriod time.Duration) error {
|
|
threshold := time.Now().Add(-gracePeriod)
|
|
|
|
servers, err := a.listServersWithMetadata(map[string]string{
|
|
"CreatedBy": "mantle",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, server := range servers {
|
|
if strings.Contains(server.Status, "DELETED") || server.Created.After(threshold) {
|
|
continue
|
|
}
|
|
|
|
if err := a.DeleteServer(server.ID); err != nil {
|
|
return fmt.Errorf("couldn't delete server %s: %v", server.ID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|