Projet

Général

Profil

0001-command-add-a-delete_tenant-command-15636.patch

Jean-Baptiste Jaillet, 21 mai 2017 22:56

Télécharger (15,8 ko)

Voir les différences:

Subject: [PATCH] command: add a delete_tenant command (#15636)

 tests/test_ctl.py        | 105 +++++++++++++++++++++++++++++++++++++++++++++
 wcs/ctl/check_hobos.py   |   9 +++-
 wcs/ctl/delete_tenant.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++
 wcs/sql.py               |  22 ++++++++--
 4 files changed, 240 insertions(+), 5 deletions(-)
 create mode 100644 wcs/ctl/delete_tenant.py
tests/test_ctl.py
3 3
import collections
4 4
from email.mime.text import MIMEText
5 5
from email.mime.multipart import MIMEMultipart
6
import psycopg2
6 7

  
7 8
from wcs.formdef import FormDef
8 9
from wcs.workflows import Workflow
......
13 14
from wcs.ctl.process_bounce import CmdProcessBounce
14 15
from wcs.ctl.wipe_data import CmdWipeData
15 16
from wcs.ctl.trigger_jumps import select_and_jump_formdata
17
from wcs.ctl.delete_tenant import CmdDeleteTenant
18
from wcs.sql import get_connection_and_cursor, cleanup_connection
16 19

  
17 20
from utilities import create_temporary_pub, clean_temporary_pub
18 21

  
......
186 189
    assert f1.status == f2.status == 'wf-%s' % st1.id
187 190
    assert not f1.workflow_data
188 191
    assert not f2.workflow_data
192

  
193

  
194
def test_delete_tenant():
195
    pub = create_temporary_pub(sql_mode=True)
196
    delete_cmd = CmdDeleteTenant()
197

  
198
    assert os.path.isdir(pub.app_dir)
199

  
200
    sub_options_class = collections.namedtuple('Options', ['force_drop'])
201
    sub_options = sub_options_class(False)
202

  
203
    delete_cmd.delete_tenant(pub, sub_options, [])
204

  
205
    assert not os.path.isdir(pub.app_dir)
206
    parent_dir = os.path.dirname(pub.app_dir)
207
    if not [filename for filename in os.listdir(parent_dir) if 'removed' in filename]:
208
        assert False
209

  
210
    conn, cur = get_connection_and_cursor()
211
    cur.execute("""SELECT schema_name
212
                FROM information_schema.schemata
213
                WHERE schema_name like '%removed%'""")
214

  
215
    assert len(cur.fetchall()) == 1
216

  
217
    clean_temporary_pub()
218
    pub = create_temporary_pub(sql_mode=True)
219

  
220
    sub_options = sub_options_class(True)
221
    delete_cmd.delete_tenant(pub, sub_options, [])
222

  
223
    conn, cur = get_connection_and_cursor(new=True)
224

  
225
    assert not os.path.isdir(pub.app_dir)
226
    cur.execute("""SELECT table_name
227
                FROM information_schema.tables
228
                WHERE table_schema = 'public' AND
229
                table_type = 'BASE TABLE'""")
230

  
231
    assert not cur.fetchall()
232

  
233
    cur.execute("""SELECT datname
234
                FROM pg_database
235
                WHERE datname = '%s'""" % pub.cfg['postgresql']['database'])
236

  
237
    assert cur.fetchall()
238

  
239
    clean_temporary_pub()
240
    pub = create_temporary_pub(sql_mode=True)
241

  
242
    cleanup_connection()
243
    sub_options = sub_options_class(True)
244
    pub.cfg['postgresql']['createdb-connection-params'] = {
245
        'user': pub.cfg['postgresql']['user'],
246
        'database': 'postgres'
247
    }
248
    delete_cmd.delete_tenant(pub, sub_options, [])
249

  
250
    pgconn = psycopg2.connect(**pub.cfg['postgresql']['createdb-connection-params'])
251
    cur = pgconn.cursor()
252

  
253
    cur.execute("""SELECT datname
254
                FROM pg_database
255
                WHERE datname = '%s'""" % pub.cfg['postgresql']['database'])
256
    assert not cur.fetchall()
257
    cur.close()
258
    pgconn.close()
259

  
260
    clean_temporary_pub()
261
    pub = create_temporary_pub(sql_mode=True)
262
    cleanup_connection()
263

  
264
    sub_options = sub_options_class(False)
265
    pub.cfg['postgresql']['createdb-connection-params'] = {
266
        'user': pub.cfg['postgresql']['user'],
267
        'database': 'postgres'
268
    }
269
    delete_cmd.delete_tenant(pub, sub_options, [])
270

  
271
    pgconn = psycopg2.connect(**pub.cfg['postgresql']['createdb-connection-params'])
272
    pgconn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
273
    cur = pgconn.cursor()
274

  
275
    cur.execute("""SELECT datname
276
                FROM pg_database
277
                WHERE datname like '%removed%'""")
278

  
279
    result = cur.fetchall()
280
    assert len(result) == 1
281

  
282
    #clean this db after test
283
    cur.execute("""DROP DATABASE %s""" % result[0][0])
284

  
285
    cur.execute("""SELECT datname
286
                FROM pg_database
287
                WHERE datname = '%s'""" % pub.cfg['postgresql']['database'])
288

  
289
    assert not cur.fetchall()
290
    cur.close()
291
    conn.close()
292

  
293
    clean_temporary_pub()
wcs/ctl/check_hobos.py
422 422
            cur.execute('''CREATE DATABASE %s''' % database_name)
423 423
        except psycopg2.Error as e:
424 424
            if e.pgcode == psycopg2.errorcodes.DUPLICATE_DATABASE:
425
                new_database = False
425
                cur.execute("""SELECT table_name
426
                            FROM information_schema.tables
427
                            WHERE table_schema = 'public' AND
428
                            table_type = 'BASE TABLE' AND
429
                            table_name = 'wcs_meta'""")
430

  
431
                if cur.fetchall():
432
                    new_database = False
426 433
            else:
427 434
                print >> sys.stderr, 'failed to create database (%s)' % \
428 435
                        psycopg2.errorcodes.lookup(e.pgcode)
wcs/ctl/delete_tenant.py
1
#w.c.s. -  web application for online forms 
2
# Copyright (C) 2005-2014  Entr'ouvert 
3
# 
4
# This program is free software; you can redistribute it and/or modify 
5
# it under the terms of the GNU General Public License as published by 
6
# the Free Software Foundation; either version 2 of the License, or 
7
# (at your option) any later version. 
8
# 
9
# This program is distributed in the hope that it will be useful, 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
12
# GNU General Public License for more details. 
13
# 
14
# You should have received a copy of the GNU General Public License 
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import os
18
import sys
19
import psycopg2
20
import psycopg2.errorcodes
21
from psycopg2 import OperationalError
22
from datetime import datetime
23
from shutil import rmtree
24

  
25
from qommon.ctl import Command, make_option
26

  
27

  
28
class CmdDeleteTenant(Command):
29
    name = 'delete_tenant'
30

  
31
    def __init__(self):
32
        Command.__init__(self, [
33
                make_option('--force-drop', action='store_true', default=False,
34
                           dest='force_drop'),
35
                ])
36

  
37
    def execute(self, base_options, sub_options, args):
38
        import publisher
39

  
40
        publisher.WcsPublisher.configure(self.config)
41
        pub = publisher.WcsPublisher.create_publisher(
42
                register_cron=False, register_tld_names=False)
43

  
44
        hostname = args[0]
45
        pub.app_dir = os.path.join(pub.app_dir, hostname)
46
        pub.set_config()
47
        self.delete_tenant(pub, sub_options, args)
48

  
49
    def delete_tenant(self, pub, options, args):
50
        postgresql_cfg = {}
51
        for k, v in pub.cfg['postgresql'].items():
52
            if v and isinstance(v, basestring):
53
                postgresql_cfg[k] = v
54

  
55
        # if there's a createdb-connection-params, we can do a DROP DATABASE with the
56
        # the option --force-drop, rename it if not
57
        createdb_cfg = pub.cfg['postgresql'].get('createdb-connection-params', {})
58
        createdb = True
59
        if not createdb_cfg:
60
            createdb_cfg = postgresql_cfg
61
            createdb = False
62
        try:
63
            pgconn = psycopg2.connect(**createdb_cfg)
64
        except psycopg2.Error as e:
65
            print >> sys.stderr, 'failed to connect to postgresql (%s)' % psycopg2.errorcodes.lookup(e.pgcode)
66
            return
67

  
68
        pgconn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
69
        cur = pgconn.cursor()
70
        try:
71
            dbname = pub.cfg['postgresql']['database']
72
            if createdb:
73
                if options.force_drop:
74
                    cur.execute('DROP DATABASE %s' % dbname)
75
                else:
76
                    deletion_date = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
77
                    cur.execute('ALTER DATABASE %s RENAME TO removed_%s_%s' % (dbname, deletion_date, dbname))
78
            else:
79
                cur.execute("""SELECT table_name
80
                            FROM information_schema.tables
81
                            WHERE table_schema = 'public' AND
82
                            table_type = 'BASE TABLE'""")
83

  
84
                tables_names = [x[0] for x in cur.fetchall()]
85

  
86
                if options.force_drop:
87
                    for table_name in tables_names:
88
                        cur.execute('DROP TABLE %s CASCADE' % table_name)
89

  
90
                else:
91
                    deletion_date = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
92
                    schema_name = 'removed_%s_%s' % (deletion_date, dbname)
93
                    cur.execute("CREATE SCHEMA %s" % schema_name[:63])
94
                    for table_name in tables_names:
95
                        cur.execute('ALTER TABLE %s SET SCHEMA %s' %
96
                                    (table_name, schema_name[:63]))
97

  
98
            if options.force_drop:
99
                rmtree(pub.app_dir)
100
            else:
101
                os.rename(pub.app_dir, pub.app_dir + '_removed_%s.invalid' % deletion_date)
102

  
103
        except psycopg2.Error as e:
104
            print >> sys.stderr, 'failed to alter database %s : (%s)' % (createdb_cfg['database'], psycopg2.errorcodes.lookup(e.pgcode))
105
            return
106

  
107
        cur.close()
108

  
109
CmdDeleteTenant.register()
wcs/sql.py
295 295
    conn, cur = get_connection_and_cursor()
296 296
    while True:
297 297
        cur.execute('''SELECT COUNT(*) FROM information_schema.tables
298
                       WHERE table_name LIKE %s''', ('formdata\\_%s\\_%%' % new_id,))
298
                       WHERE table_schema = 'public' AND
299
                       table_name LIKE %s''', ('formdata\\_%s\\_%%' % new_id,))
299 300
        if cur.fetchone()[0] == 0:
300 301
            break
301 302
        new_id += 1
......
306 307
def formdef_wipe():
307 308
    conn, cur = get_connection_and_cursor()
308 309
    cur.execute('''SELECT table_name FROM information_schema.tables
310
                   WHERE table_schema = 'public' AND
309 311
                   WHERE table_name LIKE %s''', ('formdata\\_%%\\_%%',))
310 312
    for table_name in [x[0] for x in cur.fetchall()]:
311 313
        cur.execute('''DROP TABLE %s CASCADE''' % table_name)
......
338 340
    table_name = get_formdef_table_name(formdef)
339 341

  
340 342
    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
343
                   WHERE table_schema = 'public' AND
341 344
                   WHERE table_name = %s''', (table_name,))
342 345
    if cur.fetchone()[0] == 0:
343 346
        cur.execute('''CREATE TABLE %s (id serial PRIMARY KEY,
......
358 361
                                    formdata_id integer REFERENCES %s (id) ON DELETE CASCADE)''' % (
359 362
                                    table_name, table_name))
360 363
    cur.execute('''SELECT column_name FROM information_schema.columns
361
                    WHERE table_name = %s''', (table_name,))
364
                   WHERE table_schema = 'public' AND
365
                   WHERE table_name = %s''', (table_name,))
362 366
    existing_fields = set([x[0] for x in cur.fetchall()])
363 367

  
364 368
    needed_fields = set(['id', 'user_id', 'receipt_time',
......
469 473
    table_name = 'users'
470 474

  
471 475
    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
476
                   WHERE table_schema = 'public' AND
472 477
                   WHERE table_name = %s''', (table_name,))
473 478
    if cur.fetchone()[0] == 0:
474 479
        cur.execute('''CREATE TABLE %s (id serial PRIMARY KEY,
......
483 488
                                    lasso_dump text,
484 489
                                    last_seen timestamp)''' % table_name)
485 490
    cur.execute('''SELECT column_name FROM information_schema.columns
486
                    WHERE table_name = %s''', (table_name,))
491
                   WHERE table_schema = 'public' AND
492
                   WHERE table_name = %s''', (table_name,))
487 493
    existing_fields = set([x[0] for x in cur.fetchall()])
488 494

  
489 495
    needed_fields = set(['id', 'name', 'email', 'roles', 'is_admin',
......
546 552
    table_name = 'tracking_codes'
547 553

  
548 554
    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
555
                   WHERE table_schema = 'public' AND
549 556
                   WHERE table_name = %s''', (table_name,))
550 557
    if cur.fetchone()[0] == 0:
551 558
        cur.execute('''CREATE TABLE %s (id varchar PRIMARY KEY,
552 559
                                    formdef_id varchar,
553 560
                                    formdata_id varchar)''' % table_name)
554 561
    cur.execute('''SELECT column_name FROM information_schema.columns
555
                    WHERE table_name = %s''', (table_name,))
562
                   WHERE table_schema = 'public' AND
563
                   WHERE table_name = %s''', (table_name,))
556 564
    existing_fields = set([x[0] for x in cur.fetchall()])
557 565

  
558 566
    needed_fields = set(['id', 'formdef_id', 'formdata_id'])
......
572 580
        conn, cur = get_connection_and_cursor()
573 581

  
574 582
    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
583
                   WHERE table_schema = 'public' AND
575 584
                   WHERE table_name = %s''', ('wcs_meta',))
576 585
    if cur.fetchone()[0] == 0:
577 586
        cur.execute('''CREATE TABLE wcs_meta (id serial PRIMARY KEY,
......
607 616
    if formdef:
608 617
        # remove the form view itself
609 618
        cur.execute('''SELECT table_name FROM information_schema.views
619
                       WHERE table_schema = 'public' AND
610 620
                       WHERE table_name LIKE %s''', ('wcs\\_view\\_%s\\_%%' % formdef.id ,))
611 621
    else:
612 622
        # if there's no formdef specified, remove all form views
613 623
        cur.execute('''SELECT table_name FROM information_schema.views
624
                       WHERE table_schema = 'public' AND
614 625
                       WHERE table_name LIKE %s''', ('wcs\\_view\\_%',))
615 626
    view_names = []
616 627
    while True:
......
715 726

  
716 727
def drop_global_views(conn, cur):
717 728
    cur.execute('''SELECT table_name FROM information_schema.views
729
                   WHERE table_schema = 'public' AND
718 730
                   WHERE table_name LIKE %s''', ('wcs\\_category\\_%',))
719 731
    view_names = []
720 732
    while True:
......
739 751
    view_names = [get_formdef_view_name(x) for x in FormDef.select()]
740 752

  
741 753
    cur.execute('''SELECT table_name FROM information_schema.views
754
                   WHERE table_schema = 'public' AND
742 755
                   WHERE table_name LIKE %s''', ('wcs\\_view\\_%',))
743 756
    existing_views = set()
744 757
    while True:
......
1884 1897

  
1885 1898
def migrate_global_views(conn, cur):
1886 1899
    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
1900
                   WHERE table_schema = 'public' AND
1887 1901
                   WHERE table_name = %s''', ('wcs_all_forms',))
1888 1902
    existing_fields = set([x[0] for x in cur.fetchall()])
1889 1903
    if 'formdef_id' not in existing_fields:
1890
-