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

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package openstack
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"net/http/httptest"
    10  
    11  	"github.com/go-goose/goose/v5/identity"
    12  	"github.com/go-goose/goose/v5/neutron"
    13  	"github.com/go-goose/goose/v5/nova"
    14  	gitjujutesting "github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	"github.com/juju/utils/v3"
    17  	"go.uber.org/mock/gomock"
    18  	gc "gopkg.in/check.v1"
    19  	"gopkg.in/yaml.v2"
    20  
    21  	"github.com/juju/juju/cloud"
    22  	"github.com/juju/juju/core/constraints"
    23  	"github.com/juju/juju/core/network"
    24  	"github.com/juju/juju/core/network/firewall"
    25  	"github.com/juju/juju/environs"
    26  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    27  	"github.com/juju/juju/environs/context"
    28  )
    29  
    30  // localTests contains tests which do not require a live service or test double to run.
    31  type localTests struct {
    32  	gitjujutesting.IsolationSuite
    33  }
    34  
    35  var _ = gc.Suite(&localTests{})
    36  
    37  // ported from lp:juju/juju/providers/openstack/tests/test_machine.py
    38  var addressTests = []struct {
    39  	summary    string
    40  	floatingIP string
    41  	private    []nova.IPAddress
    42  	public     []nova.IPAddress
    43  	networks   []string
    44  	expected   string
    45  	failure    error
    46  }{{
    47  	summary:  "missing",
    48  	expected: "",
    49  }, {
    50  	summary:  "empty",
    51  	private:  []nova.IPAddress{},
    52  	networks: []string{"private"},
    53  	expected: "",
    54  }, {
    55  	summary:  "private IPv4 only",
    56  	private:  []nova.IPAddress{{4, "192.168.0.1", "fixed"}},
    57  	networks: []string{"private"},
    58  	expected: "192.168.0.1",
    59  }, {
    60  	summary:  "private IPv6 only",
    61  	private:  []nova.IPAddress{{6, "fc00::1", "fixed"}},
    62  	networks: []string{"private"},
    63  	expected: "fc00::1",
    64  }, {
    65  	summary:  "private only, both IPv4 and IPv6",
    66  	private:  []nova.IPAddress{{4, "192.168.0.1", "fixed"}, {6, "fc00::1", "fixed"}},
    67  	networks: []string{"private"},
    68  	expected: "192.168.0.1",
    69  }, {
    70  	summary:  "private IPv4 plus (what HP cloud used to do)",
    71  	private:  []nova.IPAddress{{4, "10.0.0.1", "fixed"}, {4, "8.8.4.4", "fixed"}},
    72  	networks: []string{"private"},
    73  	expected: "8.8.4.4",
    74  }, {
    75  	summary:  "public IPv4 only",
    76  	public:   []nova.IPAddress{{4, "8.8.8.8", "floating"}},
    77  	networks: []string{"", "public"},
    78  	expected: "8.8.8.8",
    79  }, {
    80  	summary:  "public IPv6 only",
    81  	public:   []nova.IPAddress{{6, "2001:db8::1", "floating"}},
    82  	networks: []string{"", "public"},
    83  	expected: "2001:db8::1",
    84  }, {
    85  	summary:  "public only, both IPv4 and IPv6",
    86  	public:   []nova.IPAddress{{4, "8.8.8.8", "floating"}, {6, "2001:db8::1", "floating"}},
    87  	networks: []string{"", "public"},
    88  	expected: "8.8.8.8",
    89  }, {
    90  	summary:  "public and private both IPv4",
    91  	private:  []nova.IPAddress{{4, "10.0.0.4", "fixed"}},
    92  	public:   []nova.IPAddress{{4, "8.8.4.4", "floating"}},
    93  	networks: []string{"private", "public"},
    94  	expected: "8.8.4.4",
    95  }, {
    96  	summary:  "public and private both IPv6",
    97  	private:  []nova.IPAddress{{6, "fc00::1", "fixed"}},
    98  	public:   []nova.IPAddress{{6, "2001:db8::1", "floating"}},
    99  	networks: []string{"private", "public"},
   100  	expected: "2001:db8::1",
   101  }, {
   102  	summary:  "public, private, and localhost IPv4",
   103  	private:  []nova.IPAddress{{4, "127.0.0.4", "fixed"}, {4, "192.168.0.1", "fixed"}},
   104  	public:   []nova.IPAddress{{4, "8.8.8.8", "floating"}},
   105  	networks: []string{"private", "public"},
   106  	expected: "8.8.8.8",
   107  }, {
   108  	summary:  "public, private, and localhost IPv6",
   109  	private:  []nova.IPAddress{{6, "::1", "fixed"}, {6, "fc00::1", "fixed"}},
   110  	public:   []nova.IPAddress{{6, "2001:db8::1", "floating"}},
   111  	networks: []string{"private", "public"},
   112  	expected: "2001:db8::1",
   113  }, {
   114  	summary:  "public, private, and localhost - both IPv4 and IPv6",
   115  	private:  []nova.IPAddress{{4, "127.0.0.4", "fixed"}, {4, "192.168.0.1", "fixed"}, {6, "::1", "fixed"}, {6, "fc00::1", "fixed"}},
   116  	public:   []nova.IPAddress{{4, "8.8.8.8", "floating"}, {6, "2001:db8::1", "floating"}},
   117  	networks: []string{"private", "public"},
   118  	expected: "8.8.8.8",
   119  }, {
   120  	summary:  "custom only IPv4",
   121  	private:  []nova.IPAddress{{4, "192.168.0.1", "fixed"}},
   122  	networks: []string{"special"},
   123  	expected: "192.168.0.1",
   124  }, {
   125  	summary:  "custom only IPv6",
   126  	private:  []nova.IPAddress{{6, "fc00::1", "fixed"}},
   127  	networks: []string{"special"},
   128  	expected: "fc00::1",
   129  }, {
   130  	summary:  "custom only - both IPv4 and IPv6",
   131  	private:  []nova.IPAddress{{4, "192.168.0.1", "fixed"}, {6, "fc00::1", "fixed"}},
   132  	networks: []string{"special"},
   133  	expected: "192.168.0.1",
   134  }, {
   135  	summary:  "custom and public IPv4",
   136  	private:  []nova.IPAddress{{4, "172.16.0.1", "fixed"}},
   137  	public:   []nova.IPAddress{{4, "8.8.8.8", "floating"}},
   138  	networks: []string{"special", "public"},
   139  	expected: "8.8.8.8",
   140  }, {
   141  	summary:  "custom and public IPv6",
   142  	private:  []nova.IPAddress{{6, "fc00::1", "fixed"}},
   143  	public:   []nova.IPAddress{{6, "2001:db8::1", "floating"}},
   144  	networks: []string{"special", "public"},
   145  	expected: "2001:db8::1",
   146  }, {
   147  	summary:  "custom and public - both IPv4 and IPv6",
   148  	private:  []nova.IPAddress{{4, "172.16.0.1", "fixed"}, {6, "fc00::1", "fixed"}},
   149  	public:   []nova.IPAddress{{4, "8.8.8.8", "floating"}, {6, "2001:db8::1", "floating"}},
   150  	networks: []string{"special", "public"},
   151  	expected: "8.8.8.8",
   152  }, {
   153  	summary:    "floating and public, same address",
   154  	floatingIP: "8.8.8.8",
   155  	public:     []nova.IPAddress{{4, "8.8.8.8", "floating"}},
   156  	networks:   []string{"", "public"},
   157  	expected:   "8.8.8.8",
   158  }, {
   159  	summary:    "floating and public, different address",
   160  	floatingIP: "8.8.4.4",
   161  	public:     []nova.IPAddress{{4, "8.8.8.8", "floating"}},
   162  	networks:   []string{"", "public"},
   163  	expected:   "8.8.4.4",
   164  }, {
   165  	summary:    "floating and private",
   166  	floatingIP: "8.8.4.4",
   167  	private:    []nova.IPAddress{{4, "10.0.0.1", "fixed"}},
   168  	networks:   []string{"private"},
   169  	expected:   "8.8.4.4",
   170  }, {
   171  	summary:    "floating, custom and public",
   172  	floatingIP: "8.8.4.4",
   173  	private:    []nova.IPAddress{{4, "172.16.0.1", "fixed"}},
   174  	public:     []nova.IPAddress{{4, "8.8.8.8", "floating"}},
   175  	networks:   []string{"special", "public"},
   176  	expected:   "8.8.4.4",
   177  }}
   178  
   179  func (t *localTests) TestGetServerAddresses(c *gc.C) {
   180  	for i, t := range addressTests {
   181  		c.Logf("#%d. %s -> %s (%v)", i, t.summary, t.expected, t.failure)
   182  		addresses := make(map[string][]nova.IPAddress)
   183  		if t.private != nil {
   184  			if len(t.networks) < 1 {
   185  				addresses["private"] = t.private
   186  			} else {
   187  				addresses[t.networks[0]] = t.private
   188  			}
   189  		}
   190  		if t.public != nil {
   191  			if len(t.networks) < 2 {
   192  				addresses["public"] = t.public
   193  			} else {
   194  				addresses[t.networks[1]] = t.public
   195  			}
   196  		}
   197  		addr := InstanceAddress(t.floatingIP, addresses)
   198  		c.Check(addr, gc.Equals, t.expected)
   199  	}
   200  }
   201  
   202  func (*localTests) TestPortsToRuleInfo(c *gc.C) {
   203  	groupId := "groupid"
   204  	testCases := []struct {
   205  		about    string
   206  		rules    firewall.IngressRules
   207  		expected []neutron.RuleInfoV2
   208  	}{{
   209  		about: "single port",
   210  		rules: firewall.IngressRules{firewall.NewIngressRule(network.MustParsePortRange("80/tcp"))},
   211  		expected: []neutron.RuleInfoV2{
   212  			{
   213  				Direction:      "ingress",
   214  				IPProtocol:     "tcp",
   215  				PortRangeMin:   80,
   216  				PortRangeMax:   80,
   217  				RemoteIPPrefix: "0.0.0.0/0",
   218  				ParentGroupId:  groupId,
   219  				EthernetType:   "IPv4",
   220  			},
   221  			{
   222  				Direction:      "ingress",
   223  				IPProtocol:     "tcp",
   224  				PortRangeMin:   80,
   225  				PortRangeMax:   80,
   226  				RemoteIPPrefix: "::/0",
   227  				ParentGroupId:  groupId,
   228  				EthernetType:   "IPv6",
   229  			},
   230  		},
   231  	}, {
   232  		about: "multiple ports",
   233  		rules: firewall.IngressRules{firewall.NewIngressRule(network.MustParsePortRange("80-82/tcp"))},
   234  		expected: []neutron.RuleInfoV2{
   235  			{
   236  				Direction:      "ingress",
   237  				IPProtocol:     "tcp",
   238  				PortRangeMin:   80,
   239  				PortRangeMax:   82,
   240  				RemoteIPPrefix: "0.0.0.0/0",
   241  				ParentGroupId:  groupId,
   242  				EthernetType:   "IPv4",
   243  			},
   244  			{
   245  				Direction:      "ingress",
   246  				IPProtocol:     "tcp",
   247  				PortRangeMin:   80,
   248  				PortRangeMax:   82,
   249  				RemoteIPPrefix: "::/0",
   250  				ParentGroupId:  groupId,
   251  				EthernetType:   "IPv6",
   252  			},
   253  		},
   254  	}, {
   255  		about: "multiple port ranges",
   256  		rules: firewall.IngressRules{
   257  			firewall.NewIngressRule(network.MustParsePortRange("80-82/tcp")),
   258  			firewall.NewIngressRule(network.MustParsePortRange("100-120/tcp")),
   259  		},
   260  		expected: []neutron.RuleInfoV2{
   261  			{
   262  				Direction:      "ingress",
   263  				IPProtocol:     "tcp",
   264  				PortRangeMin:   80,
   265  				PortRangeMax:   82,
   266  				RemoteIPPrefix: "0.0.0.0/0",
   267  				ParentGroupId:  groupId,
   268  				EthernetType:   "IPv4",
   269  			}, {
   270  				Direction:      "ingress",
   271  				IPProtocol:     "tcp",
   272  				PortRangeMin:   100,
   273  				PortRangeMax:   120,
   274  				RemoteIPPrefix: "0.0.0.0/0",
   275  				ParentGroupId:  groupId,
   276  				EthernetType:   "IPv4",
   277  			}, {
   278  				Direction:      "ingress",
   279  				IPProtocol:     "tcp",
   280  				PortRangeMin:   80,
   281  				PortRangeMax:   82,
   282  				RemoteIPPrefix: "::/0",
   283  				ParentGroupId:  groupId,
   284  				EthernetType:   "IPv6",
   285  			}, {
   286  				Direction:      "ingress",
   287  				IPProtocol:     "tcp",
   288  				PortRangeMin:   100,
   289  				PortRangeMax:   120,
   290  				RemoteIPPrefix: "::/0",
   291  				ParentGroupId:  groupId,
   292  				EthernetType:   "IPv6",
   293  			},
   294  		},
   295  	}, {
   296  		about: "source range",
   297  		rules: firewall.IngressRules{firewall.NewIngressRule(network.MustParsePortRange("80-100/tcp"), "192.168.1.0/24", "0.0.0.0/0")},
   298  		expected: []neutron.RuleInfoV2{{
   299  			Direction:      "ingress",
   300  			IPProtocol:     "tcp",
   301  			PortRangeMin:   80,
   302  			PortRangeMax:   100,
   303  			RemoteIPPrefix: "192.168.1.0/24",
   304  			ParentGroupId:  groupId,
   305  			EthernetType:   "IPv4",
   306  		}, {
   307  			Direction:      "ingress",
   308  			IPProtocol:     "tcp",
   309  			PortRangeMin:   80,
   310  			PortRangeMax:   100,
   311  			RemoteIPPrefix: "0.0.0.0/0",
   312  			ParentGroupId:  groupId,
   313  			EthernetType:   "IPv4",
   314  		}},
   315  	}, {
   316  		about: "IPV4 and IPV6 CIDRs",
   317  		rules: firewall.IngressRules{firewall.NewIngressRule(network.MustParsePortRange("80-100/tcp"), "192.168.1.0/24", "2002::1234:abcd:ffff:c0a8:101/64")},
   318  		expected: []neutron.RuleInfoV2{{
   319  			Direction:      "ingress",
   320  			IPProtocol:     "tcp",
   321  			PortRangeMin:   80,
   322  			PortRangeMax:   100,
   323  			RemoteIPPrefix: "192.168.1.0/24",
   324  			ParentGroupId:  groupId,
   325  			EthernetType:   "IPv4",
   326  		}, {
   327  			Direction:      "ingress",
   328  			IPProtocol:     "tcp",
   329  			PortRangeMin:   80,
   330  			PortRangeMax:   100,
   331  			RemoteIPPrefix: "2002::1234:abcd:ffff:c0a8:101/64",
   332  			ParentGroupId:  groupId,
   333  			EthernetType:   "IPv6",
   334  		}},
   335  	}}
   336  
   337  	for i, t := range testCases {
   338  		c.Logf("test %d: %s", i, t.about)
   339  		rules := PortsToRuleInfo(groupId, t.rules)
   340  		c.Check(len(rules), gc.Equals, len(t.expected))
   341  		c.Check(rules, jc.SameContents, t.expected)
   342  	}
   343  }
   344  
   345  func (*localTests) TestSecGroupMatchesIngressRule(c *gc.C) {
   346  	proto_tcp := "tcp"
   347  	proto_udp := "udp"
   348  	port_80 := 80
   349  	port_85 := 85
   350  
   351  	testCases := []struct {
   352  		about        string
   353  		rule         firewall.IngressRule
   354  		secGroupRule neutron.SecurityGroupRuleV2
   355  		expected     bool
   356  	}{{
   357  		about: "single port",
   358  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80/tcp")),
   359  		secGroupRule: neutron.SecurityGroupRuleV2{
   360  			IPProtocol:   &proto_tcp,
   361  			PortRangeMin: &port_80,
   362  			PortRangeMax: &port_80,
   363  			EthernetType: "IPv4",
   364  		},
   365  		expected: true,
   366  	}, {
   367  		about: "multiple port",
   368  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp")),
   369  		secGroupRule: neutron.SecurityGroupRuleV2{
   370  			IPProtocol:   &proto_tcp,
   371  			PortRangeMin: &port_80,
   372  			PortRangeMax: &port_85,
   373  			EthernetType: "IPv4",
   374  		},
   375  		expected: true,
   376  	}, {
   377  		about: "nil rule components",
   378  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp")),
   379  		secGroupRule: neutron.SecurityGroupRuleV2{
   380  			IPProtocol:   nil,
   381  			PortRangeMin: nil,
   382  			PortRangeMax: nil,
   383  			EthernetType: "IPv4",
   384  		},
   385  		expected: false,
   386  	}, {
   387  		about: "nil rule component: PortRangeMin",
   388  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp"), "0.0.0.0/0", "192.168.1.0/24"),
   389  		secGroupRule: neutron.SecurityGroupRuleV2{
   390  			IPProtocol:     &proto_tcp,
   391  			PortRangeMin:   nil,
   392  			PortRangeMax:   &port_85,
   393  			RemoteIPPrefix: "192.168.100.0/24",
   394  			EthernetType:   "IPv4",
   395  		},
   396  		expected: false,
   397  	}, {
   398  		about: "nil rule component: PortRangeMax",
   399  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp"), "0.0.0.0/0", "192.168.1.0/24"),
   400  		secGroupRule: neutron.SecurityGroupRuleV2{
   401  			IPProtocol:     &proto_tcp,
   402  			PortRangeMin:   &port_85,
   403  			PortRangeMax:   nil,
   404  			RemoteIPPrefix: "192.168.100.0/24",
   405  			EthernetType:   "IPv4",
   406  		},
   407  		expected: false,
   408  	}, {
   409  		about: "mismatched port range and rule",
   410  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp")),
   411  		secGroupRule: neutron.SecurityGroupRuleV2{
   412  			IPProtocol:   &proto_udp,
   413  			PortRangeMin: &port_80,
   414  			PortRangeMax: &port_80,
   415  			EthernetType: "IPv4",
   416  		},
   417  		expected: false,
   418  	}, {
   419  		about: "default RemoteIPPrefix",
   420  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp")),
   421  		secGroupRule: neutron.SecurityGroupRuleV2{
   422  			IPProtocol:     &proto_tcp,
   423  			PortRangeMin:   &port_80,
   424  			PortRangeMax:   &port_85,
   425  			RemoteIPPrefix: "0.0.0.0/0",
   426  			EthernetType:   "IPv4",
   427  		},
   428  		expected: true,
   429  	}, {
   430  		about: "matching RemoteIPPrefix",
   431  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp"), "0.0.0.0/0", "192.168.1.0/24"),
   432  		secGroupRule: neutron.SecurityGroupRuleV2{
   433  			IPProtocol:     &proto_tcp,
   434  			PortRangeMin:   &port_80,
   435  			PortRangeMax:   &port_85,
   436  			RemoteIPPrefix: "192.168.1.0/24",
   437  			EthernetType:   "IPv4",
   438  		},
   439  		expected: true,
   440  	}, {
   441  		about: "non-matching RemoteIPPrefix",
   442  		rule:  firewall.NewIngressRule(network.MustParsePortRange("80-85/tcp"), "0.0.0.0/0", "192.168.1.0/24"),
   443  		secGroupRule: neutron.SecurityGroupRuleV2{
   444  			IPProtocol:     &proto_tcp,
   445  			PortRangeMin:   &port_80,
   446  			PortRangeMax:   &port_85,
   447  			RemoteIPPrefix: "192.168.100.0/24",
   448  			EthernetType:   "IPv4",
   449  		},
   450  		expected: false,
   451  	}}
   452  	for i, t := range testCases {
   453  		c.Logf("test %d: %s", i, t.about)
   454  		c.Check(SecGroupMatchesIngressRule(t.secGroupRule, t.rule), gc.Equals, t.expected)
   455  	}
   456  }
   457  
   458  func (s *localTests) TestDetectRegionsNoRegionName(c *gc.C) {
   459  	_, err := s.detectRegions(c)
   460  	c.Assert(err, gc.ErrorMatches, "OS_REGION_NAME environment variable not set")
   461  }
   462  
   463  func (s *localTests) TestDetectRegionsNoAuthURL(c *gc.C) {
   464  	s.PatchEnvironment("OS_REGION_NAME", "oceania")
   465  	_, err := s.detectRegions(c)
   466  	c.Assert(err, gc.ErrorMatches, "OS_AUTH_URL environment variable not set")
   467  }
   468  
   469  func (s *localTests) TestDetectRegions(c *gc.C) {
   470  	s.PatchEnvironment("OS_REGION_NAME", "oceania")
   471  	s.PatchEnvironment("OS_AUTH_URL", "http://keystone.internal")
   472  	regions, err := s.detectRegions(c)
   473  	c.Assert(err, jc.ErrorIsNil)
   474  	c.Assert(regions, jc.DeepEquals, []cloud.Region{
   475  		{Name: "oceania", Endpoint: "http://keystone.internal"},
   476  	})
   477  }
   478  
   479  func (s *localTests) detectRegions(c *gc.C) ([]cloud.Region, error) {
   480  	provider, err := environs.Provider("openstack")
   481  	c.Assert(err, jc.ErrorIsNil)
   482  	c.Assert(provider, gc.Implements, new(environs.CloudRegionDetector))
   483  	return provider.(environs.CloudRegionDetector).DetectRegions()
   484  }
   485  
   486  func (s *localTests) TestSchema(c *gc.C) {
   487  	y := []byte(`
   488  auth-types: [userpass, access-key]
   489  endpoint: http://foo.com/openstack
   490  regions: 
   491    one:
   492      endpoint: http://foo.com/bar
   493    two:
   494      endpoint: http://foo2.com/bar2
   495  `[1:])
   496  	var v interface{}
   497  	err := yaml.Unmarshal(y, &v)
   498  	c.Assert(err, jc.ErrorIsNil)
   499  	v, err = utils.ConformYAML(v)
   500  	c.Assert(err, jc.ErrorIsNil)
   501  
   502  	p, err := environs.Provider("openstack")
   503  	c.Assert(err, jc.ErrorIsNil)
   504  	err = p.CloudSchema().Validate(v)
   505  	c.Assert(err, jc.ErrorIsNil)
   506  }
   507  
   508  func (localTests) TestPingInvalidHost(c *gc.C) {
   509  	tests := []string{
   510  		"foo.com",
   511  		"http://IHopeNoOneEverBuysThisVerySpecificJujuDomainName.com",
   512  		"http://IHopeNoOneEverBuysThisVerySpecificJujuDomainName:77",
   513  	}
   514  
   515  	p, err := environs.Provider("openstack")
   516  	c.Assert(err, jc.ErrorIsNil)
   517  	callCtx := context.NewEmptyCloudCallContext()
   518  	for _, t := range tests {
   519  		err = p.Ping(callCtx, t)
   520  		if err == nil {
   521  			c.Errorf("ping %q: expected error, but got nil.", t)
   522  			continue
   523  		}
   524  		c.Check(err, gc.ErrorMatches, "(?m)No Openstack server running at "+t+".*")
   525  	}
   526  }
   527  func (localTests) TestPingNoEndpoint(c *gc.C) {
   528  	server := httptest.NewServer(http.HandlerFunc(http.NotFound))
   529  	defer server.Close()
   530  	p, err := environs.Provider("openstack")
   531  	c.Assert(err, jc.ErrorIsNil)
   532  	err = p.Ping(context.NewEmptyCloudCallContext(), server.URL)
   533  	c.Assert(err, gc.ErrorMatches, "(?m)No Openstack server running at "+server.URL+".*")
   534  }
   535  
   536  func (localTests) TestPingInvalidResponse(c *gc.C) {
   537  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   538  		fmt.Fprint(w, "Hi!")
   539  	}))
   540  	defer server.Close()
   541  	p, err := environs.Provider("openstack")
   542  	c.Assert(err, jc.ErrorIsNil)
   543  	err = p.Ping(context.NewEmptyCloudCallContext(), server.URL)
   544  	c.Assert(err, gc.ErrorMatches, "(?m)No Openstack server running at "+server.URL+".*")
   545  }
   546  
   547  func (localTests) TestPingOKCACertificate(c *gc.C) {
   548  	server := httptest.NewTLSServer(handlerFunc)
   549  	defer server.Close()
   550  	pingOk(c, server)
   551  }
   552  
   553  func (localTests) TestPingOK(c *gc.C) {
   554  	server := httptest.NewServer(handlerFunc)
   555  	defer server.Close()
   556  	pingOk(c, server)
   557  }
   558  
   559  func pingOk(c *gc.C, server *httptest.Server) {
   560  	p, err := environs.Provider("openstack")
   561  	c.Assert(err, jc.ErrorIsNil)
   562  	err = p.Ping(context.NewEmptyCloudCallContext(), server.URL)
   563  	c.Assert(err, jc.ErrorIsNil)
   564  }
   565  
   566  var handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   567  	// This line is critical, the openstack provider will reject the message
   568  	// if you return 200 like a mere mortal.
   569  	w.WriteHeader(http.StatusMultipleChoices)
   570  	_, _ = fmt.Fprint(w, `
   571  {
   572    "versions": {
   573      "values": [
   574        {
   575          "status": "stable",
   576          "updated": "2013-03-06T00:00:00Z",
   577          "media-types": [
   578            {
   579              "base": "application/json",
   580              "type": "application/vnd.openstack.identity-v3+json"
   581            },
   582            {
   583              "base": "application/xml",
   584              "type": "application/vnd.openstack.identity-v3+xml"
   585            }
   586          ],
   587          "id": "v3.0",
   588          "links": [
   589            {
   590              "href": "http://10.24.0.177:5000/v3/",
   591              "rel": "self"
   592            }
   593          ]
   594        },
   595        {
   596          "status": "stable",
   597          "updated": "2014-04-17T00:00:00Z",
   598          "media-types": [
   599            {
   600              "base": "application/json",
   601              "type": "application/vnd.openstack.identity-v2.0+json"
   602            },
   603            {
   604              "base": "application/xml",
   605              "type": "application/vnd.openstack.identity-v2.0+xml"
   606            }
   607          ],
   608          "id": "v2.0",
   609          "links": [
   610            {
   611              "href": "http://10.24.0.177:5000/v2.0/",
   612              "rel": "self"
   613            },
   614            {
   615              "href": "http://docs.openstack.org/api/openstack-identity-service/2.0/content/",
   616              "type": "text/html",
   617              "rel": "describedby"
   618            },
   619            {
   620              "href": "http://docs.openstack.org/api/openstack-identity-service/2.0/identity-dev-guide-2.0.pdf",
   621              "type": "application/pdf",
   622              "rel": "describedby"
   623            }
   624          ]
   625        }
   626      ]
   627    }
   628  }
   629  `)
   630  })
   631  
   632  type providerUnitTests struct {
   633  	gitjujutesting.IsolationSuite
   634  }
   635  
   636  var _ = gc.Suite(&providerUnitTests{})
   637  
   638  func checkIdentityClientVersionInvalid(c *gc.C, url string) {
   639  	_, err := identityClientVersion(url)
   640  	c.Check(err, gc.ErrorMatches, fmt.Sprintf("version part of identity url %s not valid", url))
   641  }
   642  
   643  func checkIdentityClientVersion(c *gc.C, url string, expversion int) {
   644  	version, err := identityClientVersion(url)
   645  	c.Assert(err, jc.ErrorIsNil)
   646  	c.Check(version, gc.Equals, expversion)
   647  }
   648  func (s *providerUnitTests) TestIdentityClientVersion_BadURLErrors(c *gc.C) {
   649  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/a")
   650  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/v")
   651  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/V")
   652  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/V/")
   653  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/100")
   654  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/vot")
   655  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/identity/vot")
   656  	checkIdentityClientVersionInvalid(c, "https://keystone.internal/identity/2")
   657  
   658  	_, err := identityClientVersion("abc123")
   659  	c.Check(err, gc.ErrorMatches, `url abc123 is malformed`)
   660  }
   661  
   662  func (s *providerUnitTests) TestIdentityClientVersion_ParsesGoodURL(c *gc.C) {
   663  	checkIdentityClientVersion(c, "https://keystone.internal/v2.0", 2)
   664  	checkIdentityClientVersion(c, "https://keystone.internal/v3.0/", 3)
   665  	checkIdentityClientVersion(c, "https://keystone.internal/v2/", 2)
   666  	checkIdentityClientVersion(c, "https://keystone.internal/V2/", 2)
   667  	checkIdentityClientVersion(c, "https://keystone.internal/internal/V2/", 2)
   668  	checkIdentityClientVersion(c, "https://keystone.internal/internal/v3.0/", 3)
   669  	checkIdentityClientVersion(c, "https://keystone.internal/internal/v3.2///", 3)
   670  	checkIdentityClientVersion(c, "https://keystone.internal", -1)
   671  	checkIdentityClientVersion(c, "https://keystone.internal/", -1)
   672  }
   673  
   674  func (s *providerUnitTests) TestNewCredentialsWithVersion3(c *gc.C) {
   675  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   676  		"version":     "3",
   677  		"username":    "user",
   678  		"password":    "secret",
   679  		"tenant-name": "someTenant",
   680  		"tenant-id":   "someID",
   681  	})
   682  	clouldSpec := environscloudspec.CloudSpec{
   683  		Type:       "openstack",
   684  		Region:     "openstack_region",
   685  		Name:       "openstack",
   686  		Endpoint:   "http://endpoint",
   687  		Credential: &creds,
   688  	}
   689  	cred, authmode, err := newCredentials(clouldSpec)
   690  	c.Assert(err, jc.ErrorIsNil)
   691  	c.Check(cred, gc.Equals, identity.Credentials{
   692  		URL:           "http://endpoint",
   693  		User:          "user",
   694  		Secrets:       "secret",
   695  		Region:        "openstack_region",
   696  		TenantName:    "someTenant",
   697  		TenantID:      "someID",
   698  		Version:       3,
   699  		Domain:        "",
   700  		UserDomain:    "",
   701  		ProjectDomain: "",
   702  	})
   703  	c.Check(authmode, gc.Equals, identity.AuthUserPassV3)
   704  }
   705  
   706  func (s *providerUnitTests) TestNewCredentialsWithFaultVersion(c *gc.C) {
   707  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   708  		"version":     "abc",
   709  		"username":    "user",
   710  		"password":    "secret",
   711  		"tenant-name": "someTenant",
   712  		"tenant-id":   "someID",
   713  	})
   714  	clouldSpec := environscloudspec.CloudSpec{
   715  		Type:       "openstack",
   716  		Region:     "openstack_region",
   717  		Name:       "openstack",
   718  		Endpoint:   "http://endpoint",
   719  		Credential: &creds,
   720  	}
   721  	_, _, err := newCredentials(clouldSpec)
   722  	c.Assert(err, gc.ErrorMatches,
   723  		"cred.Version is not a valid integer type : strconv.Atoi: parsing \"abc\": invalid syntax")
   724  }
   725  
   726  func (s *providerUnitTests) TestNewCredentialsWithoutVersion(c *gc.C) {
   727  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   728  		"username":    "user",
   729  		"password":    "secret",
   730  		"tenant-name": "someTenant",
   731  		"tenant-id":   "someID",
   732  	})
   733  	clouldSpec := environscloudspec.CloudSpec{
   734  		Type:       "openstack",
   735  		Region:     "openstack_region",
   736  		Name:       "openstack",
   737  		Endpoint:   "http://endpoint",
   738  		Credential: &creds,
   739  	}
   740  	cred, authmode, err := newCredentials(clouldSpec)
   741  	c.Assert(err, jc.ErrorIsNil)
   742  	c.Check(cred, gc.Equals, identity.Credentials{
   743  		URL:           "http://endpoint",
   744  		User:          "user",
   745  		Secrets:       "secret",
   746  		Region:        "openstack_region",
   747  		TenantName:    "someTenant",
   748  		TenantID:      "someID",
   749  		Domain:        "",
   750  		UserDomain:    "",
   751  		ProjectDomain: "",
   752  	})
   753  	c.Check(authmode, gc.Equals, identity.AuthUserPass)
   754  }
   755  
   756  func (s *providerUnitTests) TestNewCredentialsWithFaultVersionAndProjectDomainName(c *gc.C) {
   757  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   758  		"version":             "abc",
   759  		"username":            "user",
   760  		"password":            "secret",
   761  		"tenant-name":         "someTenant",
   762  		"tenant-id":           "someID",
   763  		"project-domain-name": "openstack_projectdomain",
   764  	})
   765  	clouldSpec := environscloudspec.CloudSpec{
   766  		Type:       "openstack",
   767  		Region:     "openstack_region",
   768  		Name:       "openstack",
   769  		Endpoint:   "http://endpoint",
   770  		Credential: &creds,
   771  	}
   772  	_, _, err := newCredentials(clouldSpec)
   773  	c.Assert(err, gc.NotNil)
   774  	c.Assert(err, gc.ErrorMatches,
   775  		"cred.Version is not a valid integer type : strconv.Atoi: parsing \"abc\": invalid syntax")
   776  }
   777  func (s *providerUnitTests) TestNewCredentialsWithoutVersionWithProjectDomain(c *gc.C) {
   778  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   779  		"username":            "user",
   780  		"password":            "secret",
   781  		"tenant-name":         "someTenant",
   782  		"tenant-id":           "someID",
   783  		"project-domain-name": "openstack_projectdomain",
   784  	})
   785  	clouldSpec := environscloudspec.CloudSpec{
   786  		Type:       "openstack",
   787  		Region:     "openstack_region",
   788  		Name:       "openstack",
   789  		Endpoint:   "http://endpoint",
   790  		Credential: &creds,
   791  	}
   792  	cred, authmode, err := newCredentials(clouldSpec)
   793  	c.Assert(err, jc.ErrorIsNil)
   794  	c.Check(cred, gc.Equals, identity.Credentials{
   795  		URL:           "http://endpoint",
   796  		User:          "user",
   797  		Secrets:       "secret",
   798  		Region:        "openstack_region",
   799  		TenantName:    "someTenant",
   800  		TenantID:      "someID",
   801  		Domain:        "",
   802  		UserDomain:    "",
   803  		ProjectDomain: "openstack_projectdomain",
   804  	})
   805  	c.Check(authmode, gc.Equals, identity.AuthUserPassV3)
   806  }
   807  
   808  func (s *providerUnitTests) TestNewCredentialsWithoutVersionWithUserDomain(c *gc.C) {
   809  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   810  		"username":         "user",
   811  		"password":         "secret",
   812  		"tenant-name":      "someTenant",
   813  		"tenant-id":        "someID",
   814  		"user-domain-name": "openstack_userdomain",
   815  	})
   816  	clouldSpec := environscloudspec.CloudSpec{
   817  		Type:       "openstack",
   818  		Region:     "openstack_region",
   819  		Name:       "openstack",
   820  		Endpoint:   "http://endpoint",
   821  		Credential: &creds,
   822  	}
   823  	cred, authmode, err := newCredentials(clouldSpec)
   824  	c.Assert(err, jc.ErrorIsNil)
   825  	c.Check(cred, gc.Equals, identity.Credentials{
   826  		URL:           "http://endpoint",
   827  		User:          "user",
   828  		Secrets:       "secret",
   829  		Region:        "openstack_region",
   830  		TenantName:    "someTenant",
   831  		TenantID:      "someID",
   832  		Version:       0,
   833  		Domain:        "",
   834  		UserDomain:    "openstack_userdomain",
   835  		ProjectDomain: "",
   836  	})
   837  	c.Check(authmode, gc.Equals, identity.AuthUserPassV3)
   838  }
   839  
   840  func (s *providerUnitTests) TestNewCredentialsWithVersion2(c *gc.C) {
   841  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   842  		"version":     "2",
   843  		"username":    "user",
   844  		"password":    "secret",
   845  		"tenant-name": "someTenant",
   846  		"tenant-id":   "someID",
   847  	})
   848  	clouldSpec := environscloudspec.CloudSpec{
   849  		Type:       "openstack",
   850  		Region:     "openstack_region",
   851  		Name:       "openstack",
   852  		Endpoint:   "http://endpoint",
   853  		Credential: &creds,
   854  	}
   855  	cred, authmode, err := newCredentials(clouldSpec)
   856  	c.Assert(err, jc.ErrorIsNil)
   857  	c.Check(cred, gc.Equals, identity.Credentials{
   858  		URL:           "http://endpoint",
   859  		User:          "user",
   860  		Secrets:       "secret",
   861  		Region:        "openstack_region",
   862  		TenantName:    "someTenant",
   863  		TenantID:      "someID",
   864  		Version:       2,
   865  		Domain:        "",
   866  		UserDomain:    "",
   867  		ProjectDomain: "",
   868  	})
   869  	c.Check(authmode, gc.Equals, identity.AuthUserPass)
   870  }
   871  
   872  func (s *providerUnitTests) TestNewCredentialsWithVersion2AndDomain(c *gc.C) {
   873  	creds := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{
   874  		"version":             "2",
   875  		"username":            "user",
   876  		"password":            "secret",
   877  		"tenant-name":         "someTenant",
   878  		"tenant-id":           "someID",
   879  		"project-domain-name": "openstack_projectdomain",
   880  	})
   881  	clouldSpec := environscloudspec.CloudSpec{
   882  		Type:       "openstack",
   883  		Region:     "openstack_region",
   884  		Name:       "openstack",
   885  		Endpoint:   "http://endpoint",
   886  		Credential: &creds,
   887  	}
   888  	cred, authmode, err := newCredentials(clouldSpec)
   889  	c.Assert(err, jc.ErrorIsNil)
   890  	c.Check(cred, gc.Equals, identity.Credentials{
   891  		URL:           "http://endpoint",
   892  		User:          "user",
   893  		Secrets:       "secret",
   894  		Region:        "openstack_region",
   895  		TenantName:    "someTenant",
   896  		TenantID:      "someID",
   897  		Version:       2,
   898  		Domain:        "",
   899  		UserDomain:    "",
   900  		ProjectDomain: "openstack_projectdomain",
   901  	})
   902  	c.Check(authmode, gc.Equals, identity.AuthUserPass)
   903  }
   904  
   905  func (s *providerUnitTests) TestNetworksForInstance(c *gc.C) {
   906  	ctrl := gomock.NewController(c)
   907  	defer ctrl.Finish()
   908  
   909  	netID := "network-id-foo"
   910  
   911  	mockNetworking := NewMockNetworking(ctrl)
   912  	mockNetworking.EXPECT().ResolveNetworks(netID, false).Return([]neutron.NetworkV2{{Id: netID}}, nil)
   913  
   914  	netCfg := NewMockNetworkingConfig(ctrl)
   915  
   916  	siParams := environs.StartInstanceParams{
   917  		AvailabilityZone: "eu-west-az",
   918  	}
   919  
   920  	result, err := envWithNetworking(mockNetworking, netID).networksForInstance(siParams, netCfg)
   921  
   922  	c.Assert(err, jc.ErrorIsNil)
   923  	c.Assert(result, gc.DeepEquals, []nova.ServerNetworks{
   924  		{
   925  			NetworkId: netID,
   926  			FixedIp:   "",
   927  			PortId:    "",
   928  		},
   929  	})
   930  }
   931  
   932  func (s *providerUnitTests) TestNetworksForInstanceNoConfigMultiNet(c *gc.C) {
   933  	ctrl := gomock.NewController(c)
   934  	defer ctrl.Finish()
   935  
   936  	mockNetworking := NewMockNetworking(ctrl)
   937  	mockNetworking.EXPECT().ResolveNetworks("", false).Return([]neutron.NetworkV2{
   938  		{Id: "network-id-foo"},
   939  		{Id: "network-id-bar"},
   940  	}, nil)
   941  
   942  	netCfg := NewMockNetworkingConfig(ctrl)
   943  
   944  	siParams := environs.StartInstanceParams{
   945  		AvailabilityZone: "eu-west-az",
   946  	}
   947  
   948  	result, err := envWithNetworking(mockNetworking, "").networksForInstance(siParams, netCfg)
   949  
   950  	c.Assert(err, jc.ErrorIsNil)
   951  	c.Assert(result, gc.DeepEquals, []nova.ServerNetworks{
   952  		{NetworkId: "network-id-foo"},
   953  		{NetworkId: "network-id-bar"},
   954  	})
   955  }
   956  
   957  func (s *providerUnitTests) TestNetworksForInstanceMultiConfigMultiNet(c *gc.C) {
   958  	ctrl := gomock.NewController(c)
   959  	defer ctrl.Finish()
   960  
   961  	mockNetworking := NewMockNetworking(ctrl)
   962  	mockNetworking.EXPECT().ResolveNetworks("network-id-foo", false).Return([]neutron.NetworkV2{{
   963  		Id: "network-id-foo"}}, nil)
   964  	mockNetworking.EXPECT().ResolveNetworks("network-id-bar", false).Return([]neutron.NetworkV2{{
   965  		Id: "network-id-bar"}}, nil)
   966  
   967  	netCfg := NewMockNetworkingConfig(ctrl)
   968  
   969  	siParams := environs.StartInstanceParams{
   970  		AvailabilityZone: "eu-west-az",
   971  	}
   972  
   973  	result, err := envWithNetworking(mockNetworking, "network-id-foo,network-id-bar").networksForInstance(siParams, netCfg)
   974  
   975  	c.Assert(err, jc.ErrorIsNil)
   976  	c.Assert(result, gc.DeepEquals, []nova.ServerNetworks{
   977  		{NetworkId: "network-id-foo"},
   978  		{NetworkId: "network-id-bar"},
   979  	})
   980  }
   981  
   982  func (s *providerUnitTests) TestNetworksForInstanceWithAZ(c *gc.C) {
   983  	ctrl := gomock.NewController(c)
   984  	defer ctrl.Finish()
   985  
   986  	netID := "network-id-foo"
   987  
   988  	mockNetworking := NewMockNetworking(ctrl)
   989  	mockNetworking.EXPECT().ResolveNetworks(netID, false).Return([]neutron.NetworkV2{{
   990  		Id:        netID,
   991  		SubnetIds: []string{"subnet-foo"},
   992  	}}, nil)
   993  
   994  	mockNetworking.EXPECT().CreatePort("", netID, network.Id("subnet-foo")).Return(
   995  		&neutron.PortV2{
   996  			FixedIPs: []neutron.PortFixedIPsV2{{
   997  				IPAddress: "10.10.10.1",
   998  				SubnetID:  "subnet-id",
   999  			}},
  1000  			Id:         "port-id",
  1001  			MACAddress: "mac-address",
  1002  		}, nil)
  1003  
  1004  	netCfg := NewMockNetworkingConfig(ctrl)
  1005  	netCfg.EXPECT().AddNetworkConfig(network.InterfaceInfos{{
  1006  		InterfaceName: "eth0",
  1007  		MACAddress:    "mac-address",
  1008  		Addresses:     network.NewMachineAddresses([]string{"10.10.10.1"}).AsProviderAddresses(),
  1009  		ConfigType:    network.ConfigDHCP,
  1010  		Origin:        network.OriginProvider,
  1011  	}}).Return(nil)
  1012  
  1013  	siParams := environs.StartInstanceParams{
  1014  		AvailabilityZone: "eu-west-az",
  1015  		SubnetsToZones:   []map[network.Id][]string{{"subnet-foo": {"eu-west-az", "eu-east-az"}}},
  1016  	}
  1017  
  1018  	result, err := envWithNetworking(mockNetworking, netID).networksForInstance(siParams, netCfg)
  1019  
  1020  	c.Assert(err, jc.ErrorIsNil)
  1021  	c.Assert(result, gc.DeepEquals, []nova.ServerNetworks{
  1022  		{
  1023  			NetworkId: netID,
  1024  			PortId:    "port-id",
  1025  		},
  1026  	})
  1027  }
  1028  
  1029  func (s *providerUnitTests) TestNetworksForInstanceWithAZNoConfigMultiNet(c *gc.C) {
  1030  	ctrl := gomock.NewController(c)
  1031  	defer ctrl.Finish()
  1032  
  1033  	mockNetworking := NewMockNetworking(ctrl)
  1034  	mockNetworking.EXPECT().ResolveNetworks("", false).Return([]neutron.NetworkV2{
  1035  		{
  1036  			Id:        "network-id-foo",
  1037  			SubnetIds: []string{"subnet-foo"},
  1038  		},
  1039  		{
  1040  			Id:        "network-id-bar",
  1041  			SubnetIds: []string{"subnet-bar"},
  1042  		},
  1043  	}, nil)
  1044  
  1045  	mockNetworking.EXPECT().CreatePort("", "network-id-foo", network.Id("subnet-foo")).Return(
  1046  		&neutron.PortV2{
  1047  			FixedIPs: []neutron.PortFixedIPsV2{{
  1048  				IPAddress: "10.10.10.1",
  1049  				SubnetID:  "subnet-foo",
  1050  			}},
  1051  			Id:         "port-id-foo",
  1052  			MACAddress: "mac-address-foo",
  1053  		}, nil)
  1054  
  1055  	mockNetworking.EXPECT().CreatePort("", "network-id-bar", network.Id("subnet-bar")).Return(
  1056  		&neutron.PortV2{
  1057  			FixedIPs: []neutron.PortFixedIPsV2{{
  1058  				IPAddress: "10.10.20.1",
  1059  				SubnetID:  "subnet-bar",
  1060  			}},
  1061  			Id:         "port-id-bar",
  1062  			MACAddress: "mac-address-bar",
  1063  		}, nil)
  1064  
  1065  	netCfg := NewMockNetworkingConfig(ctrl)
  1066  	netCfg.EXPECT().AddNetworkConfig(network.InterfaceInfos{
  1067  		{
  1068  			InterfaceName: "eth0",
  1069  			MACAddress:    "mac-address-foo",
  1070  			Addresses:     network.NewMachineAddresses([]string{"10.10.10.1"}).AsProviderAddresses(),
  1071  			ConfigType:    network.ConfigDHCP,
  1072  			Origin:        network.OriginProvider,
  1073  		},
  1074  		{
  1075  			InterfaceName: "eth1",
  1076  			MACAddress:    "mac-address-bar",
  1077  			Addresses:     network.NewMachineAddresses([]string{"10.10.20.1"}).AsProviderAddresses(),
  1078  			ConfigType:    network.ConfigDHCP,
  1079  			Origin:        network.OriginProvider,
  1080  		},
  1081  	}).Return(nil)
  1082  
  1083  	siParams := environs.StartInstanceParams{
  1084  		AvailabilityZone: "eu-west-az",
  1085  		SubnetsToZones: []map[network.Id][]string{
  1086  			{"subnet-foo": {"eu-west-az", "eu-east-az"}},
  1087  			{"subnet-bar": {"eu-west-az", "eu-east-az"}},
  1088  		},
  1089  	}
  1090  
  1091  	result, err := envWithNetworking(mockNetworking, "").networksForInstance(siParams, netCfg)
  1092  
  1093  	c.Assert(err, jc.ErrorIsNil)
  1094  	c.Assert(result, gc.DeepEquals, []nova.ServerNetworks{
  1095  		{
  1096  			NetworkId: "network-id-foo",
  1097  			PortId:    "port-id-foo",
  1098  		},
  1099  		{
  1100  			NetworkId: "network-id-bar",
  1101  			PortId:    "port-id-bar",
  1102  		},
  1103  	})
  1104  }
  1105  
  1106  func (s *providerUnitTests) TestNetworksForInstanceWithNoMatchingAZ(c *gc.C) {
  1107  	ctrl := gomock.NewController(c)
  1108  	defer ctrl.Finish()
  1109  
  1110  	netID := "network-id-foo"
  1111  
  1112  	mockNetworking := NewMockNetworking(ctrl)
  1113  	mockNetworking.EXPECT().ResolveNetworks(netID, false).Return([]neutron.NetworkV2{{
  1114  		Id:        netID,
  1115  		SubnetIds: []string{"subnet-foo"},
  1116  	}}, nil)
  1117  
  1118  	netCfg := NewMockNetworkingConfig(ctrl)
  1119  
  1120  	siParams := environs.StartInstanceParams{
  1121  		AvailabilityZone: "us-east-az",
  1122  		SubnetsToZones:   []map[network.Id][]string{{"subnet-foo": {"eu-west-az", "eu-east-az"}}},
  1123  		Constraints: constraints.Value{
  1124  			Spaces: &[]string{"eu-west-az"},
  1125  		},
  1126  	}
  1127  
  1128  	_, err := envWithNetworking(mockNetworking, netID).networksForInstance(siParams, netCfg)
  1129  	c.Assert(err, gc.ErrorMatches, "determining subnets in zone \"us-east-az\": subnets in AZ \"us-east-az\" not found")
  1130  }
  1131  
  1132  func (s *providerUnitTests) TestNetworksForInstanceNoSubnetAZsStillConsidered(c *gc.C) {
  1133  	ctrl := gomock.NewController(c)
  1134  	defer ctrl.Finish()
  1135  
  1136  	netID := "network-id-foo"
  1137  
  1138  	mockNetworking := NewMockNetworking(ctrl)
  1139  	exp := mockNetworking.EXPECT()
  1140  
  1141  	exp.ResolveNetworks(netID, false).Return([]neutron.NetworkV2{{
  1142  		Id:        netID,
  1143  		SubnetIds: []string{"subnet-foo", "subnet-with-az"},
  1144  	}}, nil)
  1145  
  1146  	exp.CreatePort("", netID, network.Id("subnet-foo")).Return(
  1147  		&neutron.PortV2{
  1148  			FixedIPs: []neutron.PortFixedIPsV2{{
  1149  				IPAddress: "10.10.10.1",
  1150  				SubnetID:  "subnet-id",
  1151  			}},
  1152  			Id:         "port-id",
  1153  			MACAddress: "mac-address",
  1154  		}, nil)
  1155  
  1156  	netCfg := NewMockNetworkingConfig(ctrl)
  1157  	netCfg.EXPECT().AddNetworkConfig(network.InterfaceInfos{{
  1158  		InterfaceName: "eth0",
  1159  		MACAddress:    "mac-address",
  1160  		Addresses:     network.NewMachineAddresses([]string{"10.10.10.1"}).AsProviderAddresses(),
  1161  		ConfigType:    network.ConfigDHCP,
  1162  		Origin:        network.OriginProvider,
  1163  	}}).Return(nil)
  1164  
  1165  	siParams := environs.StartInstanceParams{
  1166  		AvailabilityZone: "eu-west-az",
  1167  		SubnetsToZones: []map[network.Id][]string{{
  1168  			"subnet-foo":     {},
  1169  			"subnet-with-az": {"some-non-matching-zone"},
  1170  		}},
  1171  	}
  1172  
  1173  	result, err := envWithNetworking(mockNetworking, netID).networksForInstance(siParams, netCfg)
  1174  
  1175  	c.Assert(err, jc.ErrorIsNil)
  1176  	c.Assert(result, gc.DeepEquals, []nova.ServerNetworks{
  1177  		{
  1178  			NetworkId: netID,
  1179  			PortId:    "port-id",
  1180  		},
  1181  	})
  1182  }
  1183  
  1184  func envWithNetworking(net Networking, netCfg string) *Environ {
  1185  	return &Environ{
  1186  		ecfgUnlocked: &environConfig{
  1187  			attrs: map[string]interface{}{NetworkKey: netCfg},
  1188  		},
  1189  		networking: net,
  1190  	}
  1191  }