0001-hobo-handle-domain-change-59762.patch
tests/test_hobo.py | ||
---|---|---|
478 | 478 |
service['base_url'] = service['base_url'].strip('/') |
479 | 479 | |
480 | 480 |
pub.initialize_sql = mock.Mock() |
481 |
with mock.patch('psycopg2.connect') as connect: |
|
481 |
cursor = mock.Mock(**{'fetchall.return_value': False}) |
|
482 |
pgconn = mock.Mock(**{'cursor.return_value': cursor}) |
|
483 |
with mock.patch('psycopg2.connect', **{'return_value': pgconn}) as connect: |
|
482 | 484 |
hobo_cmd.configure_sql(service, pub) |
483 | 485 |
assert connect.call_args_list[0][1] == {'user': 'test', 'dbname': 'postgres'} |
484 | 486 |
assert connect.call_args_list[1][1] == {'user': 'fred', 'dbname': 'tests_wcs_wcs_example_net'} |
... | ... | |
491 | 493 |
assert connect.call_args_list[0][1] == {'user': 'fred', 'dbname': 'tests_wcs_wcs_example_net'} |
492 | 494 | |
493 | 495 |
pub.cfg['postgresql']['database-template-name'] = 'very_long_' * 10 + '%s' |
494 |
with mock.patch('psycopg2.connect') as connect: |
|
496 |
cursor = mock.Mock(**{'fetchall.return_value': False}) |
|
497 |
pgconn = mock.Mock(**{'cursor.return_value': cursor}) |
|
498 |
with mock.patch('psycopg2.connect', **{'return_value': pgconn}) as connect: |
|
495 | 499 |
hobo_cmd.configure_sql(service, pub) |
496 | 500 |
assert connect.call_args_list[0][1] == {'user': 'test', 'dbname': 'postgres'} |
497 | 501 |
assert connect.call_args_list[1][1] == { |
tests/test_hobo_sql.py | ||
---|---|---|
1 |
import collections |
|
2 |
import copy |
|
3 |
import json |
|
4 |
import os |
|
5 |
import random |
|
6 |
import shutil |
|
7 |
import tempfile |
|
8 |
import zipfile |
|
9 | ||
10 |
import psycopg2 |
|
11 |
import pytest |
|
12 |
from quixote import cleanup |
|
13 | ||
14 |
from wcs.ctl.check_hobos import CmdCheckHobos |
|
15 |
from wcs.publisher import WcsPublisher |
|
16 |
from wcs.sql import cleanup_connection |
|
17 | ||
18 |
from .utilities import clean_temporary_pub, create_temporary_pub |
|
19 | ||
20 |
CONFIG = { |
|
21 |
"postgresql": { |
|
22 |
"createdb-connection-params": {"database": "postgres", "user": os.environ['USER']}, |
|
23 |
"database-template-name": "%s", |
|
24 |
"user": os.environ['USER'], |
|
25 |
} |
|
26 |
} |
|
27 | ||
28 |
WCS_BASE_TENANT = 'wcsteststenant%d' % random.randint(0, 100000) |
|
29 |
WCS_TENANT = '%s.net' % WCS_BASE_TENANT |
|
30 |
WCS_DB_NAME = '%s_net' % WCS_BASE_TENANT |
|
31 | ||
32 |
NEW_WCS_BASE_TENANT = 'wcsteststenant%d' % random.randint(0, 100000) |
|
33 |
NEW_WCS_TENANT = '%s.net' % NEW_WCS_BASE_TENANT |
|
34 |
NEW_WCS_DB_NAME = '%s_net' % NEW_WCS_BASE_TENANT |
|
35 | ||
36 | ||
37 |
HOBO_JSON = { |
|
38 |
'services': [ |
|
39 |
{ |
|
40 |
'title': 'Hobo', |
|
41 |
'slug': 'hobo', |
|
42 |
'service-id': 'hobo', |
|
43 |
'base_url': 'http://hobo.example.net/', |
|
44 |
'saml-sp-metadata-url': 'http://hobo.example.net/accounts/mellon/metadata/', |
|
45 |
}, |
|
46 |
{ |
|
47 |
'service-id': 'authentic', |
|
48 |
'saml-idp-metadata-url': 'http://authentic.example.net/idp/saml2/metadata', |
|
49 |
'template_name': '', |
|
50 |
'variables': {}, |
|
51 |
'title': 'Authentic', |
|
52 |
'base_url': 'http://authentic.example.net/', |
|
53 |
'id': 3, |
|
54 |
'slug': 'authentic', |
|
55 |
'secret_key': '12345', |
|
56 |
}, |
|
57 |
{ |
|
58 |
'service-id': 'wcs', |
|
59 |
'template_name': 'publik.zip', |
|
60 |
'variables': {'xxx': 'HELLO WORLD'}, |
|
61 |
'title': 'Test wcs', |
|
62 |
'saml-sp-metadata-url': 'http://%s/saml/metadata' % WCS_TENANT, |
|
63 |
'base_url': 'http://%s/' % WCS_TENANT, |
|
64 |
'backoffice-menu-url': 'http://%s/backoffice/menu.json' % WCS_TENANT, |
|
65 |
'id': 1, |
|
66 |
'secret_key': 'eiue7aa10nt6e9*#jg2bsfvdgl)cr%4(tafibfjx9i$pgnfj#v', |
|
67 |
'slug': 'test-wcs', |
|
68 |
}, |
|
69 |
{ |
|
70 |
'service-id': 'combo', |
|
71 |
'template_name': 'portal-agent', |
|
72 |
'title': 'Portal Agents', |
|
73 |
'base_url': 'http://agents.example.net/', |
|
74 |
'secret_key': 'aaa', |
|
75 |
}, |
|
76 |
{ |
|
77 |
'service-id': 'combo', |
|
78 |
'template_name': 'portal-user', |
|
79 |
'title': 'Portal', |
|
80 |
'base_url': 'http://portal.example.net/', |
|
81 |
'secret_key': 'bbb', |
|
82 |
}, |
|
83 |
], |
|
84 |
'timestamp': '1431420355.31', |
|
85 |
} |
|
86 | ||
87 | ||
88 |
@pytest.fixture |
|
89 |
def setuptest(): |
|
90 |
cleanup_connection() |
|
91 |
createdb_cfg = CONFIG['postgresql'].get('createdb-connection-params') |
|
92 |
conn = psycopg2.connect(**createdb_cfg) |
|
93 |
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) |
|
94 |
cursor = conn.cursor() |
|
95 |
for dbname in (WCS_DB_NAME, NEW_WCS_DB_NAME): |
|
96 |
cursor.execute('DROP DATABASE IF EXISTS %s' % dbname) |
|
97 | ||
98 |
pub = create_temporary_pub() |
|
99 |
pub.cfg['language'] = {'language': 'en'} |
|
100 |
hobo_cmd = CmdCheckHobos() |
|
101 |
hobo_cmd.all_services = HOBO_JSON |
|
102 |
WcsPublisher.APP_DIR = tempfile.mkdtemp() |
|
103 | ||
104 |
skeleton_dir = os.path.join(WcsPublisher.APP_DIR, 'skeletons') |
|
105 |
os.mkdir(skeleton_dir) |
|
106 |
with open(os.path.join(skeleton_dir, 'publik.zip'), 'wb') as f: |
|
107 |
with zipfile.ZipFile(f, 'w') as z: |
|
108 |
z.writestr('config.json', json.dumps(CONFIG)) |
|
109 |
z.writestr('site-options.cfg', '[options]\npostgresql = true') |
|
110 | ||
111 |
yield pub, hobo_cmd |
|
112 | ||
113 |
clean_temporary_pub() |
|
114 |
shutil.rmtree(WcsPublisher.APP_DIR) |
|
115 |
cleanup_connection() |
|
116 |
for dbname in (WCS_DB_NAME, NEW_WCS_DB_NAME): |
|
117 |
cursor.execute('DROP DATABASE IF EXISTS %s' % dbname) |
|
118 |
conn.close() |
|
119 | ||
120 | ||
121 |
def database_exists(database): |
|
122 |
res = False |
|
123 |
cleanup_connection() |
|
124 |
createdb_cfg = CONFIG['postgresql'].get('createdb-connection-params') |
|
125 |
conn = psycopg2.connect(**createdb_cfg) |
|
126 |
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) |
|
127 |
cursor = conn.cursor() |
|
128 |
cursor.execute("SELECT 1 AS result FROM pg_database WHERE datname='%s'" % database) |
|
129 |
if cursor.fetchall(): |
|
130 |
res = True |
|
131 |
conn.close() |
|
132 |
return res |
|
133 | ||
134 | ||
135 |
def test_deploy(setuptest): |
|
136 |
_, hobo_cmd = setuptest |
|
137 |
assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT)) |
|
138 |
assert not database_exists(WCS_DB_NAME) |
|
139 |
assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT)) |
|
140 |
assert not database_exists(NEW_WCS_DB_NAME) |
|
141 | ||
142 |
cleanup() |
|
143 |
with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd: |
|
144 |
fd.write(json.dumps(HOBO_JSON)) |
|
145 |
hobo_cmd = CmdCheckHobos() |
|
146 |
base_options = {} |
|
147 |
sub_options_class = collections.namedtuple('Options', ['ignore_timestamp', 'redeploy', 'extra']) |
|
148 |
sub_options = sub_options_class(True, False, None) |
|
149 |
hobo_cmd.execute( |
|
150 |
base_options, |
|
151 |
sub_options, |
|
152 |
['http://%s/' % WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')], |
|
153 |
) |
|
154 |
assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT)) |
|
155 |
assert database_exists(WCS_DB_NAME) |
|
156 | ||
157 |
# deploy a new tenant |
|
158 |
with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd: |
|
159 |
hobo_json = copy.deepcopy(HOBO_JSON) |
|
160 |
wcs_service = hobo_json['services'][2] |
|
161 |
wcs_service['saml-sp-metadata-url'] = ('http://%s/saml/metadata' % WCS_TENANT,) |
|
162 |
wcs_service['base_url'] = 'http://%s/' % NEW_WCS_TENANT |
|
163 |
wcs_service['backoffice-menu-url'] = 'http://%s/backoffice/menu.json' % NEW_WCS_TENANT |
|
164 |
fd.write(json.dumps(hobo_json)) |
|
165 | ||
166 |
hobo_cmd.execute( |
|
167 |
base_options, |
|
168 |
sub_options, |
|
169 |
['http://%s/' % NEW_WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')], |
|
170 |
) |
|
171 |
assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT)) |
|
172 |
assert database_exists(WCS_DB_NAME) |
|
173 |
assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT)) |
|
174 |
assert database_exists(NEW_WCS_DB_NAME) |
|
175 | ||
176 | ||
177 |
def test_deploy_url_change(setuptest): |
|
178 |
_, hobo_cmd = setuptest |
|
179 |
assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT)) |
|
180 |
assert not database_exists(WCS_DB_NAME) |
|
181 |
assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT)) |
|
182 |
assert not database_exists(NEW_WCS_DB_NAME) |
|
183 | ||
184 |
cleanup() |
|
185 |
with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd: |
|
186 |
fd.write(json.dumps(HOBO_JSON)) |
|
187 |
hobo_cmd = CmdCheckHobos() |
|
188 |
base_options = {} |
|
189 |
sub_options_class = collections.namedtuple('Options', ['ignore_timestamp', 'redeploy', 'extra']) |
|
190 |
sub_options = sub_options_class(True, False, None) |
|
191 |
hobo_cmd.execute( |
|
192 |
base_options, |
|
193 |
sub_options, |
|
194 |
['http://%s/' % WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')], |
|
195 |
) |
|
196 |
assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT)) |
|
197 |
assert database_exists(WCS_DB_NAME) |
|
198 | ||
199 |
# domain change request |
|
200 |
with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd: |
|
201 |
hobo_json = copy.deepcopy(HOBO_JSON) |
|
202 |
wcs_service = hobo_json['services'][2] |
|
203 |
wcs_service['legacy_urls'] = [ |
|
204 |
{ |
|
205 |
'saml-sp-metadata-url': wcs_service['saml-sp-metadata-url'], |
|
206 |
'base_url': wcs_service['base_url'], |
|
207 |
'backoffice-menu-url': wcs_service['backoffice-menu-url'], |
|
208 |
} |
|
209 |
] |
|
210 | ||
211 |
wcs_service['saml-sp-metadata-url'] = ('http://%s/saml/metadata' % WCS_TENANT,) |
|
212 |
wcs_service['base_url'] = 'http://%s/' % NEW_WCS_TENANT |
|
213 |
wcs_service['backoffice-menu-url'] = 'http://%s/backoffice/menu.json' % NEW_WCS_TENANT |
|
214 |
fd.write(json.dumps(hobo_json)) |
|
215 | ||
216 |
hobo_cmd.execute( |
|
217 |
base_options, |
|
218 |
sub_options, |
|
219 |
['http://%s/' % NEW_WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')], |
|
220 |
) |
|
221 |
assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT)) |
|
222 |
assert not database_exists(WCS_DB_NAME) |
|
223 |
assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT)) |
|
224 |
assert database_exists(NEW_WCS_DB_NAME) |
wcs/ctl/check_hobos.py | ||
---|---|---|
127 | 127 |
service['base_url'] = base_url[:-1] |
128 | 128 | |
129 | 129 |
try: |
130 |
pub.set_tenant_by_hostname(self.get_instance_path(service), skip_sql=True) |
|
130 |
pub.set_tenant_by_hostname(self.get_instance_path(service.get('base_url')), skip_sql=True)
|
|
131 | 131 |
except UnknownTenantError: |
132 | 132 |
if not os.path.exists(global_tenants_dir): |
133 | 133 |
os.mkdir(global_tenants_dir) |
134 |
tenant_app_dir = os.path.join(global_tenants_dir, self.get_instance_path(service)) |
|
134 |
tenant_app_dir = os.path.join(global_tenants_dir, self.get_instance_path(service.get('base_url'))) |
|
135 |
# check in legacy_urls for domain change |
|
136 |
for legacy_urls in service.get('legacy_urls', []): |
|
137 |
legacy_base_url = legacy_urls.get('base_url') |
|
138 |
if legacy_base_url.endswith('/'): # wcs doesn't expect a trailing slash |
|
139 |
legacy_base_url = legacy_base_url[:-1] |
|
140 |
legacy_instance_path = self.get_instance_path(legacy_base_url) |
|
141 |
try: |
|
142 |
pub.set_tenant_by_hostname(legacy_instance_path, skip_sql=True) |
|
143 |
# rename tenant directory |
|
144 |
legacy_tenant_dir = os.path.join(global_tenants_dir, legacy_instance_path) |
|
145 |
print('rename tenant directory %s to %s' % (legacy_tenant_dir, tenant_app_dir)) |
|
146 |
os.rename(legacy_tenant_dir, tenant_app_dir) |
|
147 |
break |
|
148 |
except UnknownTenantError: |
|
149 |
pass |
|
150 | ||
135 | 151 |
if not os.path.exists(tenant_app_dir): |
152 |
# new tenant |
|
136 | 153 |
print('initializing instance in', tenant_app_dir) |
137 |
os.mkdir(tenant_app_dir) |
|
138 |
pub.set_tenant_by_hostname(self.get_instance_path(service)) |
|
154 |
os.mkdir(tenant_app_dir)
|
|
155 |
pub.set_tenant_by_hostname(self.get_instance_path(service.get('base_url')))
|
|
139 | 156 | |
140 | 157 |
if service.get('template_name'): |
141 | 158 |
skeleton_filepath = os.path.join(global_app_dir, 'skeletons', service.get('template_name')) |
142 | 159 |
if os.path.exists(skeleton_filepath): |
143 | 160 |
with open(skeleton_filepath, 'rb') as fd: |
144 | 161 |
pub.import_zip(fd) |
145 |
new_site = True |
|
162 | ||
146 | 163 |
else: |
147 | 164 |
print('updating instance in', pub.app_dir) |
148 |
new_site = False |
|
149 | 165 | |
150 | 166 |
try: |
151 | 167 |
self.configure_site_options(service, pub, ignore_timestamp=sub_options.ignore_timestamp) |
... | ... | |
154 | 170 |
return |
155 | 171 | |
156 | 172 |
pub.set_config(skip_sql=True) |
157 |
if new_site: |
|
158 |
self.configure_sql(service, pub) |
|
173 |
self.configure_sql(service, pub) |
|
159 | 174 |
self.update_configuration(service, pub) |
160 | 175 |
self.configure_authentication_methods(service, pub) |
161 | 176 | |
... | ... | |
386 | 401 |
pub.cfg['saml_identities']['registration-url'] = str('%saccounts/register/' % idp['base_url']) |
387 | 402 |
pub.write_cfg() |
388 | 403 | |
389 |
def get_instance_path(self, service):
|
|
390 |
parsed_url = urllib.parse.urlsplit(service.get('base_url'))
|
|
404 |
def get_instance_path(self, base_url):
|
|
405 |
parsed_url = urllib.parse.urlsplit(base_url)
|
|
391 | 406 |
instance_path = parsed_url.netloc |
392 | 407 |
if parsed_url.path: |
393 | 408 |
instance_path += '+%s' % parsed_url.path.replace('/', '+') |
... | ... | |
532 | 547 |
import psycopg2 |
533 | 548 |
import psycopg2.errorcodes |
534 | 549 | |
550 |
def get_domain_table_name(base_url): |
|
551 |
return base_url.replace('-', '_').replace('.', '_').replace('+', '_') |
|
552 | ||
535 | 553 |
# determine database name using the instance path |
536 |
domain_table_name = ( |
|
537 |
self.get_instance_path(service).replace('-', '_').replace('.', '_').replace('+', '_') |
|
538 |
) |
|
554 |
domain_table_name = get_domain_table_name(self.get_instance_path(service.get('base_url'))) |
|
539 | 555 | |
556 |
legacy_domain_table_names = [] |
|
557 |
for legacy_urls in service.get('legacy_urls', []): |
|
558 |
legacy_base_url = legacy_urls.get('base_url') |
|
559 |
if legacy_base_url.endswith('/'): # wcs doesn't expect a trailing slash |
|
560 |
legacy_base_url = legacy_base_url[:-1] |
|
561 |
legacy_domain_table_names.append(get_domain_table_name(self.get_instance_path(legacy_base_url))) |
|
562 | ||
563 |
legacy_database_names = [] |
|
540 | 564 |
if pub.cfg['postgresql'].get('database-template-name'): |
541 | 565 |
database_template_name = pub.cfg['postgresql'].pop('database-template-name') |
542 | 566 |
database_name = (database_template_name % domain_table_name).strip('_') |
567 |
for legacy_domain_table_name in legacy_domain_table_names: |
|
568 |
legacy_database_names.append( |
|
569 |
self.normalize_database_name( |
|
570 |
(database_template_name % legacy_domain_table_name).strip('_') |
|
571 |
) |
|
572 |
) |
|
543 | 573 |
else: |
544 | 574 |
# legacy way to create a database name, if it contained an |
545 | 575 |
# underscore character, use the first part as a prefix |
... | ... | |
569 | 599 |
pgconn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) |
570 | 600 |
cur = pgconn.cursor() |
571 | 601 |
new_database = True |
572 |
try: |
|
573 |
cur.execute('''CREATE DATABASE %s''' % database_name) |
|
574 |
except psycopg2.Error as e: |
|
575 |
if e.pgcode == psycopg2.errorcodes.DUPLICATE_DATABASE: |
|
576 |
cur.execute( |
|
577 |
"""SELECT table_name |
|
602 | ||
603 |
# check if database already exists |
|
604 |
cur.execute("SELECT 1 AS result FROM pg_database WHERE datname=%s", (database_name,)) |
|
605 |
if cur.fetchall(): |
|
606 |
# database exists, check if it is ready to go |
|
607 |
cur.execute( |
|
608 |
"""SELECT table_name |
|
578 | 609 |
FROM information_schema.tables |
579 | 610 |
WHERE table_schema = 'public' |
580 | 611 |
AND table_type = 'BASE TABLE' |
581 | 612 |
AND table_name = 'wcs_meta'""" |
582 |
) |
|
583 | ||
613 |
) |
|
614 |
if cur.fetchall(): |
|
615 |
new_database = False |
|
616 |
else: |
|
617 |
# database does not exists, check if a legacy database exists |
|
618 |
for legacy_database_name in legacy_database_names: |
|
619 |
cur.execute("SELECT 1 AS result FROM pg_database WHERE datname=%s", (legacy_database_name,)) |
|
584 | 620 |
if cur.fetchall(): |
585 |
new_database = False |
|
621 |
# legacy database exists, rename it |
|
622 |
try: |
|
623 |
# drop existing connections to DB |
|
624 |
cur.execute( |
|
625 |
'''SELECT pg_terminate_backend(pg_stat_activity.pid) |
|
626 |
FROM pg_stat_activity |
|
627 |
WHERE pg_stat_activity.datname = %s |
|
628 |
AND pid <> pg_backend_pid();''', |
|
629 |
(legacy_database_name,), |
|
630 |
) |
|
631 |
cur.execute("ALTER DATABASE %s RENAME TO %s" % (legacy_database_name, database_name)) |
|
632 |
new_database = False |
|
633 |
break |
|
634 |
except psycopg2.Error as e: |
|
635 |
print( |
|
636 |
'failed to rename database %s to %s (%s)' |
|
637 |
% (legacy_database_name, database_name, psycopg2.errorcodes.lookup(e.pgcode)), |
|
638 |
file=sys.stderr, |
|
639 |
) |
|
640 |
return |
|
641 | ||
586 | 642 |
else: |
587 |
print( |
|
588 |
'failed to create database (%s)' % psycopg2.errorcodes.lookup(e.pgcode), file=sys.stderr |
|
589 |
) |
|
590 |
return |
|
591 |
else: |
|
592 |
cur.close() |
|
643 |
try: |
|
644 |
cur.execute('''CREATE DATABASE %s''' % database_name) |
|
645 |
except psycopg2.Error as e: |
|
646 |
print( |
|
647 |
'failed to create database (%s)' % psycopg2.errorcodes.lookup(e.pgcode), |
|
648 |
file=sys.stderr, |
|
649 |
) |
|
650 |
return |
|
593 | 651 | |
652 |
cur.close() |
|
594 | 653 |
pub.cfg['postgresql']['database'] = database_name |
595 | 654 |
pub.write_cfg() |
596 | 655 |
pub.set_config(skip_sql=False) |
597 |
- |