blob: de4eb9e64463c006055bc53c26a632029b627f55 [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):
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800210 """Returns the max LastLBA from all used partitions."""
211 parts = [p for p in self.partitions if p.TypeGUID != GPT.TYPE_GUID_UNUSED]
212 return (max(p.LastLBA for p in parts)
213 if parts else self.header.FirstUsableLBA - 1)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800214
215 def GetPartitionTableBlocks(self, header=None):
216 """Returns the blocks (or LBA) of partition table from given header."""
217 if header is None:
218 header = self.header
219 size = header.PartitionEntrySize * header.PartitionEntriesNumber
Hung-Te Linf148d322018-04-13 10:24:42 +0800220 blocks = size / self.block_size
221 if size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800222 blocks += 1
223 return blocks
224
225 def Resize(self, new_size):
226 """Adjust GPT for a disk image in given size.
227
228 Args:
229 new_size: Integer for new size of disk image file.
230 """
Hung-Te Linf148d322018-04-13 10:24:42 +0800231 old_size = self.block_size * (self.header.BackupLBA + 1)
232 if new_size % self.block_size:
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800233 raise ValueError('New file size %d is not valid for image files.' %
234 new_size)
Hung-Te Linf148d322018-04-13 10:24:42 +0800235 new_blocks = new_size / self.block_size
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800236 if old_size != new_size:
237 logging.warn('Image size (%d, LBA=%d) changed from %d (LBA=%d).',
Hung-Te Linf148d322018-04-13 10:24:42 +0800238 new_size, new_blocks, old_size, old_size / self.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800239 else:
240 logging.info('Image size (%d, LBA=%d) not changed.',
241 new_size, new_blocks)
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800242 return
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800243
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800244 # Expected location
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800245 backup_lba = new_blocks - 1
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800246 last_usable_lba = backup_lba - self.header.FirstUsableLBA
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800247
Hung-Te Lind3a2e9a2018-04-19 13:07:26 +0800248 if last_usable_lba < self.header.LastUsableLBA:
249 max_used_lba = self.GetMaxUsedLBA()
250 if last_usable_lba < max_used_lba:
251 raise ValueError('Backup partition tables will overlap used partitions')
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800252
253 self.header = self.NewNamedTuple(
254 self.header,
255 BackupLBA=backup_lba,
256 LastUsableLBA=last_usable_lba)
257
258 def GetFreeSpace(self):
259 """Returns the free (available) space left according to LastUsableLBA."""
260 max_lba = self.GetMaxUsedLBA()
261 assert max_lba <= self.header.LastUsableLBA, "Partitions too large."
Hung-Te Linf148d322018-04-13 10:24:42 +0800262 return self.block_size * (self.header.LastUsableLBA - max_lba)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800263
264 def ExpandPartition(self, i):
265 """Expands a given partition to last usable LBA.
266
267 Args:
268 i: Index (0-based) of target partition.
269 """
270 # Assume no partitions overlap, we need to make sure partition[i] has
271 # largest LBA.
272 if i < 0 or i >= len(self.GetValidPartitions()):
273 raise ValueError('Partition index %d is invalid.' % (i + 1))
274 p = self.partitions[i]
275 max_used_lba = self.GetMaxUsedLBA()
276 if max_used_lba > p.LastLBA:
277 raise ValueError('Cannot expand partition %d because it is not the last '
278 'allocated partition.' % (i + 1))
279
280 old_blocks = p.LastLBA - p.FirstLBA + 1
281 p = self.NewNamedTuple(p, LastLBA=self.header.LastUsableLBA)
282 new_blocks = p.LastLBA - p.FirstLBA + 1
283 self.partitions[i] = p
284 logging.warn('Partition NR=%d expanded, size in LBA: %d -> %d.',
285 i + 1, old_blocks, new_blocks)
286
287 def UpdateChecksum(self):
288 """Updates all checksum values in GPT header."""
289 header = self.NewNamedTuple(
290 self.header,
291 CRC32=0,
292 PartitionArrayCRC32=self.GetPartitionsCRC32(self.partitions))
293 self.header = self.NewNamedTuple(
294 header,
295 CRC32=self.GetHeaderCRC32(header))
296
297 def GetBackupHeader(self):
298 """Returns the backup header according to current header."""
299 partitions_starting_lba = (
300 self.header.BackupLBA - self.GetPartitionTableBlocks())
301 header = self.NewNamedTuple(
302 self.header,
303 CRC32=0,
304 BackupLBA=self.header.CurrentLBA,
305 CurrentLBA=self.header.BackupLBA,
306 PartitionEntriesStartingLBA=partitions_starting_lba)
307 return self.NewNamedTuple(
308 header,
309 CRC32=self.GetHeaderCRC32(header))
310
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800311 def WriteToFile(self, image):
312 """Updates partition table in a disk image file.
313
314 Args:
315 image: a string as file path or a file-like object to write into.
316 """
317 if isinstance(image, basestring):
318 with open(image, 'rb+') as f:
319 return self.WriteToFile(f)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800320
321 def WriteData(name, blob, lba):
322 """Writes a blob into given location."""
323 logging.info('Writing %s in LBA %d (offset %d)',
Hung-Te Linf148d322018-04-13 10:24:42 +0800324 name, lba, lba * self.block_size)
Hung-Te Lin6977ae12018-04-17 12:20:32 +0800325 image.seek(lba * self.block_size)
326 image.write(blob)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800327
328 self.UpdateChecksum()
329 WriteData('GPT Header', self.GetHeaderBlob(self.header),
330 self.header.CurrentLBA)
331 WriteData('GPT Partitions', self.GetPartitionsBlob(self.partitions),
332 self.header.PartitionEntriesStartingLBA)
333 logging.info('Usable LBA: First=%d, Last=%d',
334 self.header.FirstUsableLBA, self.header.LastUsableLBA)
335 backup_header = self.GetBackupHeader()
336 WriteData('Backup Partitions', self.GetPartitionsBlob(self.partitions),
337 backup_header.PartitionEntriesStartingLBA)
338 WriteData('Backup Header', self.GetHeaderBlob(backup_header),
339 backup_header.CurrentLBA)
340
341
342class GPTCommands(object):
343 """Collection of GPT sub commands for command line to use.
344
345 The commands are derived from `cgpt`, but not necessary to be 100% compatible
346 with cgpt.
347 """
348
349 FORMAT_ARGS = [
Peter Shihc7156ca2018-02-26 14:46:24 +0800350 ('begin', 'beginning sector'),
351 ('size', 'partition size'),
352 ('type', 'type guid'),
353 ('unique', 'unique guid'),
354 ('label', 'label'),
355 ('Successful', 'Successful flag'),
356 ('Tries', 'Tries flag'),
357 ('Priority', 'Priority flag'),
358 ('Legacy', 'Legacy Boot flag'),
359 ('Attribute', 'raw 16-bit attribute value (bits 48-63)')]
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800360
361 def __init__(self):
362 pass
363
364 @classmethod
365 def RegisterRepair(cls, p):
366 """Registers the repair command to argparser.
367
368 Args:
369 p: An argparse parser instance.
370 """
371 p.add_argument(
372 '--expand', action='store_true', default=False,
373 help='Expands stateful partition to full disk.')
374 p.add_argument('image_file', type=argparse.FileType('rb+'),
375 help='Disk image file to repair.')
376
377 def Repair(self, args):
378 """Repair damaged GPT headers and tables."""
379 gpt = GPT.LoadFromFile(args.image_file)
380 gpt.Resize(os.path.getsize(args.image_file.name))
381
382 free_space = gpt.GetFreeSpace()
383 if args.expand:
384 if free_space:
385 gpt.ExpandPartition(0)
386 else:
387 logging.warn('- No extra space to expand.')
388 elif free_space:
389 logging.warn('Extra space found (%d, LBA=%d), '
Peter Shihc7156ca2018-02-26 14:46:24 +0800390 'use --expand to expand partitions.',
Hung-Te Linf148d322018-04-13 10:24:42 +0800391 free_space, free_space / gpt.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800392
393 gpt.WriteToFile(args.image_file)
394 print('Disk image file %s repaired.' % args.image_file.name)
395
396 @classmethod
397 def RegisterShow(cls, p):
398 """Registers the repair command to argparser.
399
400 Args:
401 p: An argparse parser instance.
402 """
403 p.add_argument('--numeric', '-n', action='store_true',
404 help='Numeric output only.')
405 p.add_argument('--quick', '-q', action='store_true',
406 help='Quick output.')
407 p.add_argument('--index', '-i', type=int, default=None,
408 help='Show specified partition only, with format args.')
409 for name, help_str in cls.FORMAT_ARGS:
410 # TODO(hungte) Alert if multiple args were specified.
411 p.add_argument('--%s' % name, '-%c' % name[0], action='store_true',
412 help='[format] %s.' % help_str)
413 p.add_argument('image_file', type=argparse.FileType('rb'),
414 help='Disk image file to show.')
415
416
417 def Show(self, args):
418 """Show partition table and entries."""
419
420 def FormatGUID(bytes_le):
421 return str(uuid.UUID(bytes_le=bytes_le)).upper()
422
423 def FormatTypeGUID(p):
424 guid_str = FormatGUID(p.TypeGUID)
425 if not args.numeric:
426 names = gpt.TYPE_GUID_MAP.get(guid_str)
427 if names:
428 return names
429 return guid_str
430
431 def FormatNames(p):
432 return p.Names.decode('utf-16-le').strip('\0')
433
434 def IsBootableType(type_guid):
435 return type_guid in gpt.TYPE_GUID_LIST_BOOTABLE
436
437 def FormatAttribute(attr):
438 if args.numeric:
439 return '[%x]' % (attr >> 48)
440 if attr & 4:
441 return 'legacy_boot=1'
442 return 'priority=%d tries=%d successful=%d' % (
443 gpt.GetAttributePriority(attr),
444 gpt.GetAttributeTries(attr),
445 gpt.GetAttributeSuccess(attr))
446
447 def ApplyFormatArgs(p):
448 if args.begin:
449 return p.FirstLBA
450 elif args.size:
451 return p.LastLBA - p.FirstLBA + 1
452 elif args.type:
453 return FormatTypeGUID(p)
454 elif args.unique:
455 return FormatGUID(p.UniqueGUID)
456 elif args.label:
457 return FormatNames(p)
458 elif args.Successful:
459 return gpt.GetAttributeSuccess(p.Attributes)
460 elif args.Priority:
461 return gpt.GetAttributePriority(p.Attributes)
462 elif args.Tries:
463 return gpt.GetAttributeTries(p.Attributes)
464 elif args.Legacy:
465 raise NotImplementedError
466 elif args.Attribute:
467 return '[%x]' % (p.Attributes >> 48)
468 else:
469 return None
470
471 def IsFormatArgsSpecified():
472 return any(getattr(args, arg[0]) for arg in self.FORMAT_ARGS)
473
474 gpt = GPT.LoadFromFile(args.image_file)
475 fmt = '%12s %11s %7s %s'
476 fmt2 = '%32s %s: %s'
477 header = ('start', 'size', 'part', 'contents')
478
479 if IsFormatArgsSpecified() and args.index is None:
480 raise ValueError('Format arguments must be used with -i.')
481
482 partitions = gpt.GetValidPartitions()
483 if not (args.index is None or 0 < args.index <= len(partitions)):
484 raise ValueError('Invalid partition index: %d' % args.index)
485
486 do_print_gpt_blocks = False
487 if not (args.quick or IsFormatArgsSpecified()):
488 print(fmt % header)
489 if args.index is None:
490 do_print_gpt_blocks = True
491
492 if do_print_gpt_blocks:
493 print(fmt % (gpt.header.CurrentLBA, 1, '', 'Pri GPT header'))
494 print(fmt % (gpt.header.PartitionEntriesStartingLBA,
495 gpt.GetPartitionTableBlocks(), '', 'Pri GPT table'))
496
497 for i, p in enumerate(partitions):
498 if args.index is not None and i != args.index - 1:
499 continue
500
501 if IsFormatArgsSpecified():
502 print(ApplyFormatArgs(p))
503 continue
504
505 type_guid = FormatGUID(p.TypeGUID)
506 print(fmt % (p.FirstLBA, p.LastLBA - p.FirstLBA + 1, i + 1,
507 FormatTypeGUID(p) if args.quick else
508 'Label: "%s"' % FormatNames(p)))
509
510 if not args.quick:
511 print(fmt2 % ('', 'Type', FormatTypeGUID(p)))
512 print(fmt2 % ('', 'UUID', FormatGUID(p.UniqueGUID)))
513 if args.numeric or IsBootableType(type_guid):
514 print(fmt2 % ('', 'Attr', FormatAttribute(p.Attributes)))
515
516 if do_print_gpt_blocks:
517 f = args.image_file
Hung-Te Linf148d322018-04-13 10:24:42 +0800518 f.seek(gpt.header.BackupLBA * gpt.block_size)
Hung-Te Linc772e1a2017-04-14 16:50:50 +0800519 backup_header = gpt.ReadHeader(f)
520 print(fmt % (backup_header.PartitionEntriesStartingLBA,
521 gpt.GetPartitionTableBlocks(backup_header), '',
522 'Sec GPT table'))
523 print(fmt % (gpt.header.BackupLBA, 1, '', 'Sec GPT header'))
524
525 def Create(self, args):
526 """Create or reset GPT headers and tables."""
527 del args # Not used yet.
528 raise NotImplementedError
529
530 def Add(self, args):
531 """Add, edit or remove a partition entry."""
532 del args # Not used yet.
533 raise NotImplementedError
534
535 def Boot(self, args):
536 """Edit the PMBR sector for legacy BIOSes."""
537 del args # Not used yet.
538 raise NotImplementedError
539
540 def Find(self, args):
541 """Locate a partition by its GUID."""
542 del args # Not used yet.
543 raise NotImplementedError
544
545 def Prioritize(self, args):
546 """Reorder the priority of all kernel partitions."""
547 del args # Not used yet.
548 raise NotImplementedError
549
550 def Legacy(self, args):
551 """Switch between GPT and Legacy GPT."""
552 del args # Not used yet.
553 raise NotImplementedError
554
555 @classmethod
556 def RegisterAllCommands(cls, subparsers):
557 """Registers all available commands to an argparser subparsers instance."""
558 subcommands = [('show', cls.Show, cls.RegisterShow),
559 ('repair', cls.Repair, cls.RegisterRepair)]
560 for name, invocation, register_command in subcommands:
561 register_command(subparsers.add_parser(name, help=invocation.__doc__))
562
563
564def main():
565 parser = argparse.ArgumentParser(description='GPT Utility.')
566 parser.add_argument('--verbose', '-v', action='count', default=0,
567 help='increase verbosity.')
568 parser.add_argument('--debug', '-d', action='store_true',
569 help='enable debug output.')
570 subparsers = parser.add_subparsers(help='Sub-command help.', dest='command')
571 GPTCommands.RegisterAllCommands(subparsers)
572
573 args = parser.parse_args()
574 log_level = max(logging.WARNING - args.verbose * 10, logging.DEBUG)
575 if args.debug:
576 log_level = logging.DEBUG
577 logging.basicConfig(format='%(module)s:%(funcName)s %(message)s',
578 level=log_level)
579 commands = GPTCommands()
580 try:
581 getattr(commands, args.command.capitalize())(args)
582 except Exception as e:
583 if args.verbose or args.debug:
584 logging.exception('Failure in command [%s]', args.command)
585 exit('ERROR: %s' % e)
586
587
588if __name__ == '__main__':
589 main()