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 }