~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
11882.2.2 by Jonathan Lange
Clear up a heck of a lot of imports from canonical.launchpad.interfaces.
40
from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
41
from lp.registry.interfaces.mailinglist import IMailingListSet
42
from lp.registry.interfaces.person import IPersonSet
11568.1.1 by Curtis Hovey
Move mailman to lp.services. Fixed staging.txt and logging.txt that were broken by
43
from lp.services.mailman.config import configure_prefix
11684.1.2 by Curtis Hovey
Convert mlist-sync to a LaunchpadScript. Refactored the staging test to reliably run.
44
from lp.services.scripts.base import LaunchpadScript
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
45
46
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
47
RSYNC_OPTIONS = ('-avz', '--delete')
48
RSYNC_COMMAND = '/usr/bin/rsync'
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
49
RSYNC_SUBDIRECTORIES = ('archives', 'backups', 'lists', 'mhonarc')
50
SPACE = ' '
51
52
11684.1.2 by Curtis Hovey
Convert mlist-sync to a LaunchpadScript. Refactored the staging test to reliably run.
53
class MailingListSyncScript(LaunchpadScript):
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
54
    """
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
55
    %prog [options] source_url
56
57
    Sync the Mailman data structures between production and staging.  This
58
    takes the most efficient route by rsync'ing over the list pickles, raw
59
    archive mboxes, and mhonarc files, then it fixes up anything that needs
60
    fixing.  This does /not/ sync over any qfiles because staging doesn't send
61
    emails anyway.
62
63
    source_url is required and it is the rsync source url which contains
64
    mailman's var directory.  The destination is taken from the launchpad.conf
65
    file.
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
66
    """
67
68
    loglevel = logging.INFO
69
    description = 'Sync the Mailman data structures with the database.'
70
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
71
    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
72
        self.usage = textwrap.dedent(self.__doc__)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
73
        super(MailingListSyncScript, self).__init__(name, dbuser)
74
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
75
    def add_my_options(self):
76
        """See `LaunchpadScript`."""
77
        # Add optional override of production mailing list host name.  In real
78
        # use, it's always going to be lists.launchpad.net so that makes a
79
        # reasonable default.  Some testing environments may override this.
80
        self.parser.add_option('--hostname', default='lists.launchpad.net',
81
                               help=('The hostname for the production '
82
                                     'mailing list system.  This is used to '
83
                                     'resolve teams which have multiple '
84
                                     'email addresses.'))
85
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
86
    def syncMailmanDirectories(self, source_url):
87
        """Synchronize the Mailman directories.
88
89
        :param source_url: the base url of the source
90
        """
91
        # This can't be done at module global scope.
5723.8.8 by Barry Warsaw
Fix lint issues, either by suppression or fix.
92
        # pylint: disable-msg=F0401
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
93
        from Mailman import mm_cfg
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
94
        # Start by rsync'ing over the entire $vardir/lists, $vardir/archives,
95
        # $vardir/backups, and $vardir/mhonarc directories.  We specifically
96
        # 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.
97
        destination_url = config.mailman.build_var_dir
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
98
        # Do one rsync for all subdirectories.
99
        rsync_command = [RSYNC_COMMAND]
100
        rsync_command.extend(RSYNC_OPTIONS)
101
        rsync_command.append('--exclude=%s' % mm_cfg.MAILMAN_SITE_LIST)
102
        rsync_command.append('--exclude=%s.mbox' % mm_cfg.MAILMAN_SITE_LIST)
103
        rsync_command.extend(os.path.join(source_url, subdirectory)
104
                             for subdirectory in RSYNC_SUBDIRECTORIES)
105
        rsync_command.append(destination_url)
106
        self.logger.info('executing: %s', SPACE.join(rsync_command))
107
        process = subprocess.Popen(rsync_command,
108
                                   stdout=subprocess.PIPE,
109
                                   stderr=subprocess.PIPE)
110
        stdout, stderr = process.communicate()
111
        if process.returncode == 0:
112
            self.logger.info('%s', stdout)
113
        else:
114
            self.logger.error('rsync command failed with exit status: %s',
115
                              process.returncode)
116
            self.logger.error('STDOUT:\n%s\nSTDERR:\n%s', stdout, stderr)
117
        return process.returncode
118
119
    def fixHostnames(self):
120
        """Fix up the host names in Mailman and the LP database."""
121
        # These can't be done at module global scope.
5723.8.8 by Barry Warsaw
Fix lint issues, either by suppression or fix.
122
        # pylint: disable-msg=F0401
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
123
        from Mailman import Utils
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
124
        from Mailman import mm_cfg
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
125
        from Mailman.MailList import MailList
126
127
        # Grab a couple of useful components.
128
        email_address_set = getUtility(IEmailAddressSet)
129
        mailing_list_set = getUtility(IMailingListSet)
130
131
        # Clean things up per mailing list.
132
        for list_name in Utils.list_names():
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
133
            # Skip the site list.
134
            if list_name == mm_cfg.MAILMAN_SITE_LIST:
135
                continue
136
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
137
            # The first thing to clean up is the mailing list pickles.  There
138
            # are things like host names in some attributes that need to be
139
            # converted.  The following opens a locked list.
140
            mailing_list = MailList(list_name)
141
            try:
142
                mailing_list.host_name = mm_cfg.DEFAULT_EMAIL_HOST
143
                mailing_list.web_page_url = (
144
                    mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST)
145
                mailing_list.Save()
146
            finally:
147
                mailing_list.Unlock()
148
149
            # Patch up the email address for the list in the Launchpad
150
            # database.
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
151
            lp_mailing_list = mailing_list_set.get(list_name)
152
            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
153
                # We found a mailing list in Mailman that does not exist in
154
                # the Launchpad database.  This can happen if we rsync'd the
155
                # Mailman directories after the lists were created, but we
156
                # copied the LP database /before/ the lists were created.
157
                # If we don't delete the Mailman lists, we won't be able to
158
                # create the mailing lists on staging.
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
159
                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
160
                self.deleteMailmanList(list_name)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
161
                continue
162
5723.8.7 by Barry Warsaw
Add support for testing the mlist-sync.py script, and also merge the changes
163
            # Clean up the team email addresses corresponding to their mailing
164
            # lists.  Note that teams can have two email addresses if they
165
            # 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
166
            team = getUtility(IPersonSet).getByName(list_name)
167
            mlist_addresses = email_address_set.getByPerson(team)
168
            if mlist_addresses.count() == 0:
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
169
                self.logger.error('No LP email address for: %s', list_name)
12736.14.4 by Curtis Hovey
Added test to verify the teams second email address is not updated by mlist-sync.
170
            else:
171
                # Teams can have both a mailing list and a contact address.
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
172
                old_address = '%s@%s' % (list_name, self.options.hostname)
173
                for email_address in mlist_addresses:
174
                    if email_address.email == old_address:
6667.1.2 by Barry Warsaw
Lint cleanups
175
                        new_address = lp_mailing_list.address
176
                        removeSecurityProxy(email_address).email = new_address
177
                        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
178
                        break
179
                else:
180
                    self.logger.error('No change to LP email address for: %s',
181
                                      list_name)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
182
5719.1.1 by Barry Warsaw
Fix to handle teams with both a mailing list and a contact address (i.e. 2
183
    def deleteMailmanList(self, list_name):
184
        """Delete all Mailman data structures for `list_name`."""
185
        mailman_bindir = os.path.normpath(os.path.join(
5863.9.7 by Curtis Hovey
Refacortings per review.
186
            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
187
        process = subprocess.Popen(('./rmlist', '-a', list_name),
188
                                   stdout=subprocess.PIPE,
189
                                   stderr=subprocess.PIPE,
190
                                   cwd=mailman_bindir)
191
        stdout, stderr = process.communicate()
192
        if process.returncode == 0:
193
            self.logger.info('%s', stdout)
194
        else:
195
            self.logger.error('rmlist command failed with exit status: %s',
196
                              process.returncode)
197
            self.logger.error('STDOUT:\n%s\nSTDERR:\n%s', stdout, stderr)
198
            # Keep going.
199
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
200
    def main(self):
11684.1.2 by Curtis Hovey
Convert mlist-sync to a LaunchpadScript. Refactored the staging test to reliably run.
201
        """See `LaunchpadScript`."""
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
202
        source_url = None
203
        if len(self.args) == 0:
204
            self.parser.error('Missing source_url')
205
        elif len(self.args) > 1:
206
            self.parser.error('Too many arguments')
207
        else:
208
            source_url = self.args[0]
209
210
        # We need to get to the Mailman API.  Set up the paths so that Mailman
211
        # can be imported.  This can't be done at module global scope.
5863.9.7 by Curtis Hovey
Refacortings per review.
212
        mailman_path = configure_prefix(config.mailman.build_prefix)
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
213
        sys.path.append(mailman_path)
214
215
        retcode = self.syncMailmanDirectories(source_url)
216
        if retcode != 0:
217
            return retcode
218
219
        self.fixHostnames()
5655.1.1 by Barry Warsaw
Update the sync script so that it properly handles synchronizing Mailman data
220
221
        # All done; commit the database changes.
222
        self.txn.commit()
223
        return 0
5427.2.2 by Barry Warsaw
A script to be used on staging to synchronize the Mailman data structures with
224
225
226
if __name__ == '__main__':
5655.1.3 by Barry Warsaw
Changes in response to Francis's review:
227
    script = MailingListSyncScript('scripts.mlist-sync', 'mlist-sync')
228
    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
229
    sys.exit(status)