1package openstack
2
3import (
4	"crypto/tls"
5	"crypto/x509"
6	"fmt"
7	"io/ioutil"
8	"net/http"
9	"time"
10
11	"github.com/docker/machine/libmachine/log"
12	"github.com/docker/machine/libmachine/mcnutils"
13	"github.com/docker/machine/libmachine/version"
14	"github.com/rackspace/gophercloud"
15	"github.com/rackspace/gophercloud/openstack"
16	compute_ips "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
17	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
18	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop"
19	"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
20	"github.com/rackspace/gophercloud/openstack/compute/v2/images"
21	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
22	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
23	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
24	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
25	"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
26	"github.com/rackspace/gophercloud/pagination"
27)
28
29type Client interface {
30	Authenticate(d *Driver) error
31	InitComputeClient(d *Driver) error
32	InitIdentityClient(d *Driver) error
33	InitNetworkClient(d *Driver) error
34
35	CreateInstance(d *Driver) (string, error)
36	GetInstanceState(d *Driver) (string, error)
37	StartInstance(d *Driver) error
38	StopInstance(d *Driver) error
39	RestartInstance(d *Driver) error
40	DeleteInstance(d *Driver) error
41	WaitForInstanceStatus(d *Driver, status string) error
42	GetInstanceIPAddresses(d *Driver) ([]IPAddress, error)
43	GetPublicKey(keyPairName string) ([]byte, error)
44	CreateKeyPair(d *Driver, name string, publicKey string) error
45	DeleteKeyPair(d *Driver, name string) error
46	GetNetworkID(d *Driver) (string, error)
47	GetFlavorID(d *Driver) (string, error)
48	GetImageID(d *Driver) (string, error)
49	AssignFloatingIP(d *Driver, floatingIP *FloatingIP) error
50	GetFloatingIPs(d *Driver) ([]FloatingIP, error)
51	GetFloatingIPPoolID(d *Driver) (string, error)
52	GetInstancePortID(d *Driver) (string, error)
53	GetTenantID(d *Driver) (string, error)
54}
55
56type GenericClient struct {
57	Provider *gophercloud.ProviderClient
58	Compute  *gophercloud.ServiceClient
59	Identity *gophercloud.ServiceClient
60	Network  *gophercloud.ServiceClient
61}
62
63func (c *GenericClient) CreateInstance(d *Driver) (string, error) {
64	serverOpts := servers.CreateOpts{
65		Name:             d.MachineName,
66		FlavorRef:        d.FlavorId,
67		ImageRef:         d.ImageId,
68		UserData:         d.UserData,
69		SecurityGroups:   d.SecurityGroups,
70		AvailabilityZone: d.AvailabilityZone,
71	}
72	if d.NetworkId != "" {
73		serverOpts.Networks = []servers.Network{
74			{
75				UUID: d.NetworkId,
76			},
77		}
78	}
79
80	log.Info("Creating machine...")
81
82	server, err := servers.Create(c.Compute, keypairs.CreateOptsExt{
83		serverOpts,
84		d.KeyPairName,
85	}).Extract()
86	if err != nil {
87		return "", err
88	}
89	return server.ID, nil
90}
91
92const (
93	Floating string = "floating"
94	Fixed    string = "fixed"
95)
96
97type IPAddress struct {
98	Network     string
99	AddressType string
100	Address     string
101	Version     int
102	Mac         string
103}
104
105type FloatingIP struct {
106	Id        string
107	Ip        string
108	NetworkId string
109	PortId    string
110	Pool      string
111	MachineId string
112}
113
114func (c *GenericClient) GetInstanceState(d *Driver) (string, error) {
115	server, err := c.GetServerDetail(d)
116	if err != nil {
117		return "", err
118	}
119	return server.Status, nil
120}
121
122func (c *GenericClient) StartInstance(d *Driver) error {
123	if result := startstop.Start(c.Compute, d.MachineId); result.Err != nil {
124		return result.Err
125	}
126	return nil
127}
128
129func (c *GenericClient) StopInstance(d *Driver) error {
130	if result := startstop.Stop(c.Compute, d.MachineId); result.Err != nil {
131		return result.Err
132	}
133	return nil
134}
135
136func (c *GenericClient) RestartInstance(d *Driver) error {
137	if result := servers.Reboot(c.Compute, d.MachineId, servers.SoftReboot); result.Err != nil {
138		return result.Err
139	}
140	return nil
141}
142
143func (c *GenericClient) DeleteInstance(d *Driver) error {
144	if result := servers.Delete(c.Compute, d.MachineId); result.Err != nil {
145		return result.Err
146	}
147	return nil
148}
149
150func (c *GenericClient) WaitForInstanceStatus(d *Driver, status string) error {
151	return mcnutils.WaitForSpecificOrError(func() (bool, error) {
152		current, err := servers.Get(c.Compute, d.MachineId).Extract()
153		if err != nil {
154			return true, err
155		}
156
157		if current.Status == "ERROR" {
158			return true, fmt.Errorf("Instance creation failed. Instance is in ERROR state")
159		}
160
161		if current.Status == status {
162			return true, nil
163		}
164
165		return false, nil
166	}, (d.ActiveTimeout / 4), 4*time.Second)
167}
168
169func (c *GenericClient) GetInstanceIPAddresses(d *Driver) ([]IPAddress, error) {
170	server, err := c.GetServerDetail(d)
171	if err != nil {
172		return nil, err
173	}
174	addresses := []IPAddress{}
175	for network, networkAddresses := range server.Addresses {
176		for _, element := range networkAddresses.([]interface{}) {
177			address := element.(map[string]interface{})
178			version, ok := address["version"].(float64)
179			if !ok {
180				// Assume IPv4 if no version present.
181				version = 4
182			}
183
184			addr := IPAddress{
185				Network:     network,
186				AddressType: Fixed,
187				Address:     address["addr"].(string),
188				Version:     int(version),
189			}
190
191			if tp, ok := address["OS-EXT-IPS:type"]; ok {
192				addr.AddressType = tp.(string)
193			}
194			if mac, ok := address["OS-EXT-IPS-MAC:mac_addr"]; ok {
195				addr.Mac = mac.(string)
196			}
197
198			addresses = append(addresses, addr)
199		}
200	}
201
202	return addresses, nil
203}
204
205func (c *GenericClient) GetNetworkID(d *Driver) (string, error) {
206	return c.getNetworkID(d, d.NetworkName)
207}
208
209func (c *GenericClient) GetFloatingIPPoolID(d *Driver) (string, error) {
210	return c.getNetworkID(d, d.FloatingIpPool)
211}
212
213func (c *GenericClient) getNetworkID(d *Driver, networkName string) (string, error) {
214	opts := networks.ListOpts{Name: networkName}
215	pager := networks.List(c.Network, opts)
216	networkID := ""
217
218	err := pager.EachPage(func(page pagination.Page) (bool, error) {
219		networkList, err := networks.ExtractNetworks(page)
220		if err != nil {
221			return false, err
222		}
223
224		for _, n := range networkList {
225			if n.Name == networkName {
226				networkID = n.ID
227				return false, nil
228			}
229		}
230
231		return true, nil
232	})
233
234	return networkID, err
235}
236
237func (c *GenericClient) GetFlavorID(d *Driver) (string, error) {
238	pager := flavors.ListDetail(c.Compute, nil)
239	flavorID := ""
240
241	err := pager.EachPage(func(page pagination.Page) (bool, error) {
242		flavorList, err := flavors.ExtractFlavors(page)
243		if err != nil {
244			return false, err
245		}
246
247		for _, f := range flavorList {
248			if f.Name == d.FlavorName {
249				flavorID = f.ID
250				return false, nil
251			}
252		}
253
254		return true, nil
255	})
256
257	return flavorID, err
258}
259
260func (c *GenericClient) GetImageID(d *Driver) (string, error) {
261	opts := images.ListOpts{Name: d.ImageName}
262	pager := images.ListDetail(c.Compute, opts)
263	imageID := ""
264
265	err := pager.EachPage(func(page pagination.Page) (bool, error) {
266		imageList, err := images.ExtractImages(page)
267		if err != nil {
268			return false, err
269		}
270
271		for _, i := range imageList {
272			if i.Name == d.ImageName {
273				imageID = i.ID
274				return false, nil
275			}
276		}
277
278		return true, nil
279	})
280
281	return imageID, err
282}
283
284func (c *GenericClient) GetTenantID(d *Driver) (string, error) {
285	pager := tenants.List(c.Identity, nil)
286	tenantId := ""
287
288	err := pager.EachPage(func(page pagination.Page) (bool, error) {
289		tenantList, err := tenants.ExtractTenants(page)
290		if err != nil {
291			return false, err
292		}
293
294		for _, i := range tenantList {
295			if i.Name == d.TenantName {
296				tenantId = i.ID
297				return false, nil
298			}
299		}
300
301		return true, nil
302	})
303
304	return tenantId, err
305}
306
307func (c *GenericClient) GetPublicKey(keyPairName string) ([]byte, error) {
308	kp, err := keypairs.Get(c.Compute, keyPairName).Extract()
309	if err != nil {
310		return nil, err
311	}
312	return []byte(kp.PublicKey), nil
313}
314
315func (c *GenericClient) CreateKeyPair(d *Driver, name string, publicKey string) error {
316	opts := keypairs.CreateOpts{
317		Name:      name,
318		PublicKey: publicKey,
319	}
320	if result := keypairs.Create(c.Compute, opts); result.Err != nil {
321		return result.Err
322	}
323	return nil
324}
325
326func (c *GenericClient) DeleteKeyPair(d *Driver, name string) error {
327	if result := keypairs.Delete(c.Compute, name); result.Err != nil {
328		return result.Err
329	}
330	return nil
331}
332
333func (c *GenericClient) GetServerDetail(d *Driver) (*servers.Server, error) {
334	server, err := servers.Get(c.Compute, d.MachineId).Extract()
335	if err != nil {
336		return nil, err
337	}
338	return server, nil
339}
340
341func (c *GenericClient) AssignFloatingIP(d *Driver, floatingIP *FloatingIP) error {
342	if d.ComputeNetwork {
343		return c.assignNovaFloatingIP(d, floatingIP)
344	}
345	return c.assignNeutronFloatingIP(d, floatingIP)
346}
347
348func (c *GenericClient) assignNovaFloatingIP(d *Driver, floatingIP *FloatingIP) error {
349	if floatingIP.Ip == "" {
350		f, err := compute_ips.Create(c.Compute, compute_ips.CreateOpts{
351			Pool: d.FloatingIpPool,
352		}).Extract()
353		if err != nil {
354			return err
355		}
356		floatingIP.Ip = f.IP
357		floatingIP.Pool = f.Pool
358	}
359	return compute_ips.Associate(c.Compute, d.MachineId, floatingIP.Ip).Err
360}
361
362func (c *GenericClient) assignNeutronFloatingIP(d *Driver, floatingIP *FloatingIP) error {
363	portID, err := c.GetInstancePortID(d)
364	if err != nil {
365		return err
366	}
367	if floatingIP.Id == "" {
368		f, err := floatingips.Create(c.Network, floatingips.CreateOpts{
369			FloatingNetworkID: d.FloatingIpPoolId,
370			PortID:            portID,
371		}).Extract()
372		if err != nil {
373			return err
374		}
375		floatingIP.Id = f.ID
376		floatingIP.Ip = f.FloatingIP
377		floatingIP.NetworkId = f.FloatingNetworkID
378		floatingIP.PortId = f.PortID
379		return nil
380	}
381	_, err = floatingips.Update(c.Network, floatingIP.Id, floatingips.UpdateOpts{
382		PortID: portID,
383	}).Extract()
384	if err != nil {
385		return err
386	}
387	return nil
388}
389
390func (c *GenericClient) GetFloatingIPs(d *Driver) ([]FloatingIP, error) {
391	if d.ComputeNetwork {
392		return c.getNovaNetworkFloatingIPs(d)
393	}
394	return c.getNeutronNetworkFloatingIPs(d)
395}
396
397func (c *GenericClient) getNovaNetworkFloatingIPs(d *Driver) ([]FloatingIP, error) {
398	pager := compute_ips.List(c.Compute)
399
400	ips := []FloatingIP{}
401	err := pager.EachPage(func(page pagination.Page) (continue_paging bool, err error) {
402		continue_paging, err = true, nil
403		ipListing, err := compute_ips.ExtractFloatingIPs(page)
404
405		for _, ip := range ipListing {
406			if ip.InstanceID == "" && ip.Pool == d.FloatingIpPool {
407				ips = append(ips, FloatingIP{
408					Id:   ip.ID,
409					Ip:   ip.IP,
410					Pool: ip.Pool,
411				})
412			}
413		}
414		return
415	})
416	return ips, err
417}
418
419func (c *GenericClient) getNeutronNetworkFloatingIPs(d *Driver) ([]FloatingIP, error) {
420	log.Debug("Listing floating IPs", map[string]string{
421		"FloatingNetworkId": d.FloatingIpPoolId,
422		"TenantID":          d.TenantId,
423	})
424	pager := floatingips.List(c.Network, floatingips.ListOpts{
425		FloatingNetworkID: d.FloatingIpPoolId,
426		TenantID:          d.TenantId,
427	})
428
429	ips := []FloatingIP{}
430	err := pager.EachPage(func(page pagination.Page) (bool, error) {
431		floatingipList, err := floatingips.ExtractFloatingIPs(page)
432		if err != nil {
433			return false, err
434		}
435		for _, f := range floatingipList {
436			ips = append(ips, FloatingIP{
437				Id:        f.ID,
438				Ip:        f.FloatingIP,
439				NetworkId: f.FloatingNetworkID,
440				PortId:    f.PortID,
441			})
442		}
443		return true, nil
444	})
445
446	if err != nil {
447		return nil, err
448	}
449	return ips, nil
450}
451
452func (c *GenericClient) GetInstancePortID(d *Driver) (string, error) {
453	pager := ports.List(c.Network, ports.ListOpts{
454		DeviceID:  d.MachineId,
455		NetworkID: d.NetworkId,
456	})
457
458	var portID string
459	err := pager.EachPage(func(page pagination.Page) (bool, error) {
460		portList, err := ports.ExtractPorts(page)
461		if err != nil {
462			return false, err
463		}
464		for _, port := range portList {
465			portID = port.ID
466			return false, nil
467		}
468		return true, nil
469	})
470
471	if err != nil {
472		return "", err
473	}
474	return portID, nil
475}
476
477func (c *GenericClient) InitComputeClient(d *Driver) error {
478	if c.Compute != nil {
479		return nil
480	}
481
482	compute, err := openstack.NewComputeV2(c.Provider, gophercloud.EndpointOpts{
483		Region:       d.Region,
484		Availability: c.getEndpointType(d),
485	})
486	if err != nil {
487		return err
488	}
489	c.Compute = compute
490	return nil
491}
492
493func (c *GenericClient) InitIdentityClient(d *Driver) error {
494	if c.Identity != nil {
495		return nil
496	}
497
498	identity := openstack.NewIdentityV2(c.Provider)
499	c.Identity = identity
500	return nil
501}
502
503func (c *GenericClient) InitNetworkClient(d *Driver) error {
504	if c.Network != nil {
505		return nil
506	}
507
508	network, err := openstack.NewNetworkV2(c.Provider, gophercloud.EndpointOpts{
509		Region:       d.Region,
510		Availability: c.getEndpointType(d),
511	})
512	if err != nil {
513		return err
514	}
515	c.Network = network
516	return nil
517}
518
519func (c *GenericClient) getEndpointType(d *Driver) gophercloud.Availability {
520	switch d.EndpointType {
521	case "internalURL":
522		return gophercloud.AvailabilityInternal
523	case "adminURL":
524		return gophercloud.AvailabilityAdmin
525	}
526	return gophercloud.AvailabilityPublic
527}
528
529func (c *GenericClient) Authenticate(d *Driver) error {
530	if c.Provider != nil {
531		return nil
532	}
533
534	log.Debug("Authenticating...", map[string]interface{}{
535		"AuthUrl":    d.AuthUrl,
536		"Insecure":   d.Insecure,
537		"CaCert":     d.CaCert,
538		"DomainID":   d.DomainID,
539		"DomainName": d.DomainName,
540		"Username":   d.Username,
541		"TenantName": d.TenantName,
542		"TenantID":   d.TenantId,
543	})
544
545	opts := gophercloud.AuthOptions{
546		IdentityEndpoint: d.AuthUrl,
547		DomainID:         d.DomainID,
548		DomainName:       d.DomainName,
549		Username:         d.Username,
550		Password:         d.Password,
551		TenantName:       d.TenantName,
552		TenantID:         d.TenantId,
553		AllowReauth:      true,
554	}
555
556	provider, err := openstack.NewClient(opts.IdentityEndpoint)
557	if err != nil {
558		return err
559	}
560
561	c.Provider = provider
562
563	c.Provider.UserAgent.Prepend(fmt.Sprintf("docker-machine/v%d", version.APIVersion))
564
565	err = c.SetTLSConfig(d)
566	if err != nil {
567		return err
568	}
569
570	err = openstack.Authenticate(c.Provider, opts)
571	if err != nil {
572		return err
573	}
574
575	return nil
576}
577
578func (c *GenericClient) SetTLSConfig(d *Driver) error {
579
580	config := &tls.Config{}
581	config.InsecureSkipVerify = d.Insecure
582
583	if d.CaCert != "" {
584		// Use custom CA certificate(s) for root of trust
585		certpool := x509.NewCertPool()
586		pem, err := ioutil.ReadFile(d.CaCert)
587		if err != nil {
588			log.Error("Unable to read specified CA certificate(s)")
589			return err
590		}
591
592		ok := certpool.AppendCertsFromPEM(pem)
593		if !ok {
594			return fmt.Errorf("Ill-formed CA certificate(s) PEM file")
595		}
596		config.RootCAs = certpool
597	}
598
599	transport := &http.Transport{TLSClientConfig: config}
600	c.Provider.HTTPClient.Transport = transport
601	return nil
602}
603