]> git.decadent.org.uk Git - dak.git/blob - dak/admin.py
dak/admin.py: add component management commands
[dak.git] / dak / admin.py
1 #!/usr/bin/env python
2
3 """Configure dak parameters in the database"""
4 # Copyright (C) 2009  Mark Hymers <mhy@debian.org>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 ################################################################################
21
22 import sys
23
24 import apt_pkg
25
26 from daklib import utils
27 from daklib.dbconn import *
28 from sqlalchemy.orm.exc import NoResultFound
29
30 ################################################################################
31
32 dispatch = {}
33 dryrun = False
34
35 ################################################################################
36 def warn(msg):
37     print >> sys.stderr, msg
38
39 def die(msg, exit_code=1):
40     print >> sys.stderr, msg
41     sys.exit(exit_code)
42
43 def die_arglen(args, args_needed, msg):
44     if len(args) < args_needed:
45         die(msg)
46
47 def usage(exit_code=0):
48     """Perform administrative work on the dak database."""
49
50     print """Usage: dak admin COMMAND
51 Perform administrative work on the dak database.
52
53   -h, --help          show this help and exit.
54   -n, --dry-run       don't do anything, just show what would have been done
55                       (only applies to add or rm operations).
56
57   Commands can use a long or abbreviated form:
58
59   config / c:
60      c db                   show db config
61      c db-shell             show db config in a usable form for psql
62      c NAME                 show option NAME as set in configuration table
63
64   keyring / k:
65      k list-all             list all keyrings
66      k list-binary          list all keyrings with a NULL source acl
67      k list-source          list all keyrings with a non NULL source acl
68      k add-buildd NAME ARCH...   add buildd keyring with upload permission
69                                  for the given architectures
70
71   architecture / a:
72      a list                 show a list of architectures
73      a rm ARCH              remove an architecture (will only work if
74                             no longer linked to any suites)
75      a add ARCH DESCRIPTION [SUITELIST]
76                             add architecture ARCH with DESCRIPTION.
77                             If SUITELIST is given, add to each of the
78                             suites at the same time
79
80   component:
81      component list         show a list of components
82      component rm COMPONENT remove a component (will only work if
83                             empty)
84      component add NAME DESCRIPTION ORDERING
85                             add component NAME with DESCRIPTION.
86                             Ordered at ORDERING.
87
88   suite / s:
89      s list                 show a list of suites
90      s show SUITE           show config details for a suite
91      s add SUITE VERSION [ label=LABEL ] [ description=DESCRIPTION ]
92                          [ origin=ORIGIN ] [ codename=CODENAME ]
93                          [ signingkey=SIGNINGKEY ] [ archive=ARCHIVE ]
94                             add suite SUITE, version VERSION.
95                             label, description, origin, codename
96                             and signingkey are optional.
97
98      s add-all-arches SUITE VERSION... as "s add" but adds suite-architecture
99                             relationships for all architectures
100
101   suite-architecture / s-a:
102      s-a list               show the architectures for all suites
103      s-a list-suite ARCH    show the suites an ARCH is in
104      s-a list-arch SUITE    show the architectures in a SUITE
105      s-a add SUITE ARCH     add ARCH to suite
106      s-a rm SUITE ARCH      remove ARCH from suite (will only work if
107                             no packages remain for the arch in the suite)
108
109   archive:
110      archive list           list all archives
111      archive add NAME ROOT DESCRIPTION [primary-mirror=MIRROR] [tainted=1]
112                             add archive NAME with path ROOT,
113                             primary mirror MIRROR.
114      archive rm NAME        remove archive NAME (will only work if there are
115                             no files and no suites in the archive)
116      archive rename OLD NEW rename archive OLD to NEW
117
118   version-check / v-c:
119      v-c list                        show version checks for all suites
120      v-c list-suite SUITE            show version checks for suite SUITE
121      v-c add SUITE CHECK REFERENCE   add a version check for suite SUITE
122      v-c rm SUITE CHECK REFERENCE    remove a version check
123        where
124          CHECK     is one of Enhances, MustBeNewerThan, MustBeOlderThan
125          REFERENCE is another suite name
126 """
127     sys.exit(exit_code)
128
129 ################################################################################
130
131 def __architecture_list(d, args):
132     q = d.session().query(Architecture).order_by(Architecture.arch_string)
133     for j in q.all():
134         # HACK: We should get rid of source from the arch table
135         if j.arch_string == 'source': continue
136         print j.arch_string
137     sys.exit(0)
138
139 def __architecture_add(d, args):
140     die_arglen(args, 4, "E: adding an architecture requires a name and a description")
141     print "Adding architecture %s" % args[2]
142     suites = [str(x) for x in args[4:]]
143     if len(suites) > 0:
144         print "Adding to suites %s" % ", ".join(suites)
145     if not dryrun:
146         try:
147             s = d.session()
148             a = Architecture()
149             a.arch_string = str(args[2]).lower()
150             a.description = str(args[3])
151             s.add(a)
152             for sn in suites:
153                 su = get_suite(sn, s)
154                 if su is not None:
155                     a.suites.append(su)
156                 else:
157                     warn("W: Cannot find suite %s" % su)
158             s.commit()
159         except IntegrityError as e:
160             die("E: Integrity error adding architecture %s (it probably already exists)" % args[2])
161         except SQLAlchemyError as e:
162             die("E: Error adding architecture %s (%s)" % (args[2], e))
163     print "Architecture %s added" % (args[2])
164
165 def __architecture_rm(d, args):
166     die_arglen(args, 3, "E: removing an architecture requires at least a name")
167     print "Removing architecture %s" % args[2]
168     if not dryrun:
169         try:
170             s = d.session()
171             a = get_architecture(args[2].lower(), s)
172             if a is None:
173                 die("E: Cannot find architecture %s" % args[2])
174             s.delete(a)
175             s.commit()
176         except IntegrityError as e:
177             die("E: Integrity error removing architecture %s (suite-arch entries probably still exist)" % args[2])
178         except SQLAlchemyError as e:
179             die("E: Error removing architecture %s (%s)" % (args[2], e))
180     print "Architecture %s removed" % args[2]
181
182 def architecture(command):
183     args = [str(x) for x in command]
184     Cnf = utils.get_conf()
185     d = DBConn()
186
187     die_arglen(args, 2, "E: architecture needs at least a command")
188
189     mode = args[1].lower()
190     if mode == 'list':
191         __architecture_list(d, args)
192     elif mode == 'add':
193         __architecture_add(d, args)
194     elif mode == 'rm':
195         __architecture_rm(d, args)
196     else:
197         die("E: architecture command unknown")
198
199 dispatch['architecture'] = architecture
200 dispatch['a'] = architecture
201
202 ################################################################################
203
204 def component_list():
205     session = DBConn().session()
206     for component in session.query(Component).order_by(Component.component_name):
207         print "{0} ordering={1}".format(component.component_name, component.ordering)
208
209 def component_add(args):
210     (name, description, ordering) = args[0:3]
211
212     attributes = dict(
213         component_name=name,
214         description=description,
215         ordering=ordering,
216         )
217
218     for option in args[3:]:
219         (key, value) = option.split('=')
220         attributes[key] = value
221
222     session = DBConn().session()
223
224     component = Component()
225     for key, value in attributes.iteritems():
226         setattr(component, key, value)
227
228     session.add(component)
229     session.flush()
230
231     if dryrun:
232         session.rollback()
233     else:
234         session.commit()
235
236 def component_rm(name):
237     session = DBConn().session()
238     component = get_component(name, session)
239     session.delete(component)
240     session.flush()
241
242     if dryrun:
243         session.rollback()
244     else:
245         session.commit()
246
247 def component_rename(oldname, newname):
248     session = DBConn().session()
249     component = get_component(oldname, session)
250     component.component_name = newname
251     session.flush()
252
253     if dryrun:
254         session.rollback()
255     else:
256         session.commit()
257
258 def component(command):
259     mode = command[1]
260     if mode == 'list':
261         component_list()
262     elif mode == 'rename':
263         component_rename(command[2], command[3])
264     elif mode == 'add':
265         component_add(command[2:])
266     elif mode == 'rm':
267         component_rm(command[2])
268     else:
269         die("E: component command unknown")
270
271 dispatch['component'] = component
272
273 ################################################################################
274
275 def __suite_list(d, args):
276     s = d.session()
277     for j in s.query(Suite).order_by(Suite.suite_name).all():
278         print j.suite_name
279
280 def __suite_show(d, args):
281     if len(args) < 2:
282         die("E: showing an suite entry requires a suite")
283
284     s = d.session()
285     su = get_suite(args[2].lower())
286     if su is None:
287         die("E: can't find suite entry for %s" % (args[2].lower()))
288
289     print su.details()
290
291 def __suite_add(d, args, addallarches=False):
292     die_arglen(args, 4, "E: adding a suite requires at least a name and a version")
293     suite_name = args[2].lower()
294     version = args[3]
295     rest = args[3:]
296
297     def get_field(field):
298         for varval in args:
299             if varval.startswith(field + '='):
300                 return varval.split('=')[1]
301         return None
302
303     print "Adding suite %s" % suite_name
304     if not dryrun:
305         try:
306             s = d.session()
307             suite = Suite()
308             suite.suite_name = suite_name
309             suite.overridecodename = suite_name
310             suite.version = version
311             suite.label = get_field('label')
312             suite.description = get_field('description')
313             suite.origin = get_field('origin')
314             suite.codename = get_field('codename')
315             signingkey = get_field('signingkey')
316             if signingkey is not None:
317                 suite.signingkeys = [signingkey.upper()]
318             archive_name = get_field('archive')
319             if archive_name is not None:
320                 suite.archive = get_archive(archive_name, s)
321             else:
322                 suite.archive = s.query(Archive).filter(~Archive.archive_name.in_(['build-queues', 'new', 'policy'])).one()
323             suite.srcformats = s.query(SrcFormat).all()
324             s.add(suite)
325             s.flush()
326         except IntegrityError as e:
327             die("E: Integrity error adding suite %s (it probably already exists)" % suite_name)
328         except SQLAlchemyError as e:
329             die("E: Error adding suite %s (%s)" % (suite_name, e))
330     print "Suite %s added" % (suite_name)
331
332     if addallarches:
333         arches = []
334         q = s.query(Architecture).order_by(Architecture.arch_string)
335         for arch in q.all():
336             suite.architectures.append(arch)
337             arches.append(arch.arch_string)
338
339         print "Architectures %s added to %s" % (','.join(arches), suite_name)
340
341     s.commit()
342
343 def __suite_rm(d, args):
344     die_arglen(args, 3, "E: removing a suite requires at least a name")
345     name = args[2]
346     print "Removing suite {0}".format(name)
347     if not dryrun:
348         try:
349             s = d.session()
350             su = get_suite(name.lower())
351             if su is None:
352                 die("E: Cannot find suite {0}".format(name))
353             s.delete(su)
354             s.commit()
355         except IntegrityError as e:
356             die("E: Integrity error removing suite {0} (suite-arch entries probably still exist)".format(name))
357         except SQLAlchemyError as e:
358             die("E: Error removing suite {0} ({1})".format(name, e))
359     print "Suite {0} removed".format(name)
360
361 def suite(command):
362     args = [str(x) for x in command]
363     Cnf = utils.get_conf()
364     d = DBConn()
365
366     die_arglen(args, 2, "E: suite needs at least a command")
367
368     mode = args[1].lower()
369
370     if mode == 'list':
371         __suite_list(d, args)
372     elif mode == 'show':
373         __suite_show(d, args)
374     elif mode == 'rm':
375         __suite_rm(d, args)
376     elif mode == 'add':
377         __suite_add(d, args, False)
378     elif mode == 'add-all-arches':
379         __suite_add(d, args, True)
380     else:
381         die("E: suite command unknown")
382
383 dispatch['suite'] = suite
384 dispatch['s'] = suite
385
386 ################################################################################
387
388 def __suite_architecture_list(d, args):
389     s = d.session()
390     for j in s.query(Suite).order_by(Suite.suite_name):
391         architectures = j.get_architectures(skipsrc = True, skipall = True)
392         print j.suite_name + ': ' + \
393               ', '.join([a.arch_string for a in architectures])
394
395 def __suite_architecture_listarch(d, args):
396     die_arglen(args, 3, "E: suite-architecture list-arch requires a suite")
397     suite = get_suite(args[2].lower(), d.session())
398     if suite is None:
399         die('E: suite %s is invalid' % args[2].lower())
400     a = suite.get_architectures(skipsrc = True, skipall = True)
401     for j in a:
402         print j.arch_string
403
404
405 def __suite_architecture_listsuite(d, args):
406     die_arglen(args, 3, "E: suite-architecture list-suite requires an arch")
407     architecture = get_architecture(args[2].lower(), d.session())
408     if architecture is None:
409         die("E: architecture %s is invalid" % args[2].lower())
410     for j in architecture.suites:
411         print j.suite_name
412
413
414 def __suite_architecture_add(d, args):
415     if len(args) < 3:
416         die("E: adding a suite-architecture entry requires a suite and arch")
417
418     s = d.session()
419
420     suite = get_suite(args[2].lower(), s)
421     if suite is None: die("E: Can't find suite %s" % args[2].lower())
422
423     arch = get_architecture(args[3].lower(), s)
424     if arch is None: die("E: Can't find architecture %s" % args[3].lower())
425
426     if not dryrun:
427         try:
428             suite.architectures.append(arch)
429             s.commit()
430         except IntegrityError as e:
431             die("E: Can't add suite-architecture entry (%s, %s) - probably already exists" % (args[2].lower(), args[3].lower()))
432         except SQLAlchemyError as e:
433             die("E: Can't add suite-architecture entry (%s, %s) - %s" % (args[2].lower(), args[3].lower(), e))
434
435     print "Added suite-architecture entry for %s, %s" % (args[2].lower(), args[3].lower())
436
437
438 def __suite_architecture_rm(d, args):
439     if len(args) < 3:
440         die("E: removing an suite-architecture entry requires a suite and arch")
441
442     s = d.session()
443     if not dryrun:
444         try:
445             suite_name = args[2].lower()
446             suite = get_suite(suite_name, s)
447             if suite is None:
448                 die('E: no such suite %s' % suite_name)
449             arch_string = args[3].lower()
450             architecture = get_architecture(arch_string, s)
451             if architecture not in suite.architectures:
452                 die("E: architecture %s not found in suite %s" % (arch_string, suite_name))
453             suite.architectures.remove(architecture)
454             s.commit()
455         except IntegrityError as e:
456             die("E: Can't remove suite-architecture entry (%s, %s) - it's probably referenced" % (args[2].lower(), args[3].lower()))
457         except SQLAlchemyError as e:
458             die("E: Can't remove suite-architecture entry (%s, %s) - %s" % (args[2].lower(), args[3].lower(), e))
459
460     print "Removed suite-architecture entry for %s, %s" % (args[2].lower(), args[3].lower())
461
462
463 def suite_architecture(command):
464     args = [str(x) for x in command]
465     Cnf = utils.get_conf()
466     d = DBConn()
467
468     die_arglen(args, 2, "E: suite-architecture needs at least a command")
469
470     mode = args[1].lower()
471
472     if mode == 'list':
473         __suite_architecture_list(d, args)
474     elif mode == 'list-arch':
475         __suite_architecture_listarch(d, args)
476     elif mode == 'list-suite':
477         __suite_architecture_listsuite(d, args)
478     elif mode == 'add':
479         __suite_architecture_add(d, args)
480     elif mode == 'rm':
481         __suite_architecture_rm(d, args)
482     else:
483         die("E: suite-architecture command unknown")
484
485 dispatch['suite-architecture'] = suite_architecture
486 dispatch['s-a'] = suite_architecture
487
488 ################################################################################
489
490 def archive_list():
491     session = DBConn().session()
492     for archive in session.query(Archive).order_by(Archive.archive_name):
493         print "{0} path={1} description={2} tainted={3}".format(archive.archive_name, archive.path, archive.description, archive.tainted)
494
495 def archive_add(args):
496     (name, path, description) = args[0:3]
497
498     attributes = dict(
499         archive_name=name,
500         path=path,
501         description=description,
502         )
503
504     for option in args[3:]:
505         (key, value) = option.split('=')
506         attributes[key] = value
507
508     session = DBConn().session()
509
510     archive = Archive()
511     for key, value in attributes.iteritems():
512         setattr(archive, key, value)
513
514     session.add(archive)
515     session.flush()
516
517     if dryrun:
518         session.rollback()
519     else:
520         session.commit()
521
522 def archive_rm(name):
523     session = DBConn().session()
524     archive = get_archive(name, session)
525     session.delete(archive)
526     session.flush()
527
528     if dryrun:
529         session.rollback()
530     else:
531         session.commit()
532
533 def archive_rename(oldname, newname):
534     session = DBConn().session()
535     archive = get_archive(oldname, session)
536     archive.archive_name = newname
537     session.flush()
538
539     if dryrun:
540         session.rollback()
541     else:
542         session.commit()
543
544 def archive(command):
545     mode = command[1]
546     if mode == 'list':
547         archive_list()
548     elif mode == 'rename':
549         archive_rename(command[2], command[3])
550     elif mode == 'add':
551         archive_add(command[2:])
552     elif mode == 'rm':
553         archive_rm(command[2])
554     else:
555         die("E: archive command unknown")
556
557 dispatch['archive'] = archive
558
559 ################################################################################
560
561 def __version_check_list(d):
562     session = d.session()
563     for s in session.query(Suite).order_by(Suite.suite_name):
564         __version_check_list_suite(d, s.suite_name)
565
566 def __version_check_list_suite(d, suite_name):
567     vcs = get_version_checks(suite_name)
568     for vc in vcs:
569         print "%s %s %s" % (suite_name, vc.check, vc.reference.suite_name)
570
571 def __version_check_add(d, suite_name, check, reference_name):
572     suite = get_suite(suite_name)
573     if not suite:
574         die("E: Could not find suite %s." % (suite_name))
575     reference = get_suite(reference_name)
576     if not reference:
577         die("E: Could not find reference suite %s." % (reference_name))
578
579     session = d.session()
580     vc = VersionCheck()
581     vc.suite = suite
582     vc.check = check
583     vc.reference = reference
584     session.add(vc)
585     session.commit()
586
587 def __version_check_rm(d, suite_name, check, reference_name):
588     suite = get_suite(suite_name)
589     if not suite:
590         die("E: Could not find suite %s." % (suite_name))
591     reference = get_suite(reference_name)
592     if not reference:
593         die("E: Could not find reference suite %s." % (reference_name))
594
595     session = d.session()
596     try:
597       vc = session.query(VersionCheck).filter_by(suite=suite, check=check, reference=reference).one()
598       session.delete(vc)
599       session.commit()
600     except NoResultFound:
601       print "W: version-check not found."
602
603 def version_check(command):
604     args = [str(x) for x in command]
605     Cnf = utils.get_conf()
606     d = DBConn()
607
608     die_arglen(args, 2, "E: version-check needs at least a command")
609     mode = args[1].lower()
610
611     if mode == 'list':
612         __version_check_list(d)
613     elif mode == 'list-suite':
614         if len(args) != 3:
615             die("E: version-check list-suite needs a single parameter")
616         __version_check_list_suite(d, args[2])
617     elif mode == 'add':
618         if len(args) != 5:
619             die("E: version-check add needs three parameters")
620         __version_check_add(d, args[2], args[3], args[4])
621     elif mode == 'rm':
622         if len(args) != 5:
623             die("E: version-check rm needs three parameters")
624         __version_check_rm(d, args[2], args[3], args[4])
625     else:
626         die("E: version-check command unknown")
627
628 dispatch['version-check'] = version_check
629 dispatch['v-c'] = version_check
630
631 ################################################################################
632
633 def show_config(command):
634     args = [str(x) for x in command]
635     cnf = utils.get_conf()
636
637     die_arglen(args, 2, "E: config needs at least a command")
638
639     mode = args[1].lower()
640
641     if mode == 'db':
642         connstr = ""
643         if cnf.has_key("DB::Service"):
644             # Service mode
645             connstr = "postgresql://service=%s" % cnf["DB::Service"]
646         elif cnf.has_key("DB::Host"):
647             # TCP/IP
648             connstr = "postgres://%s" % cnf["DB::Host"]
649             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
650                 connstr += ":%s" % cnf["DB::Port"]
651             connstr += "/%s" % cnf["DB::Name"]
652         else:
653             # Unix Socket
654             connstr = "postgres:///%s" % cnf["DB::Name"]
655             if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
656                 connstr += "?port=%s" % cnf["DB::Port"]
657         print connstr
658     elif mode == 'db-shell':
659         e = []
660         if cnf.has_key("DB::Service"):
661             e.append('PGSERVICE')
662             print "PGSERVICE=%s" % cnf["DB::Service"]
663         if cnf.has_key("DB::Name"):
664             e.append('PGDATABASE')
665             print "PGDATABASE=%s" % cnf["DB::Name"]
666         if cnf.has_key("DB::Host"):
667             print "PGHOST=%s" % cnf["DB::Host"]
668             e.append('PGHOST')
669         if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
670             print "PGPORT=%s" % cnf["DB::Port"]
671             e.append('PGPORT')
672         print "export " + " ".join(e)
673     elif mode == 'get':
674         print cnf.get(args[2])
675     else:
676         session = DBConn().session()
677         try:
678             o = session.query(DBConfig).filter_by(name = mode).one()
679             print o.value
680         except NoResultFound:
681             print "W: option '%s' not set" % mode
682
683 dispatch['config'] = show_config
684 dispatch['c'] = show_config
685
686 ################################################################################
687
688 def show_keyring(command):
689     args = [str(x) for x in command]
690     cnf = utils.get_conf()
691
692     die_arglen(args, 2, "E: keyring needs at least a command")
693
694     mode = args[1].lower()
695
696     d = DBConn()
697
698     q = d.session().query(Keyring).filter(Keyring.active == True)
699
700     if mode == 'list-all':
701         pass
702     elif mode == 'list-binary':
703         q = q.join(Keyring.acl).filter(ACL.allow_source == False)
704     elif mode == 'list-source':
705         q = q.join(Keyring.acl).filter(ACL.allow_source == True)
706     else:
707         die("E: keyring command unknown")
708
709     for k in q.all():
710         print k.keyring_name
711
712 def keyring_add_buildd(command):
713     name = command[2]
714     arch_names = command[3:]
715
716     session = DBConn().session()
717     arches = session.query(Architecture).filter(Architecture.arch_string.in_(arch_names))
718
719     acl = ACL()
720     acl.name = 'buildd-{0}'.format('+'.join(arch_names))
721     acl.architectures.update(arches)
722     acl.allow_new = True
723     acl.allow_binary = True
724     acl.allow_binary_only = True
725     acl.allow_hijack = True
726     session.add(acl)
727
728     k = Keyring()
729     k.keyring_name = name
730     k.acl = acl
731     k.priority = 10
732     session.add(k)
733
734     session.commit()
735
736 def keyring(command):
737     if command[1].startswith('list-'):
738         show_keyring(command)
739     elif command[1] == 'add-buildd':
740         keyring_add_buildd(command)
741     else:
742         die("E: keyring command unknown")
743
744 dispatch['keyring'] = keyring
745 dispatch['k'] = keyring
746
747 ################################################################################
748
749 def main():
750     """Perform administrative work on the dak database"""
751     global dryrun
752     Cnf = utils.get_conf()
753     arguments = [('h', "help", "Admin::Options::Help"),
754                  ('n', "dry-run", "Admin::Options::Dry-Run")]
755     for i in [ "help", "dry-run" ]:
756         if not Cnf.has_key("Admin::Options::%s" % (i)):
757             Cnf["Admin::Options::%s" % (i)] = ""
758
759     arguments = apt_pkg.parse_commandline(Cnf, arguments, sys.argv)
760
761     options = Cnf.subtree("Admin::Options")
762     if options["Help"] or len(arguments) < 1:
763         usage()
764     if options["Dry-Run"]:
765         dryrun = True
766
767     subcommand = str(arguments[0])
768
769     if subcommand in dispatch.keys():
770         dispatch[subcommand](arguments)
771     else:
772         die("E: Unknown command")
773
774 ################################################################################
775
776 if __name__ == '__main__':
777     main()