This repository has been archived by the owner on Nov 14, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
/
compute_instance_v2_networking.go
530 lines (452 loc) · 16.4 KB
/
compute_instance_v2_networking.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
// This set of code handles all functions required to configure networking
// on an openstack_compute_instance_v2 resource.
//
// This is a complicated task because it's not possible to obtain all
// information in a single API call. In fact, it even traverses multiple
// OpenStack services.
//
// The end result, from the user's point of view, is a structured set of
// understandable network information within the instance resource.
package openstack
import (
"fmt"
"log"
"os"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
"github.com/hashicorp/terraform/helper/schema"
)
// InstanceNIC is a structured representation of a Gophercloud servers.Server
// virtual NIC.
type InstanceNIC struct {
FixedIPv4 string
FixedIPv6 string
MAC string
}
// InstanceAddresses is a collection of InstanceNICs, grouped by the
// network name. An instance/server could have multiple NICs on the same
// network.
type InstanceAddresses struct {
NetworkName string
InstanceNICs []InstanceNIC
}
// InstanceNetwork represents a collection of network information that a
// Terraform instance needs to satisfy all network information requirements.
type InstanceNetwork struct {
UUID string
Name string
Port string
FixedIP string
AccessNetwork bool
}
// getAllInstanceNetworks loops through the networks defined in the Terraform
// configuration and structures that information into something standard that
// can be consumed by both OpenStack and Terraform.
//
// This would be simple, except we have ensure both the network name and
// network ID have been determined. This isn't just for the convenience of a
// user specifying a human-readable network name, but the network information
// returned by an OpenStack instance only has the network name set! So if a
// user specified a network ID, there's no way to correlate it to the instance
// unless we know both the name and ID.
//
// Not only that, but we have to account for two OpenStack network services
// running: nova-network (legacy) and Neutron (current).
//
// In addition, if a port was specified, not all of the port information
// will be displayed, such as multiple fixed and floating IPs. This resource
// isn't currently configured for that type of flexibility. It's better to
// reference the actual port resource itself.
//
// So, let's begin the journey.
func getAllInstanceNetworks(d *schema.ResourceData, meta interface{}) ([]InstanceNetwork, error) {
var instanceNetworks []InstanceNetwork
networks := d.Get("network").([]interface{})
for _, v := range networks {
network := v.(map[string]interface{})
networkID := network["uuid"].(string)
networkName := network["name"].(string)
portID := network["port"].(string)
if networkID == "" && networkName == "" && portID == "" {
return nil, fmt.Errorf(
"At least one of network.uuid, network.name, or network.port must be set.")
}
// If a user specified both an ID and name, that makes things easy
// since both name and ID are already satisfied. No need to query
// further.
if networkID != "" && networkName != "" {
v := InstanceNetwork{
UUID: networkID,
Name: networkName,
Port: portID,
FixedIP: network["fixed_ip_v4"].(string),
AccessNetwork: network["access_network"].(bool),
}
instanceNetworks = append(instanceNetworks, v)
continue
}
// But if at least one of name or ID was missing, we have to query
// for that other piece.
//
// Priority is given to a port since a network ID or name usually isn't
// specified when using a port.
//
// Next priority is given to the network ID since it's guaranteed to be
// an exact match.
queryType := "name"
queryTerm := networkName
if networkID != "" {
queryType = "id"
queryTerm = networkID
}
if portID != "" {
queryType = "port"
queryTerm = portID
}
networkInfo, err := getInstanceNetworkInfo(d, meta, queryType, queryTerm)
if err != nil {
return nil, err
}
v := InstanceNetwork{
Port: portID,
FixedIP: network["fixed_ip_v4"].(string),
AccessNetwork: network["access_network"].(bool),
}
if networkInfo["uuid"] != nil {
v.UUID = networkInfo["uuid"].(string)
}
if networkInfo["name"] != nil {
v.Name = networkInfo["name"].(string)
}
instanceNetworks = append(instanceNetworks, v)
}
log.Printf("[DEBUG] getAllInstanceNetworks: %#v", instanceNetworks)
return instanceNetworks, nil
}
// getInstanceNetworkInfo will query for network information in order to make
// an accurate determination of a network's name and a network's ID.
//
// We will try to first query the Neutron network service and fall back to the
// legacy nova-network service if that fails.
//
// If OS_NOVA_NETWORK is set, query nova-network even if Neutron is available.
// This is to be able to explicitly test the nova-network API.
func getInstanceNetworkInfo(
d *schema.ResourceData, meta interface{}, queryType, queryTerm string) (map[string]interface{}, error) {
config := meta.(*Config)
if _, ok := os.LookupEnv("OS_NOVA_NETWORK"); !ok {
networkClient, err := config.networkingV2Client(GetRegion(d, config))
if err == nil {
networkInfo, err := getInstanceNetworkInfoNeutron(networkClient, queryType, queryTerm)
if err != nil {
return nil, fmt.Errorf("Error trying to get network information from the Network API: %s", err)
}
return networkInfo, nil
}
}
log.Printf("[DEBUG] Unable to obtain a network client")
computeClient, err := config.computeV2Client(GetRegion(d, config))
if err != nil {
return nil, fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
networkInfo, err := getInstanceNetworkInfoNovaNet(computeClient, queryType, queryTerm)
if err != nil {
return nil, fmt.Errorf("Error trying to get network information from the Nova API: %s", err)
}
return networkInfo, nil
}
// getInstanceNetworkInfoNovaNet will query the os-tenant-networks API for
// the network information.
func getInstanceNetworkInfoNovaNet(
client *gophercloud.ServiceClient, queryType, queryTerm string) (map[string]interface{}, error) {
// If somehow a port ended up here, we should just error out.
if queryType == "port" {
return nil, fmt.Errorf(
"Unable to query a port (%s) using the Nova API", queryTerm)
}
// test to see if the tenantnetworks api is available
log.Printf("[DEBUG] testing for os-tenant-networks")
tenantNetworksAvailable := true
allPages, err := tenantnetworks.List(client).AllPages()
if err != nil {
switch err.(type) {
case gophercloud.ErrDefault404:
tenantNetworksAvailable = false
case gophercloud.ErrDefault403:
tenantNetworksAvailable = false
case gophercloud.ErrUnexpectedResponseCode:
tenantNetworksAvailable = false
default:
return nil, fmt.Errorf(
"An error occurred while querying the Nova API for network information: %s", err)
}
}
if !tenantNetworksAvailable {
// we can't query the APIs for more information, but in some cases
// the information provided is enough
log.Printf("[DEBUG] os-tenant-networks disabled.")
return map[string]interface{}{queryType: queryTerm}, nil
}
networkList, err := tenantnetworks.ExtractNetworks(allPages)
if err != nil {
return nil, fmt.Errorf(
"An error occurred while querying the Nova API for network information: %s", err)
}
var networkFound bool
var network tenantnetworks.Network
for _, v := range networkList {
if queryType == "id" && v.ID == queryTerm {
networkFound = true
network = v
break
}
if queryType == "name" && v.Name == queryTerm {
networkFound = true
network = v
break
}
}
if networkFound {
v := map[string]interface{}{
"uuid": network.ID,
"name": network.Name,
}
log.Printf("[DEBUG] getInstanceNetworkInfoNovaNet: %#v", v)
return v, nil
}
return nil, fmt.Errorf("Could not find any matching network for %s %s", queryType, queryTerm)
}
// getInstanceNetworkInfoNeutron will query the neutron API for the network
// information.
func getInstanceNetworkInfoNeutron(
client *gophercloud.ServiceClient, queryType, queryTerm string) (map[string]interface{}, error) {
// If a port was specified, use it to look up the network ID
// and then query the network as if a network ID was originally used.
if queryType == "port" {
listOpts := ports.ListOpts{
ID: queryTerm,
}
allPages, err := ports.List(client, listOpts).AllPages()
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
allPorts, err := ports.ExtractPorts(allPages)
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
var port ports.Port
switch len(allPorts) {
case 0:
return nil, fmt.Errorf("Could not find any matching port for %s %s", queryType, queryTerm)
case 1:
port = allPorts[0]
default:
return nil, fmt.Errorf("More than one port found for %s %s", queryType, queryTerm)
}
queryType = "id"
queryTerm = port.NetworkID
}
listOpts := networks.ListOpts{
Status: "ACTIVE",
}
switch queryType {
case "name":
listOpts.Name = queryTerm
default:
listOpts.ID = queryTerm
}
allPages, err := networks.List(client, listOpts).AllPages()
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
allNetworks, err := networks.ExtractNetworks(allPages)
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
var network networks.Network
switch len(allNetworks) {
case 0:
return nil, fmt.Errorf("Could not find any matching network for %s %s", queryType, queryTerm)
case 1:
network = allNetworks[0]
default:
return nil, fmt.Errorf("More than one network found for %s %s", queryType, queryTerm)
}
v := map[string]interface{}{
"uuid": network.ID,
"name": network.Name,
}
log.Printf("[DEBUG] getInstanceNetworkInfoNeutron: %#v", v)
return v, nil
}
// getInstanceAddresses parses a Gophercloud server.Server's Address field into
// a structured InstanceAddresses struct.
func getInstanceAddresses(addresses map[string]interface{}) []InstanceAddresses {
var allInstanceAddresses []InstanceAddresses
for networkName, v := range addresses {
instanceAddresses := InstanceAddresses{
NetworkName: networkName,
}
for _, v := range v.([]interface{}) {
instanceNIC := InstanceNIC{}
var exists bool
v := v.(map[string]interface{})
if v, ok := v["OS-EXT-IPS-MAC:mac_addr"].(string); ok {
instanceNIC.MAC = v
}
if v["OS-EXT-IPS:type"] == "fixed" || v["OS-EXT-IPS:type"] == nil {
switch v["version"].(float64) {
case 6:
instanceNIC.FixedIPv6 = fmt.Sprintf("[%s]", v["addr"].(string))
default:
instanceNIC.FixedIPv4 = v["addr"].(string)
}
}
// To associate IPv4 and IPv6 on the right NIC,
// key on the mac address and fill in the blanks.
for i, v := range instanceAddresses.InstanceNICs {
if v.MAC == instanceNIC.MAC {
exists = true
if instanceNIC.FixedIPv6 != "" {
instanceAddresses.InstanceNICs[i].FixedIPv6 = instanceNIC.FixedIPv6
}
if instanceNIC.FixedIPv4 != "" {
instanceAddresses.InstanceNICs[i].FixedIPv4 = instanceNIC.FixedIPv4
}
}
}
if !exists {
instanceAddresses.InstanceNICs = append(instanceAddresses.InstanceNICs, instanceNIC)
}
}
allInstanceAddresses = append(allInstanceAddresses, instanceAddresses)
}
log.Printf("[DEBUG] Addresses: %#v", addresses)
log.Printf("[DEBUG] allInstanceAddresses: %#v", allInstanceAddresses)
return allInstanceAddresses
}
// expandInstanceNetworks takes network information found in []InstanceNetwork
// and builds a Gophercloud []servers.Network for use in creating an Instance.
func expandInstanceNetworks(allInstanceNetworks []InstanceNetwork) []servers.Network {
var networks []servers.Network
for _, v := range allInstanceNetworks {
n := servers.Network{
UUID: v.UUID,
Port: v.Port,
FixedIP: v.FixedIP,
}
networks = append(networks, n)
}
return networks
}
// flattenInstanceNetworks collects instance network information from different
// sources and aggregates it all together into a map array.
func flattenInstanceNetworks(
d *schema.ResourceData, meta interface{}) ([]map[string]interface{}, error) {
config := meta.(*Config)
computeClient, err := config.computeV2Client(GetRegion(d, config))
if err != nil {
return nil, fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
server, err := servers.Get(computeClient, d.Id()).Extract()
if err != nil {
return nil, CheckDeleted(d, err, "server")
}
allInstanceAddresses := getInstanceAddresses(server.Addresses)
allInstanceNetworks, err := getAllInstanceNetworks(d, meta)
if err != nil {
return nil, err
}
networks := []map[string]interface{}{}
// If there were no instance networks returned, this means that there
// was not a network specified in the Terraform configuration. When this
// happens, the instance will be launched on a "default" network, if one
// is available. If there isn't, the instance will fail to launch, so
// this is a safe assumption at this point.
if len(allInstanceNetworks) == 0 {
for _, instanceAddresses := range allInstanceAddresses {
for _, instanceNIC := range instanceAddresses.InstanceNICs {
v := map[string]interface{}{
"name": instanceAddresses.NetworkName,
"fixed_ip_v4": instanceNIC.FixedIPv4,
"fixed_ip_v6": instanceNIC.FixedIPv6,
"mac": instanceNIC.MAC,
}
// Use the same method as getAllInstanceNetworks to get the network uuid
networkInfo, err := getInstanceNetworkInfo(d, meta, "name", instanceAddresses.NetworkName)
if err != nil {
log.Printf("[WARN] Error getting default network uuid: %s", err)
} else {
if v["uuid"] != nil {
v["uuid"] = networkInfo["uuid"].(string)
} else {
log.Printf("[WARN] Could not get default network uuid")
}
}
networks = append(networks, v)
}
}
log.Printf("[DEBUG] flattenInstanceNetworks: %#v", networks)
return networks, nil
}
// Loop through all networks and addresses, merge relevant address details.
for _, instanceNetwork := range allInstanceNetworks {
for _, instanceAddresses := range allInstanceAddresses {
// Skip if instanceAddresses has no NICs
if len(instanceAddresses.InstanceNICs) == 0 {
continue
}
if instanceNetwork.Name == instanceAddresses.NetworkName {
// Only use one NIC since it's possible the user defined another NIC
// on this same network in another Terraform network block.
instanceNIC := instanceAddresses.InstanceNICs[0]
copy(instanceAddresses.InstanceNICs, instanceAddresses.InstanceNICs[1:])
v := map[string]interface{}{
"name": instanceAddresses.NetworkName,
"fixed_ip_v4": instanceNIC.FixedIPv4,
"fixed_ip_v6": instanceNIC.FixedIPv6,
"mac": instanceNIC.MAC,
"uuid": instanceNetwork.UUID,
"port": instanceNetwork.Port,
"access_network": instanceNetwork.AccessNetwork,
}
networks = append(networks, v)
}
}
}
log.Printf("[DEBUG] flattenInstanceNetworks: %#v", networks)
return networks, nil
}
// getInstanceAccessAddresses determines the best IP address to communicate
// with the instance. It does this by looping through all networks and looking
// for a valid IP address. Priority is given to a network that was flagged as
// an access_network.
func getInstanceAccessAddresses(
d *schema.ResourceData, networks []map[string]interface{}) (string, string) {
var hostv4, hostv6 string
// Loop through all networks
// If the network has a valid fixed v4 or fixed v6 address
// and hostv4 or hostv6 is not set, set hostv4/hostv6.
// If the network is an "access_network" overwrite hostv4/hostv6.
for _, n := range networks {
var accessNetwork bool
if an, ok := n["access_network"].(bool); ok && an {
accessNetwork = true
}
if fixedIPv4, ok := n["fixed_ip_v4"].(string); ok && fixedIPv4 != "" {
if hostv4 == "" || accessNetwork {
hostv4 = fixedIPv4
}
}
if fixedIPv6, ok := n["fixed_ip_v6"].(string); ok && fixedIPv6 != "" {
if hostv6 == "" || accessNetwork {
hostv6 = fixedIPv6
}
}
}
log.Printf("[DEBUG] OpenStack Instance Network Access Addresses: %s, %s", hostv4, hostv6)
return hostv4, hostv6
}