github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/repository/trusty/haproxy/hooks/tests/test_reverseproxy_hooks.py (about) 1 import os 2 import base64 3 import yaml 4 5 from testtools import TestCase 6 from mock import patch, call 7 8 import hooks 9 10 11 class ReverseProxyRelationTest(TestCase): 12 13 def setUp(self): 14 super(ReverseProxyRelationTest, self).setUp() 15 16 self.config_get = self.patch_hook("config_get") 17 self.config_get.return_value = {"monitoring_port": "10000", 18 "peering_mode": "active-passive"} 19 self.relations_of_type = self.patch_hook("relations_of_type") 20 self.get_config_services = self.patch_hook("get_config_services") 21 self.log = self.patch_hook("log") 22 self.write_service_config = self.patch_hook("write_service_config") 23 self.apply_peer_config = self.patch_hook("apply_peer_config") 24 self.apply_peer_config.side_effect = lambda value: value 25 26 def patch_hook(self, hook_name): 27 mock_controller = patch.object(hooks, hook_name) 28 mock = mock_controller.start() 29 self.addCleanup(mock_controller.stop) 30 return mock 31 32 def test_relation_data_returns_none(self): 33 self.get_config_services.return_value = { 34 "service": { 35 "service_name": "service", 36 }, 37 } 38 self.relations_of_type.return_value = [] 39 self.assertIs(None, hooks.create_services()) 40 self.log.assert_called_once_with("No backend servers, exiting.") 41 self.write_service_config.assert_not_called() 42 43 def test_relation_data_returns_no_relations(self): 44 self.get_config_services.return_value = { 45 "service": { 46 "service_name": "service", 47 }, 48 } 49 self.relations_of_type.return_value = [] 50 self.assertIs(None, hooks.create_services()) 51 self.log.assert_called_once_with("No backend servers, exiting.") 52 self.write_service_config.assert_not_called() 53 54 def test_relation_no_services(self): 55 self.get_config_services.return_value = {} 56 self.relations_of_type.return_value = [ 57 {"port": 4242, 58 "__unit__": "foo/0", 59 "hostname": "backend.1", 60 "private-address": "1.2.3.4"}, 61 ] 62 self.assertIs(None, hooks.create_services()) 63 self.log.assert_called_once_with("No services configured, exiting.") 64 self.write_service_config.assert_not_called() 65 66 def test_no_port_in_relation_data(self): 67 self.get_config_services.return_value = { 68 "service": { 69 "service_name": "service", 70 }, 71 } 72 self.relations_of_type.return_value = [ 73 {"private-address": "1.2.3.4", 74 "__unit__": "foo/0"}, 75 ] 76 self.assertIs(None, hooks.create_services()) 77 self.log.assert_has_calls([call.log( 78 "No port in relation data for 'foo/0', skipping.")]) 79 self.write_service_config.assert_not_called() 80 81 def test_no_private_address_in_relation_data(self): 82 self.get_config_services.return_value = { 83 "service": { 84 "service_name": "service", 85 }, 86 } 87 self.relations_of_type.return_value = [ 88 {"port": 4242, 89 "__unit__": "foo/0"}, 90 ] 91 self.assertIs(None, hooks.create_services()) 92 self.log.assert_has_calls([call.log( 93 "No private-address in relation data for 'foo/0', skipping.")]) 94 self.write_service_config.assert_not_called() 95 96 def test_relation_unknown_service(self): 97 self.get_config_services.return_value = { 98 "service": { 99 "service_name": "service", 100 }, 101 } 102 self.relations_of_type.return_value = [ 103 {"port": 4242, 104 "hostname": "backend.1", 105 "service_name": "invalid", 106 "private-address": "1.2.3.4", 107 "__unit__": "foo/0"}, 108 ] 109 self.assertIs(None, hooks.create_services()) 110 self.log.assert_has_calls([call.log( 111 "Service 'invalid' does not exist.")]) 112 self.write_service_config.assert_not_called() 113 114 def test_no_relation_but_has_servers_from_config(self): 115 self.get_config_services.return_value = { 116 None: { 117 "service_name": "service", 118 }, 119 "service": { 120 "service_name": "service", 121 "servers": [ 122 ("legacy-backend", "1.2.3.1", 4242, ["maxconn 42"]), 123 ] 124 }, 125 } 126 self.relations_of_type.return_value = [] 127 128 expected = { 129 'service': { 130 'service_name': 'service', 131 'service_host': '0.0.0.0', 132 'service_port': 10002, 133 'servers': [ 134 ("legacy-backend", "1.2.3.1", 4242, ["maxconn 42"]), 135 ], 136 }, 137 } 138 self.assertEqual(expected, hooks.create_services()) 139 self.write_service_config.assert_called_with(expected) 140 141 def test_relation_default_service(self): 142 self.get_config_services.return_value = { 143 None: { 144 "service_name": "service", 145 }, 146 "service": { 147 "service_name": "service", 148 }, 149 } 150 self.relations_of_type.return_value = [ 151 {"port": 4242, 152 "hostname": "backend.1", 153 "private-address": "1.2.3.4", 154 "__unit__": "foo/0"}, 155 ] 156 157 expected = { 158 'service': { 159 'service_name': 'service', 160 'service_host': '0.0.0.0', 161 'service_port': 10002, 162 'servers': [('foo-0-4242', '1.2.3.4', 4242, [])], 163 }, 164 } 165 self.assertEqual(expected, hooks.create_services()) 166 self.write_service_config.assert_called_with(expected) 167 168 def test_with_service_options(self): 169 self.get_config_services.return_value = { 170 None: { 171 "service_name": "service", 172 }, 173 "service": { 174 "service_name": "service", 175 "server_options": ["maxconn 4"], 176 }, 177 } 178 self.relations_of_type.return_value = [ 179 {"port": 4242, 180 "hostname": "backend.1", 181 "private-address": "1.2.3.4", 182 "__unit__": "foo/0"}, 183 ] 184 185 expected = { 186 'service': { 187 'service_name': 'service', 188 'service_host': '0.0.0.0', 189 'service_port': 10002, 190 'server_options': ["maxconn 4"], 191 'servers': [('foo-0-4242', '1.2.3.4', 192 4242, ["maxconn 4"])], 193 }, 194 } 195 self.assertEqual(expected, hooks.create_services()) 196 self.write_service_config.assert_called_with(expected) 197 198 def test_with_service_name(self): 199 self.get_config_services.return_value = { 200 None: { 201 "service_name": "service", 202 }, 203 "foo_service": { 204 "service_name": "foo_service", 205 "server_options": ["maxconn 4"], 206 }, 207 } 208 self.relations_of_type.return_value = [ 209 {"port": 4242, 210 "hostname": "backend.1", 211 "service_name": "foo_service", 212 "private-address": "1.2.3.4", 213 "__unit__": "foo/0"}, 214 ] 215 216 expected = { 217 'foo_service': { 218 'service_name': 'foo_service', 219 'service_host': '0.0.0.0', 220 'service_port': 10002, 221 'server_options': ["maxconn 4"], 222 'servers': [('foo-0-4242', '1.2.3.4', 223 4242, ["maxconn 4"])], 224 }, 225 } 226 self.assertEqual(expected, hooks.create_services()) 227 self.write_service_config.assert_called_with(expected) 228 229 def test_no_service_name_unit_name_match_service_name(self): 230 self.get_config_services.return_value = { 231 None: { 232 "service_name": "foo_service", 233 }, 234 "foo_service": { 235 "service_name": "foo_service", 236 "server_options": ["maxconn 4"], 237 }, 238 } 239 self.relations_of_type.return_value = [ 240 {"port": 4242, 241 "hostname": "backend.1", 242 "private-address": "1.2.3.4", 243 "__unit__": "foo/1"}, 244 ] 245 246 expected = { 247 'foo_service': { 248 'service_name': 'foo_service', 249 'service_host': '0.0.0.0', 250 'service_port': 10002, 251 'server_options': ["maxconn 4"], 252 'servers': [('foo-1-4242', '1.2.3.4', 253 4242, ["maxconn 4"])], 254 }, 255 } 256 self.assertEqual(expected, hooks.create_services()) 257 self.write_service_config.assert_called_with(expected) 258 259 def test_with_sitenames_match_service_name(self): 260 self.get_config_services.return_value = { 261 None: { 262 "service_name": "service", 263 }, 264 "foo_srv": { 265 "service_name": "foo_srv", 266 "server_options": ["maxconn 4"], 267 }, 268 } 269 self.relations_of_type.return_value = [ 270 {"port": 4242, 271 "hostname": "backend.1", 272 "sitenames": "foo_srv bar_srv", 273 "private-address": "1.2.3.4", 274 "__unit__": "foo/0"}, 275 ] 276 277 expected = { 278 'foo_srv': { 279 'service_name': 'foo_srv', 280 'service_host': '0.0.0.0', 281 'service_port': 10002, 282 'server_options': ["maxconn 4"], 283 'servers': [('foo-0-4242', '1.2.3.4', 284 4242, ["maxconn 4"])], 285 }, 286 } 287 self.assertEqual(expected, hooks.create_services()) 288 self.write_service_config.assert_called_with(expected) 289 290 def test_with_juju_services_match_service_name(self): 291 self.get_config_services.return_value = { 292 None: { 293 "service_name": "service", 294 }, 295 "foo_service": { 296 "service_name": "foo_service", 297 "server_options": ["maxconn 4"], 298 }, 299 } 300 self.relations_of_type.return_value = [ 301 {"port": 4242, 302 "hostname": "backend.1", 303 "private-address": "1.2.3.4", 304 "__unit__": "foo/1"}, 305 ] 306 307 expected = { 308 'foo_service': { 309 'service_name': 'foo_service', 310 'service_host': '0.0.0.0', 311 'service_port': 10002, 312 'server_options': ["maxconn 4"], 313 'servers': [('foo-1-4242', '1.2.3.4', 314 4242, ["maxconn 4"])], 315 }, 316 } 317 318 result = hooks.create_services() 319 320 self.assertEqual(expected, result) 321 self.write_service_config.assert_called_with(expected) 322 323 def test_with_sitenames_no_match_but_unit_name(self): 324 self.get_config_services.return_value = { 325 None: { 326 "service_name": "service", 327 }, 328 "foo": { 329 "service_name": "foo", 330 "server_options": ["maxconn 4"], 331 }, 332 } 333 self.relations_of_type.return_value = [ 334 {"port": 4242, 335 "hostname": "backend.1", 336 "sitenames": "bar_service baz_service", 337 "private-address": "1.2.3.4", 338 "__unit__": "foo/0"}, 339 ] 340 341 expected = { 342 'foo': { 343 'service_name': 'foo', 344 'service_host': '0.0.0.0', 345 'service_port': 10002, 346 'server_options': ["maxconn 4"], 347 'servers': [('foo-0-4242', '1.2.3.4', 348 4242, ["maxconn 4"])], 349 }, 350 } 351 self.assertEqual(expected, hooks.create_services()) 352 self.write_service_config.assert_called_with(expected) 353 354 def test_with_multiple_units_in_relation(self): 355 """ 356 Have multiple units specifying "services" in the relation. 357 Make sure data is created correctly with create_services() 358 """ 359 self.get_config_services.return_value = { 360 None: { 361 "service_name": "service", 362 }, 363 } 364 self.relations_of_type.return_value = [ 365 {"port": 4242, 366 "private-address": "1.2.3.4", 367 "__unit__": "foo/0", 368 "services": yaml.safe_dump([{ 369 "service_name": "service", 370 "servers": [('foo-0', '1.2.3.4', 371 4242, ["maxconn 4"])] 372 }]) 373 }, 374 {"port": 4242, 375 "private-address": "1.2.3.5", 376 "__unit__": "foo/1", 377 "services": yaml.safe_dump([{ 378 "service_name": "service", 379 "servers": [('foo-0', '1.2.3.5', 380 4242, ["maxconn 4"])] 381 }]) 382 }, 383 ] 384 385 expected = { 386 'service': { 387 'service_name': 'service', 388 'service_host': '0.0.0.0', 389 'service_port': 10002, 390 'servers': [ 391 ['foo-0', '1.2.3.4', 4242, ["maxconn 4"]], 392 ['foo-0', '1.2.3.5', 4242, ["maxconn 4"]] 393 ] 394 }, 395 } 396 self.assertEqual(expected, hooks.create_services()) 397 self.write_service_config.assert_called_with(expected) 398 399 def test_with_multiple_units_and_backends_in_relation(self): 400 """ 401 Have multiple units specifying "services" in the relation 402 using the "backends" option. Make sure data is created correctly 403 with create_services() 404 """ 405 self.get_config_services.return_value = { 406 None: { 407 "service_name": "service", 408 }, 409 } 410 self.relations_of_type.return_value = [ 411 {"port": 4242, 412 "private-address": "1.2.3.4", 413 "__unit__": "foo/0", 414 "services": yaml.safe_dump([{ 415 "service_name": "service", 416 "servers": [('foo-0', '1.2.3.4', 417 4242, ["maxconn 4"])], 418 "backends": [ 419 {"backend_name": "foo-bar", 420 "servers": [('foo-bar-0', '2.2.2.2', 421 2222, ["maxconn 4"])], 422 }, 423 ] 424 }]) 425 }, 426 {"port": 4242, 427 "private-address": "1.2.3.5", 428 "__unit__": "foo/1", 429 "services": yaml.safe_dump([{ 430 "service_name": "service", 431 "servers": [('foo-0', '1.2.3.5', 432 4242, ["maxconn 4"])], 433 "backends": [ 434 {"backend_name": "foo-bar", 435 "servers": [('foo-bar-1', '2.2.2.3', 436 3333, ["maxconn 4"])], 437 }, 438 ] 439 }]) 440 }, 441 ] 442 443 expected = { 444 'service': { 445 'service_name': 'service', 446 'service_host': '0.0.0.0', 447 'service_port': 10002, 448 'servers': [ 449 ['foo-0', '1.2.3.4', 4242, ["maxconn 4"]], 450 ['foo-0', '1.2.3.5', 4242, ["maxconn 4"]] 451 ], 452 'backends': [ 453 {"backend_name": "foo-bar", 454 "servers": [ 455 ['foo-bar-0', '2.2.2.2', 2222, ["maxconn 4"]], 456 ['foo-bar-1', '2.2.2.3', 3333, ["maxconn 4"]], 457 ], 458 }, 459 ] 460 }, 461 } 462 self.assertEqual(expected, hooks.create_services()) 463 self.write_service_config.assert_called_with(expected) 464 465 @patch.dict(os.environ, {"JUJU_UNIT_NAME": "foo/1"}) 466 def test_with_multiple_units_in_relation_scaleout(self): 467 """ 468 Test multiple units in scaleout mode. 469 Ensure no indirection layer gets created. 470 """ 471 self.config_get.return_value["peering_mode"] = "active-active" 472 self.get_config_services.return_value = { 473 None: { 474 "service_name": "service", 475 }, 476 } 477 unit_get = self.patch_hook("unit_get") 478 unit_get.return_value = "1.2.4.5" 479 self.relations_of_type.return_value = [ 480 {"port": 4242, 481 "private-address": "1.2.4.4", 482 "__unit__": "foo/0", 483 "services": yaml.safe_dump([{ 484 "service_name": "service", 485 "servers": [('foo-0', '1.2.3.4', 486 4242, ["maxconn 4"])] 487 }]) 488 }, 489 490 {"__unit__": "foo/1", 491 "hostname": "foo-1", 492 "private-address": "1.2.4.4", 493 "all_services": yaml.dump([ 494 {"service_name": "service", 495 "service_host": "0.0.0.0", 496 "service_options": ["balance leastconn"], 497 "service_port": 4242}, 498 ]) 499 }, 500 ] 501 502 expected = { 503 'service': { 504 'service_name': 'service', 505 'service_host': '0.0.0.0', 506 'service_port': 10002, 507 'servers': [ 508 ['foo-0', '1.2.3.4', 4242, ["maxconn 4"]], 509 ] 510 }, 511 } 512 self.assertEqual(expected, hooks.create_services()) 513 self.write_service_config.assert_called_with(expected) 514 515 def test_merge_service(self): 516 """ Make sure merge_services maintains "server" entries. """ 517 s1 = {'service_name': 'f', 'servers': [['f', '4', 4, ['maxconn 4']]]} 518 s2 = {'service_name': 'f', 'servers': [['f', '5', 5, ['maxconn 4']]]} 519 520 expected = {'service_name': 'f', 'servers': [ 521 ['f', '4', 4, ['maxconn 4']], 522 ['f', '5', 5, ['maxconn 4']]]} 523 524 self.assertEqual(expected, hooks.merge_service(s1, s2)) 525 526 def test_merge_service_removes_duplicates(self): 527 """ 528 Make sure merge services strips strict duplicates from the 529 'servers' entries. 530 """ 531 s1 = {'servers': [['f', '4', 4, ['maxconn 4']]]} 532 s2 = {'servers': [['f', '4', 4, ['maxconn 4']]]} 533 expected = {'servers': [['f', '4', 4, ['maxconn 4']]]} 534 self.assertEqual(expected, hooks.merge_service(s1, s2)) 535 536 def test_merge_service_merge_order(self): 537 """ Make sure merge_services prefers the left side. """ 538 s1 = {'service_name': 'left', 'foo': 'bar'} 539 s2 = {'service_name': 'right', 'bar': 'baz'} 540 541 expected = {'service_name': 'left', 'foo': 'bar', 'bar': 'baz'} 542 self.assertEqual(expected, hooks.merge_service(s1, s2)) 543 544 def test_merge_service_old_backend_without_name(self): 545 """Backends in old_service without name raise an exception.""" 546 547 s1 = {'backends': [{'servers': []}]} 548 s2 = {'backends': []} 549 self.assertRaises( 550 hooks.InvalidRelationDataError, hooks.merge_service, s1, s2) 551 552 def test_merge_service_new_backend_without_name(self): 553 """Backends in new_service without name raise an exception.""" 554 555 s1 = {'backends': []} 556 s2 = {'backends': [{'servers': []}]} 557 self.assertRaises( 558 hooks.InvalidRelationDataError, hooks.merge_service, s1, s2) 559 560 def test_merge_service_no_old_backend(self): 561 """ 562 If the old service config has no backends, the backends from the 563 new config is used.. 564 """ 565 566 s1 = {} 567 s2 = {'backends': [ 568 {'backend_name': 'webapp', 569 'servers': [['webapp-1', '10.0.0.2', 8090, []]]}, 570 ]} 571 self.assertEqual(s2, hooks.merge_service(s1, s2)) 572 573 def test_merge_service_no_new_backend(self): 574 """ 575 If the new service config has no backends, the backends from the 576 old config is used.. 577 """ 578 579 s1 = {'backends': [ 580 {'backend_name': 'webapp', 581 'servers': [['webapp-1', '10.0.0.2', 8090, []]]}, 582 ]} 583 s2 = {} 584 self.assertEqual(s1, hooks.merge_service(s1, s2)) 585 586 def test_merge_service_backend_name_matching(self): 587 """Backends are merged by backend_name.""" 588 589 s1 = {'backends': [ 590 {'backend_name': 'api', 591 'servers': [['api-0', '10.0.0.1', 9080, []]]}, 592 {'backend_name': 'webapp', 593 'servers': [['webapp-0', '10.0.0.1', 8090, []]]}, 594 ]} 595 s2 = {'backends': [ 596 {'backend_name': 'webapp', 597 'servers': [['webapp-1', '10.0.0.2', 8090, []]]}, 598 ]} 599 expected = { 600 'backends': [ 601 {'backend_name': 'api', 602 'servers': [['api-0', '10.0.0.1', 9080, []]]}, 603 {'backend_name': 'webapp', 604 'servers': [['webapp-0', '10.0.0.1', 8090, []], 605 ['webapp-1', '10.0.0.2', 8090, []]]}, 606 ] 607 } 608 self.assertEqual(expected, hooks.merge_service(s1, s2)) 609 610 def test_join_reverseproxy_relation(self): 611 """ 612 When haproxy joins a reverseproxy relation it advertises its public 613 IP and public certificate by setting values on the relation. 614 """ 615 ssl_cert = base64.b64encode("<cert data>") 616 self.config_get.return_value = {"ssl_cert": ssl_cert} 617 unit_get = self.patch_hook("unit_get") 618 unit_get.return_value = "1.2.3.4" 619 relation_id = self.patch_hook("relation_id") 620 relation_id.return_value = "reverseproxy:1" 621 relation_set = self.patch_hook("relation_set") 622 hooks.reverseproxy_interface(hook_name="joined") 623 unit_get.assert_called_once_with("public-address") 624 relation_set.assert_called_once_with( 625 relation_id="reverseproxy:1", 626 relation_settings={ 627 "public-address": "1.2.3.4", 628 "ssl_cert": ssl_cert}) 629 630 def test_join_reverseproxy_relation_with_selfsigned_cert(self): 631 """ 632 When haproxy joins a reverseproxy relation and a self-signed 633 certificate is configured, then it's included in the relation. 634 """ 635 self.config_get.return_value = {"ssl_cert": "SELFSIGNED"} 636 unit_get = self.patch_hook("unit_get") 637 unit_get.return_value = "1.2.3.4" 638 relation_id = self.patch_hook("relation_id") 639 relation_id.return_value = "reverseproxy:1" 640 get_selfsigned_cert = self.patch_hook("get_selfsigned_cert") 641 get_selfsigned_cert.return_value = ("<self-signed>", None) 642 relation_set = self.patch_hook("relation_set") 643 hooks.reverseproxy_interface(hook_name="joined") 644 unit_get.assert_called_once_with("public-address") 645 ssl_cert = base64.b64encode("<self-signed>") 646 relation_set.assert_called_once_with( 647 relation_id="reverseproxy:1", 648 relation_settings={ 649 "public-address": "1.2.3.4", 650 "ssl_cert": ssl_cert})