diff --git a/hooks/keystone-fid-service-provider-relation-broken b/hooks/keystone-fid-service-provider-relation-broken new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-broken @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone-fid-service-provider-relation-changed b/hooks/keystone-fid-service-provider-relation-changed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-changed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone-fid-service-provider-relation-departed b/hooks/keystone-fid-service-provider-relation-departed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-departed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone-fid-service-provider-relation-joined b/hooks/keystone-fid-service-provider-relation-joined new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-joined @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone_context.py b/hooks/keystone_context.py index edf138f6..6ff8aa00 100644 --- a/hooks/keystone_context.py +++ b/hooks/keystone_context.py @@ -14,6 +14,7 @@ import hashlib import os +import json from base64 import b64decode @@ -39,6 +40,9 @@ from charmhelpers.core.hookenv import ( leader_get, DEBUG, INFO, + related_units, + relation_ids, + relation_get, ) from charmhelpers.core.strutils import ( @@ -405,3 +409,46 @@ class TokenFlushContext(context.OSContextGenerator): 'token_flush': is_elected_leader(DC_RESOURCE_NAME) } return ctxt + + +class KeystoneFIDServiceProviderContext(context.OSContextGenerator): + interfaces = ['keystone-fid-service-provider'] + + def __call__(self): + fid_sp_keys = ['protocol-name', 'remote-id-attribute'] + fid_sps = [] + for rid in relation_ids("keystone-fid-service-provider"): + for unit in related_units(rid): + rdata = relation_get(unit=unit, rid=rid) + if set(rdata).issuperset(set(fid_sp_keys)): + fid_sps.append({ + k: json.loads(v) for k, v in rdata.items() + if k in fid_sp_keys + }) + # populate the context with data from one or more + # service providers + ctxt = ({'fid_sps': fid_sps} + if fid_sps else {}) + return ctxt + + +class WebSSOTrustedDashboardContext(context.OSContextGenerator): + interfaces = ['websso-trusted-dashboard'] + + def __call__(self): + trusted_dashboard_keys = ['scheme', 'hostname', 'path'] + trusted_dashboards = set() + for rid in relation_ids("websso-trusted-dashboard"): + for unit in related_units(rid): + rdata = relation_get(unit=unit, rid=rid) + if set(rdata).issuperset(set(trusted_dashboard_keys)): + scheme = rdata.get('scheme') + hostname = rdata.get('hostname') + path = rdata.get('path') + url = '{}{}{}'.format(scheme, hostname, path) + trusted_dashboards.add(url) + # populate the context with data from one or more + # service providers + ctxt = ({'trusted_dashboards': trusted_dashboards} + if trusted_dashboards else {}) + return ctxt diff --git a/hooks/keystone_hooks.py b/hooks/keystone_hooks.py index ca56c1ec..d7407f60 100755 --- a/hooks/keystone_hooks.py +++ b/hooks/keystone_hooks.py @@ -40,6 +40,7 @@ from charmhelpers.core.hookenv import ( status_set, open_port, is_leader, + relation_id, ) from charmhelpers.core.host import ( @@ -121,7 +122,7 @@ from keystone_utils import ( ADMIN_DOMAIN, ADMIN_PROJECT, create_or_show_domain, - keystone_service, + restart_keystone, ) from charmhelpers.contrib.hahelpers.cluster import ( @@ -272,6 +273,7 @@ def config_changed_postupgrade(): update_all_identity_relation_units() update_all_domain_backends() + update_all_fid_backends() # Ensure sync request is sent out (needed for any/all ssl change) send_ssl_sync_request() @@ -381,6 +383,17 @@ def update_all_domain_backends(): domain_backend_changed(relation_id=rid, unit=unit) +def update_all_fid_backends(): + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring keystone-fid-service-provider relation as it is' + ' not supported on releases older than Ocata') + return + """If there are any config changes, e.g. for domain or service port + make sure to update those for all relation-level buckets""" + for rid in relation_ids('keystone-fid-service-provider'): + update_keystone_fid_service_provider(relation_id=rid) + + def leader_init_db_if_ready(use_current_context=False): """ Initialise the keystone db if it is ready and mark it as initialised. @@ -784,11 +797,7 @@ def domain_backend_changed(relation_id=None, unit=None): domain_nonce_key = 'domain-restart-nonce-{}'.format(domain_name) db = unitdata.kv() if restart_nonce != db.get(domain_nonce_key): - if not is_unit_paused_set(): - if snap_install_requested(): - service_restart('snap.keystone.*') - else: - service_restart(keystone_service()) + restart_keystone() db.set(domain_nonce_key, restart_nonce) db.flush() @@ -869,6 +878,80 @@ def update_nrpe_config(): nrpe_setup.write() +@hooks.hook('keystone-fid-service-provider-relation-joined', + 'keystone-fid-service-provider-relation-changed') +def keystone_fid_service_provider_changed(): + if get_api_version() < 3: + log('Identity federation is only supported with keystone v3') + return + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring keystone-fid-service-provider relation as it is' + ' not supported on releases older than Ocata') + return + # for the join case a keystone public-facing hostname and service + # port need to be set + update_keystone_fid_service_provider(relation_id=relation_id()) + + # handle relation data updates (if any), e.g. remote_id_attribute + # and a restart will be handled via a nonce, not restart_on_change + CONFIGS.write(KEYSTONE_CONF) + + # The relation is container-scoped so this keystone unit's unitdata + # will only contain a nonce of a single fid subordinate for a given + # fid backend (relation id) + restart_nonce = relation_get('restart-nonce') + if restart_nonce: + nonce = json.loads(restart_nonce) + # multiplex by relation id for multiple federated identity + # provider charms + fid_nonce_key = 'fid-restart-nonce-{}'.format(relation_id()) + db = unitdata.kv() + if restart_nonce != db.get(fid_nonce_key): + restart_keystone() + db.set(fid_nonce_key, nonce) + db.flush() + + +@hooks.hook('keystone-fid-service-provider-relation-broken') +def keystone_fid_service_provider_broken(): + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring keystone-fid-service-provider relation as it is' + ' not supported on releases older than Ocata') + return + + restart_keystone() + + +@hooks.hook('websso-trusted-dashboard-relation-joined', + 'websso-trusted-dashboard-relation-changed', + 'websso-trusted-dashboard-relation-broken') +@restart_on_change(restart_map(), restart_functions=restart_function_map()) +def websso_trusted_dashboard_changed(): + if get_api_version() < 3: + log('WebSSO is only supported with keystone v3') + return + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring WebSSO relation as it is not supported on' + ' releases older than Ocata') + return + CONFIGS.write(KEYSTONE_CONF) + + +def update_keystone_fid_service_provider(relation_id=None): + tls_enabled = (config('ssl_cert') is not None and + config('ssl_key') is not None) + # reactive endpoints implementation on the other side, hence + # json-encoded values + fid_settings = { + 'hostname': json.dumps(config('os-public-hostname')), + 'port': json.dumps(config('service-port')), + 'tls-enabled': json.dumps(tls_enabled), + } + + relation_set(relation_id=relation_id, + relation_settings=fid_settings) + + def main(): try: hooks.execute(sys.argv) diff --git a/hooks/keystone_utils.py b/hooks/keystone_utils.py index ec7e077e..0002e19a 100644 --- a/hooks/keystone_utils.py +++ b/hooks/keystone_utils.py @@ -72,6 +72,7 @@ from charmhelpers.contrib.openstack.utils import ( install_os_snaps, get_snaps_install_info_from_origin, enable_memcache, + is_unit_paused_set, ) from charmhelpers.core.strutils import ( @@ -245,7 +246,9 @@ BASE_RESOURCE_MAP = OrderedDict([ keystone_context.HAProxyContext(), context.BindHostContext(), context.WorkerConfigContext(), - context.MemcacheContext(package='keystone')], + context.MemcacheContext(package='keystone'), + keystone_context.KeystoneFIDServiceProviderContext(), + keystone_context.WebSSOTrustedDashboardContext()], }), (KEYSTONE_LOGGER_CONF, { 'contexts': [keystone_context.KeystoneLoggingContext()], @@ -2574,3 +2577,11 @@ def post_snap_install(): if os.path.exists(PASTE_SRC): log("Perfoming post snap install tasks", INFO) shutil.copy(PASTE_SRC, PASTE_DST) + + +def restart_keystone(): + if not is_unit_paused_set(): + if snap_install_requested(): + service_restart('snap.keystone.*') + else: + service_restart(keystone_service()) diff --git a/hooks/websso-trusted-dashboard-relation-broken b/hooks/websso-trusted-dashboard-relation-broken new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-broken @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/websso-trusted-dashboard-relation-changed b/hooks/websso-trusted-dashboard-relation-changed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-changed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/websso-trusted-dashboard-relation-departed b/hooks/websso-trusted-dashboard-relation-departed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-departed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/websso-trusted-dashboard-relation-joined b/hooks/websso-trusted-dashboard-relation-joined new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-joined @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index 3dadf6af..2e80061d 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -39,6 +39,11 @@ requires: domain-backend: interface: keystone-domain-backend scope: container + keystone-fid-service-provider: + interface: keystone-fid-service-provider + scope: container + websso-trusted-dashboard: + interface: websso-trusted-dashboard peers: cluster: interface: keystone-ha diff --git a/templates/ocata/keystone.conf b/templates/ocata/keystone.conf index 4ce530da..6fc6bd98 100644 --- a/templates/ocata/keystone.conf +++ b/templates/ocata/keystone.conf @@ -67,7 +67,7 @@ driver = {{ assignment_backend }} [oauth1] [auth] -methods = external,password,token,oauth1 +methods = external,password,token,oauth1,mapped,openid password = keystone.auth.plugins.password.Password token = keystone.auth.plugins.token.Token oauth1 = keystone.auth.plugins.oauth1.OAuth @@ -115,3 +115,5 @@ group_allow_delete = False admin_project_domain_name = {{ admin_domain_name }} admin_project_name = admin {% endif -%} + +{% include "parts/section-federation" %} diff --git a/templates/openstack_https_frontend.conf b/templates/openstack_https_frontend.conf new file mode 100644 index 00000000..e0e42296 --- /dev/null +++ b/templates/openstack_https_frontend.conf @@ -0,0 +1,30 @@ +{% if endpoints -%} +{% for ext_port in ext_ports -%} +Listen {{ ext_port }} +{% endfor -%} +{% for address, endpoint, ext, int in endpoints -%} + + ServerName {{ endpoint }} + SSLEngine on + SSLProtocol +TLSv1 +TLSv1.1 +TLSv1.2 + SSLCipherSuite HIGH:!RC4:!MD5:!aNULL:!eNULL:!EXP:!LOW:!MEDIUM + SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }} + # See LP 1484489 - this is to support <= 2.4.7 and >= 2.4.8 + SSLCertificateChainFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }} + SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }} + ProxyPass / http://localhost:{{ int }}/ + ProxyPassReverse / http://localhost:{{ int }}/ + ProxyPreserveHost on + RequestHeader set X-Forwarded-Proto "https" + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endfor -%} + + Order deny,allow + Allow from all + + + Order allow,deny + Allow from all + +{% endif -%} diff --git a/templates/parts/section-federation b/templates/parts/section-federation new file mode 100644 index 00000000..65ee99ed --- /dev/null +++ b/templates/parts/section-federation @@ -0,0 +1,10 @@ +{% if trusted_dashboards %} +[federation] +{% for dashboard_url in trusted_dashboards -%} +trusted_dashboard = {{ dashboard_url }} +{% endfor -%} +{% endif %} +{% for sp in fid_sps -%} +[{{ sp['protocol-name'] }}] +remote_id_attribute = {{ sp['remote-id-attribute'] }} +{% endfor -%} diff --git a/templates/wsgi-openstack-api.conf b/templates/wsgi-openstack-api.conf new file mode 100644 index 00000000..942e2b29 --- /dev/null +++ b/templates/wsgi-openstack-api.conf @@ -0,0 +1,94 @@ +# Configuration file maintained by Juju. Local changes may be overwritten. + +{% if port -%} +Listen {{ port }} +{% endif -%} + +{% if admin_port -%} +Listen {{ admin_port }} +{% endif -%} + +{% if public_port -%} +Listen {{ public_port }} +{% endif -%} + +{% if port -%} + + WSGIDaemonProcess {{ service_name }} processes={{ processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ service_name }} + WSGIScriptAlias / {{ script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog /var/log/apache2/{{ service_name }}_error.log + CustomLog /var/log/apache2/{{ service_name }}_access.log combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endif -%} + +{% if admin_port -%} + + WSGIDaemonProcess {{ service_name }}-admin processes={{ admin_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ service_name }}-admin + WSGIScriptAlias / {{ admin_script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog /var/log/apache2/{{ service_name }}_error.log + CustomLog /var/log/apache2/{{ service_name }}_access.log combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endif -%} + +{% if public_port -%} + + WSGIDaemonProcess {{ service_name }}-public processes={{ public_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ service_name }}-public + WSGIScriptAlias / {{ public_script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog /var/log/apache2/{{ service_name }}_error.log + CustomLog /var/log/apache2/{{ service_name }}_access.log combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endif -%} diff --git a/unit_tests/test_keystone_contexts.py b/unit_tests/test_keystone_contexts.py index d8d256e0..42a7b7e5 100644 --- a/unit_tests/test_keystone_contexts.py +++ b/unit_tests/test_keystone_contexts.py @@ -217,3 +217,204 @@ class TestKeystoneContexts(CharmTestCase): mock_is_elected_leader.return_value = True self.assertEqual({'token_flush': True}, ctxt()) + + @patch.object(context, 'relation_ids') + @patch.object(context, 'related_units') + @patch.object(context, 'relation_get') + def test_keystone_fid_service_provider_rdata( + self, mock_relation_get, mock_related_units, + mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'keystone-fid-service-provider': { + 'keystone-fid-service-provider:0', + 'keystone-fid-service-provider:1', + 'keystone-fid-service-provider:2' + } + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + + def related_units_side_effect(rid): + return { + 'keystone-fid-service-provider:0': ['sp-mellon/0'], + 'keystone-fid-service-provider:1': ['sp-shib/0'], + 'keystone-fid-service-provider:2': ['sp-oidc/0'], + }[rid] + mock_related_units.side_effect = related_units_side_effect + + def relation_get_side_effect(unit, rid): + # one unit only as the relation is container-scoped + return { + "keystone-fid-service-provider:0": { + "sp-mellon/0": { + "ingress-address": '10.0.0.10', + "protocol-name": '"saml2"', + "remote-id-attribute": '"MELLON_IDP"', + }, + }, + "keystone-fid-service-provider:1": { + "sp-shib/0": { + "ingress-address": '10.0.0.10', + "protocol-name": '"mapped"', + "remote-id-attribute": '"Shib-Identity-Provider"', + }, + }, + "keystone-fid-service-provider:2": { + "sp-oidc/0": { + "ingress-address": '10.0.0.10', + "protocol-name": '"oidc"', + "remote-id-attribute": '"HTTP_OIDC_ISS"', + }, + }, + }[rid][unit] + + mock_relation_get.side_effect = relation_get_side_effect + ctxt = context.KeystoneFIDServiceProviderContext() + + self.maxDiff = None + self.assertItemsEqual( + ctxt(), + { + "fid_sps": [ + { + "protocol-name": "saml2", + "remote-id-attribute": "MELLON_IDP", + }, + { + "protocol-name": "mapped", + "remote-id-attribute": "Shib-Identity-Provider", + }, + { + "protocol-name": "oidc", + "remote-id-attribute": "HTTP_OIDC_ISS", + }, + ] + } + ) + + @patch.object(context, 'relation_ids') + def test_keystone_fid_service_provider_empty( + self, mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'keystone-fid-service-provider': {} + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + ctxt = context.KeystoneFIDServiceProviderContext() + + self.maxDiff = None + self.assertItemsEqual(ctxt(), {}) + + @patch.object(context, 'relation_ids') + @patch.object(context, 'related_units') + @patch.object(context, 'relation_get') + def test_websso_trusted_dashboard_urls_generated( + self, mock_relation_get, mock_related_units, + mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'websso-trusted-dashboard': { + 'websso-trusted-dashboard:0', + 'websso-trusted-dashboard:1', + 'websso-trusted-dashboard:2' + } + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + + def related_units_side_effect(rid): + return { + 'websso-trusted-dashboard:0': ['dashboard-blue/0', + 'dashboard-blue/1'], + 'websso-trusted-dashboard:1': ['dashboard-red/0', + 'dashboard-red/1'], + 'websso-trusted-dashboard:2': ['dashboard-green/0', + 'dashboard-green/1'] + }[rid] + mock_related_units.side_effect = related_units_side_effect + + def relation_get_side_effect(unit, rid): + return { + "websso-trusted-dashboard:0": { + "dashboard-blue/0": { # dns-ha + "ingress-address": '10.0.0.10', + "scheme": "https://", + "hostname": "horizon.intranet.test", + "path": "/auth/websso/", + }, + "dashboard-blue/1": { # dns-ha + "ingress-address": '10.0.0.11', + "scheme": "https://", + "hostname": "horizon.intranet.test", + "path": "/auth/websso/", + }, + }, + "websso-trusted-dashboard:1": { + "dashboard-red/0": { # vip + "ingress-address": '10.0.0.12', + "scheme": "https://", + "hostname": "10.0.0.100", + "path": "/auth/websso/", + }, + "dashboard-red/1": { # vip + "ingress-address": '10.0.0.13', + "scheme": "https://", + "hostname": "10.0.0.100", + "path": "/auth/websso/", + }, + }, + "websso-trusted-dashboard:2": { + "dashboard-green/0": { # vip-less, dns-ha-less + "ingress-address": '10.0.0.14', + "scheme": "http://", + "hostname": "10.0.0.14", + "path": "/auth/websso/", + }, + "dashboard-green/1": { + "ingress-address": '10.0.0.15', + "scheme": "http://", + "hostname": "10.0.0.15", + "path": "/auth/websso/", + }, + }, + }[rid][unit] + + mock_relation_get.side_effect = relation_get_side_effect + ctxt = context.WebSSOTrustedDashboardContext() + + self.maxDiff = None + self.assertEqual( + ctxt(), + { + 'trusted_dashboards': set([ + 'https://horizon.intranet.test/auth/websso/', + 'https://10.0.0.100/auth/websso/', + 'http://10.0.0.14/auth/websso/', + 'http://10.0.0.15/auth/websso/', + ]) + } + ) + + @patch.object(context, 'relation_ids') + def test_websso_trusted_dashboard_empty( + self, mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'websso-trusted-dashboard': {} + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + ctxt = context.WebSSOTrustedDashboardContext() + + self.maxDiff = None + self.assertItemsEqual(ctxt(), {}) diff --git a/unit_tests/test_keystone_hooks.py b/unit_tests/test_keystone_hooks.py index b3c8c8d8..a214b757 100644 --- a/unit_tests/test_keystone_hooks.py +++ b/unit_tests/test_keystone_hooks.py @@ -93,7 +93,6 @@ TO_PATCH = [ 'update_nrpe_config', 'ensure_ssl_dirs', 'is_db_ready', - 'keystone_service', 'create_or_show_domain', 'get_api_version', # other @@ -441,6 +440,7 @@ class KeystoneRelationTests(CharmTestCase): self.assertTrue(update.called) self.assertTrue(mock_update_domains.called) + @patch.object(hooks, 'os_release') @patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'initialise_pki') @patch.object(hooks, 'is_db_initialised') @@ -460,7 +460,9 @@ class KeystoneRelationTests(CharmTestCase): ensure_ssl_dir, mock_db_init, mock_initialise_pki, - mock_run_in_apache): + mock_run_in_apache, + os_release): + os_release.return_value = 'ocata' self.enable_memcache.return_value = False mock_run_in_apache.return_value = False ensure_ssl_cert.return_value = False @@ -1087,9 +1089,14 @@ class KeystoneRelationTests(CharmTestCase): @patch.object(hooks, 'is_unit_paused_set') @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') def test_domain_backend_changed_complete(self, + service_restart, + run_in_apache, is_db_initialised, is_unit_paused_set): + run_in_apache.return_value = True self.get_api_version.return_value = 3 self.relation_get.side_effect = ['mydomain', 'nonce2'] self.is_leader.return_value = True @@ -1099,7 +1106,6 @@ class KeystoneRelationTests(CharmTestCase): mock_kv.get.return_value = None self.unitdata.kv.return_value = mock_kv is_unit_paused_set.return_value = False - self.keystone_service.return_value = 'apache2' hooks.domain_backend_changed() @@ -1113,16 +1119,21 @@ class KeystoneRelationTests(CharmTestCase): rid=None), ]) self.create_or_show_domain.assert_called_with('mydomain') - self.service_restart.assert_called_with('apache2') + service_restart.assert_called_with('apache2') mock_kv.set.assert_called_with('domain-restart-nonce-mydomain', 'nonce2') self.assertTrue(mock_kv.flush.called) @patch.object(hooks, 'is_unit_paused_set') @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') def test_domain_backend_changed_complete_follower(self, + service_restart, + run_in_apache, is_db_initialised, is_unit_paused_set): + run_in_apache.return_value = True self.get_api_version.return_value = 3 self.relation_get.side_effect = ['mydomain', 'nonce2'] self.is_leader.return_value = False @@ -1132,7 +1143,6 @@ class KeystoneRelationTests(CharmTestCase): mock_kv.get.return_value = None self.unitdata.kv.return_value = mock_kv is_unit_paused_set.return_value = False - self.keystone_service.return_value = 'apache2' hooks.domain_backend_changed() @@ -1147,7 +1157,84 @@ class KeystoneRelationTests(CharmTestCase): ]) # Only lead unit will create the domain self.assertFalse(self.create_or_show_domain.called) - self.service_restart.assert_called_with('apache2') + service_restart.assert_called_with('apache2') mock_kv.set.assert_called_with('domain-restart-nonce-mydomain', 'nonce2') self.assertTrue(mock_kv.flush.called) + + @patch.object(hooks, 'os_release') + @patch.object(hooks, 'relation_id') + @patch.object(hooks, 'is_unit_paused_set') + @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') + def test_fid_service_provider_changed_complete( + self, + service_restart, + run_in_apache, + is_db_initialised, + is_unit_paused_set, + relation_id, os_release): + os_release.return_value = 'ocata' + rel = 'keystone-fid-service-provider:0' + relation_id.return_value = rel + run_in_apache.return_value = True + self.get_api_version.return_value = 3 + self.relation_get.side_effect = ['"nonce2"'] + self.is_leader.return_value = True + self.is_db_ready.return_value = True + is_db_initialised.return_value = True + mock_kv = MagicMock() + mock_kv.get.return_value = None + self.unitdata.kv.return_value = mock_kv + is_unit_paused_set.return_value = False + + hooks.keystone_fid_service_provider_changed() + + self.assertTrue(self.get_api_version.called) + self.relation_get.assert_has_calls([ + call('restart-nonce'), + ]) + service_restart.assert_called_with('apache2') + mock_kv.set.assert_called_with( + 'fid-restart-nonce-{}'.format(rel), 'nonce2') + self.assertTrue(mock_kv.flush.called) + + @patch.object(hooks, 'os_release') + @patch.object(hooks, 'relation_id') + @patch.object(hooks, 'is_unit_paused_set') + @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') + def test_fid_service_provider_changed_complete_follower( + self, + service_restart, + run_in_apache, + is_db_initialised, + is_unit_paused_set, + relation_id, os_release): + os_release.return_value = 'ocata' + rel = 'keystone-fid-service-provider:0' + relation_id.return_value = rel + run_in_apache.return_value = True + self.get_api_version.return_value = 3 + self.relation_get.side_effect = ['"nonce2"'] + self.is_leader.return_value = False + self.is_db_ready.return_value = True + is_db_initialised.return_value = True + mock_kv = MagicMock() + mock_kv.get.return_value = None + self.unitdata.kv.return_value = mock_kv + is_unit_paused_set.return_value = False + + hooks.keystone_fid_service_provider_changed() + + self.assertTrue(self.get_api_version.called) + self.relation_get.assert_has_calls([ + call('restart-nonce'), + ]) + service_restart.assert_called_with('apache2') + mock_kv.set.assert_called_with( + 'fid-restart-nonce-{}'.format(rel), + 'nonce2') + self.assertTrue(mock_kv.flush.called)