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  }