github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/openstack/networking_test.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package openstack
     5  
     6  import (
     7  	"github.com/go-goose/goose/v5/neutron"
     8  	"github.com/go-goose/goose/v5/nova"
     9  	"github.com/juju/errors"
    10  	jujutesting "github.com/juju/testing"
    11  	jc "github.com/juju/testing/checkers"
    12  	"go.uber.org/mock/gomock"
    13  	gc "gopkg.in/check.v1"
    14  
    15  	"github.com/juju/juju/core/instance"
    16  	"github.com/juju/juju/core/network"
    17  	"github.com/juju/juju/environs"
    18  )
    19  
    20  type networkingSuite struct {
    21  	jujutesting.IsolationSuite
    22  
    23  	base    *MockNetworkingBase
    24  	neutron *MockNetworkingNeutron
    25  	nova    *MockNetworkingNova
    26  	client  *MockNetworkingAuthenticatingClient
    27  	ecfg    *MockNetworkingEnvironConfig
    28  
    29  	serverAZ        string
    30  	externalNetwork string
    31  	ip              string
    32  	ip2             string
    33  	ip3             string
    34  }
    35  
    36  var _ = gc.Suite(&networkingSuite{})
    37  
    38  func (s *networkingSuite) SetUpTest(c *gc.C) {
    39  	s.serverAZ = "test-me"
    40  	s.externalNetwork = "ext-net"
    41  	s.ip = "10.4.5.6"
    42  	s.ip2 = "10.4.5.42"
    43  	s.ip3 = "10.4.5.75"
    44  }
    45  
    46  func (s *networkingSuite) TestAllocatePublicIPConfiguredExternalNetwork(c *gc.C) {
    47  	// Get a FIP for an instance with a configured external-network,
    48  	// which has available FIPs. Other external networks do exist -
    49  	// at last 1 in the same AZ as the instance. Should get the FIP
    50  	// on the configured external-network.
    51  	defer s.setupMocks(c).Finish()
    52  	s.expectExternalNetwork()
    53  	s.expectListFloatingIPsV2FromConfig()
    54  	s.expectListExternalNetworksV2() // resolveNeutronNetwork()
    55  	s.expectListInternalNetworksV2()
    56  	s.expectListExternalNetworksV2() // getExternalNeutronNetworksByAZ()
    57  
    58  	fip, err := s.runAllocatePublicIP()
    59  	c.Assert(err, jc.ErrorIsNil)
    60  	c.Assert(fip, gc.NotNil)
    61  	c.Assert(*fip, gc.Equals, s.ip)
    62  }
    63  
    64  func (s *networkingSuite) TestAllocatePublicIPUnconfiguredExternalNetwork(c *gc.C) {
    65  	// Get a FIP for an instance with an external network in the same AZ
    66  	// having an available FIP.  The first external network in the list
    67  	// does not have an available FIP.  No configured external-networks.
    68  	defer s.setupMocks(c).Finish()
    69  	s.externalNetwork = ""
    70  	s.expectExternalNetwork()
    71  	s.expectListFloatingIPsV2NotFromConfig()
    72  	s.expectListInternalNetworksV2()
    73  	s.expectListExternalNetworksV2() // getExternalNeutronNetworksByAZ()
    74  
    75  	fip, err := s.runAllocatePublicIP()
    76  	c.Assert(err, jc.ErrorIsNil)
    77  	c.Assert(fip, gc.NotNil)
    78  	c.Assert(*fip, gc.Equals, s.ip2)
    79  }
    80  
    81  func (s *networkingSuite) TestAllocatePublicIPUnconfiguredExternalNetworkMultiAZ(c *gc.C) {
    82  	// Get a FIP for an instance with an external network in the same AZ
    83  	// having an available FIP. This external network exists in multiple
    84  	// AZ, the one we want is not first in the list. The first external
    85  	// network in the list does not have an available FIP.  No configured
    86  	// external-networks.
    87  	defer s.setupMocks(c).Finish()
    88  	s.externalNetwork = ""
    89  	s.expectExternalNetwork()
    90  	s.expectListFloatingIPsV2NotFromConfig()
    91  	s.expectListInternalNetworksV2()
    92  	s.expectListExternalNetworksV2MultiAZ() // getExternalNeutronNetworksByAZ()
    93  
    94  	fip, err := s.runAllocatePublicIP()
    95  	c.Assert(err, jc.ErrorIsNil)
    96  	c.Assert(fip, gc.NotNil)
    97  	c.Assert(*fip, gc.Equals, s.ip2)
    98  }
    99  
   100  func (s *networkingSuite) TestAllocatePublicIPFail(c *gc.C) {
   101  	// Find external-networks, but none have an available FIP, nor
   102  	// are they able to create one.
   103  	defer s.setupMocks(c).Finish()
   104  	s.expectExternalNetwork()
   105  	s.expectListFloatingIPsV2Empty()
   106  	s.expectListExternalNetworksV2() // resolveNeutronNetwork()
   107  	s.expectListInternalNetworksV2()
   108  	s.expectListExternalNetworksV2() // getExternalNeutronNetworksByAZ()
   109  	s.expectAllocateFloatingIPV2FailAll()
   110  
   111  	fip, err := s.runAllocatePublicIP()
   112  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   113  	c.Assert(fip, gc.IsNil)
   114  }
   115  
   116  func (s *networkingSuite) TestAllocatePublicIPEmtpyAZEqualEmptyString(c *gc.C) {
   117  	// Test for lp: 1891227 fix.  An empty slice for AZ should be
   118  	// treated as an empty string AZ.
   119  	s.serverAZ = ""
   120  	defer s.setupMocks(c).Finish()
   121  	s.externalNetwork = ""
   122  	s.expectListExternalNetworksV2() // resolveNeutronNetwork()
   123  	s.expectExternalNetwork()
   124  	s.neutron.EXPECT().ListNetworksV2(gomock.Any()).Return([]neutron.NetworkV2{
   125  		{
   126  			Id:   "deadbeef-0bad-400d-8000-4b1d0d06f00d",
   127  			Name: "test-me",
   128  		},
   129  	}, nil).AnyTimes()
   130  	s.expectListFloatingIPsV2NotFromConfig()
   131  
   132  	fip, err := s.runAllocatePublicIP()
   133  	c.Assert(err, jc.ErrorIsNil)
   134  	c.Assert(fip, gc.NotNil)
   135  	c.Assert(*fip, gc.Equals, s.ip2)
   136  }
   137  
   138  func (s *networkingSuite) TestAllocatePublicIPNoneAvailable(c *gc.C) {
   139  	// Get a FIP for an instance with an external network in the same AZ
   140  	// having an available FIP.  No FIPs are available in the configured
   141  	// external network, so allocate one.  The first network fails to
   142  	// allocate, the 2nd succeeds.
   143  	defer s.setupMocks(c).Finish()
   144  	s.expectExternalNetwork()
   145  	s.expectListFloatingIPsV2FromConfigInUse()
   146  	s.expectListExternalNetworksV2() // resolveNeutronNetwork()
   147  	s.expectListInternalNetworksV2() // findNetworkAZForHostAddrs()
   148  	s.expectListExternalNetworksV2() // getExternalNeutronNetworksByAZ()
   149  	s.expectAllocateFloatingIPV2()
   150  
   151  	fip, err := s.runAllocatePublicIP()
   152  	c.Assert(err, jc.ErrorIsNil)
   153  	c.Assert(fip, gc.NotNil)
   154  	c.Assert(*fip, gc.Equals, s.ip3)
   155  }
   156  
   157  func (s *networkingSuite) TestAllocatePublicIPFailNoNetworkInAZ(c *gc.C) {
   158  	// No external network in same AZ as the instance is found, no
   159  	// external network is configured.
   160  	defer s.setupMocks(c).Finish()
   161  	s.externalNetwork = ""
   162  	s.expectExternalNetwork()
   163  	s.expectListInternalNetworksV2()        // findNetworkAZForHostAddrs()
   164  	s.expectListExternalNetworksV2NotInAZ() // getExternalNeutronNetworksByAZ()
   165  
   166  	fip, err := s.runAllocatePublicIP()
   167  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   168  	c.Assert(fip, gc.IsNil)
   169  }
   170  
   171  func (s *networkingSuite) TestNetworkInterfaces(c *gc.C) {
   172  	defer s.expectNeutronCalls(c).Finish()
   173  	s.externalNetwork = ""
   174  	s.expectListSubnets()
   175  
   176  	s.neutron.EXPECT().ListFloatingIPsV2(gomock.Any()).Return([]neutron.FloatingIPV2{
   177  		{
   178  			FixedIP: "10.0.0.2",
   179  			IP:      "10.245.164.31",
   180  		},
   181  	}, nil)
   182  
   183  	s.neutron.EXPECT().ListPortsV2(gomock.Any()).Return([]neutron.PortV2{
   184  		{
   185  			DeviceId:    "another-instance",
   186  			DeviceOwner: "compute:nova",
   187  		},
   188  		{
   189  			Id:          "nic-0",
   190  			DeviceId:    "inst-0",
   191  			NetworkId:   "deadbeef-0bad-400d-8000-4b1ddbeefbeef",
   192  			DeviceOwner: "compute:nova",
   193  			MACAddress:  "aa:bb:cc:dd:ee:ff",
   194  			Status:      "ACTIVE",
   195  			FixedIPs: []neutron.PortFixedIPsV2{
   196  				{
   197  					IPAddress: "192.168.0.2",
   198  					SubnetID:  "sub-42",
   199  				},
   200  				{
   201  					IPAddress: "10.0.0.2",
   202  					SubnetID:  "sub-665",
   203  				},
   204  			},
   205  		},
   206  		{
   207  			Id:          "nic-1",
   208  			DeviceId:    "inst-0",
   209  			NetworkId:   "deadbeef-0bad-400d-8000-4b1ddbeefbeef",
   210  			DeviceOwner: "compute:nova",
   211  			MACAddress:  "10:20:30:40:50:60",
   212  			Status:      "N/A",
   213  			FixedIPs: []neutron.PortFixedIPsV2{
   214  				{
   215  					IPAddress: "192.168.0.42",
   216  					SubnetID:  "sub-42",
   217  				},
   218  			},
   219  		},
   220  	}, nil)
   221  
   222  	nn := &NeutronNetworking{NetworkingBase: s.base}
   223  
   224  	res, err := nn.NetworkInterfaces([]instance.Id{"inst-0"})
   225  	c.Assert(err, jc.ErrorIsNil)
   226  
   227  	c.Assert(res, gc.HasLen, 1)
   228  	c.Assert(res[0], gc.HasLen, 2, gc.Commentf("expected to get 2 NICs for machine-0"))
   229  
   230  	nic0 := res[0][0]
   231  	c.Assert(nic0.InterfaceType, gc.Equals, network.EthernetDevice)
   232  	c.Assert(nic0.Origin, gc.Equals, network.OriginProvider)
   233  	c.Assert(nic0.Disabled, jc.IsFalse)
   234  	c.Assert(nic0.MACAddress, gc.Equals, "aa:bb:cc:dd:ee:ff")
   235  	c.Assert(nic0.Addresses, gc.DeepEquals, network.ProviderAddresses{
   236  		network.NewMachineAddress(
   237  			"192.168.0.2",
   238  			network.WithCIDR("192.168.0.0/24"),
   239  			network.WithScope(network.ScopeCloudLocal),
   240  			network.WithConfigType(network.ConfigStatic),
   241  		).AsProviderAddress(),
   242  		network.NewMachineAddress(
   243  			"10.0.0.2",
   244  			network.WithCIDR("10.0.0.0/24"),
   245  			network.WithScope(network.ScopeCloudLocal),
   246  			network.WithConfigType(network.ConfigStatic),
   247  		).AsProviderAddress(),
   248  	})
   249  	c.Assert(nic0.ShadowAddresses, gc.DeepEquals, network.ProviderAddresses{
   250  		network.NewMachineAddress(
   251  			"10.245.164.31",
   252  			network.WithScope(network.ScopePublic),
   253  		).AsProviderAddress(),
   254  	})
   255  	c.Assert(nic0.ProviderId, gc.Equals, network.Id("nic-0"))
   256  	c.Assert(nic0.ProviderNetworkId, gc.Equals, network.Id("deadbeef-0bad-400d-8000-4b1ddbeefbeef"))
   257  	c.Assert(nic0.ProviderSubnetId, gc.Equals, network.Id("sub-42"), gc.Commentf("expected NIC to use the provider subnet ID for the primary NIC address"))
   258  
   259  	nic1 := res[0][1]
   260  	c.Assert(nic1.InterfaceType, gc.Equals, network.EthernetDevice)
   261  	c.Assert(nic1.Origin, gc.Equals, network.OriginProvider)
   262  	c.Assert(nic1.Disabled, jc.IsTrue, gc.Commentf("expected device to be listed as disabled"))
   263  	c.Assert(nic1.MACAddress, gc.Equals, "10:20:30:40:50:60")
   264  	c.Assert(nic1.Addresses, gc.DeepEquals, network.ProviderAddresses{
   265  		network.NewMachineAddress(
   266  			"192.168.0.42",
   267  			network.WithCIDR("192.168.0.0/24"),
   268  			network.WithScope(network.ScopeCloudLocal),
   269  			network.WithConfigType(network.ConfigStatic),
   270  		).AsProviderAddress(),
   271  	})
   272  	c.Assert(nic1.ProviderId, gc.Equals, network.Id("nic-1"))
   273  	c.Assert(nic1.ProviderNetworkId, gc.Equals, network.Id("deadbeef-0bad-400d-8000-4b1ddbeefbeef"))
   274  	c.Assert(nic1.ProviderSubnetId, gc.Equals, network.Id("sub-42"), gc.Commentf("expected NIC to use the provider subnet ID for the primary NIC address"))
   275  }
   276  
   277  func (s *networkingSuite) TestNetworkInterfacesPartialMatch(c *gc.C) {
   278  	defer s.expectNeutronCalls(c).Finish()
   279  	s.externalNetwork = ""
   280  	s.expectListSubnets()
   281  
   282  	s.neutron.EXPECT().ListFloatingIPsV2(gomock.Any()).Return(nil, nil)
   283  
   284  	s.neutron.EXPECT().ListPortsV2(gomock.Any()).Return([]neutron.PortV2{
   285  		{
   286  			Id:          "nic-0",
   287  			DeviceId:    "inst-0",
   288  			NetworkId:   "deadbeef-0bad-400d-8000-4b1ddbeefbeef",
   289  			DeviceOwner: "compute:nova",
   290  			MACAddress:  "aa:bb:cc:dd:ee:ff",
   291  			Status:      "ACTIVE",
   292  		},
   293  	}, nil)
   294  
   295  	nn := &NeutronNetworking{NetworkingBase: s.base}
   296  
   297  	res, err := nn.NetworkInterfaces([]instance.Id{"inst-0", "bogus-0"})
   298  	c.Assert(err, gc.Equals, environs.ErrPartialInstances)
   299  
   300  	c.Assert(res, gc.HasLen, 2)
   301  	c.Assert(res[0], gc.HasLen, 1, gc.Commentf("expected to get 1 NIC for inst-0"))
   302  	c.Assert(res[1], gc.IsNil, gc.Commentf("expected a nil slice for non-matched machines"))
   303  }
   304  
   305  func (s *networkingSuite) expectNeutronCalls(c *gc.C) *gomock.Controller {
   306  	ctrl := gomock.NewController(c)
   307  
   308  	s.client = NewMockNetworkingAuthenticatingClient(ctrl)
   309  	s.client.EXPECT().TenantId().Return("TenantId").AnyTimes()
   310  
   311  	s.neutron = NewMockNetworkingNeutron(ctrl)
   312  
   313  	s.ecfg = NewMockNetworkingEnvironConfig(ctrl)
   314  
   315  	s.base = NewMockNetworkingBase(ctrl)
   316  	bExp := s.base.EXPECT()
   317  	bExp.neutron().Return(s.neutron).AnyTimes()
   318  	bExp.client().Return(s.client).AnyTimes()
   319  	bExp.ecfg().Return(s.ecfg).AnyTimes()
   320  
   321  	return ctrl
   322  }
   323  
   324  func (s *networkingSuite) expectListSubnets() {
   325  	s.ecfg.EXPECT().networks().Return([]string{"int-net"})
   326  
   327  	s.expectExternalNetwork()
   328  	s.neutron.EXPECT().ListNetworksV2(gomock.Any()).Return([]neutron.NetworkV2{
   329  		{
   330  			Id:                "deadbeef-0bad-400d-8000-4b1ddbeefbeef",
   331  			Name:              "int-net",
   332  			AvailabilityZones: []string{s.serverAZ},
   333  		},
   334  	}, nil)
   335  	s.neutron.EXPECT().ListSubnetsV2().Return([]neutron.SubnetV2{
   336  		{
   337  			Id:        "sub-42",
   338  			NetworkId: "deadbeef-0bad-400d-8000-4b1ddbeefbeef",
   339  			Cidr:      "192.168.0.0/24",
   340  		},
   341  		{
   342  			Id:        "sub-665",
   343  			NetworkId: "deadbeef-0bad-400d-8000-4b1ddbeefbeef",
   344  			Cidr:      "10.0.0.0/24",
   345  		},
   346  	}, nil)
   347  	s.neutron.EXPECT().GetNetworkV2("deadbeef-0bad-400d-8000-4b1ddbeefbeef").Return(&neutron.NetworkV2{
   348  		AvailabilityZones: []string{"mars"},
   349  	}, nil).AnyTimes()
   350  }
   351  
   352  func (s *networkingSuite) setupMocks(c *gc.C) *gomock.Controller {
   353  	ctrl := gomock.NewController(c)
   354  
   355  	s.neutron = NewMockNetworkingNeutron(ctrl)
   356  	s.nova = NewMockNetworkingNova(ctrl)
   357  	s.client = NewMockNetworkingAuthenticatingClient(ctrl)
   358  	s.ecfg = NewMockNetworkingEnvironConfig(ctrl)
   359  
   360  	s.base = NewMockNetworkingBase(ctrl)
   361  	bExp := s.base.EXPECT()
   362  	bExp.client().Return(s.client).AnyTimes()
   363  	bExp.neutron().Return(s.neutron).AnyTimes()
   364  	bExp.nova().Return(s.nova)
   365  	bExp.ecfg().Return(s.ecfg)
   366  
   367  	s.client.EXPECT().TenantId().Return("TenantId").AnyTimes()
   368  	s.nova.EXPECT().GetServer(gomock.Any()).Return(&nova.ServerDetail{
   369  		Addresses: map[string][]nova.IPAddress{
   370  			"int-net": {},
   371  		},
   372  		AvailabilityZone: s.serverAZ,
   373  	}, nil)
   374  
   375  	return ctrl
   376  }
   377  
   378  func (s *networkingSuite) runAllocatePublicIP() (*string, error) {
   379  	networking := &NeutronNetworking{NetworkingBase: s.base}
   380  	return networking.AllocatePublicIP(instance.Id("32"))
   381  }
   382  
   383  func (s *networkingSuite) expectListFloatingIPsV2FromConfig() {
   384  	s.neutron.EXPECT().ListFloatingIPsV2(gomock.Any()).Return([]neutron.FloatingIPV2{
   385  		{FloatingNetworkId: "deadbeef-0bad-400d-8000-4b1ddeadbeef", IP: s.ip},
   386  	}, nil)
   387  }
   388  
   389  func (s *networkingSuite) expectListFloatingIPsV2FromConfigInUse() {
   390  	s.neutron.EXPECT().ListFloatingIPsV2(gomock.Any()).Return([]neutron.FloatingIPV2{{
   391  		FloatingNetworkId: "deadbeef-0bad-400d-8000-4b1ddeadbeef",
   392  		FixedIP:           "10.7.8.9",
   393  		IP:                s.ip,
   394  	}}, nil)
   395  }
   396  
   397  func (s *networkingSuite) expectListFloatingIPsV2NotFromConfig() {
   398  	s.neutron.EXPECT().ListFloatingIPsV2(gomock.Any()).Return([]neutron.FloatingIPV2{
   399  		{FloatingNetworkId: "deadbeef-0bad-400d-8000-4b1d0d06f00d", IP: s.ip2},
   400  	}, nil)
   401  }
   402  
   403  func (s *networkingSuite) expectListFloatingIPsV2Empty() {
   404  	s.neutron.EXPECT().ListFloatingIPsV2(gomock.Any()).Return([]neutron.FloatingIPV2{}, nil)
   405  }
   406  
   407  func (s *networkingSuite) expectExternalNetwork() {
   408  	s.ecfg.EXPECT().externalNetwork().Return(s.externalNetwork)
   409  }
   410  
   411  func (s *networkingSuite) expectListExternalNetworksV2() {
   412  	s.neutron.EXPECT().ListNetworksV2(gomock.Any()).Return([]neutron.NetworkV2{
   413  		{
   414  			Id:                "deadbeef-0bad-400d-8000-4b1ddeadbeef",
   415  			Name:              s.externalNetwork,
   416  			External:          true,
   417  			AvailabilityZones: []string{s.serverAZ},
   418  		}, {
   419  			Name:              "do-not-pick-me",
   420  			External:          true,
   421  			AvailabilityZones: []string{"failme"},
   422  		}, {
   423  			Id:                "deadbeef-0bad-400d-8000-4b1d0d06f00d",
   424  			Name:              "unconfigured-ext-net",
   425  			External:          true,
   426  			AvailabilityZones: []string{s.serverAZ},
   427  		},
   428  	}, nil)
   429  }
   430  
   431  func (s *networkingSuite) expectListInternalNetworksV2() {
   432  	s.neutron.EXPECT().ListNetworksV2(gomock.Any()).Return([]neutron.NetworkV2{
   433  		{
   434  			Id:                "deadbeef-0bad-400d-8000-4b1ddbeefbeef",
   435  			Name:              "int-net",
   436  			AvailabilityZones: []string{s.serverAZ},
   437  		}, {
   438  			Name:              "internal-do-not-pick-me",
   439  			AvailabilityZones: []string{"failme"},
   440  		}, {
   441  			Id:                "deadbeef-0bad-400d-8000-4b1d8273450d",
   442  			Name:              "unconfigured-int-net",
   443  			AvailabilityZones: []string{s.serverAZ},
   444  		},
   445  	}, nil)
   446  }
   447  
   448  func (s *networkingSuite) expectListExternalNetworksV2MultiAZ() {
   449  	s.neutron.EXPECT().ListNetworksV2(gomock.Any()).Return([]neutron.NetworkV2{
   450  		{
   451  			Name:              "do-not-pick-me",
   452  			AvailabilityZones: []string{"failme"},
   453  		}, {
   454  			Id:                "deadbeef-0bad-400d-8000-4b1d0d06f00d",
   455  			Name:              "unconfigured-ext-net",
   456  			AvailabilityZones: []string{"other", s.serverAZ},
   457  		},
   458  	}, nil).AnyTimes()
   459  }
   460  
   461  func (s *networkingSuite) expectListExternalNetworksV2NotInAZ() {
   462  	s.neutron.EXPECT().ListNetworksV2(gomock.Any()).Return([]neutron.NetworkV2{
   463  		{
   464  			Name:              "do-not-pick-me",
   465  			AvailabilityZones: []string{"failme"},
   466  		}, {
   467  			Id:                "deadbeef-0bad-400d-8000-4b1d0d06f00d",
   468  			Name:              "unconfigured-ext-net",
   469  			AvailabilityZones: []string{"other"},
   470  		},
   471  	}, nil).AnyTimes()
   472  }
   473  
   474  func (s *networkingSuite) expectAllocateFloatingIPV2() {
   475  	s.neutron.EXPECT().AllocateFloatingIPV2("deadbeef-0bad-400d-8000-4b1ddeadbeef").Return(nil, errors.NotFoundf("fip"))
   476  	s.neutron.EXPECT().AllocateFloatingIPV2("deadbeef-0bad-400d-8000-4b1d0d06f00d").Return(&neutron.FloatingIPV2{
   477  		FloatingNetworkId: "deadbeef-0bad-400d-8000-4b1d0d06f00d",
   478  		IP:                s.ip3,
   479  	}, nil)
   480  }
   481  
   482  func (s *networkingSuite) expectAllocateFloatingIPV2FailAll() {
   483  	s.neutron.EXPECT().AllocateFloatingIPV2(gomock.Any()).Return(nil, errors.NotFoundf("fip")).AnyTimes()
   484  }