debexpo_importer.py 22.8 KB
Newer Older
1
#! /usr/bin/env python
Jonny Lamb's avatar
Jonny Lamb committed
2
3
# -*- coding: utf-8 -*-
#
Jonny Lamb's avatar
Jonny Lamb committed
4
#   debexpo-importer — executable script to import new packages
Jonny Lamb's avatar
Jonny Lamb committed
5
6
7
#
#   This file is part of debexpo - http://debexpo.workaround.org
#
Jonny Lamb's avatar
Jonny Lamb committed
8
#   Copyright © 2008 Jonny Lamb <jonny@debian.org>
Jonny Lamb's avatar
Jonny Lamb committed
9
#
10
11
12
13
14
15
#   Permission is hereby granted, free of charge, to any person obtaining a
#   copy of this software and associated documentation files (the "Software"),
#   to deal in the Software without restriction, including without limitation
#   the rights to use, copy, modify, merge, publish, distribute, sublicense,
#   and/or sell copies of the Software, and to permit persons to whom the
#   Software is furnished to do so, subject to the following conditions:
Jonny Lamb's avatar
Jonny Lamb committed
16
#
17
18
#   The above copyright notice and this permission notice shall be included in
#   all copies or substantial portions of the Software.
Jonny Lamb's avatar
Jonny Lamb committed
19
#
20
21
22
23
24
25
26
27
28
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#   DEALINGS IN THE SOFTWARE.

""" Executable script to import new packages. """
29

Jonny Lamb's avatar
Jonny Lamb committed
30
31
32
33
34
35
__author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb'
__license__ = 'MIT'

from optparse import OptionParser
import ConfigParser
36
from datetime import datetime
37
from debian import deb822
Jonny Lamb's avatar
Jonny Lamb committed
38
39
40
import logging
import logging.config
import os
41
import re
Jonny Lamb's avatar
Jonny Lamb committed
42
43
import sys
import shutil
44
from stat import *
45
import pylons
46
import email.utils
Jonny Lamb's avatar
Jonny Lamb committed
47

48
49
from sqlalchemy import exceptions

Christoph Haas's avatar
Christoph Haas committed
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# Horrible imports
from debexpo.model import meta
import debexpo.lib.helpers as h
from debexpo.lib.utils import parse_section, md5sum
from debexpo.lib.email import Email
from debexpo.lib.plugins import Plugins
from debexpo.lib import constants
from pylons import tmpl_context as c
#from pylons import url
from routes.util import url_for as url

# Import model objects
from debexpo.model import meta
from debexpo.model.users import User
from debexpo.model.packages import Package
from debexpo.model.package_versions import PackageVersion
from debexpo.model.source_packages import SourcePackage
from debexpo.model.binary_packages import BinaryPackage
from debexpo.model.package_files import PackageFile
from debexpo.model.package_info import PackageInfo
from debexpo.model.package_subscriptions import PackageSubscription

# Import debexpo modules
from paste.deploy import appconfig
from debexpo.config.environment import load_environment
from debexpo.lib.email import Email
from debexpo.lib.changes import Changes
from debexpo.lib.repository import Repository
from debexpo.lib.plugins import Plugins
79
from debexpo.lib.filesystem import CheckFiles
80
from debexpo.lib.gnupg import GnuPG
Christoph Haas's avatar
Christoph Haas committed
81

Jonny Lamb's avatar
Jonny Lamb committed
82
83
84
log = None

class Importer(object):
85
86
87
88
    """
    Class to handle the package that is uploaded and wants to be imported into the database.
    """

89
    def __init__(self, changes, ini, skip_email, skip_gpg):
90
91
92
93
94
95
96
97
98
99
100
101
        """
        Object constructor. Sets class fields to sane values.

        ``self``
            Object pointer.

        ``changes``
            Name `changes` file to import. This is given from the upload controller.

        ``ini``
            Path to debexpo configuration file. This is given from the upload controller.

102
103
        ``skip_email``
            If this is set to true, send no email.
104
        """
105
        self.changes_file_unqualified = changes
106
        self.ini_file = os.path.abspath(ini)
107
        self.actually_send_email = not bool(skip_email)
108
        self.skip_gpg = skip_gpg
109

110
        self.user_id = None
111
        self.changes = None
112
        self.user = None
Jonny Lamb's avatar
Jonny Lamb committed
113

114
115
116
117
118
119
    def send_email(self, email, *args, **kwargs):
        if self.actually_send_email:
            email.send(*args, **kwargs)
        else:
            logging.info("Skipping email send: %s %s", args, kwargs)

120
121
122
123
124
    @property
    def changes_file(self):
        incoming_dir = pylons.config['debexpo.upload.incoming']
        return os.path.join(incoming_dir, self.changes_file_unqualified)

125
    def _remove_changes(self):
126
127
128
        """
        Removes the `changes` file.
        """
129
130
        if os.path.exists(self.changes_file):
                os.remove(self.changes_file)
131

132
133
134
135
136
137
    def _remove_temporary_files(self):
        if hasattr(self, 'files_to_remove'):
            for file in self.files_to_remove:
                if os.path.exists(file):
                    os.remove(file)

138
    def _remove_files(self):
139
140
141
        """
        Removes all the files uploaded.
        """
142
143
        if hasattr(self, 'files'):
            for file in self.files:
144
145
                if os.path.exists(file):
                    os.remove(file)
146
147

        self._remove_changes()
148
        self._remove_temporary_files()
149

150
    def _fail(self, reason, use_log=True):
151
152
153
154
155
156
157
158
159
160
161
162
163
164
        """
        Fail the upload by sending a reason for failure to the log and then remove all
        uploaded files.

        A package is `fail`ed if there is a problem with debexpo, **not** if there's
        something wrong with the package.

        ``reason``
            String of why it failed.

        ``use_log``
            Whether to use the log. This should only be False when actually loading the log fails.
            In this case, the reason is printed to stderr.
        """
165
        if use_log:
166
            log.critical(reason)
167
168
169
170
        else:
            print >> sys.stderr, reason

        self._remove_files()
171

172
173
        if self.user is not None:
            email = Email('importer_fail_maintainer')
174
            package = self.changes.get('Source', '')
175

176
177

            self.send_email(email, [self.user.email], package=package)
178
179

        email = Email('importer_fail_admin')
180
        self.send_email(email, [pylons.config['debexpo.email']], message=reason)
181

182
183
184
        sys.exit(1)

    def _reject(self, reason):
185
186
187
188
189
190
191
192
193
        """
        Reject the package by sending a reason for failure to the log and then remove all
        uploaded files.

        A package is `reject`ed if there is a problem with the package.

        ``reason``
            String of why it failed.
        """
194
        log.error('Rejected: %s' % reason)
195
196
197

        self._remove_files()

198
        if self.user is not None:
199
            email = Email('importer_reject_maintainer')
200
            package = self.changes.get('Source', '')
201

202
            self.send_email(email, [self.user.email], package=package, message=reason)
203
204
        sys.exit(1)

Jonny Lamb's avatar
Jonny Lamb committed
205
    def _setup_logging(self):
206
207
208
209
        """
        Parse the config file and create the ``log`` object for other methods to log their
        actions.
        """
Jonny Lamb's avatar
Jonny Lamb committed
210
211
212
213
        global log

        # Parse the ini file to validate it
        parser = ConfigParser.ConfigParser()
214
        parser.read(self.ini_file)
Jonny Lamb's avatar
Jonny Lamb committed
215

216
        # Check for the presence of [loggers] in self.ini_file
Jonny Lamb's avatar
Jonny Lamb committed
217
218
219
        if not parser.has_section('loggers'):
            self._fail('Config file does not have [loggers] section', use_log=False)

220
        logging.config.fileConfig(self.ini_file)
Jonny Lamb's avatar
Jonny Lamb committed
221
222
223
224
225
226

        # Use "name.pid" to avoid importer confusions in the logs
        logger_name = 'debexpo.importer.%s' % os.getpid()
        log = logging.getLogger(logger_name)

    def _setup(self):
227
228
229
230
        """
        Set up logging, import pylons/paste/debexpo modules, parse config file, create config
        class and chdir to the incoming directory.
        """
Jonny Lamb's avatar
Jonny Lamb committed
231
        # Look for ini file
232
        if not os.path.isfile(self.ini_file):
Jonny Lamb's avatar
Jonny Lamb committed
233
234
            self._fail('Cannot find ini file')

235
236
        self._setup_logging()

Jonny Lamb's avatar
Jonny Lamb committed
237
        # Import debexpo root directory
238
        sys.path.append(os.path.dirname(self.ini_file))
Jonny Lamb's avatar
Jonny Lamb committed
239
240

        # Initialize Pylons app
241
        conf = appconfig('config:' + self.ini_file)
242
        pylons.config = load_environment(conf.global_conf, conf.local_conf)
Jonny Lamb's avatar
Jonny Lamb committed
243
244

        # Change into the incoming directory
245
246
247
        incoming_dir = pylons.config['debexpo.upload.incoming']
        logging.info("Changing dir to %s", incoming_dir)
        os.chdir(incoming_dir)
Jonny Lamb's avatar
Jonny Lamb committed
248
249

        # Look for the changes file
250
        if not os.path.isfile(self.changes_file):
Jonny Lamb's avatar
Jonny Lamb committed
251
252
            self._fail('Cannot find changes file')

253
    def _create_db_entries(self, qa):
254
255
256
        """
        Create entries in the Database for the package upload.
        """
257
258
259
260

        def _package_description(raw):
            return raw[2:].replace('      - ', ' - ')

261
        log.debug('Creating database entries')
262

263

264
        # Parse component and section from field in changes
265
        component, section = parse_section(self.changes['files'][0]['section'])
266
267

        # Check whether package is already in the database
268
        package_query = meta.session.query(Package).filter_by(name=self.changes['Source'])
269
        if package_query.count() == 1:
270
            log.debug('Package %s already exists in the database' % self.changes['Source'])
271
            package = package_query.one()
272
273
            # Update description to make sure it reflects the latest upload
            package.description = _package_description(self.changes['Description'])
274
        else:
275
            log.debug('Package %s is new to the system' % self.changes['Source'])
276
            package = Package(name=self.changes['Source'], user=self.user)
277
            package.description = _package_description(self.changes['Description'])
278
	    package.needs_sponsor = 0
Christoph Haas's avatar
Christoph Haas committed
279
            meta.session.add(package)
280
281
282
283
284

        # No need to check whether there is the same source name and same version as an existing
        # entry in the database as the upload controller tested whether similar filenames existed
        # in the repository. The only way this would be wrong is if the filename had a different
        # version in than the Version field in changes..
285
286
287
288
289
290

        try:
            closes = self.changes['Closes']
        except KeyError:
            closes = None

291
292
293
294
295
296
        # TODO: fix these magic numbers
        if qa.stop():
            qa_status = 1
        else:
            qa_status = 0

297
        maintainer_matches = re.compile(r'(.*) <(.*)>').match(self.changes['Changed-By'])
298
299
        maintainer = maintainer_matches.group(2)

300
301
        package_version = PackageVersion(package=package, version=self.changes['Version'],
            section=section, distribution=self.changes['Distribution'], qa_status=qa_status,
302
            component=component, priority=self.changes.get_priority(), closes=closes,
303
            uploaded=datetime.now(), maintainer=maintainer)
Christoph Haas's avatar
Christoph Haas committed
304
        meta.session.add(package_version)
305

306
        source_package = SourcePackage(package_version=package_version)
Christoph Haas's avatar
Christoph Haas committed
307
        meta.session.add(source_package)
308

309
        binary_package = None
310
311

        # Add PackageFile objects to the database for each uploaded file
312
        for file in self.files:
313
            filename = os.path.join(self.changes.get_pool_path(), file)
314
315
316
317
318
319
320
            # This exception should be never caught.
            # It implies something went wrong before, as we expect a file which does not exist
            try:
                sum = md5sum(os.path.join(pylons.config['debexpo.repository'], filename))
            except AttributeError as e:
                self._fail("Could not calculate MD5 sum: %s" % (e))

321
            size = os.stat(os.path.join(pylons.config['debexpo.repository'], filename))[ST_SIZE]
322

323
324
325
            # Check for binary or source package file
            if file.endswith('.deb'):
                # Only create a BinaryPackage if there actually binary package files
326
                if binary_package is None:
327
                    binary_package = BinaryPackage(package_version=package_version, arch=file[:-4].split('_')[-1])
Christoph Haas's avatar
Christoph Haas committed
328
                    meta.session.add(binary_package)
329

Christoph Haas's avatar
Christoph Haas committed
330
                meta.session.add(PackageFile(filename=filename, binary_package=binary_package, size=size, md5sum=sum))
331
            else:
Christoph Haas's avatar
Christoph Haas committed
332
                meta.session.add(PackageFile(filename=filename, source_package=source_package, size=size, md5sum=sum))
333

334
335
336
        meta.session.commit()
        log.warning("Finished adding PackageFile objects.")

337
338
        # Add PackageInfo objects to the database for the package_version
        for result in qa.result:
Christoph Haas's avatar
Christoph Haas committed
339
            meta.session.add(PackageInfo(package_version=package_version, from_plugin=result.from_plugin,
340
                outcome=result.outcome, rich_data=result.data, severity=result.severity))
341

342
        # Commit all changes to the database
343
        meta.session.commit()
344
        log.debug('Committed package data to the database')
Jonny Lamb's avatar
Jonny Lamb committed
345

346
        subscribers = meta.session.query(PackageSubscription).filter_by(package=self.changes['Source']).filter(\
Jonny Lamb's avatar
Jonny Lamb committed
347
            PackageSubscription.level <= constants.SUBSCRIPTION_LEVEL_UPLOADS).all()
348

349
350
        if len(subscribers) > 0:
            email = Email('package_uploaded')
351
            self.send_email(email, [s.user.email for s in subscribers], package=self.changes['Source'],
352
                version=self.changes['Version'], user=self.user)
353

354
            log.debug('Sent out package subscription emails')
355

356
357
        # Send success email to uploader
        email = Email('successful_upload')
358
        dsc_url = pylons.config['debexpo.server'] + '/debian/' + self.changes.get_pool_path() + '/' + self.changes.get_dsc()
Christoph Haas's avatar
Christoph Haas committed
359
        rfs_url = pylons.config['debexpo.server'] + url('rfs', packagename=self.changes['Source'])
360
        self.send_email(email, [self.user.email], package=self.changes['Source'],
361
362
            dsc_url=dsc_url, rfs_url=rfs_url)

Jonny Lamb's avatar
Jonny Lamb committed
363
    def main(self):
364
365
366
367
368
369
        """
        Actually start the import of the package.

        Do several environment sanity checks, move files into the right place, and then
        create the database entries for the imported package.
        """
Jonny Lamb's avatar
Jonny Lamb committed
370
371
372
        # Set up importer
        self._setup()

373
        log.debug('Importer started with arguments: %s' % sys.argv[1:])
374
        filecheck = CheckFiles()
375
376
377
378
379
380
381
382
383
384
385
386
        signature = GnuPG()

        # Try parsing the changes file, but fail if there's an error.
        try:
            self.changes = Changes(filename=self.changes_file)
            filecheck.test_files_present(self.changes)
            filecheck.test_md5sum(self.changes)
        except Exception as e:
            self._remove_changes()
            # XXX: The user won't ever see this message. The changes file was
            # invalid, we don't know whom send it to
            self._reject("Your changes file appears invalid. Refusing your upload\n%s" % (e.message))
387

Jonny Lamb's avatar
Jonny Lamb committed
388

389
390

        # Determine user from changed-by field
391
392
        if self.user_id is None:
            maintainer_string = self.changes.get('Changed-By')
393
            log.debug("Determining user from 'Changed-By:' field: %s" % maintainer_string)
394
395
396
397
398
            maintainer_realname, maintainer_email_address = email.utils.parseaddr(maintainer_string)
            log.debug("Changed-By's email address is: %s", maintainer_email_address)
            self.user = meta.session.query(User).filter_by(
                    email=maintainer_email_address).filter_by(verification=None).first()
            if self.user is None:
399
400
                # generate user object, but only to send out reject message
                self.user = User(id=-1, name=maintainer_realname, email=maintainer_email_address)
401
                self._remove_changes()
402
403
404
                self._reject('Couldn\'t find user %s. Exiting.' % self.user.email)
            log.debug("User found in database. Has id: %s", self.user.id)
            self.user_id = self.user.id
405

406
        # Next, find out whether the changes file was signed with a valid signature, if not reject immediately
407
408
        if not self.skip_gpg:
            if not signature.is_signed(self.changes_file):
409
410
                self._remove_changes()
                self._reject('Your upload does not appear to be signed')
411
412
            (gpg_out, gpg_status) = signature.verify_sig_full(self.changes_file)
            if gpg_status != 0:
413
414
                self._remove_changes()
                self._reject('Your upload does not contain a valid signature. Output was:\n%s' % (gpg_out))
415
            log.debug("GPG signature matches user %s" % (self.user.email))
Jonny Lamb's avatar
Jonny Lamb committed
416

417
        self.files = self.changes.get_files()
418
        self.files_to_remove = []
419

420
        distribution = self.changes['Distribution'].lower()
421
        allowed_distributions = ('oldstable', 'stable', 'unstable', 'experimental', 'stable-backports', 'oldstable-backports',
422
            'oldstable-backports-sloppy', 'oldstable-security', 'stable-security', 'testing-security', 'stable-proposed-updates',
423
            'testing-proposed-updates', 'sid', 'wheezy', 'squeeze', 'lenny', 'squeeze-backports', 'lenny-backports',
424
425
            'lenny-security', 'lenny-backports-sloppy', 'lenny-volatile', 'squeeze-security', 'squeeze-updates', 'wheezy-security',
            'unreleased')
426
427
428
        if distribution not in allowed_distributions:
            self._remove_changes()
            self._reject("You are not uploading to one of those Debian distributions: %s" %
429
                (reduce(lambda x,xs: x + " " + xs, allowed_distributions)))
430

431
432
        # Look whether the orig tarball is present, and if not, try and get it from
        # the repository.
433
        (orig, orig_file_found) = filecheck.find_orig_tarball(self.changes)
434
        if orig_file_found != constants.ORIG_TARBALL_LOCATION_LOCAL:
435
            log.debug("Upload does not contain orig.tar.gz - trying to find it elsewhere")
436
        if orig and orig_file_found == constants.ORIG_TARBALL_LOCATION_REPOSITORY:
437
            filename = os.path.join(pylons.config['debexpo.repository'],
438
439
                self.changes.get_pool_path(), orig)
            if os.path.isfile(filename):
440
                log.debug("Found tar.gz in repository as %s" % (filename))
441
                shutil.copy(filename, pylons.config['debexpo.upload.incoming'])
442
443
444
445
                # We need the orig.tar.gz for the import run, plugins need to extract the source package
                # also Lintian needs it. However the orig.tar.gz is in the repository already, so we can
                # remove it later
                self.files_to_remove.append(orig)
446

447
        destdir = pylons.config['debexpo.repository']
448

449

450
        # Check whether the files are already present
Christoph Haas's avatar
Christoph Haas committed
451
        log.debug("Checking whether files are already in the repository")
452
453
        toinstall = []
        pool_dir = os.path.join(destdir, self.changes.get_pool_path())
454
        log.debug("Pool directory: %s", pool_dir)
455
        for file in self.files:
456
            if os.path.isfile(file) and os.path.isfile(os.path.join(pool_dir, file)):
457
                log.warning('%s is being installed even though it already exists' % file)
458
459
                toinstall.append(file)
            elif os.path.isfile(file):
460
                log.debug('File %s is safe to install' % os.path.join(pool_dir, file))
461
462
463
                toinstall.append(file)
            # skip another corner case, where the dsc contains a orig.tar.gz but wasn't uploaded
            # by doing nothing here for that case
464
465
466
467
468
469
470
471
472

        # Run post-upload plugins.
        post_upload = Plugins('post-upload', self.changes, self.changes_file,
            user_id=self.user_id)
        if post_upload.stop():
            log.critical('post-upload plugins failed')
            self._remove_changes()
            sys.exit(1)

473
        # Check whether a post-upload plugin has got the orig tarball from somewhere.
474
        if not orig_file_found and not filecheck.is_native_package(self.changes):
475
            (orig, orig_file_found) = filecheck.find_orig_tarball(self.changes)
476
            if orig_file_found == constants.ORIG_TARBALL_LOCATION_NOT_FOUND:
477
                # When coming here it means:
478
                # a) The uploader did not include a orig.tar.gz in his upload
479
480
481
                # b) We couldn't find a orig.tar.gz in our repository
                # c) No plugin could get the orig.tar.gz
                # ... time to give up
482
                self._remove_changes()
483
484
                if orig == None:
                    orig = "any original tarball (orig.tar.gz)"
485
486
487
                self._reject("Rejecting incomplete upload. "
                    "You did not upload %s and we didn't find it on any of our alternative resources.\n" \
                    "If you tried to upload a package which only increased the Debian revision part, make sure you include the full source (pass -sa to dpkg-buildpackage)" %
488
                    ( orig ))
489
490
            else:
                toinstall.append(orig)
491

Jonny Lamb's avatar
Jonny Lamb committed
492
        # Check whether the debexpo.repository variable is set
493
        if 'debexpo.repository' not in pylons.config:
Jonny Lamb's avatar
Jonny Lamb committed
494
495
496
            self._fail('debexpo.repository not set')

        # Check whether debexpo.repository is a directory
497
        if not os.path.isdir(pylons.config['debexpo.repository']):
Jonny Lamb's avatar
Jonny Lamb committed
498
499
500
            self._fail('debexpo.repository is not a directory')

        # Check whether debexpo.repository is writeable
501
        if not os.access(pylons.config['debexpo.repository'], os.W_OK):
Jonny Lamb's avatar
Jonny Lamb committed
502
503
            self._fail('debexpo.repository is not writeable')

504
        qa = Plugins('qa', self.changes, self.changes_file, user_id=self.user_id)
Jonny Lamb's avatar
Jonny Lamb committed
505
506
        if qa.stop():
            self._reject('QA plugins failed the package')
Jonny Lamb's avatar
Jonny Lamb committed
507

508
509
        # Loop through parent directories in the target installation directory to make sure they
        # all exist. If not, create them.
510
        for dir in self.changes.get_pool_path().split('/'):
511
            destdir = os.path.join(destdir, dir)
512

513
            if not os.path.isdir(destdir):
514
                log.debug('Creating directory: %s' % destdir)
515
                os.mkdir(destdir)
Jonny Lamb's avatar
Jonny Lamb committed
516
517

        # Install files in repository
518
        for file in toinstall:
519
            log.debug("Installing new file %s" % (file))
520
            shutil.move(file, os.path.join(destdir, file))
Jonny Lamb's avatar
Jonny Lamb committed
521

522
        self._remove_temporary_files()
Jonny Lamb's avatar
Jonny Lamb committed
523
        # Create the database rows
524
        self._create_db_entries(qa)
Jonny Lamb's avatar
Jonny Lamb committed
525

526
527
528
529
530
531
532
        # Execute post-successful-upload plugins
        f = open(self.changes_file)
        changes_contents = f.read()
        f.close()
        Plugins('post-successful-upload', self.changes, self.changes_file,
            changes_contents=changes_contents)

533
        # Remove the changes file
Jonny Lamb's avatar
Jonny Lamb committed
534
535
        self._remove_changes()

536
        # Refresh the Sources/Packages files.
537
        log.debug('Updating Sources and Packages files')
538
        r = Repository(pylons.config['debexpo.repository'])
539
540
        r.update()

541
        log.debug('Done')
542

Asheesh Laroia's avatar
Asheesh Laroia committed
543
def main():
544
    parser = OptionParser(usage="%prog -c FILE -i FILE [--skip-email] [--skip-gpg-check]")
Jonny Lamb's avatar
Jonny Lamb committed
545
546
547
548
549
550
    parser.add_option('-c', '--changes', dest='changes',
                      help='Path to changes file to import',
                      metavar='FILE', default=None)
    parser.add_option('-i', '--ini', dest='ini',
                      help='Path to application ini file',
                      metavar='FILE', default=None)
Asheesh Laroia's avatar
Asheesh Laroia committed
551
    parser.add_option('--skip-email', dest='skip_email',
552
                      action="store_true", help="Skip sending emails")
553
554
    parser.add_option('--skip-gpg-check', dest='skip_gpg',
                      action="store_true", help="Skip the GPG signedness check")
Jonny Lamb's avatar
Jonny Lamb committed
555
556
557

    (options, args) = parser.parse_args()

558
    if not options.changes or not options.ini:
Jonny Lamb's avatar
Jonny Lamb committed
559
560
561
        parser.print_help()
        sys.exit(0)

562
    i = Importer(options.changes, options.ini, options.skip_email, options.skip_gpg)
Jonny Lamb's avatar
Jonny Lamb committed
563
564

    i.main()
Asheesh Laroia's avatar
Asheesh Laroia committed
565
    return 0
566
567
568

if __name__=='__main__':
    main()