~launchpad-pqm/launchpad/devel

8687.15.9 by Karl Fogel
Add the copyright header block to more files (everything under database/).
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
3
4
"""Common helpers for replication scripts."""
5
5799.1.42 by Stuart Bishop
Review feedback, round 1
6
__metaclass__ = type
7
__all__ = []
8
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
9
import subprocess
5799.1.30 by Stuart Bishop
Work in progress
10
from tempfile import NamedTemporaryFile
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
11
from textwrap import dedent
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
12
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
13
from canonical.config import config
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
14
from canonical.database.sqlbase import connect, sqlvalues
5799.1.26 by Stuart Bishop
Work in progress
15
from canonical.database.postgresql import (
5799.1.58 by Stuart Bishop
Hardcode slony user for slony daemon connections
16
    fqn, all_tables_in_schema, all_sequences_in_schema, ConnectionString
5799.1.26 by Stuart Bishop
Work in progress
17
    )
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
18
from canonical.launchpad.scripts.logger import log, DEBUG2
5799.1.20 by Stuart Bishop
Work in progress
19
5799.1.26 by Stuart Bishop
Work in progress
20
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
21
# The Slony-I clustername we use with Launchpad. Hardcoded because there
22
# is no point changing this, ever.
23
CLUSTERNAME = 'sl'
5799.1.26 by Stuart Bishop
Work in progress
24
25
# The namespace in the database used to contain all the Slony-I tables.
26
CLUSTER_NAMESPACE = '_%s' % CLUSTERNAME
27
9795.4.2 by Stuart Bishop
Review feedback
28
# Replication set id constants. Don't change these without DBA help.
29
LPMAIN_SET_ID = 1
30
HOLDING_SET_ID = 666
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
31
LPMIRROR_SET_ID = 4
9795.4.2 by Stuart Bishop
Review feedback
32
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
33
# Seed tables for the lpmain replication set to be passed to
5799.1.26 by Stuart Bishop
Work in progress
34
# calculate_replication_set().
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
35
LPMAIN_SEED = frozenset([
5799.1.26 by Stuart Bishop
Work in progress
36
    ('public', 'account'),
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
37
    ('public', 'openidnonce'),
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
38
    ('public', 'openidassociation'),
5799.1.26 by Stuart Bishop
Work in progress
39
    ('public', 'person'),
40
    ('public', 'launchpaddatabaserevision'),
9795.4.27 by Stuart Bishop
Cache database replication lag information as querying the live Slony tables can be slow
41
    ('public', 'databasereplicationlag'),
5799.1.26 by Stuart Bishop
Work in progress
42
    ('public', 'fticache'),
43
    ('public', 'nameblacklist'),
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
44
    ('public', 'openidconsumerassociation'),
45
    ('public', 'openidconsumernonce'),
5799.1.26 by Stuart Bishop
Work in progress
46
    ('public', 'codeimportmachine'),
47
    ('public', 'scriptactivity'),
48
    ('public', 'standardshipitrequest'),
49
    ('public', 'bugtag'),
50
    ('public', 'launchpadstatistic'),
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
51
    ('public', 'parsedapachelog'),
52
    ('public', 'shipitsurvey'),
7675.395.48 by Stuart Bishop
Add DatabaseReplicationLag as a seed table to pass staging restore sanity checks
53
    ('public', 'databasereplicationlag'),
7675.395.122 by Stuart Bishop
Fix SuggestivePOTemplate and mark FeatureFlag as part of the lpmain replication set
54
    ('public', 'featureflag'),
7675.395.128 by Stuart Bishop
Fix staging rebuild
55
    # suggestivepotemplate can be removed when the
56
    # suggestivepotemplate.potemplate foreign key constraint exists on
57
    # production.
58
    ('public', 'suggestivepotemplate'),
5799.1.26 by Stuart Bishop
Work in progress
59
    ])
60
61
# Explicitly list tables that should not be replicated. This includes the
62
# session tables, as these might exist in developer databases but will not
63
# exist in the production launchpad database.
64
IGNORED_TABLES = set([
7675.414.5 by Stuart Bishop
Make replication ignore lp_* tables, as we are going to replicate them manually
65
    # Session tables that in some situations will exist in the main lp
66
    # database.
67
    'public.secret', 'public.sessiondata', 'public.sessionpkgdata',
68
    # Mirror tables, per Bug #489078. These tables have their own private
69
    # replication set that is setup manually.
7675.395.63 by Stuart Bishop
lp_account needs to be ignored by our replication scripts as it is handled manually
70
    'public.lp_account',
7675.395.143 by Stuart Bishop
Ship OpenIdIdentifier to ISD via the lp_OpenIdIdentifier mirror table
71
    'public.lp_openididentifier',
7675.414.5 by Stuart Bishop
Make replication ignore lp_* tables, as we are going to replicate them manually
72
    'public.lp_person',
73
    'public.lp_personlocation',
74
    'public.lp_teamparticipation',
7675.686.9 by Stuart Bishop
Basic database utilization report
75
    # Database statistics
76
    'public.databasetablestats',
77
    'public.databasecpustats',
7675.395.176 by Stuart Bishop
Capture table disk utilization metrics from pgstattuple
78
    'public.databasediskutilization',
7675.395.97 by Stuart Bishop
Don't replicate the OAuthNonce table - busy table and little point
79
    # Don't replicate OAuthNonce - too busy and no real gain.
80
    'public.oauthnonce',
9795.4.18 by Stuart Bishop
Review feedback
81
    # Ubuntu SSO database. These tables where created manually by ISD
82
    # and the Launchpad scripts should not mess with them. Eventually
9795.4.19 by Stuart Bishop
Speling
83
    # these tables will be in a totally separate database.
9795.4.18 by Stuart Bishop
Review feedback
84
    'public.auth_permission',
85
    'public.auth_group',
86
    'public.auth_user',
87
    'public.auth_message',
88
    'public.django_content_type',
89
    'public.auth_permission',
90
    'public.django_session',
91
    'public.django_site',
92
    'public.django_admin_log',
93
    'public.ssoopenidrpconfig',
94
    'public.auth_group_permissions',
95
    'public.auth_user_groups',
9795.4.14 by Stuart Bishop
Handle database dumps containing login service tables, a side effect from standalone authservice progress
96
    'public.auth_user_user_permissions',
7675.395.51 by Stuart Bishop
Add more tables created on production db for ISD
97
    'public.oauth_nonce',
98
    'public.oauth_consumer',
99
    'public.oauth_token',
100
    'public.api_user',
101
    'public.oauth_consumer_id_seq',
102
    'public.api_user_id_seq',
103
    'public.oauth_nonce_id_seq',
7675.414.5 by Stuart Bishop
Make replication ignore lp_* tables, as we are going to replicate them manually
104
    ])
9795.4.14 by Stuart Bishop
Handle database dumps containing login service tables, a side effect from standalone authservice progress
105
106
# Calculate IGNORED_SEQUENCES
107
IGNORED_SEQUENCES = set('%s_id_seq' % table for table in IGNORED_TABLES)
5799.1.26 by Stuart Bishop
Work in progress
108
109
5799.1.30 by Stuart Bishop
Work in progress
110
def slony_installed(con):
111
    """Return True if the connected database is part of a Launchpad Slony-I
112
    cluster.
113
    """
114
    cur = con.cursor()
115
    cur.execute("""
116
        SELECT TRUE FROM pg_class,pg_namespace
117
        WHERE
118
            nspname = %s
119
            AND relname = 'sl_table'
120
            AND pg_class.relnamespace = pg_namespace.oid
121
        """ % sqlvalues(CLUSTER_NAMESPACE))
122
    return cur.fetchone() is not None
123
124
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
125
class TableReplicationInfo:
126
    """Internal table replication details."""
127
    table_id = None
128
    replication_set_id = None
129
    master_node_id = None
130
131
    def __init__(self, con, namespace, table_name):
132
        cur = con.cursor()
133
        cur.execute("""
134
            SELECT tab_id, tab_set, set_origin
135
            FROM %s.sl_table, %s.sl_set
136
            WHERE tab_set = set_id
137
                AND tab_nspname = %s
138
                AND tab_relname = %s
139
            """ % (
140
                (CLUSTER_NAMESPACE, CLUSTER_NAMESPACE)
141
                + sqlvalues(namespace, table_name)))
142
        row = cur.fetchone()
143
        if row is None:
144
            raise LookupError(fqn(namespace, table_name))
145
        self.table_id, self.replication_set_id, self.master_node_id = row
146
147
5799.1.42 by Stuart Bishop
Review feedback, round 1
148
def sync(timeout):
5799.1.20 by Stuart Bishop
Work in progress
149
    """Generate a sync event and wait for it to complete on all nodes.
7675.414.6 by Stuart Bishop
Split the DB patch so tables can be created and replicated before rollout
150
5799.1.20 by Stuart Bishop
Work in progress
151
    This means that all pending events have propagated and are in sync
152
    to the point in time this method was called. This might take several
153
    hours if there is a large backlog of work to replicate.
5799.1.30 by Stuart Bishop
Work in progress
154
5799.1.42 by Stuart Bishop
Review feedback, round 1
155
    :param timeout: Number of seconds to wait for the sync. 0 to block
156
                    indefinitely.
5799.1.20 by Stuart Bishop
Work in progress
157
    """
5799.1.42 by Stuart Bishop
Review feedback, round 1
158
    return execute_slonik("", sync=timeout)
5799.1.20 by Stuart Bishop
Work in progress
159
160
7178.4.1 by Stuart Bishop
Script to repair a db built from a slony node dump
161
def execute_slonik(script, sync=None, exit_on_fail=True, auto_preamble=True):
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
162
    """Use the slonik command line tool to run a slonik script.
163
164
    :param script: The script as a string. Preamble should not be included.
5799.1.20 by Stuart Bishop
Work in progress
165
5799.1.30 by Stuart Bishop
Work in progress
166
    :param sync: Number of seconds to wait for sync before failing. 0 to
167
                 block indefinitely.
5799.1.42 by Stuart Bishop
Review feedback, round 1
168
169
    :param exit_on_fail: If True, on failure of the slonik script
13465.2.16 by Stuart Bishop
Run upgrade.py in-process to save startup overhead
170
                         SystemExit is raised using the slonik return code.
5799.1.42 by Stuart Bishop
Review feedback, round 1
171
7178.4.1 by Stuart Bishop
Script to repair a db built from a slony node dump
172
    :param auto_preamble: If True, the generated preamble will be
173
                          automatically included.
174
5799.1.46 by Stuart Bishop
Review tweaks
175
    :returns: True if the script completed successfully. False if
5799.1.42 by Stuart Bishop
Review feedback, round 1
176
              exit_on_fail is False and the script failed for any reason.
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
177
    """
178
5799.1.30 by Stuart Bishop
Work in progress
179
    # Add the preamble and optional sync to the script.
7178.4.1 by Stuart Bishop
Script to repair a db built from a slony node dump
180
    if auto_preamble:
181
        script = preamble() + script
182
5799.1.20 by Stuart Bishop
Work in progress
183
    if sync is not None:
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
184
        sync_script = dedent("""\
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
185
            sync (id = @master_node);
5799.1.20 by Stuart Bishop
Work in progress
186
            wait for event (
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
187
                origin = @master_node, confirmed = ALL,
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
188
                wait on = @master_node, timeout = %d);
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
189
            """ % sync)
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
190
        script = script + sync_script
5799.1.20 by Stuart Bishop
Work in progress
191
5799.1.30 by Stuart Bishop
Work in progress
192
    # Copy the script to a NamedTemporaryFile rather than just pumping it
193
    # to slonik via stdin. This way it can be examined if slonik appears
194
    # to hang.
195
    script_on_disk = NamedTemporaryFile(prefix="slonik", suffix=".sk")
196
    print >> script_on_disk, script
197
    script_on_disk.flush()
198
199
    # Run slonik
200
    log.debug("Executing slonik script %s" % script_on_disk.name)
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
201
    log.log(DEBUG2, 'Running script:\n%s' % script)
5799.1.30 by Stuart Bishop
Work in progress
202
    returncode = subprocess.call(['slonik', script_on_disk.name])
203
204
    if returncode != 0:
5799.1.20 by Stuart Bishop
Work in progress
205
        log.error("slonik script failed")
206
        if exit_on_fail:
13465.2.16 by Stuart Bishop
Run upgrade.py in-process to save startup overhead
207
            raise SystemExit(1)
5799.1.30 by Stuart Bishop
Work in progress
208
209
    return returncode == 0
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
210
211
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
212
class Node:
213
    """Simple data structure for holding information about a Slony node."""
214
    def __init__(self, node_id, nickname, connection_string, is_master):
215
        self.node_id = node_id
216
        self.nickname = nickname
217
        self.connection_string = connection_string
218
        self.is_master = is_master
219
220
221
def _get_nodes(con, query):
222
    """Return a list of Nodes."""
223
    if not slony_installed(con):
224
        return []
225
    cur = con.cursor()
226
    cur.execute(query)
227
    nodes = []
228
    for node_id, nickname, connection_string, is_master in cur.fetchall():
229
        nodes.append(Node(node_id, nickname, connection_string, is_master))
230
    return nodes
231
232
233
def get_master_node(con, set_id=1):
234
    """Return the master Node, or None if the cluster is still being setup."""
235
    nodes = _get_nodes(con, """
236
        SELECT DISTINCT
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
237
            set_origin AS node_id,
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
238
            'master',
239
            pa_conninfo AS connection_string,
240
            True
241
        FROM _sl.sl_set
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
242
        LEFT OUTER JOIN _sl.sl_path ON set_origin = pa_server
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
243
        WHERE set_id = %d
244
        """ % set_id)
245
    if not nodes:
246
        return None
247
    assert len(nodes) == 1, "More than one master found for set %s" % set_id
248
    return nodes[0]
249
250
251
def get_slave_nodes(con, set_id=1):
252
    """Return the list of slave Nodes."""
253
    return _get_nodes(con, """
254
        SELECT DISTINCT
255
            pa_server AS node_id,
256
            'slave' || pa_server,
257
            pa_conninfo AS connection_string,
258
            False
259
        FROM _sl.sl_set
260
        JOIN _sl.sl_subscribe ON set_id = sub_set
261
        JOIN _sl.sl_path ON sub_receiver = pa_server
262
        WHERE
263
            set_id = %d
264
        ORDER BY node_id
265
        """ % set_id)
266
267
268
def get_nodes(con, set_id=1):
269
    """Return a list of all Nodes."""
270
    master_node = get_master_node(con, set_id)
271
    if master_node is None:
272
        return []
273
    else:
274
        return [master_node] + get_slave_nodes(con, set_id)
275
276
277
def get_all_cluster_nodes(con):
278
    """Return a list of all Nodes in the cluster.
279
280
    node.is_master will be None, as this boolean doesn't make sense
281
    in the context of a cluster rather than a single replication set.
282
    """
283
    if not slony_installed(con):
284
        return []
285
    nodes = _get_nodes(con, """
286
        SELECT DISTINCT
287
            pa_server AS node_id,
288
            'node' || pa_server || '_node',
289
            pa_conninfo AS connection_string,
290
            NULL
291
        FROM _sl.sl_path
292
        ORDER BY node_id
293
        """)
294
    if not nodes:
295
        # There are no subscriptions yet, so no paths. Generate the
296
        # master Node.
297
        cur = con.cursor()
298
        cur.execute("SELECT no_id from _sl.sl_node")
299
        node_ids = [row[0] for row in cur.fetchall()]
300
        if len(node_ids) == 0:
301
            return []
302
        assert len(node_ids) == 1, "Multiple nodes but no paths."
303
        master_node_id = node_ids[0]
304
        master_connection_string = ConnectionString(
7675.395.43 by Stuart Bishop
Update replication scripts to cope with recent ro mode changes
305
            config.database.rw_main_master)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
306
        master_connection_string.user = 'slony'
307
        return [Node(
308
            master_node_id, 'node%d_node' % master_node_id,
309
            master_connection_string, True)]
310
    return nodes
311
312
313
def preamble(con=None):
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
314
    """Return the preable needed at the start of all slonik scripts."""
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
315
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
316
    if con is None:
317
        con = connect('slony')
318
319
    master_node = get_master_node(con)
320
    nodes = get_all_cluster_nodes(con)
321
    if master_node is None and len(nodes) == 1:
322
        master_node = nodes[0]
323
324
    preamble = [dedent("""\
325
        #
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
326
        # Every slonik script must start with a clustername, which cannot
327
        # be changed once the cluster is initialized.
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
328
        #
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
329
        cluster name = sl;
330
331
        # Symbolic ids for replication sets.
7675.395.72 by Stuart Bishop
Remove remaining authdb awareness from replication scripts
332
        define lpmain_set   %d;
333
        define holding_set  %d;
334
        define lpmirror_set %d;
335
        """ % (LPMAIN_SET_ID, HOLDING_SET_ID, LPMIRROR_SET_ID))]
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
336
337
    if master_node is not None:
338
        preamble.append(dedent("""\
339
        # Symbolic id for the main replication set master node.
340
        define master_node %d;
341
        define master_node_conninfo '%s';
342
        """ % (master_node.node_id, master_node.connection_string)))
343
344
    for node in nodes:
345
        preamble.append(dedent("""\
346
            define %s %d;
347
            define %s_conninfo '%s';
348
            node @%s admin conninfo = @%s_conninfo;
349
            """ % (
350
                node.nickname, node.node_id,
351
                node.nickname, node.connection_string,
352
                node.nickname, node.nickname)))
9795.4.2 by Stuart Bishop
Review feedback
353
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
354
    return '\n\n'.join(preamble)
9795.4.2 by Stuart Bishop
Review feedback
355
5799.1.26 by Stuart Bishop
Work in progress
356
357
def calculate_replication_set(cur, seeds):
358
    """Return the minimal set of tables and sequences needed in a
359
    replication set containing the seed table.
360
361
    A replication set must contain all tables linked by foreign key
362
    reference to the given table, and sequences used to generate keys.
7675.395.101 by Stuart Bishop
Ignore OAuthNonce better. Make foreign key constraint ON DELETE CASCADE so replication doesn't break when moving the lpmain origin to a new node
363
    Tables and sequences can be added to the IGNORED_TABLES and
364
    IGNORED_SEQUENCES lists for cases where we known can safely ignore
365
    this restriction.
5799.1.26 by Stuart Bishop
Work in progress
366
367
    :param seeds: [(namespace, tablename), ...]
368
369
    :returns: (tables, sequences)
370
    """
371
    # Results
372
    tables = set()
373
    sequences = set()
374
375
    # Our pending set to check
376
    pending_tables = set(seeds)
377
378
    # Generate the set of tables that reference the seed directly
379
    # or indirectly via foreign key constraints, including the seed itself.
380
    while pending_tables:
381
        namespace, tablename = pending_tables.pop()
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
382
383
        # Skip if the table doesn't exist - we might have seeds listed that
384
        # have been removed or are yet to be created.
385
        cur.execute("""
386
            SELECT TRUE
387
            FROM pg_class, pg_namespace
388
            WHERE pg_class.relnamespace = pg_namespace.oid
389
                AND pg_namespace.nspname = %s
390
                AND pg_class.relname = %s
391
            """ % sqlvalues(namespace, tablename))
392
        if cur.fetchone() is None:
393
            log.debug("Table %s.%s doesn't exist" % (namespace, tablename))
394
            continue
395
5799.1.26 by Stuart Bishop
Work in progress
396
        tables.add((namespace, tablename))
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
397
5799.1.26 by Stuart Bishop
Work in progress
398
        # Find all tables that reference the current (seed) table
399
        # and all tables that the seed table references.
400
        cur.execute("""
401
            SELECT ref_namespace.nspname, ref_class.relname
402
            FROM
5799.1.42 by Stuart Bishop
Review feedback, round 1
403
                -- One of the seed tables
5799.1.26 by Stuart Bishop
Work in progress
404
                pg_class AS seed_class,
405
                pg_namespace AS seed_namespace,
5799.1.42 by Stuart Bishop
Review feedback, round 1
406
407
                -- A table referencing the seed, or being referenced by
408
                -- the seed.
5799.1.26 by Stuart Bishop
Work in progress
409
                pg_class AS ref_class,
410
                pg_namespace AS ref_namespace,
5799.1.42 by Stuart Bishop
Review feedback, round 1
411
5799.1.26 by Stuart Bishop
Work in progress
412
                pg_constraint
413
            WHERE
414
                seed_class.relnamespace = seed_namespace.oid
415
                AND ref_class.relnamespace = ref_namespace.oid
5799.1.42 by Stuart Bishop
Review feedback, round 1
416
5799.1.26 by Stuart Bishop
Work in progress
417
                AND seed_namespace.nspname = %s
418
                AND seed_class.relname = %s
5799.1.42 by Stuart Bishop
Review feedback, round 1
419
5799.1.46 by Stuart Bishop
Review tweaks
420
                -- Foreign key constraints are all we care about.
5799.1.42 by Stuart Bishop
Review feedback, round 1
421
                AND pg_constraint.contype = 'f'
422
423
                -- We want tables referenced by, or referred to, the
424
                -- seed table.
5799.1.26 by Stuart Bishop
Work in progress
425
                AND ((pg_constraint.conrelid = ref_class.oid
426
                        AND pg_constraint.confrelid = seed_class.oid)
427
                    OR (pg_constraint.conrelid = seed_class.oid
428
                        AND pg_constraint.confrelid = ref_class.oid)
429
                    )
430
            """ % sqlvalues(namespace, tablename))
431
        for namespace, tablename in cur.fetchall():
432
            key = (namespace, tablename)
7675.395.101 by Stuart Bishop
Ignore OAuthNonce better. Make foreign key constraint ON DELETE CASCADE so replication doesn't break when moving the lpmain origin to a new node
433
            if (key not in tables and key not in pending_tables
434
                and '%s.%s' % (namespace, tablename) not in IGNORED_TABLES):
5799.1.26 by Stuart Bishop
Work in progress
435
                pending_tables.add(key)
436
437
    # Generate the set of sequences that are linked to any of our set of
438
    # tables. We assume these are all sequences created by creation of
439
    # serial or bigserial columns, or other sequences OWNED BY a particular
440
    # column.
441
    for namespace, tablename in tables:
442
        cur.execute("""
443
            SELECT seq
444
            FROM (
445
                SELECT pg_get_serial_sequence(%s, attname) AS seq
446
                FROM pg_namespace, pg_class, pg_attribute
447
                WHERE pg_namespace.nspname = %s
448
                    AND pg_class.relnamespace = pg_namespace.oid
449
                    AND pg_class.relname = %s
450
                    AND pg_attribute.attrelid = pg_class.oid
451
                    AND pg_attribute.attisdropped IS FALSE
452
                ) AS whatever
453
            WHERE seq IS NOT NULL;
454
            """ % sqlvalues(fqn(namespace, tablename), namespace, tablename))
7675.395.101 by Stuart Bishop
Ignore OAuthNonce better. Make foreign key constraint ON DELETE CASCADE so replication doesn't break when moving the lpmain origin to a new node
455
        for sequence, in cur.fetchall():
456
            if sequence not in IGNORED_SEQUENCES:
457
                sequences.add(sequence)
5799.1.26 by Stuart Bishop
Work in progress
458
459
    # We can't easily convert the sequence name to (namespace, name) tuples,
460
    # so we might as well convert the tables to dot notation for consistancy.
7178.4.10 by Stuart Bishop
Lint cleanup
461
    tables = set(fqn(namespace, tablename) for namespace, tablename in tables)
5799.1.26 by Stuart Bishop
Work in progress
462
463
    return tables, sequences
464
465
5799.1.29 by Stuart Bishop
Tweak
466
def discover_unreplicated(cur):
5799.1.26 by Stuart Bishop
Work in progress
467
    """Inspect the database for tables and sequences in the public schema
468
    that are not in a replication set.
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
469
5799.1.26 by Stuart Bishop
Work in progress
470
    :returns: (unreplicated_tables_set, unreplicated_sequences_set)
471
    """
472
    all_tables = all_tables_in_schema(cur, 'public')
473
    all_sequences = all_sequences_in_schema(cur, 'public')
474
13011.4.5 by Stuart Bishop
Replication should ignore tables and sequences with names starting with 'temp_'
475
    # Ignore any tables and sequences starting with temp_. These are
476
    # transient and not to be replicated per Bug #778338.
477
    all_tables = set(
478
        table for table in all_tables
479
            if not table.startswith('public.temp_'))
480
    all_sequences = set(
481
        sequence for sequence in all_sequences
482
            if not sequence.startswith('public.temp_'))
483
5799.1.26 by Stuart Bishop
Work in progress
484
    cur.execute("""
485
        SELECT tab_nspname, tab_relname FROM %s
486
        WHERE tab_nspname = 'public'
487
        """ % fqn(CLUSTER_NAMESPACE, "sl_table"))
488
    replicated_tables = set(fqn(*row) for row in cur.fetchall())
489
490
    cur.execute("""
491
        SELECT seq_nspname, seq_relname FROM %s
492
        WHERE seq_nspname = 'public'
493
        """ % fqn(CLUSTER_NAMESPACE, "sl_sequence"))
494
    replicated_sequences = set(fqn(*row) for row in cur.fetchall())
495
496
    return (
497
        all_tables - replicated_tables - IGNORED_TABLES,
9795.4.14 by Stuart Bishop
Handle database dumps containing login service tables, a side effect from standalone authservice progress
498
        all_sequences - replicated_sequences - IGNORED_SEQUENCES)
5799.1.26 by Stuart Bishop
Work in progress
499
500
501
class ReplicationConfigError(Exception):
502
    """Exception raised by validate_replication_sets() when our replication
503
    setup is misconfigured.
504
    """
505
506
507
def validate_replication(cur):
508
    """Raise a ReplicationSetupError if there is something wrong with
509
    our replication sets.
510
511
    This might include tables exist that are not in a replication set,
512
    or tables that exist in multiple replication sets for example.
513
514
    These is not necessarily limits with what Slony-I allows, but might
515
    be due to policies we have made (eg. a table allowed in just one
516
    replication set).
517
    """
5799.1.29 by Stuart Bishop
Tweak
518
    unrepl_tables, unrepl_sequences = discover_unreplicated(cur)
5799.1.26 by Stuart Bishop
Work in progress
519
    if unrepl_tables:
520
        raise ReplicationConfigError(
521
            "Unreplicated tables: %s" % repr(unrepl_tables))
522
    if unrepl_sequences:
523
        raise ReplicationConfigError(
524
            "Unreplicated sequences: %s" % repr(unrepl_sequences))
525
526
    lpmain_tables, lpmain_sequences = calculate_replication_set(
527
        cur, LPMAIN_SEED)