blob: aa38da95f5d97811ea7a711891fb3fcb93a0c5c3 [file] [log] [blame]
You-Cheng Syud5692942018-01-04 14:40:59 +08001#!/usr/bin/env python
Hung-Te Linc772e1a2017-04-14 16:50:50 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""An utility to manipulate GPT on a disk image.
7
8Chromium OS factory software usually needs to access partitions from disk
9images. However, there is no good, lightweight, and portable GPT utility.
10Most Chromium OS systems use `cgpt`, but that's not by default installed on
11Ubuntu. Most systems have parted (GNU) or partx (util-linux-ng) but they have
12their own problems.
13
14For example, when a disk image is resized (usually enlarged for putting more
15resources on stateful partition), GPT table must be updated. However,
16 - `parted` can't repair partition without interactive console in exception
17 handler.
18 - `partx` cannot fix headers nor make changes to partition table.
19 - `cgpt repair` does not fix `LastUsableLBA` so we cannot enlarge partition.
20 - `gdisk` is not installed on most systems.
21
22As a result, we need a dedicated tool to help processing GPT.
23
24This pygpt.py provides a simple and customized implementation for processing
25GPT, as a replacement for `cgpt`.
26"""
27
28
29from __future__ import print_function
30
31import argparse
32import binascii
33import collections
34import logging
35import os
36import struct
37import uuid
38
39
40# The binascii.crc32 returns signed integer, so CRC32 in in struct must be
41# declared as 'signed' (l) instead of 'unsigned' (L).
42# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_.28LBA_1.29
43HEADER_FORMAT = """
44 8s Signature
45 4s Revision
46 L HeaderSize
47 l CRC32
48 4s Reserved
49 Q CurrentLBA
50 Q BackupLBA
51 Q FirstUsableLBA
52 Q LastUsableLBA
53 16s DiskGUID
54 Q PartitionEntriesStartingLBA
55 L PartitionEntriesNumber
56 L PartitionEntrySize
57 l PartitionArrayCRC32
58"""
59
60# http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries
61PARTITION_FORMAT = """
62 16s TypeGUID
63 16s UniqueGUID
64 Q FirstLBA
65 Q LastLBA
66 Q Attributes
67 72s Names
68"""
69
70
71def BuildStructFormatAndNamedTuple(name, description):
72 """Builds the format string for struct and create corresponding namedtuple.
73
74 Args:
75 name: A string for name of the named tuple.
76 description: A string with struct descriptor and attribute name.
77
78 Returns:
79 A pair of (struct_format, namedtuple_class).
80 """
81 elements = description.split()
82 struct_format = '<' + ''.join(elements[::2])
83 tuple_class = collections.namedtuple(name, elements[1::2])
84 return (struct_format, tuple_class)
85
86
87class GPT(object):
88 """A GPT helper class.
89
90 To load GPT from an existing disk image file, use `LoadFromFile`.
91 After modifications were made, use `WriteToFile` to commit changes.
92
93 Attributes:
94 header: A namedtuple of GPT header.
95 partitions: A list of GPT partition entry nametuple.
96 """
97
98 HEADER_FORMAT, HEADER_CLASS = BuildStructFormatAndNamedTuple(
99 'Header', HEADER_FORMAT)
100 PARTITION_FORMAT, PARTITION_CLASS = BuildStructFormatAndNamedTuple(
101 'Partition', PARTITION_FORMAT)
Hung-Te Linf148d322018-04-13 10:24:42 +0800102 DEFAULT_BLOCK_SIZE = 512
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800103 HEADER_SIGNATURE = 'EFI PART'
104 TYPE_GUID_UNUSED = '\x00' * 16
105 TYPE_GUID_MAP = {
106 '00000000-0000-0000-0000-000000000000': 'Unused',
107 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7': 'Linux data',
108 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309': 'ChromeOS kernel',
109 '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC': 'ChromeOS rootfs',
110 '2E0A753D-9E48-43B0-8337-B15192CB1B5E': 'ChromeOS reserved',
111 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3': 'ChromeOS firmware',
112 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': 'EFI System Partition',
113 }
114 TYPE_GUID_LIST_BOOTABLE = [
115 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309', # ChromeOS kernel
116 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B', # EFI System Partition
117 ]
118
119 def __init__(self):
120 self.header = None
121 self.partitions = None
Hung-Te Linf148d322018-04-13 10:24:42 +0800122 self.block_size = self.DEFAULT_BLOCK_SIZE
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800123
124 @staticmethod
125 def GetAttributeSuccess(attrs):
126 return (attrs >> 56) & 1
127
128 @staticmethod
129 def GetAttributeTries(attrs):
130 return (attrs >> 52) & 0xf
131
132 @staticmethod
133 def GetAttributePriority(attrs):
134 return (attrs >> 48) & 0xf
135
136 @staticmethod
137 def NewNamedTuple(base, **dargs):
138 """Builds a new named tuple based on dargs."""
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800139 return base._replace(**dargs)
140
141 @classmethod
142 def ReadHeader(cls, f):
143 return cls.HEADER_CLASS(*struct.unpack(
144 cls.HEADER_FORMAT, f.read(struct.calcsize(cls.HEADER_FORMAT))))
145
146 @classmethod
147 def ReadPartitionEntry(cls, f):
148 return cls.PARTITION_CLASS(*struct.unpack(
149 cls.PARTITION_FORMAT, f.read(struct.calcsize(cls.PARTITION_FORMAT))))
150
151 @classmethod
152 def GetHeaderBlob(cls, header):
153 return struct.pack(cls.HEADER_FORMAT, *header)
154
155 @classmethod
156 def GetHeaderCRC32(cls, header):
157 return binascii.crc32(cls.GetHeaderBlob(cls.NewNamedTuple(header, CRC32=0)))
158
159 @classmethod
160 def GetPartitionsBlob(cls, partitions):
161 return ''.join(struct.pack(cls.PARTITION_FORMAT, *partition)
162 for partition in partitions)
163
164 @classmethod
165 def GetPartitionsCRC32(cls, partitions):
166 return binascii.crc32(cls.GetPartitionsBlob(partitions))
167
168 @classmethod
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800169 def LoadFromFile(cls, image):
170 """Loads a GPT table from give disk image file object.
171
172 Args:
173 image: a string as file path or a file-like object to read from.
174 """
175 if isinstance(image, basestring):
176 with open(image, 'rb') as f:
177 return cls.LoadFromFile(f)
178
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800179 gpt = GPT()
Hung-Te Linf148d322018-04-13 10:24:42 +0800180 # Try DEFAULT_BLOCK_SIZE, then 4K.
181 for block_size in [cls.DEFAULT_BLOCK_SIZE, 4096]:
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800182 image.seek(block_size * 1)
183 header = gpt.ReadHeader(image)
Hung-Te Linf148d322018-04-13 10:24:42 +0800184 if header.Signature == cls.HEADER_SIGNATURE:
185 gpt.block_size = block_size
186 break
187 else:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800188 raise ValueError('Invalid signature in GPT header.')
Hung-Te Linf148d322018-04-13 10:24:42 +0800189
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800190 image.seek(gpt.block_size * header.PartitionEntriesStartingLBA)
191 partitions = [gpt.ReadPartitionEntry(image)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800192 for unused_i in range(header.PartitionEntriesNumber)]
193 gpt.header = header
194 gpt.partitions = partitions
195 return gpt
196
197 def GetValidPartitions(self):
198 """Returns the list of partitions before entry with empty type GUID.
199
200 In partition table, the first entry with empty type GUID indicates end of
201 valid partitions. In most implementations all partitions after that should
202 be zeroed.
203 """
204 for i, p in enumerate(self.partitions):
205 if p.TypeGUID == self.TYPE_GUID_UNUSED:
206 return self.partitions[:i]
207 return self.partitions
208
209 def GetMaxUsedLBA(self):
210 """Returns the max LastLBA from all valid partitions."""
211 return max(p.LastLBA for p in self.GetValidPartitions())
212
213 def GetMinUsedLBA(self):
214 """Returns the min FirstLBA from all valid partitions."""
215 return min(p.FirstLBA for p in self.GetValidPartitions())
216
217 def GetPartitionTableBlocks(self, header=None):
218 """Returns the blocks (or LBA) of partition table from given header."""
219 if header is None:
220 header = self.header
221 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800222 blocks = size / self.block_size
223 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800224 blocks += 1
225 return blocks
226
227 def Resize(self, new_size):
228 """Adjust GPT for a disk image in given size.
229
230 Args:
231 new_size: Integer for new size of disk image file.
232 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800233 old_size = self.block_size * (self.header.BackupLBA + 1)
234 if new_size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800235 raise ValueError('New file size %d is not valid for image files.' %
236 new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800237 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800238 if old_size != new_size:
239 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800240 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800241 else:
242 logging.info('Image size (%d, LBA=%d) not changed.',
243 new_size, new_blocks)
244
245 # Re-calculate all related fields.
246 backup_lba = new_blocks - 1
247 partitions_blocks = self.GetPartitionTableBlocks()
248
249 # To add allow adding more blocks for partition table, we should reserve
250 # same space between primary and backup partition tables and real
251 # partitions.
252 min_used_lba = self.GetMinUsedLBA()
253 max_used_lba = self.GetMaxUsedLBA()
254 primary_reserved = min_used_lba - self.header.PartitionEntriesStartingLBA
255 primary_last_lba = (self.header.PartitionEntriesStartingLBA +
256 partitions_blocks - 1)
257
258 if primary_last_lba >= min_used_lba:
259 raise ValueError('Partition table overlaps partitions.')
260 if max_used_lba + partitions_blocks >= backup_lba:
261 raise ValueError('Partitions overlaps backup partition table.')
262
263 last_usable_lba = backup_lba - primary_reserved - 1
264 if last_usable_lba < max_used_lba:
265 last_usable_lba = max_used_lba
266
267 self.header = self.NewNamedTuple(
268 self.header,
269 BackupLBA=backup_lba,
270 LastUsableLBA=last_usable_lba)
271
272 def GetFreeSpace(self):
273 """Returns the free (available) space left according to LastUsableLBA."""
274 max_lba = self.GetMaxUsedLBA()
275 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800276 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800277
278 def ExpandPartition(self, i):
279 """Expands a given partition to last usable LBA.
280
281 Args:
282 i: Index (0-based) of target partition.
283 """
284 # Assume no partitions overlap, we need to make sure partition[i] has
285 # largest LBA.
286 if i < 0 or i >= len(self.GetValidPartitions()):
287 raise ValueError('Partition index %d is invalid.' % (i + 1))
288 p = self.partitions[i]
289 max_used_lba = self.GetMaxUsedLBA()
290 if max_used_lba > p.LastLBA:
291 raise ValueError('Cannot expand partition %d because it is not the last '
292 'allocated partition.' % (i + 1))
293
294 old_blocks = p.LastLBA - p.FirstLBA + 1
295 p = self.NewNamedTuple(p, LastLBA=self.header.LastUsableLBA)
296 new_blocks = p.LastLBA - p.FirstLBA + 1
297 self.partitions[i] = p
298 logging.warn('Partition NR=%d expanded, size in LBA: %d -> %d.',
299 i + 1, old_blocks, new_blocks)
300
301 def UpdateChecksum(self):
302 """Updates all checksum values in GPT header."""
303 header = self.NewNamedTuple(
304 self.header,
305 CRC32=0,
306 PartitionArrayCRC32=self.GetPartitionsCRC32(self.partitions))
307 self.header = self.NewNamedTuple(
308 header,
309 CRC32=self.GetHeaderCRC32(header))
310
311 def GetBackupHeader(self):
312 """Returns the backup header according to current header."""
313 partitions_starting_lba = (
314 self.header.BackupLBA - self.GetPartitionTableBlocks())
315 header = self.NewNamedTuple(
316 self.header,
317 CRC32=0,
318 BackupLBA=self.header.CurrentLBA,
319 CurrentLBA=self.header.BackupLBA,
320 PartitionEntriesStartingLBA=partitions_starting_lba)
321 return self.NewNamedTuple(
322 header,
323 CRC32=self.GetHeaderCRC32(header))
324
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800325 def WriteToFile(self, image):
326 """Updates partition table in a disk image file.
327
328 Args:
329 image: a string as file path or a file-like object to write into.
330 """
331 if isinstance(image, basestring):
332 with open(image, 'rb+') as f:
333 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800334
335 def WriteData(name, blob, lba):
336 """Writes a blob into given location."""
337 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800338 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800339 image.seek(lba * self.block_size)
340 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800341
342 self.UpdateChecksum()
343 WriteData('GPT Header', self.GetHeaderBlob(self.header),
344 self.header.CurrentLBA)
345 WriteData('GPT Partitions', self.GetPartitionsBlob(self.partitions),
346 self.header.PartitionEntriesStartingLBA)
347 logging.info('Usable LBA: First=%d, Last=%d',
348 self.header.FirstUsableLBA, self.header.LastUsableLBA)
349 backup_header = self.GetBackupHeader()
350 WriteData('Backup Partitions', self.GetPartitionsBlob(self.partitions),
351 backup_header.PartitionEntriesStartingLBA)
352 WriteData('Backup Header', self.GetHeaderBlob(backup_header),
353 backup_header.CurrentLBA)
354
355
356class GPTCommands(object):
357 """Collection of GPT sub commands for command line to use.
358
359 The commands are derived from `cgpt`, but not necessary to be 100% compatible
360 with cgpt.
361 """
362
363 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800364 ('begin', 'beginning sector'),
365 ('size', 'partition size'),
366 ('type', 'type guid'),
367 ('unique', 'unique guid'),
368 ('label', 'label'),
369 ('Successful', 'Successful flag'),
370 ('Tries', 'Tries flag'),
371 ('Priority', 'Priority flag'),
372 ('Legacy', 'Legacy Boot flag'),
373 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800374
375 def __init__(self):
376 pass
377
378 @classmethod
379 def RegisterRepair(cls, p):
380 """Registers the repair command to argparser.
381
382 Args:
383 p: An argparse parser instance.
384 """
385 p.add_argument(
386 '--expand', action='store_true', default=False,
387 help='Expands stateful partition to full disk.')
388 p.add_argument('image_file', type=argparse.FileType('rb+'),
389 help='Disk image file to repair.')
390
391 def Repair(self, args):
392 """Repair damaged GPT headers and tables."""
393 gpt = GPT.LoadFromFile(args.image_file)
394 gpt.Resize(os.path.getsize(args.image_file.name))
395
396 free_space = gpt.GetFreeSpace()
397 if args.expand:
398 if free_space:
399 gpt.ExpandPartition(0)
400 else:
401 logging.warn('- No extra space to expand.')
402 elif free_space:
403 logging.warn('Extra space found (%d, LBA=%d), '
Peter Shihc7156ca2018-02-26 14:46:24 +0800404 'use --expand to expand partitions.',
Hung-Te Linf148d322018-04-13 10:24:42 +0800405 free_space, free_space / gpt.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800406
407 gpt.WriteToFile(args.image_file)
408 print('Disk image file %s repaired.' % args.image_file.name)
409
410 @classmethod
411 def RegisterShow(cls, p):
412 """Registers the repair command to argparser.
413
414 Args:
415 p: An argparse parser instance.
416 """
417 p.add_argument('--numeric', '-n', action='store_true',
418 help='Numeric output only.')
419 p.add_argument('--quick', '-q', action='store_true',
420 help='Quick output.')
421 p.add_argument('--index', '-i', type=int, default=None,
422 help='Show specified partition only, with format args.')
423 for name, help_str in cls.FORMAT_ARGS:
424 # TODO(hungte) Alert if multiple args were specified.
425 p.add_argument('--%s' % name, '-%c' % name[0], action='store_true',
426 help='[format] %s.' % help_str)
427 p.add_argument('image_file', type=argparse.FileType('rb'),
428 help='Disk image file to show.')
429
430
431 def Show(self, args):
432 """Show partition table and entries."""
433
434 def FormatGUID(bytes_le):
435 return str(uuid.UUID(bytes_le=bytes_le)).upper()
436
437 def FormatTypeGUID(p):
438 guid_str = FormatGUID(p.TypeGUID)
439 if not args.numeric:
440 names = gpt.TYPE_GUID_MAP.get(guid_str)
441 if names:
442 return names
443 return guid_str
444
445 def FormatNames(p):
446 return p.Names.decode('utf-16-le').strip('\0')
447
448 def IsBootableType(type_guid):
449 return type_guid in gpt.TYPE_GUID_LIST_BOOTABLE
450
451 def FormatAttribute(attr):
452 if args.numeric:
453 return '[%x]' % (attr >> 48)
454 if attr & 4:
455 return 'legacy_boot=1'
456 return 'priority=%d tries=%d successful=%d' % (
457 gpt.GetAttributePriority(attr),
458 gpt.GetAttributeTries(attr),
459 gpt.GetAttributeSuccess(attr))
460
461 def ApplyFormatArgs(p):
462 if args.begin:
463 return p.FirstLBA
464 elif args.size:
465 return p.LastLBA - p.FirstLBA + 1
466 elif args.type:
467 return FormatTypeGUID(p)
468 elif args.unique:
469 return FormatGUID(p.UniqueGUID)
470 elif args.label:
471 return FormatNames(p)
472 elif args.Successful:
473 return gpt.GetAttributeSuccess(p.Attributes)
474 elif args.Priority:
475 return gpt.GetAttributePriority(p.Attributes)
476 elif args.Tries:
477 return gpt.GetAttributeTries(p.Attributes)
478 elif args.Legacy:
479 raise NotImplementedError
480 elif args.Attribute:
481 return '[%x]' % (p.Attributes >> 48)
482 else:
483 return None
484
485 def IsFormatArgsSpecified():
486 return any(getattr(args, arg[0]) for arg in self.FORMAT_ARGS)
487
488 gpt = GPT.LoadFromFile(args.image_file)
489 fmt = '%12s %11s %7s %s'
490 fmt2 = '%32s %s: %s'
491 header = ('start', 'size', 'part', 'contents')
492
493 if IsFormatArgsSpecified() and args.index is None:
494 raise ValueError('Format arguments must be used with -i.')
495
496 partitions = gpt.GetValidPartitions()
497 if not (args.index is None or 0 < args.index <= len(partitions)):
498 raise ValueError('Invalid partition index: %d' % args.index)
499
500 do_print_gpt_blocks = False
501 if not (args.quick or IsFormatArgsSpecified()):
502 print(fmt % header)
503 if args.index is None:
504 do_print_gpt_blocks = True
505
506 if do_print_gpt_blocks:
507 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
508 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
509 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
510
511 for i, p in enumerate(partitions):
512 if args.index is not None and i != args.index - 1:
513 continue
514
515 if IsFormatArgsSpecified():
516 print(ApplyFormatArgs(p))
517 continue
518
519 type_guid = FormatGUID(p.TypeGUID)
520 print(fmt % (p.FirstLBA, p.LastLBA - p.FirstLBA + 1, i + 1,
521 FormatTypeGUID(p) if args.quick else
522 'Label: "%s"' % FormatNames(p)))
523
524 if not args.quick:
525 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
526 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
527 if args.numeric or IsBootableType(type_guid):
528 print(fmt2 % ('', 'Attr', FormatAttribute(p.Attributes)))
529
530 if do_print_gpt_blocks:
531 f = args.image_file
Hung-Te Linf148d322018-04-13 10:24:42 +0800532 f.seek(gpt.header.BackupLBA * gpt.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800533 backup_header = gpt.ReadHeader(f)
534 print(fmt % (backup_header.PartitionEntriesStartingLBA,
535 gpt.GetPartitionTableBlocks(backup_header), '',
536 'Sec GPT table'))
537 print(fmt % (gpt.header.BackupLBA, 1, '', 'Sec GPT header'))
538
539 def Create(self, args):
540 """Create or reset GPT headers and tables."""
541 del args # Not used yet.
542 raise NotImplementedError
543
544 def Add(self, args):
545 """Add, edit or remove a partition entry."""
546 del args # Not used yet.
547 raise NotImplementedError
548
549 def Boot(self, args):
550 """Edit the PMBR sector for legacy BIOSes."""
551 del args # Not used yet.
552 raise NotImplementedError
553
554 def Find(self, args):
555 """Locate a partition by its GUID."""
556 del args # Not used yet.
557 raise NotImplementedError
558
559 def Prioritize(self, args):
560 """Reorder the priority of all kernel partitions."""
561 del args # Not used yet.
562 raise NotImplementedError
563
564 def Legacy(self, args):
565 """Switch between GPT and Legacy GPT."""
566 del args # Not used yet.
567 raise NotImplementedError
568
569 @classmethod
570 def RegisterAllCommands(cls, subparsers):
571 """Registers all available commands to an argparser subparsers instance."""
572 subcommands = [('show', cls.Show, cls.RegisterShow),
573 ('repair', cls.Repair, cls.RegisterRepair)]
574 for name, invocation, register_command in subcommands:
575 register_command(subparsers.add_parser(name, help=invocation.__doc__))
576
577
578def main():
579 parser = argparse.ArgumentParser(description='GPT Utility.')
580 parser.add_argument('--verbose', '-v', action='count', default=0,
581 help='increase verbosity.')
582 parser.add_argument('--debug', '-d', action='store_true',
583 help='enable debug output.')
584 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
585 GPTCommands.RegisterAllCommands(subparsers)
586
587 args = parser.parse_args()
588 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
589 if args.debug:
590 log_level = logging.DEBUG
591 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
592 level=log_level)
593 commands = GPTCommands()
594 try:
595 getattr(commands, args.command.capitalize())(args)
596 except Exception as e:
597 if args.verbose or args.debug:
598 logging.exception('Failure in command [%s]', args.command)
599 exit('ERROR: %s' % e)
600
601
602if __name__ == '__main__':
603 main()