~launchpad-pqm/launchpad/devel

10637.3.1 by Guilherme Salgado
Use the default python version instead of a hard-coded version
1
#!/usr/bin/python -S
8687.15.22 by Karl Fogel
Add the copyright header block to the remaining .py files.
2
#
3
# Copyright 2009 Canonical Ltd.  This software is licensed under the
4
# GNU Affero General Public License version 3 (see the file LICENSE).
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
5
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
6
"""Sync Mailman data from one Launchpad to another."""
7
6916.1.1 by Curtis Hovey
Fixed comment formats to fidn missing persons, dates, and some bugs.
8
# XXX BarryWarsaw 2008-02-12:
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
9
# Things this script does NOT do correctly.
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
10
#
11
# - Fix up the deactivated lists.  This isn't done because that data lives in
12
#   the backed up tar file, so handling this would mean untar'ing, tricking
13
#   Mailman into loading the pickle (or manually loading and patching), then
14
#   re-tar'ing.  I don't think it's worth it because the only thing that will
15
#   be broken is if a list that's deactivated on production is re-activated on
16
#   staging.
17
#
18
# - Backpatch all the message footers and RFC 2369 headers of the messages in
19
#   the archive.  To do this, we'd have to iterate through all messages,
20
#   tweaking the List-* headers (easy) and ripping apart the footers,
21
#   recalculating them and reattaching them (difficult).  Doing the iteration
22
#   and update is quite painful in Python 2.4, but would be easier with Python
23
#   2.5's new mailbox module.  /Then/ we'd have to regenerate the archives.
24
#   Not doing this means that some of the links in staging's MHonArc archive
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
25
#   will point to production archives.
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
26
9641.1.7 by Gary Poster
fix scripts to work
27
# pylint: disable-msg=W0403
28
import _pythonpath
29
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
30
import os
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
31
import sys
32
import logging
33
import textwrap
34
import subprocess
35
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
36
from zope.component import getUtility
6667.1.2 by Barry Warsaw
Lint cleanups
37
from zope.security.proxy import removeSecurityProxy
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
38
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
39
from canonical.config import config
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
40
from canonical.launchpad.interfaces import (
41
    IEmailAddressSet, IMailingListSet, IPersonSet)
5863.9.7 by Curtis Hovey
Refacortings per review.
42
from canonical.launchpad.mailman.config import configure_prefix
8356.1.9 by Leonard Richardson
Renamed the base script module in scripts/, which module_rename.py didn't touch because it wasn't under lib/.
43
from lp.services.scripts.base import LaunchpadCronScript
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
44
45
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
46
RSYNC_OPTIONS = ('-avz', '--delete')
47
RSYNC_COMMAND = '/usr/bin/rsync'
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
48
RSYNC_SUBDIRECTORIES = ('archives', 'backups', 'lists', 'mhonarc')
49
SPACE = ' '
50
51
52
class MailingListSyncScript(LaunchpadCronScript):
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
53
    """
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
54
    %prog [options] source_url
55
56
    Sync the Mailman data structures between production and staging.  This
57
    takes the most efficient route by rsync'ing over the list pickles, raw
58
    archive mboxes, and mhonarc files, then it fixes up anything that needs
59
    fixing.  This does /not/ sync over any qfiles because staging doesn't send
60
    emails anyway.
61
62
    source_url is required and it is the rsync source url which contains
63
    mailman's var directory.  The destination is taken from the launchpad.conf
64
    file.
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
65
    """
66
67
    loglevel = logging.INFO
68
    description = 'Sync the Mailman data structures with the database.'
69
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
70
    def __init__(self, name, dbuser=None):
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
71
        self.usage = textwrap.dedent(self.__doc__)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
72
        super(MailingListSyncScript, self).__init__(name, dbuser)
73
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
74
    def add_my_options(self):
75
        """See `LaunchpadScript`."""
76
        # Add optional override of production mailing list host name.  In real
77
        # use, it's always going to be lists.launchpad.net so that makes a
78
        # reasonable default.  Some testing environments may override this.
79
        self.parser.add_option('--hostname', default='lists.launchpad.net',
80
                               help=('The hostname for the production '
81
                                     'mailing list system.  This is used to '
82
                                     'resolve teams which have multiple '
83
                                     'email addresses.'))
84
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
85
    def syncMailmanDirectories(self, source_url):
86
        """Synchronize the Mailman directories.
87
88
        :param source_url: the base url of the source
89
        """
90
        # This can't be done at module global scope.
5723.8.8 by Barry Warsaw
Fix lint issues, either by suppression or fix.
91
        # pylint: disable-msg=F0401
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
92
        from Mailman import mm_cfg
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
93
        # Start by rsync'ing over the entire $vardir/lists, $vardir/archives,
94
        # $vardir/backups, and $vardir/mhonarc directories.  We specifically
95
        # do not rsync the data, locks, logs, qfiles, or spam directories.
5863.9.1 by Curtis Hovey
Updated configs and code to used simple datatypes. Changes may still be needed aftert testing.
96
        destination_url = config.mailman.build_var_dir
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
97
        # Do one rsync for all subdirectories.
98
        rsync_command = [RSYNC_COMMAND]
99
        rsync_command.extend(RSYNC_OPTIONS)
100
        rsync_command.append('--exclude=%s' % mm_cfg.MAILMAN_SITE_LIST)
101
        rsync_command.append('--exclude=%s.mbox' % mm_cfg.MAILMAN_SITE_LIST)
102
        rsync_command.extend(os.path.join(source_url, subdirectory)
103
                             for subdirectory in RSYNC_SUBDIRECTORIES)
104
        rsync_command.append(destination_url)
105
        self.logger.info('executing: %s', SPACE.join(rsync_command))
106
        process = subprocess.Popen(rsync_command,
107
                                   stdout=subprocess.PIPE,
108
                                   stderr=subprocess.PIPE)
109
        stdout, stderr = process.communicate()
110
        if process.returncode == 0:
111
            self.logger.info('%s', stdout)
112
        else:
113
            self.logger.error('rsync command failed with exit status: %s',
114
                              process.returncode)
115
            self.logger.error('STDOUT:\n%s\nSTDERR:\n%s', stdout, stderr)
116
        return process.returncode
117
118
    def fixHostnames(self):
119
        """Fix up the host names in Mailman and the LP database."""
120
        # These can't be done at module global scope.
5723.8.8 by Barry Warsaw
Fix lint issues, either by suppression or fix.
121
        # pylint: disable-msg=F0401
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
122
        from Mailman import Utils
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
123
        from Mailman import mm_cfg
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
124
        from Mailman.MailList import MailList
125
126
        # Grab a couple of useful components.
127
        email_address_set = getUtility(IEmailAddressSet)
128
        mailing_list_set = getUtility(IMailingListSet)
129
130
        # Clean things up per mailing list.
131
        for list_name in Utils.list_names():
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
132
            # Skip the site list.
133
            if list_name == mm_cfg.MAILMAN_SITE_LIST:
134
                continue
135
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
136
            # The first thing to clean up is the mailing list pickles.  There
137
            # are things like host names in some attributes that need to be
138
            # converted.  The following opens a locked list.
139
            mailing_list = MailList(list_name)
140
            try:
141
                mailing_list.host_name = mm_cfg.DEFAULT_EMAIL_HOST
142
                mailing_list.web_page_url = (
143
                    mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST)
144
                mailing_list.Save()
145
            finally:
146
                mailing_list.Unlock()
147
148
            # Patch up the email address for the list in the Launchpad
149
            # database.
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
150
            lp_mailing_list = mailing_list_set.get(list_name)
151
            if lp_mailing_list is None:
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
152
                # We found a mailing list in Mailman that does not exist in
153
                # the Launchpad database.  This can happen if we rsync'd the
154
                # Mailman directories after the lists were created, but we
155
                # copied the LP database /before/ the lists were created.
156
                # If we don't delete the Mailman lists, we won't be able to
157
                # create the mailing lists on staging.
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
158
                self.logger.error('No LP mailing list for: %s', list_name)
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
159
                self.deleteMailmanList(list_name)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
160
                continue
161
5723.8.7 by Barry Warsaw
Add support for testing the mlist-sync.py script, and also merge the changes
162
            # Clean up the team email addresses corresponding to their mailing
163
            # lists.  Note that teams can have two email addresses if they
164
            # have a different contact address.
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
165
            team = getUtility(IPersonSet).getByName(list_name)
166
            mlist_addresses = email_address_set.getByPerson(team)
167
            if mlist_addresses.count() == 0:
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
168
                self.logger.error('No LP email address for: %s', list_name)
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
169
            elif mlist_addresses.count() > 1:
170
                # This team has both a mailing list and a contact address.  We
171
                # only want to change the former, but we need some heuristics
172
                # to figure out which is which.
173
                old_address = '%s@%s' % (list_name, self.options.hostname)
174
                for email_address in mlist_addresses:
175
                    if email_address.email == old_address:
6667.1.2 by Barry Warsaw
Lint cleanups
176
                        new_address = lp_mailing_list.address
177
                        removeSecurityProxy(email_address).email = new_address
178
                        self.logger.info('%s -> %s', old_address, new_address)
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
179
                        break
180
                else:
181
                    self.logger.error('No change to LP email address for: %s',
182
                                      list_name)
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
183
            else:
6667.1.2 by Barry Warsaw
Lint cleanups
184
                email_address = removeSecurityProxy(mlist_addresses[0])
185
                old_address = email_address.email
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
186
                email_address.email = lp_mailing_list.address
6667.1.2 by Barry Warsaw
Lint cleanups
187
                self.logger.info('%s -> %s',
188
                                 old_address, lp_mailing_list.address)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
189
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
190
    def deleteMailmanList(self, list_name):
191
        """Delete all Mailman data structures for `list_name`."""
192
        mailman_bindir = os.path.normpath(os.path.join(
5863.9.7 by Curtis Hovey
Refacortings per review.
193
            configure_prefix(config.mailman.build_prefix), 'bin'))
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
194
        process = subprocess.Popen(('./rmlist', '-a', list_name),
195
                                   stdout=subprocess.PIPE,
196
                                   stderr=subprocess.PIPE,
197
                                   cwd=mailman_bindir)
198
        stdout, stderr = process.communicate()
199
        if process.returncode == 0:
200
            self.logger.info('%s', stdout)
201
        else:
202
            self.logger.error('rmlist command failed with exit status: %s',
203
                              process.returncode)
204
            self.logger.error('STDOUT:\n%s\nSTDERR:\n%s', stdout, stderr)
205
            # Keep going.
206
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
207
    def main(self):
208
        """See `LaunchpadCronScript`."""
209
        source_url = None
210
        if len(self.args) == 0:
211
            self.parser.error('Missing source_url')
212
        elif len(self.args) > 1:
213
            self.parser.error('Too many arguments')
214
        else:
215
            source_url = self.args[0]
216
217
        # We need to get to the Mailman API.  Set up the paths so that Mailman
218
        # can be imported.  This can't be done at module global scope.
5863.9.7 by Curtis Hovey
Refacortings per review.
219
        mailman_path = configure_prefix(config.mailman.build_prefix)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
220
        sys.path.append(mailman_path)
221
222
        retcode = self.syncMailmanDirectories(source_url)
223
        if retcode != 0:
224
            return retcode
225
226
        self.fixHostnames()
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
227
228
        # All done; commit the database changes.
229
        self.txn.commit()
230
        return 0
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
231
232
233
if __name__ == '__main__':
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
234
    script = MailingListSyncScript('scripts.mlist-sync', 'mlist-sync')
235
    status = script.lock_and_run()
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
236
    sys.exit(status)