~launchpad-pqm/launchpad/devel

10637.3.7 by Guilherme Salgado
merge devel
1
#!/usr/bin/python -S
8687.15.9 by Karl Fogel
Add the copyright header block to more files (everything under database/).
2
#
3
# Copyright 2009 Canonical Ltd.  This software is licensed under the
4
# GNU Affero General Public License version 3 (see the file LICENSE).
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
5
5799.1.46 by Stuart Bishop
Review tweaks
6
"""Generate a report on the replication setup.
7
8
This report spits out whatever we consider useful for checking up on and
9
diagnosing replication. This report will grow over time, and maybe some
10
bits of this will move to seperate monitoring systems or reports.
11
12
See the Slony-I documentation for more discussion on the data presented
13
by this report.
14
"""
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
15
5799.1.42 by Stuart Bishop
Review feedback, round 1
16
__metaclass__ = type
17
__all__ = []
18
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
19
import _pythonpath
20
21
from cgi import escape as html_escape
22
from cStringIO import StringIO
23
from optparse import OptionParser
24
import sys
25
26
from canonical.database.sqlbase import connect, quote_identifier, sqlvalues
27
from canonical.launchpad.scripts import db_options
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
28
import replication.helpers
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
29
30
31
class Table:
32
    labels = None # List of labels to render as the first row of the table.
33
    rows = None # List of rows, each row being a list of strings.
34
35
    def __init__(self, labels=None):
36
        if labels is None:
37
            self.labels = []
38
        else:
39
            self.labels = labels[:]
40
        self.rows = []
41
7675.251.2 by Stuart Bishop
Fix listen report which never worked
42
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
43
class HtmlReport:
44
45
    def alert(self, text):
46
        """Return text marked up to be noticed."""
47
        return '<span style="alert">%s</span>' % html_escape(text)
48
49
    def table(self, table):
50
        """Return the rendered table."""
51
        out = StringIO()
52
        print >> out, "<table>"
53
        if table.labels:
54
            print >> out, "<tr>"
55
            for label in table.labels:
56
                print >> out, "<th>%s</th>" % html_escape(unicode(label))
57
            print >> out, "</tr>"
58
59
        for row in table.rows:
60
            print >> out, "<tr>"
61
            for cell in row:
62
                print >> out, "<td>%s</td>" % html_escape(unicode(cell))
63
            print >> out, "</tr>"
64
65
        print >> out, "</table>"
66
67
        return out.getvalue()
68
69
5799.1.27 by Stuart Bishop
Text mode report
70
class TextReport:
71
    def alert(self, text):
72
        return text
73
74
    def table(self, table):
75
        max_col_widths = []
76
        for label in table.labels:
77
            max_col_widths.append(len(label))
78
        for row in table.rows:
79
            row = list(row) # We need len()
80
            for col_idx in range(0,len(row)):
81
                col = row[col_idx]
82
                max_col_widths[col_idx] = max(
83
                    len(str(row[col_idx])), max_col_widths[col_idx])
84
85
        out = StringIO()
86
        for label_idx in range(0, len(table.labels)):
87
            print >> out, table.labels[label_idx].ljust(
88
                max_col_widths[label_idx]),
89
        print >> out
90
        for width in max_col_widths:
91
            print >> out, '='*width,
92
        print >> out
93
        for row in table.rows:
94
            row = list(row)
95
            for col_idx in range(0, len(row)):
96
                print >> out, str(row[col_idx]).ljust(max_col_widths[col_idx]),
97
            print >> out
98
        print >> out
99
100
        return out.getvalue()
101
102
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
103
def node_overview_report(cur, options):
5799.1.42 by Stuart Bishop
Review feedback, round 1
104
    """Dumps the sl_node table in a human readable format.
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
105
5799.1.42 by Stuart Bishop
Review feedback, round 1
106
    This report tells us which nodes are active and which are inactive.
107
    """
5799.1.27 by Stuart Bishop
Text mode report
108
    report = options.mode()
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
109
    table = Table(["Node", "Comment", "Active"])
110
111
    cur.execute("""
112
        SELECT no_id, no_comment, no_active
113
        FROM sl_node
114
        ORDER BY no_comment
115
        """)
116
    for node_id, node_comment, node_active in cur.fetchall():
117
        if node_active:
118
            node_active_text = 'Active'
119
        else:
120
            node_active_text = report.alert('Inactive')
121
        table.rows.append([
122
            'Node %d' % node_id, node_comment, node_active_text])
123
124
    return report.table(table)
125
126
127
def paths_report(cur, options):
5799.1.42 by Stuart Bishop
Review feedback, round 1
128
    """Dumps the sl_paths table in a human readable format.
129
130
    This report describes how nodes will attempt to connect to each other
131
    if they need to, allowing you to sanity check the settings and pick up
132
    obvious misconfigurations that would stop Slony daemons from being able
133
    to connect to one or more nodes, blocking replication.
134
    """
5799.1.27 by Stuart Bishop
Text mode report
135
    report = options.mode()
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
136
    table = Table(["From client node", "To server node", "Via connection"])
137
138
    cur.execute("""
139
        SELECT pa_client, pa_server, pa_conninfo
140
        FROM sl_path
141
        ORDER BY pa_client, pa_server
142
        """)
143
    for row in cur.fetchall():
144
        table.rows.append(row)
145
146
    return report.table(table)
147
148
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
149
def listen_report(cur, options):
5799.1.42 by Stuart Bishop
Review feedback, round 1
150
    """Dumps the sl_listen table in a human readable format.
151
152
    This report shows you the tree of which nodes a node needs to check
153
    for events on.
154
    """
5799.1.27 by Stuart Bishop
Text mode report
155
    report = options.mode()
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
156
    table = Table(["Node", "Listens To", "Via"])
157
158
    cur.execute("""
159
        SELECT li_receiver, li_origin, li_provider
7675.251.2 by Stuart Bishop
Fix listen report which never worked
160
        FROM sl_listen
161
        ORDER BY li_receiver, li_origin, li_provider
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
162
        """)
163
    for row in cur.fetchall():
7675.251.2 by Stuart Bishop
Fix listen report which never worked
164
        table.rows.append(['Node %s' % node for node in row])
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
165
    return report.table(table)
166
167
168
def subscribe_report(cur, options):
5799.1.42 by Stuart Bishop
Review feedback, round 1
169
    """Dumps the sl_subscribe table in a human readable format.
170
171
    This report shows the subscription tree - which nodes provide
172
    a replication set to which subscriber.
173
    """
174
5799.1.27 by Stuart Bishop
Text mode report
175
    report = options.mode()
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
176
    table = Table([
5799.1.42 by Stuart Bishop
Review feedback, round 1
177
        "Set", "Is Provided By", "Is Received By",
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
178
        "Is Forwardable", "Is Active"])
179
    cur.execute("""
180
        SELECT sub_set, sub_provider, sub_receiver, sub_forward, sub_active
181
        FROM sl_subscribe ORDER BY sub_set, sub_provider, sub_receiver
182
        """)
183
    for set_, provider, receiver, forward, active in cur.fetchall():
184
        if active:
185
            active_text = 'Active'
186
        else:
187
            active_text = report.alert('Inactive')
188
        table.rows.append([
189
            "Set %d" % set_, "Node %d" % provider, "Node %d" % receiver,
190
            str(forward), active_text])
191
    return report.table(table)
192
193
194
def tables_report(cur, options):
5799.1.42 by Stuart Bishop
Review feedback, round 1
195
    """Dumps the sl_table table in a human readable format.
196
197
    This report shows which tables are being replicated and in which
198
    replication set. It also importantly shows the internal Slony id used
199
    for a table, which is needed for slonik scripts as slonik is incapable
200
    of doing the tablename -> Slony id mapping itself.
201
    """
5799.1.27 by Stuart Bishop
Text mode report
202
    report = options.mode()
5799.1.31 by Stuart Bishop
Add newly created tables to lpmain replication set
203
    table = Table(["Set", "Schema", "Table", "Table Id"])
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
204
    cur.execute("""
205
        SELECT tab_set, nspname, relname, tab_id, tab_idxname, tab_comment
206
        FROM sl_table, pg_class, pg_namespace
207
        WHERE tab_reloid = pg_class.oid AND relnamespace = pg_namespace.oid
208
        ORDER BY tab_set, nspname, relname
209
        """)
210
    for set_, namespace, tablename, table_id, key, comment in cur.fetchall():
211
        table.rows.append([
5799.1.31 by Stuart Bishop
Add newly created tables to lpmain replication set
212
            "Set %d" % set_, namespace, tablename, str(table_id)])
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
213
    return report.table(table)
214
215
216
def sequences_report(cur, options):
5799.1.42 by Stuart Bishop
Review feedback, round 1
217
    """Dumps the sl_sequences table in a human readable format.
218
219
    This report shows which sequences are being replicated and in which
220
    replication set. It also importantly shows the internal Slony id used
221
    for a sequence, which is needed for slonik scripts as slonik is incapable
222
    of doing the tablename -> Slony id mapping itself.
223
    """
5799.1.27 by Stuart Bishop
Text mode report
224
    report = options.mode()
5799.1.31 by Stuart Bishop
Add newly created tables to lpmain replication set
225
    table = Table(["Set", "Schema", "Sequence", "Sequence Id"])
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
226
    cur.execute("""
227
        SELECT seq_set, nspname, relname, seq_id, seq_comment
228
        FROM sl_sequence, pg_class, pg_namespace
229
        WHERE seq_reloid = pg_class.oid AND relnamespace = pg_namespace.oid
230
        ORDER BY seq_set, nspname, relname
231
        """)
232
    for set_, namespace, tablename, table_id, comment in cur.fetchall():
233
        table.rows.append([
5799.1.31 by Stuart Bishop
Add newly created tables to lpmain replication set
234
            "Set %d" % set_, namespace, tablename, str(table_id)])
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
235
    return report.table(table)
236
237
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
238
def main():
239
    parser = OptionParser()
240
5799.1.27 by Stuart Bishop
Text mode report
241
    parser.add_option(
242
        "-f", "--format", dest="mode", default="text",
243
        choices=['text', 'html'],
244
        help="Output format MODE", metavar="MODE")
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
245
    db_options(parser)
246
247
    options, args = parser.parse_args()
248
5799.1.27 by Stuart Bishop
Text mode report
249
    if options.mode == "text":
250
        options.mode = TextReport
251
    elif options.mode == "html":
252
        options.mode = HtmlReport
253
    else:
254
        assert False, "Unknown mode %s" % options.mode
255
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
256
    con = connect(options.dbuser)
257
    cur = con.cursor()
258
259
    cur.execute(
260
            "SELECT TRUE FROM pg_namespace WHERE nspname=%s"
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
261
            % sqlvalues(replication.helpers.CLUSTER_NAMESPACE))
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
262
263
    if cur.fetchone() is None:
264
        parser.error(
265
                "No Slony-I cluster called %s in that database"
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
266
                % replication.helpers.CLUSTERNAME)
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
267
        return 1
268
269
270
    # Set our search path to the schema of the cluster we care about.
271
    cur.execute(
5799.1.55 by Stuart Bishop
Improve initialize, less magic dev setup
272
            "SET search_path TO %s, public"
7140.4.4 by Stuart Bishop
Delint replication scripts and fix trivial bug in report.py
273
            % quote_identifier(replication.helpers.CLUSTER_NAMESPACE))
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
274
275
    print node_overview_report(cur, options)
276
    print paths_report(cur, options)
5799.1.22 by Stuart Bishop
Finish initial status reporting tool
277
    print listen_report(cur, options)
278
    print subscribe_report(cur, options)
279
    print tables_report(cur, options)
280
    print sequences_report(cur, options)
5799.1.12 by Stuart Bishop
Replication maintenance scripts, work in progress
281
282
283
if __name__ == '__main__':
284
    sys.exit(main())