Projet

Général

Profil

0001-hobo-handle-domain-change-59762.patch

Emmanuel Cazenave, 21 décembre 2021 18:27

Télécharger (18,6 ko)

Voir les différences:

Subject: [PATCH] hobo: handle domain change (#59762)

 tests/test_hobo.py     |   8 +-
 tests/test_hobo_sql.py | 224 +++++++++++++++++++++++++++++++++++++++++
 wcs/ctl/check_hobos.py | 115 +++++++++++++++------
 3 files changed, 317 insertions(+), 30 deletions(-)
 create mode 100644 tests/test_hobo_sql.py
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
-