forked from juju/juju
-
Notifications
You must be signed in to change notification settings - Fork 0
/
environ_network.go
466 lines (399 loc) · 15.5 KB
/
environ_network.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
// Copyright 2020 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package lxd
import (
"fmt"
"net"
"sort"
"strings"
"github.com/juju/collections/set"
"github.com/juju/errors"
"github.com/juju/names/v4"
lxdapi "github.com/lxc/lxd/shared/api"
"github.com/juju/juju/core/instance"
"github.com/juju/juju/core/network"
"github.com/juju/juju/environs"
"github.com/juju/juju/environs/context"
)
var _ environs.Networking = (*environ)(nil)
// Subnets returns basic information about subnets known by the provider for
// the environment.
func (e *environ) Subnets(ctx context.ProviderCallContext, inst instance.Id, subnetIDs []network.Id) ([]network.SubnetInfo, error) {
srv := e.server()
// All containers will have the same view on the LXD network. If an
// instance ID is provided, the best we can do is to also ensure the
// container actually exists at the cost of an additional API call.
if inst != instance.UnknownId {
contList, err := srv.FilterContainers(string(inst))
if err != nil {
return nil, errors.Trace(err)
} else if len(contList) == 0 {
return nil, errors.NotFoundf("container with instance ID %q", inst)
}
}
// Query the lxd server name; we will use that as the AZ name for any
// subnets that we report.
serverInfo, _, err := srv.GetServer()
if err != nil {
return nil, errors.Annotate(err, "looking up lxd server details")
}
azName := serverInfo.Environment.ServerName
networkNames, err := srv.GetNetworkNames()
if err != nil {
if isErrMissingAPIExtension(err, "network") {
return nil, errors.NewNotSupported(nil, `subnet discovery requires the "network" extension to be enabled on the lxd server`)
}
return nil, errors.Trace(err)
}
var keepList set.Strings
if len(subnetIDs) != 0 {
keepList = set.NewStrings()
for _, id := range subnetIDs {
keepList.Add(string(id))
}
}
var (
subnets []network.SubnetInfo
uniqueSubnetIDs = set.NewStrings()
)
for _, networkName := range networkNames {
// Query the details for this network and skip non-bridge networks.
networkDetails, _, err := srv.GetNetwork(networkName)
if err != nil {
return nil, errors.Annotatef(err, "querying lxd server for details of network %q", networkName)
} else if networkDetails.Type != "bridge" {
continue
}
state, err := srv.GetNetworkState(networkName)
if err != nil {
// Unfortunately, LXD on bionic and earlier does not
// support the network_state extension out of the box
// so this call will fail. If that's the case then
// use a fallback method for detecting subnets.
if isErrMissingAPIExtension(err, "network_state") {
return e.subnetDetectionFallback(srv, inst, keepList, azName)
}
return nil, errors.Annotatef(err, "querying lxd server for state of network %q", networkName)
}
// We are only interested in networks that are up.
if state.State != "up" {
continue
}
for _, stateAddr := range state.Addresses {
netAddr := network.NewProviderAddress(stateAddr.Address)
if netAddr.Scope == network.ScopeLinkLocal || netAddr.Scope == network.ScopeMachineLocal {
continue
}
subnetID, cidr, err := makeSubnetIDForNetwork(networkName, stateAddr.Address, stateAddr.Netmask)
if err != nil {
return nil, errors.Trace(err)
}
if uniqueSubnetIDs.Contains(subnetID) {
continue
} else if keepList != nil && !keepList.Contains(subnetID) {
continue
}
uniqueSubnetIDs.Add(subnetID)
subnets = append(subnets, makeSubnetInfo(network.Id(subnetID), makeNetworkID(networkName), cidr, azName))
}
}
return subnets, nil
}
// subnetDetectionFallback provides a fallback mechanism for subnet discovery
// on older LXD versions (e.g. the ones that ship with xenial and bionic) which
// do not come with the network_state API extension enabled.
//
// The fallback exploits the fact that subnet discovery is performed after the
// controller spins up. To this end, the method will query any of the available
// juju containers and attempt to reconstruct the subnet information based on
// the devices present inside the container.
//
// Caveat: this method offers lower data fidelity compared to Subnets() as it
// cannot accurately detect the CIDRs for any host devices that are not bridged
// into the container.
func (e *environ) subnetDetectionFallback(srv Server, inst instance.Id, keepSubnetIDs set.Strings, azName string) ([]network.SubnetInfo, error) {
logger.Warningf("falling back to subnet discovery via introspection of devices bridged to the controller container; consider upgrading to a newer LXD version and running 'juju reload-spaces' to get full subnet discovery for the LXD host")
// If no instance ID is specified, list the alive containers, query the
// state of the first one on the list and use it to extrapolate the
// subnet layout.
if inst == instance.UnknownId {
aliveConts, err := srv.AliveContainers("juju-")
if err != nil {
return nil, errors.Trace(err)
} else if len(aliveConts) == 0 {
return nil, errors.New("no alive containers detected")
}
inst = instance.Id(aliveConts[0].Name)
}
container, state, err := getContainerDetails(srv, string(inst))
if err != nil {
return nil, errors.Trace(err)
}
var (
subnets []network.SubnetInfo
uniqueSubnetIDs = set.NewStrings()
)
for guestNetworkName, netInfo := range state.Network {
hostNetworkName := hostNetworkForGuestNetwork(container, guestNetworkName)
if hostNetworkName == "" { // doesn't have a parent; assume non-bridged NIC
continue
}
// Ignore loopback devices and NICs in down state.
if detectInterfaceType(netInfo.Type) == network.LoopbackDevice || netInfo.State != "up" {
continue
}
for _, guestAddr := range netInfo.Addresses {
netAddr := network.NewProviderAddress(guestAddr.Address)
if netAddr.Scope == network.ScopeLinkLocal || netAddr.Scope == network.ScopeMachineLocal {
continue
}
// Use the detected host network name and the guest
// address details to generate a subnetID for the host.
subnetID, cidr, err := makeSubnetIDForNetwork(hostNetworkName, guestAddr.Address, guestAddr.Netmask)
if err != nil {
return nil, errors.Trace(err)
}
if uniqueSubnetIDs.Contains(subnetID) {
continue
} else if keepSubnetIDs != nil && !keepSubnetIDs.Contains(subnetID) {
continue
}
uniqueSubnetIDs.Add(subnetID)
subnets = append(subnets, makeSubnetInfo(network.Id(subnetID), makeNetworkID(hostNetworkName), cidr, azName))
}
}
return subnets, nil
}
func makeNetworkID(networkName string) network.Id {
return network.Id(fmt.Sprintf("net-%s", networkName))
}
func makeSubnetIDForNetwork(networkName, address, mask string) (string, string, error) {
_, netCIDR, err := net.ParseCIDR(fmt.Sprintf("%s/%s", address, mask))
if err != nil {
return "", "", errors.Annotatef(err, "calculating CIDR for network %q", networkName)
}
cidr := netCIDR.String()
subnetID := fmt.Sprintf("subnet-%s-%s", networkName, cidr)
return subnetID, cidr, nil
}
func makeSubnetInfo(subnetID network.Id, networkID network.Id, cidr, azName string) network.SubnetInfo {
return network.SubnetInfo{
ProviderId: subnetID,
ProviderNetworkId: networkID,
CIDR: cidr,
VLANTag: 0,
AvailabilityZones: []string{azName},
}
}
// NetworkInterfaces returns a slice with the network interfaces that
// correspond to the given instance IDs. If no instances where found, but there
// was no other error, it will return ErrNoInstances. If some but not all of
// the instances were found, the returned slice will have some nil slots, and
// an ErrPartialInstances error will be returned.
func (e *environ) NetworkInterfaces(_ context.ProviderCallContext, ids []instance.Id) ([]network.InterfaceInfos, error) {
var (
missing int
srv = e.server()
res = make([]network.InterfaceInfos, len(ids))
)
for instIdx, id := range ids {
container, state, err := getContainerDetails(srv, string(id))
if err != nil {
if errors.IsNotFound(err) {
missing++
continue
}
return nil, errors.Annotatef(err, "retrieving network interface info for instance %q", id)
} else if len(state.Network) == 0 {
continue
}
// Sort interfaces by name to ensure consistent device indexes
// across calls when we iterate the container's network map.
guestNetworkNames := make([]string, 0, len(state.Network))
for network := range state.Network {
guestNetworkNames = append(guestNetworkNames, network)
}
sort.Strings(guestNetworkNames)
var devIdx int
for _, guestNetworkName := range guestNetworkNames {
netInfo := state.Network[guestNetworkName]
// Ignore loopback devices
if detectInterfaceType(netInfo.Type) == network.LoopbackDevice {
continue
}
ni, err := makeInterfaceInfo(container, guestNetworkName, netInfo)
if err != nil {
return nil, errors.Annotatef(err, "retrieving network interface info for instance %q", id)
} else if len(ni.Addresses) == 0 {
continue
}
ni.DeviceIndex = devIdx
devIdx++
res[instIdx] = append(res[instIdx], ni)
}
}
if missing > 0 {
// Found at least one instance
if missing != len(res) {
return res, environs.ErrPartialInstances
}
return nil, environs.ErrNoInstances
}
return res, nil
}
func makeInterfaceInfo(container *lxdapi.Container, guestNetworkName string, netInfo lxdapi.ContainerStateNetwork) (network.InterfaceInfo, error) {
var ni = network.InterfaceInfo{
MACAddress: netInfo.Hwaddr,
MTU: netInfo.Mtu,
InterfaceName: guestNetworkName,
ParentInterfaceName: hostNetworkForGuestNetwork(container, guestNetworkName),
InterfaceType: detectInterfaceType(netInfo.Type),
Origin: network.OriginProvider,
}
// We cannot tell from the API response whether the
// interface uses a static or DHCP configuration.
// Assume static unless this is a loopback device.
configType := network.ConfigStatic
if ni.InterfaceType == network.LoopbackDevice {
configType = network.ConfigLoopback
}
if ni.ParentInterfaceName != "" {
ni.ProviderNetworkId = makeNetworkID(ni.ParentInterfaceName)
}
// Iterate the list of addresses assigned to this interface ignoring
// any link-local ones. The first non link-local address is treated as
// the primary address and is used to populate the interface CIDR and
// subnet ID fields.
for _, addr := range netInfo.Addresses {
netAddr := network.NewProviderAddress(addr.Address)
if netAddr.Scope == network.ScopeLinkLocal || netAddr.Scope == network.ScopeMachineLocal {
continue
}
// Use the parent bridge name to match the subnet IDs reported
// by the Subnets() method.
subnetID, cidr, err := makeSubnetIDForNetwork(ni.ParentInterfaceName, addr.Address, addr.Netmask)
if err != nil {
return network.InterfaceInfo{}, errors.Trace(err)
}
netAddr.CIDR = cidr
netAddr.ConfigType = configType
ni.Addresses = append(ni.Addresses, netAddr)
// Only set provider IDs based on the first address.
// TODO (manadart 2021-03-24): We should associate the provider ID for
// the subnet with the address.
if len(ni.Addresses) > 1 {
continue
}
ni.ProviderSubnetId = network.Id(subnetID)
ni.ProviderId = network.Id(fmt.Sprintf("nic-%s", netInfo.Hwaddr))
}
return ni, nil
}
func detectInterfaceType(lxdIfaceType string) network.LinkLayerDeviceType {
switch lxdIfaceType {
case "bridge":
return network.BridgeDevice
case "broadcast":
return network.EthernetDevice
case "loopback":
return network.LoopbackDevice
default:
return network.UnknownDevice
}
}
func hostNetworkForGuestNetwork(container *lxdapi.Container, guestNetwork string) string {
if container.ExpandedDevices == nil {
return ""
}
devInfo, found := container.ExpandedDevices[guestNetwork]
if !found {
return ""
}
if name, found := devInfo["network"]; found { // lxd 4+
return name
} else if name, found := devInfo["parent"]; found { // lxd 3
return name
}
return ""
}
func getContainerDetails(srv Server, containerID string) (*lxdapi.Container, *lxdapi.ContainerState, error) {
cont, _, err := srv.GetContainer(containerID)
if err != nil {
if isErrNotFound(err) {
return nil, nil, errors.NotFoundf("container %q", containerID)
}
return nil, nil, errors.Trace(err)
}
state, _, err := srv.GetContainerState(containerID)
if err != nil {
if isErrNotFound(err) {
return nil, nil, errors.NotFoundf("container %q", containerID)
}
return nil, nil, errors.Trace(err)
}
return cont, state, nil
}
// isErrNotFound returns true if the LXD server returned back a "not found" error.
func isErrNotFound(err error) bool {
// Unfortunately the lxd client does not expose error
// codes so we need to match against a string here.
return err != nil && strings.Contains(err.Error(), "not found")
}
// isErrMissingAPIExtension returns true if the LXD server returned back an
// "API extension not found" error.
func isErrMissingAPIExtension(err error, ext string) bool {
// Unfortunately the lxd client does not expose error
// codes so we need to match against a string here.
return err != nil && strings.Contains(err.Error(), fmt.Sprintf("server is missing the required %q API extension", ext))
}
// SuperSubnets returns information about aggregated subnet.
func (*environ) SuperSubnets(context.ProviderCallContext) ([]string, error) {
return nil, errors.NotSupportedf("super subnets")
}
// SupportsSpaces returns whether the current environment supports
// spaces. The returned error satisfies errors.IsNotSupported(),
// unless a general API failure occurs.
func (e *environ) SupportsSpaces(context.ProviderCallContext) (bool, error) {
// Really old lxd versions (e.g. xenial/ppc64) do not even support the
// network API extension so the subnet discovery code path will not
// work there.
return e.hasLXDNetworkAPISupport()
}
// AreSpacesRoutable returns whether the communication between the
// two spaces can use cloud-local addresses.
func (*environ) AreSpacesRoutable(context.ProviderCallContext, *environs.ProviderSpaceInfo, *environs.ProviderSpaceInfo) (bool, error) {
return false, errors.NotSupportedf("spaces")
}
// SupportsContainerAddresses returns true if the current environment is
// able to allocate addresses for containers.
func (*environ) SupportsContainerAddresses(context.ProviderCallContext) (bool, error) {
return false, nil
}
// AllocateContainerAddresses allocates a static subnets for each of the
// container NICs in preparedInfo, hosted by the hostInstanceID. Returns the
// network config including all allocated addresses on success.
func (*environ) AllocateContainerAddresses(context.ProviderCallContext, instance.Id, names.MachineTag, network.InterfaceInfos) (network.InterfaceInfos, error) {
return nil, errors.NotSupportedf("container address allocation")
}
// ReleaseContainerAddresses releases the previously allocated
// addresses matching the interface details passed in.
func (*environ) ReleaseContainerAddresses(context.ProviderCallContext, []network.ProviderInterfaceInfo) error {
return errors.NotSupportedf("container address allocation")
}
// SSHAddresses filters the input addresses to those suitable for SSH use.
func (*environ) SSHAddresses(ctx context.ProviderCallContext, addresses network.SpaceAddresses) (network.SpaceAddresses, error) {
return addresses, nil
}
// hasLXDNetworkAPISupport makes a request to the networks API endpoint and
// checks whether the lxd server supports the network API extension or not. Any
// other error except "missing API extension" will be returned to the caller.
func (e *environ) hasLXDNetworkAPISupport() (bool, error) {
srv := e.server()
_, err := srv.GetNetworkNames()
if isErrMissingAPIExtension(err, "network") {
return false, nil
} else if err != nil {
return false, errors.Trace(err)
}
return true, nil
}