blob: f8e1782f168e366680969b95f24fc05b717df5c2 [file] [log] [blame]
Kuang-che Wu41e8b592018-09-25 17:01:30 +08001#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
3# Copyright 2018 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Helper script to prepare source trees for ChromeOS bisection.
7
8Typical usage:
9
10 Initial setup:
11 $ %(prog)s init --chromeos
12 $ %(prog)s init --chrome
13 $ %(prog)s init --android=pi-arc-dev
14
15 Sync code if necessary:
16 $ %(prog)s sync
17
18 Create source trees for bisection
19 $ %(prog)s new --session=12345
20
21 After bisection finished, delete trees
22 $ %(prog)s delete --session=12345
23"""
24from __future__ import print_function
25import argparse
26import csv
27import logging
28import os
Kuang-che Wu67be74b2018-10-15 14:17:26 +080029import time
Kuang-che Wu41e8b592018-09-25 17:01:30 +080030import urllib2
Kuang-che Wu67be74b2018-10-15 14:17:26 +080031import urlparse
Kuang-che Wu41e8b592018-09-25 17:01:30 +080032
33from bisect_kit import common
34from bisect_kit import configure
35from bisect_kit import gclient_util
36from bisect_kit import git_util
Kuang-che Wudc714412018-10-17 16:06:39 +080037from bisect_kit import locking
Kuang-che Wu41e8b592018-09-25 17:01:30 +080038from bisect_kit import repo_util
39from bisect_kit import util
40
41DEFAULT_MIRROR_BASE = os.path.expanduser('~/git-mirrors')
42DEFAULT_WORK_BASE = os.path.expanduser('~/bisect-workdir')
43CHECKOUT_TEMPLATE_NAME = 'template'
44
45logger = logging.getLogger(__name__)
46
47
48class DefaultProjectPathFactory(object):
49 """Factory for chromeos/chrome/android source tree paths."""
50
51 def __init__(self, mirror_base, work_base, session):
52 self.mirror_base = mirror_base
53 self.work_base = work_base
54 self.session = session
55
56 def get_chromeos_mirror(self):
57 return os.path.join(self.mirror_base, 'chromeos')
58
59 def get_chromeos_tree(self):
60 return os.path.join(self.work_base, self.session, 'chromeos')
61
62 def get_android_mirror(self, branch):
63 return os.path.join(self.mirror_base, 'android.%s' % branch)
64
65 def get_android_tree(self, branch):
66 return os.path.join(self.work_base, self.session, 'android.%s' % branch)
67
68 def get_chrome_cache(self):
69 return os.path.join(self.mirror_base, 'chrome')
70
71 def get_chrome_tree(self):
72 return os.path.join(self.work_base, self.session, 'chrome')
73
74
75def subvolume_or_makedirs(opts, path):
76 if os.path.exists(path):
77 return
78
79 path = os.path.abspath(path)
80 if opts.btrfs:
81 dirname, basename = os.path.split(path)
82 if not os.path.exists(dirname):
83 os.makedirs(dirname)
84 util.check_call('btrfs', 'subvolume', 'create', basename, cwd=dirname)
85 else:
86 os.makedirs(path)
87
88
89def is_btrfs_subvolume(path):
90 if util.check_output('stat', '-f', '--format=%T', path).strip() != 'btrfs':
91 return False
92 return util.check_output('stat', '--format=%i', path).strip() == '256'
93
94
95def snapshot_or_copytree(src, dst):
96 assert os.path.isdir(src), '%s does not exist' % src
97 assert os.path.isdir(os.path.dirname(dst))
98
99 # Make sure dst do not exist, otherwise it becomes "dst/name" (one extra
100 # depth) instead of "dst".
101 assert not os.path.exists(dst)
102
103 if is_btrfs_subvolume(src):
104 util.check_call('btrfs', 'subvolume', 'snapshot', src, dst)
105 else:
106 # -a for recursion and preserve all attributes.
107 util.check_call('cp', '-a', src, dst)
108
109
Kuang-che Wubfa64482018-10-16 11:49:49 +0800110def collect_removed_manifest_repos(repo_dir, last_sync_time, only_branch=None):
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800111 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu16cd6e22018-10-17 12:06:12 +0800112 util.check_call('git', 'fetch', cwd=manifest_dir)
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800113
114 manifest_path = 'default.xml'
115 manifest_full_path = os.path.join(manifest_dir, manifest_path)
116 # hack for chromeos symlink
117 if os.path.islink(manifest_full_path):
118 manifest_path = os.readlink(manifest_full_path)
119
120 parser = repo_util.ManifestParser(manifest_dir)
121 latest = None
122 removed = {}
123 for _, git_rev in reversed(
124 parser.enumerate_manifest_commits(last_sync_time, None, manifest_path)):
125 root = parser.parse_xml_recursive(git_rev, manifest_path)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800126 if (only_branch and root.find('default') is not None and
127 root.find('default').get('revision') != only_branch):
128 break
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800129 entries = parser.process_parsed_result(root)
130 if latest is None:
131 assert entries is not None
132 latest = entries
133 continue
134
135 for path, path_spec in entries.items():
136 if path in latest:
137 continue
138 if path in removed:
139 continue
140 removed[path] = path_spec
141
142 return removed
143
144
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800145def setup_chromeos_repos(opts, path_factory):
146 chromeos_mirror = path_factory.get_chromeos_mirror()
147 chromeos_tree = path_factory.get_chromeos_tree()
148 subvolume_or_makedirs(opts, chromeos_mirror)
149 subvolume_or_makedirs(opts, chromeos_tree)
150
151 manifest_url = (
152 'https://chrome-internal.googlesource.com/chromeos/manifest-internal')
153 repo_url = 'https://chromium.googlesource.com/external/repo.git'
154
155 if os.path.exists(os.path.join(chromeos_mirror, '.repo', 'manifests')):
156 logger.warning(
157 '%s has already been initialized, assume it is setup properly',
158 chromeos_mirror)
159 else:
160 logger.info('repo init for chromeos mirror')
161 repo_util.init(
162 chromeos_mirror,
163 manifest_url=manifest_url,
164 repo_url=repo_url,
165 mirror=True)
166
167 local_manifest_dir = os.path.join(chromeos_mirror, '.repo',
168 'local_manifests')
169 os.mkdir(local_manifest_dir)
170 with open(os.path.join(local_manifest_dir, 'manifest-versions.xml'),
171 'w') as f:
172 f.write('''<?xml version="1.0" encoding="UTF-8"?>
173 <manifest>
174 <project name="chromeos/manifest-versions" remote="cros-internal" />
175 </manifest>
176 ''')
177
178 logger.info('repo init for chromeos tree')
179 repo_util.init(
180 chromeos_tree,
181 manifest_url=manifest_url,
182 repo_url=repo_url,
183 reference=chromeos_mirror)
184
Kuang-che Wudc714412018-10-17 16:06:39 +0800185 with locking.lock_file(
186 os.path.join(chromeos_mirror, locking.LOCK_FILE_FOR_MIRROR_SYNC)):
187 logger.info('repo sync for chromeos mirror (this takes hours; be patient)')
188 repo_util.sync(chromeos_mirror)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800189
190 logger.info('repo sync for chromeos tree')
191 repo_util.sync(chromeos_tree)
192
193
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800194def read_last_sync_time(repo_dir):
195 timestamp_path = os.path.join(repo_dir, 'last_sync_time')
196 if os.path.exists(timestamp_path):
197 with open(timestamp_path) as f:
198 return int(f.read())
199 else:
200 # 4 months should be enough for most bisect cases.
201 return int(time.time()) - 86400 * 120
202
203
204def write_sync_time(repo_dir, sync_time):
205 timestamp_path = os.path.join(repo_dir, 'last_sync_time')
206 with open(timestamp_path, 'w') as f:
207 f.write('%d\n' % sync_time)
208
209
210def write_extra_manifest_to_mirror(repo_dir, removed):
211 local_manifest_dir = os.path.join(repo_dir, '.repo', 'local_manifests')
212 if not os.path.exists(local_manifest_dir):
213 os.mkdir(local_manifest_dir)
214 with open(os.path.join(local_manifest_dir, 'deleted-repos.xml'), 'w') as f:
215 f.write('''<?xml version="1.0" encoding="UTF-8"?>\n<manifest>\n''')
216 remotes = {}
217 for path_spec in removed.values():
218 scheme, netloc, remote_path = urlparse.urlsplit(path_spec.repo_url)[:3]
219 assert remote_path[0] == '/'
220 remote_path = remote_path[1:]
221 if (scheme, netloc) not in remotes:
222 remote_name = 'remote_for_deleted_repo_%s' % (scheme + netloc)
223 remotes[scheme, netloc] = remote_name
224 f.write(''' <remote name="%s" fetch="%s" />\n''' %
225 (remote_name, '%s://%s' % (scheme, netloc)))
Kuang-che Wubfa64482018-10-16 11:49:49 +0800226 f.write(
227 ''' <project name="%s" path="%s" remote="%s" revision="%s" />\n''' %
228 (remote_path, path_spec.path, remotes[scheme, netloc], path_spec.at))
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800229 f.write('''</manifest>\n''')
230
231
Kuang-che Wubfa64482018-10-16 11:49:49 +0800232def generate_extra_manifest_for_deleted_repo(repo_dir, only_branch=None):
233 last_sync_time = read_last_sync_time(repo_dir)
234 removed = collect_removed_manifest_repos(
235 repo_dir, last_sync_time, only_branch=only_branch)
236 write_extra_manifest_to_mirror(repo_dir, removed)
237 logger.info('since last sync, %d repo got removed', len(removed))
238
239
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800240def sync_chromeos_code(opts, path_factory):
241 del opts # unused
242
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800243 start_sync_time = int(time.time())
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800244 chromeos_mirror = path_factory.get_chromeos_mirror()
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800245
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800246 logger.info('repo sync for chromeos mirror')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800247 generate_extra_manifest_for_deleted_repo(chromeos_mirror)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800248 repo_util.sync(chromeos_mirror)
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800249 write_sync_time(chromeos_mirror, start_sync_time)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800250
251 logger.info('repo sync for chromeos tree')
252 chromeos_tree = path_factory.get_chromeos_tree()
253 repo_util.sync(chromeos_tree)
254
255 # test_that may use this ssh key and ssh complains its permission is too open
256 util.check_call(
257 'chmod',
258 'o-r,g-r',
259 'src/scripts/mod_for_test_scripts/ssh_keys/testing_rsa',
260 cwd=chromeos_tree)
261
262
263def query_chrome_latest_branch():
264 result = None
265 r = urllib2.urlopen('https://omahaproxy.appspot.com/all')
266 for row in csv.DictReader(r):
267 if row['true_branch'].isdigit():
268 result = max(result, int(row['true_branch']))
269 return result
270
271
272def setup_chrome_repos(opts, path_factory):
273 chrome_cache = path_factory.get_chrome_cache()
274 subvolume_or_makedirs(opts, chrome_cache)
275 chrome_tree = path_factory.get_chrome_tree()
276 subvolume_or_makedirs(opts, chrome_tree)
277
278 latest_branch = query_chrome_latest_branch()
279 logger.info('latest chrome branch is %d', latest_branch)
280 assert latest_branch
281 spec = '''
282solutions = [
283 { "name" : "buildspec",
284 "url" : "https://chrome-internal.googlesource.com/a/chrome/tools/buildspec.git",
285 "deps_file" : "branches/%d/DEPS",
286 "custom_deps" : {
287 },
288 "custom_vars": {'checkout_src_internal': True},
289 },
290]
291target_os = ['chromeos']
292cache_dir = %r
293''' % (latest_branch, chrome_cache)
294
Kuang-che Wudc714412018-10-17 16:06:39 +0800295 with locking.lock_file(
296 os.path.join(chrome_cache, locking.LOCK_FILE_FOR_MIRROR_SYNC)):
297 logger.info('gclient config for chrome')
298 gclient_util.config(chrome_tree, spec=spec)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800299
Kuang-che Wudc714412018-10-17 16:06:39 +0800300 is_first_sync = not os.listdir(chrome_cache)
301 if is_first_sync:
302 logger.info('gclient sync for chrome (this takes hours; be patient)')
303 else:
304 logger.info('gclient sync for chrome')
305 gclient_util.sync(
306 chrome_tree, with_branch_heads=True, with_tags=True, ignore_locks=True)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800307
Kuang-che Wudc714412018-10-17 16:06:39 +0800308 # It's possible that some repos are removed from latest branch and thus
309 # their commit history is not fetched in recent gclient sync. So we call
310 # 'git fetch' for all existing git mirrors.
311 # TODO(kcwu): only sync repos not in DEPS files of latest branch
312 logger.info('additional sync for chrome mirror')
313 for git_repo_name in os.listdir(chrome_cache):
314 # another gclient is running or leftover of previous run; skip
315 if git_repo_name.startswith('_cache_tmp'):
316 continue
317 git_repo = os.path.join(chrome_cache, git_repo_name)
318 if not git_util.is_git_rev(git_repo):
319 continue
320 util.check_call('git', 'fetch', cwd=git_repo)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800321
322
323def sync_chrome_code(opts, path_factory):
324 # The sync step is identical to the initial gclient config step.
325 setup_chrome_repos(opts, path_factory)
326
327
328def setup_android_repos(opts, path_factory, branch):
329 android_mirror = path_factory.get_android_mirror(branch)
330 android_tree = path_factory.get_android_tree(branch)
331 subvolume_or_makedirs(opts, android_mirror)
332 subvolume_or_makedirs(opts, android_tree)
333
334 manifest_url = ('persistent-https://googleplex-android.git.corp.google.com'
335 '/platform/manifest')
336 repo_url = 'https://gerrit.googlesource.com/git-repo'
337
338 if os.path.exists(os.path.join(android_mirror, '.repo', 'manifests')):
339 logger.warning(
340 '%s has already been initialized, assume it is setup properly',
341 android_mirror)
342 else:
343 logger.info('repo init for android mirror branch=%s', branch)
344 repo_util.init(
345 android_mirror,
346 manifest_url=manifest_url,
347 repo_url=repo_url,
348 manifest_branch=branch,
349 mirror=True)
350
351 logger.info('repo init for android tree branch=%s', branch)
352 repo_util.init(
353 android_tree,
354 manifest_url=manifest_url,
355 repo_url=repo_url,
356 manifest_branch=branch,
357 reference=android_mirror)
358
359 logger.info('repo sync for android mirror (this takes hours; be patient)')
360 repo_util.sync(android_mirror, current_branch=True)
361
362 logger.info('repo sync for android tree branch=%s', branch)
363 repo_util.sync(android_tree, current_branch=True)
364
365
366def sync_android_code(opts, path_factory, branch):
367 del opts # unused
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800368 start_sync_time = int(time.time())
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800369 android_mirror = path_factory.get_android_mirror(branch)
370 android_tree = path_factory.get_android_tree(branch)
371
Kuang-che Wudc714412018-10-17 16:06:39 +0800372 with locking.lock_file(
373 os.path.join(android_mirror, locking.LOCK_FILE_FOR_MIRROR_SYNC)):
374 logger.info('repo sync for android mirror branch=%s', branch)
375 # Android usually big jump between milestone releases and add/delete lots of
376 # repos when switch releases. Because it's infeasible to bisect between such
377 # big jump, the deleted repo is useless. In order to save disk, do not sync
378 # repos deleted in other branches.
379 generate_extra_manifest_for_deleted_repo(android_mirror, only_branch=branch)
380 repo_util.sync(android_mirror, current_branch=True)
381 write_sync_time(android_mirror, start_sync_time)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800382
383 logger.info('repo sync for android tree branch=%s', branch)
384 repo_util.sync(android_tree, current_branch=True)
385
386
387def cmd_init(opts):
388 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
389 CHECKOUT_TEMPLATE_NAME)
390
391 if opts.chromeos:
392 setup_chromeos_repos(opts, path_factory)
393 if opts.chrome:
394 setup_chrome_repos(opts, path_factory)
395 for branch in opts.android:
396 setup_android_repos(opts, path_factory, branch)
397
398
399def enumerate_android_branches_available(base):
400 branches = []
401 for name in os.listdir(base):
402 if name.startswith('android.'):
403 branches.append(name.partition('.')[2])
404 return branches
405
406
407def cmd_sync(opts):
408 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
409 CHECKOUT_TEMPLATE_NAME)
410
411 sync_all = False
412 if not opts.chromeos and not opts.chrome and not opts.android:
413 logger.info('sync trees for all')
414 sync_all = True
415
416 if sync_all or opts.chromeos:
417 sync_chromeos_code(opts, path_factory)
418 if sync_all or opts.chrome:
419 sync_chrome_code(opts, path_factory)
420
421 if sync_all:
422 android_branches = enumerate_android_branches_available(opts.mirror_base)
423 else:
424 android_branches = opts.android
425 for branch in android_branches:
426 sync_android_code(opts, path_factory, branch)
427
428
429def cmd_new(opts):
430 work_dir = os.path.join(opts.work_base, opts.session)
431 if not os.path.exists(work_dir):
432 os.makedirs(work_dir)
433
434 template_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
435 CHECKOUT_TEMPLATE_NAME)
436 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
437 opts.session)
438
439 prepare_all = False
440 if not opts.chromeos and not opts.chrome and not opts.android:
441 logger.info('prepare trees for all')
442 prepare_all = True
443
444 chromeos_template = template_factory.get_chromeos_tree()
445 if (prepare_all and os.path.exists(chromeos_template)) or opts.chromeos:
446 logger.info('prepare tree for chromeos, %s',
447 path_factory.get_chromeos_tree())
448 snapshot_or_copytree(chromeos_template, path_factory.get_chromeos_tree())
449
450 chrome_template = template_factory.get_chrome_tree()
451 if (prepare_all and os.path.exists(chrome_template)) or opts.chrome:
452 logger.info('prepare tree for chrome, %s', path_factory.get_chrome_tree())
453 snapshot_or_copytree(chrome_template, path_factory.get_chrome_tree())
454
455 if prepare_all:
456 android_branches = enumerate_android_branches_available(opts.mirror_base)
457 else:
458 android_branches = opts.android
459 for branch in android_branches:
460 logger.info('prepare tree for android branch=%s, %s', branch,
461 path_factory.get_android_tree(branch))
462 snapshot_or_copytree(
463 template_factory.get_android_tree(branch),
464 path_factory.get_android_tree(branch))
465
466
467def delete_tree(path):
468 if is_btrfs_subvolume(path):
469 util.check_call('btrfs', 'subvolume', 'delete', path)
470 else:
471 util.check_call('rm', '-rf', path)
472
473
474def cmd_list(opts):
475 print('%-20s %s' % ('Session', 'Path'))
476 for name in os.listdir(opts.work_base):
477 if name == CHECKOUT_TEMPLATE_NAME:
478 continue
479 path = os.path.join(opts.work_base, name)
480 print('%-20s %s' % (name, path))
481
482
483def cmd_delete(opts):
484 assert opts.session
485 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
486 opts.session)
487
488 chromeos_tree = path_factory.get_chromeos_tree()
489 if os.path.exists(chromeos_tree):
490 if os.path.exists(os.path.join(chromeos_tree, 'chromite')):
491 # ignore error
492 util.call('cros_sdk', '--unmount', cwd=chromeos_tree)
493 delete_tree(chromeos_tree)
494
495 chrome_tree = path_factory.get_chrome_tree()
496 if os.path.exists(chrome_tree):
497 delete_tree(chrome_tree)
498
499 android_branches = enumerate_android_branches_available(opts.mirror_base)
500 for branch in android_branches:
501 android_tree = path_factory.get_android_tree(branch)
502 if os.path.exists(android_tree):
503 delete_tree(android_tree)
504
505 os.rmdir(os.path.join(opts.work_base, opts.session))
506
507
508def create_parser():
509 parser = argparse.ArgumentParser(
510 formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__)
511 parser.add_argument(
512 '--mirror_base',
513 metavar='MIRROR_BASE',
514 default=configure.get('MIRROR_BASE', DEFAULT_MIRROR_BASE),
515 help='Directory for mirrors (default: %(default)s)')
516 parser.add_argument(
517 '--work_base',
518 metavar='WORK_BASE',
519 default=configure.get('WORK_BASE', DEFAULT_WORK_BASE),
520 help='Directory for bisection working directories (default: %(default)s)')
521 common.add_common_arguments(parser)
522 subparsers = parser.add_subparsers(
523 dest='command', title='commands', metavar='<command>')
524
525 parser_init = subparsers.add_parser(
526 'init', help='Mirror source trees and create template checkout')
527 parser_init.add_argument(
528 '--chrome', action='store_true', help='init chrome mirror and tree')
529 parser_init.add_argument(
530 '--chromeos', action='store_true', help='init chromeos mirror and tree')
531 parser_init.add_argument(
532 '--android',
533 metavar='BRANCH',
534 action='append',
535 default=[],
536 help='init android mirror and tree of BRANCH')
537 parser_init.add_argument(
538 '--btrfs',
539 action='store_true',
540 help='create btrfs subvolume for source tree')
541 parser_init.set_defaults(func=cmd_init)
542
543 parser_sync = subparsers.add_parser(
544 'sync',
545 help='Sync source trees',
546 description='Sync all if no projects are specified '
547 '(--chrome, --chromeos, or --android)')
548 parser_sync.add_argument(
549 '--chrome', action='store_true', help='sync chrome mirror and tree')
550 parser_sync.add_argument(
551 '--chromeos', action='store_true', help='sync chromeos mirror and tree')
552 parser_sync.add_argument(
553 '--android',
554 metavar='BRANCH',
555 action='append',
556 default=[],
557 help='sync android mirror and tree of BRANCH')
558 parser_sync.set_defaults(func=cmd_sync)
559
560 parser_new = subparsers.add_parser(
561 'new',
562 help='Create new source checkout for bisect',
563 description='Create for all if no projects are specified '
564 '(--chrome, --chromeos, or --android)')
565 parser_new.add_argument('--session', required=True)
566 parser_new.add_argument(
567 '--chrome', action='store_true', help='create chrome checkout')
568 parser_new.add_argument(
569 '--chromeos', action='store_true', help='create chromeos checkout')
570 parser_new.add_argument(
571 '--android',
572 metavar='BRANCH',
573 action='append',
574 default=[],
575 help='create android checkout of BRANCH')
576 parser_new.set_defaults(func=cmd_new)
577
578 parser_list = subparsers.add_parser(
579 'list', help='List existing sessions with source checkout')
580 parser_list.set_defaults(func=cmd_list)
581
582 parser_delete = subparsers.add_parser('delete', help='Delete source checkout')
583 parser_delete.add_argument('--session', required=True)
584 parser_delete.set_defaults(func=cmd_delete)
585
586 return parser
587
588
589def main():
590 common.init()
591 parser = create_parser()
592 opts = parser.parse_args()
593 common.config_logging(opts)
594
595 opts.func(opts)
596
597
598if __name__ == '__main__':
599 main()