├── .gitignore ├── README.md ├── argparse.py └── opsmtools.py /.gitignore: -------------------------------------------------------------------------------- 1 | # editor artefacts 2 | *.swp 3 | *.so 4 | 5 | # compilation artefacts 6 | *.pyc 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opsmtools 2 | 3 | **opsmtools** exposes the [MongoDB Ops Manager API](https://docs.opsmanager.mongodb.com/current/reference/api/) 4 | endpoints through a simple command line script with minimal dependencies. 5 | 6 | **not** all endpoint are supported yet, please contribute. 7 | 8 | Supported Endpoints 9 | ------------------- 10 | 11 | + Getting Hosts 12 | + COMING SOON: Getting host metrics 13 | + Getting Alerts 14 | + Getting Alert Configurations 15 | + Deleting Alert Configurations 16 | + Migrating Alert Configuration from one Ops Mgr to another Ops Mgr instance 17 | + Getting clusters 18 | + Listing snapshots from a clusters backups 19 | + Downloading a particular snapshot or just the 'latest' 20 | + Downloading point-in-time backup 21 | + Restoring backups: download and deploy any backup to another MongoDB instance 22 | 23 | Installation 24 | ------------ 25 | 26 | 27 | You need to have a version of Python installed in order to use mtools. Python 28 | 2.6.x (you will need to install ``argparse`` and ``requests``) and Python 2.7.x are currently supported. To check your Python version, 29 | run `python --version` on the command line. Python 3.x is currently not supported. 30 | 31 | ### opsmtools Installation 32 | 33 | #### Installation from source 34 | 35 | The easiest way to install opsmtools is with `curl`: 36 | 37 | $curl -OL https://raw.githubusercontent.com/jasonmimick/opsmtools/master/opsmtools.py 38 | $chmod +x opsmtools.py 39 | 40 | Or you can use your favorite method to download the single python file. 41 | 42 | Usage 43 | ------ 44 | 45 | ``` 46 | usage: opsmtools.py [-h] --host HOST --group GROUP --username USERNAME 47 | --apikey APIKEY [--getClusters] [--getHosts] [--getAlerts] 48 | [--getAlertConfigs] [--deleteAlertConfigs] 49 | [--postAlertConfigs] [--migrateAlertConfigs] 50 | [--getSnapshots] [--createRestore] [--createRestoreLatest] 51 | [--createRestoreAndDeploy] [--targetHost TARGETHOST] 52 | [--targetGroup TARGETGROUP] 53 | [--targetUsername TARGETUSERNAME] 54 | [--targetApikey TARGETAPIKEY] 55 | [--targetPassword TARGETPASSWORD] 56 | [--targetAuthenticationDatabase TARGETAUTHENTICATIONDATABASE] 57 | [--targetAuthenticationMechanism TARGETAUTHENTICATIONMECHANISM] 58 | [--alertConfigsSource ALERTCONFIGSSOURCE] 59 | [--clusterId CLUSTERID] [--snapshotId SNAPSHOTID] 60 | [--snapshotTimestamp SNAPSHOTTIMESTAMP] 61 | [--snapshotIncrement SNAPSHOTINCREMENT] 62 | [--restoreNamespace RESTORENAMESPACE] 63 | [--outDirectory OUTDIRECTORY] 64 | [--restoreAndDeployTempPort RESTOREANDDEPLOYTEMPPORT] 65 | [--restoreAndDeployTempMongodArgs RESTOREANDDEPLOYTEMPMONGODARGS] 66 | [--restoreAndDeployDropFromTarget] [--continueOnError] 67 | [--verbose] 68 | 69 | Get alerts from MongoDB Ops/Cloud Manager 70 | 71 | optional arguments: 72 | -h, --help show this help message and exit 73 | --getClusters get cluster information 74 | --getHosts get host information 75 | --getAlerts get alerts 76 | --getAlertConfigs get alert configurations 77 | --deleteAlertConfigs delete ALL alert configs from host 78 | --postAlertConfigs post ALL alert configs to host 79 | --migrateAlertConfigs 80 | migrate ALL alert configs from host to target 81 | --getSnapshots get list of snapshots for a given --clusterId 82 | --createRestore create a restore job from a given --clusterId for a 83 | given --snapshotId 84 | --createRestoreLatest 85 | create a restore job for the lastest snapshotId 86 | --createRestoreAndDeploy 87 | create a restore job from a given --clusterId for a 88 | given --snapshotId (or --snapshotTimestamp, 89 | --snapshotIncrement is optional), download and unpack 90 | it, then deploy data in --restoreNamespace to 91 | --targetHost NOTE: you must have the same or higher 92 | version of Mongo binaries installed on the machine 93 | running this script as running on the --targetHost! 94 | --targetHost TARGETHOST 95 | target OpsMgr/MongoDB host with protocol and port 96 | --targetGroup TARGETGROUP 97 | target OpsMgr group id 98 | --targetUsername TARGETUSERNAME 99 | target OpsMgr/MongoDB host user name 100 | --targetApikey TARGETAPIKEY 101 | target OpsMgr api key for target user 102 | --targetPassword TARGETPASSWORD 103 | target MongoDB instance password for target user 104 | --targetAuthenticationDatabase TARGETAUTHENTICATIONDATABASE 105 | target MongoDB instance authentication database target 106 | user 107 | --targetAuthenticationMechanism TARGETAUTHENTICATIONMECHANISM 108 | target MongoDB instance authentication mechanism 109 | target user 110 | --alertConfigsSource ALERTCONFIGSSOURCE 111 | A file containing JSON alert configs or "-" for STDIN 112 | --clusterId CLUSTERID 113 | id of replica set or sharded cluster for snapshots 114 | --snapshotId SNAPSHOTID 115 | id of a snapshot to restore 116 | --snapshotTimestamp SNAPSHOTTIMESTAMP 117 | point-in-time restore timestamp, format 118 | 2014-07-09T09:20:00Z 119 | --snapshotIncrement SNAPSHOTINCREMENT 120 | point-in-time restore increment, a positive integer 121 | --restoreNamespace RESTORENAMESPACE 122 | Namespace(s) to restore from snapshot. Use "*" for all 123 | databases. Use "foo.*" for all collections in the foo 124 | database. Use "foo.bar" for just the bar collection in 125 | the foo database. 126 | --outDirectory OUTDIRECTORY 127 | optional directory to save downloaded snapshot 128 | --restoreAndDeployTempPort RESTOREANDDEPLOYTEMPPORT 129 | optional port number to run temporary mongod to 130 | restore snapshot from, default is 27229 131 | --restoreAndDeployTempMongodArgs RESTOREANDDEPLOYTEMPMONGODARGS 132 | optional arguments for temp mongod to restore snapshot 133 | from. 134 | --restoreAndDeployDropFromTarget 135 | optional arguments for mongorestore process used to 136 | restore snapshot to --targetHost. For example "--drop" 137 | to force a drop of the collection(s) to be restored. 138 | --continueOnError for operations that issue multiple API calls, set this 139 | flag to fail to report errors but keep going 140 | --verbose enable versbose output for troubleshooting 141 | 142 | required named arguments: 143 | --host HOST the OpsMgr host with protocol and port, e.g. 144 | http://server.com:8080 145 | --group GROUP the OpsMgr group id 146 | --username USERNAME OpsMgr user name 147 | --apikey APIKEY OpsMgr api key for the user 148 | ``` 149 | 150 | Optional dependencies 151 | --------------------- 152 | 153 | Use ```pip``` to install ```terminaltables``` to get nice ascii 154 | tables when calling ```--getAlerts``` and other operations. 155 | 156 | Examples 157 | -------- 158 | 159 | If you are getting errors, remember to add the ```--verbose``` flag 160 | to your command for useful debugging information. 161 | 162 | *Example 1:* Get alerts. 163 | 164 | ``` 165 | $./opsmtools.py --host http://my.opsmgr.server:8080 \ 166 | --group 57598b14e4b01b9f37aadaf8 \ 167 | --username jason.mimick@mongodb.com --apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ 168 | --getAlerts --format table 169 | +Alerts from http://my.opsmgr.server:8080--------------------------------+ 170 | | eventTypeName | status | created | replicaSetName | 171 | +-----------------------+--------+----------------------+----------------+ 172 | | MONITORING_AGENT_DOWN | CLOSED | 2016-06-13T17:34:21Z | | 173 | +-----------------------+--------+----------------------+----------------+ 174 | | | | Number alerts | 4 | 175 | +-----------------------+--------+----------------------+----------------+ 176 | ``` 177 | 178 | *Example 2:* Restore a backup from Ops Manager to another MongoDB instance: 179 | 180 | ``` 181 | ./opsmtools.py --host http://ec2-127-0-0-1.compute-1.amazonaws.com:8080 \ 182 | --group 57598b14e4b01b9f37aadaf8 \ 183 | --username jason.mimick@mongodb.com --apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ 184 | --clusterId 575ec8aee4b0fc4aec781dfb --outDirectory ~/work/dbdump/ \ 185 | --snapshotTimestamp 2016-06-13T15:15:48Z \ 186 | --targetHost ec2-127-0-0-91.compute-1.amazonaws.com:27102 \ 187 | --restoreNamespace test.foo \ 188 | --restoreAndDeployTempPort 29299 \ 189 | --restoreAndDeployDropFromTarget \ 190 | --createRestoreAndDeploy 191 | ``` 192 | Credits 193 | ------- 194 | 195 | opsmtools was inspired by (and borrows heavily from) the excellent [mtools](https://github.com/rueckstiess/mtools) suite. 196 | Thanks, Thomas! 197 | 198 | Disclaimer 199 | ---------- 200 | 201 | This software is not supported by [MongoDB, Inc.](http://www.mongodb.com) under any of their commercial support subscriptions or otherwise. 202 | Any usage of opsmtools is at your own risk. 203 | Bug reports, feature requests and questions can be posted in the [Issues](https://github.com/jasonmimick/opsmtools/issues?state=open) section here on github. 204 | 205 | Author: [jason.mimick@mongodb.com](jason.mimick@mongodb.com) 206 | -------------------------------------------------------------------------------- /argparse.py: -------------------------------------------------------------------------------- 1 | # Author: Steven J. Bethard . 2 | # Maintainer: Thomas Waldmann 3 | 4 | """Command-line parsing library 5 | 6 | This module is an optparse-inspired command-line parsing library that: 7 | 8 | - handles both optional and positional arguments 9 | - produces highly informative usage messages 10 | - supports parsers that dispatch to sub-parsers 11 | 12 | The following is a simple usage example that sums integers from the 13 | command-line and writes the result to a file:: 14 | 15 | parser = argparse.ArgumentParser( 16 | description='sum the integers at the command line') 17 | parser.add_argument( 18 | 'integers', metavar='int', nargs='+', type=int, 19 | help='an integer to be summed') 20 | parser.add_argument( 21 | '--log', default=sys.stdout, type=argparse.FileType('w'), 22 | help='the file where the sum should be written') 23 | args = parser.parse_args() 24 | args.log.write('%s' % sum(args.integers)) 25 | args.log.close() 26 | 27 | The module contains the following public classes: 28 | 29 | - ArgumentParser -- The main entry point for command-line parsing. As the 30 | example above shows, the add_argument() method is used to populate 31 | the parser with actions for optional and positional arguments. Then 32 | the parse_args() method is invoked to convert the args at the 33 | command-line into an object with attributes. 34 | 35 | - ArgumentError -- The exception raised by ArgumentParser objects when 36 | there are errors with the parser's actions. Errors raised while 37 | parsing the command-line are caught by ArgumentParser and emitted 38 | as command-line messages. 39 | 40 | - FileType -- A factory for defining types of files to be created. As the 41 | example above shows, instances of FileType are typically passed as 42 | the type= argument of add_argument() calls. 43 | 44 | - Action -- The base class for parser actions. Typically actions are 45 | selected by passing strings like 'store_true' or 'append_const' to 46 | the action= argument of add_argument(). However, for greater 47 | customization of ArgumentParser actions, subclasses of Action may 48 | be defined and passed as the action= argument. 49 | 50 | - HelpFormatter, RawDescriptionHelpFormatter, RawTextHelpFormatter, 51 | ArgumentDefaultsHelpFormatter -- Formatter classes which 52 | may be passed as the formatter_class= argument to the 53 | ArgumentParser constructor. HelpFormatter is the default, 54 | RawDescriptionHelpFormatter and RawTextHelpFormatter tell the parser 55 | not to change the formatting for help text, and 56 | ArgumentDefaultsHelpFormatter adds information about argument defaults 57 | to the help. 58 | 59 | All other classes in this module are considered implementation details. 60 | (Also note that HelpFormatter and RawDescriptionHelpFormatter are only 61 | considered public as object names -- the API of the formatter objects is 62 | still considered an implementation detail.) 63 | """ 64 | 65 | __version__ = '1.4.0' # we use our own version number independant of the 66 | # one in stdlib and we release this on pypi. 67 | 68 | __external_lib__ = True # to make sure the tests really test THIS lib, 69 | # not the builtin one in Python stdlib 70 | 71 | __all__ = [ 72 | 'ArgumentParser', 73 | 'ArgumentError', 74 | 'ArgumentTypeError', 75 | 'FileType', 76 | 'HelpFormatter', 77 | 'ArgumentDefaultsHelpFormatter', 78 | 'RawDescriptionHelpFormatter', 79 | 'RawTextHelpFormatter', 80 | 'Namespace', 81 | 'Action', 82 | 'ONE_OR_MORE', 83 | 'OPTIONAL', 84 | 'PARSER', 85 | 'REMAINDER', 86 | 'SUPPRESS', 87 | 'ZERO_OR_MORE', 88 | ] 89 | 90 | 91 | import copy as _copy 92 | import os as _os 93 | import re as _re 94 | import sys as _sys 95 | import textwrap as _textwrap 96 | 97 | from gettext import gettext as _ 98 | 99 | try: 100 | set 101 | except NameError: 102 | # for python < 2.4 compatibility (sets module is there since 2.3): 103 | from sets import Set as set 104 | 105 | try: 106 | basestring 107 | except NameError: 108 | basestring = str 109 | 110 | try: 111 | sorted 112 | except NameError: 113 | # for python < 2.4 compatibility: 114 | def sorted(iterable, reverse=False): 115 | result = list(iterable) 116 | result.sort() 117 | if reverse: 118 | result.reverse() 119 | return result 120 | 121 | 122 | def _callable(obj): 123 | return hasattr(obj, '__call__') or hasattr(obj, '__bases__') 124 | 125 | 126 | SUPPRESS = '==SUPPRESS==' 127 | 128 | OPTIONAL = '?' 129 | ZERO_OR_MORE = '*' 130 | ONE_OR_MORE = '+' 131 | PARSER = 'A...' 132 | REMAINDER = '...' 133 | _UNRECOGNIZED_ARGS_ATTR = '_unrecognized_args' 134 | 135 | # ============================= 136 | # Utility functions and classes 137 | # ============================= 138 | 139 | class _AttributeHolder(object): 140 | """Abstract base class that provides __repr__. 141 | 142 | The __repr__ method returns a string in the format:: 143 | ClassName(attr=name, attr=name, ...) 144 | The attributes are determined either by a class-level attribute, 145 | '_kwarg_names', or by inspecting the instance __dict__. 146 | """ 147 | 148 | def __repr__(self): 149 | type_name = type(self).__name__ 150 | arg_strings = [] 151 | for arg in self._get_args(): 152 | arg_strings.append(repr(arg)) 153 | for name, value in self._get_kwargs(): 154 | arg_strings.append('%s=%r' % (name, value)) 155 | return '%s(%s)' % (type_name, ', '.join(arg_strings)) 156 | 157 | def _get_kwargs(self): 158 | return sorted(self.__dict__.items()) 159 | 160 | def _get_args(self): 161 | return [] 162 | 163 | 164 | def _ensure_value(namespace, name, value): 165 | if getattr(namespace, name, None) is None: 166 | setattr(namespace, name, value) 167 | return getattr(namespace, name) 168 | 169 | 170 | # =============== 171 | # Formatting Help 172 | # =============== 173 | 174 | class HelpFormatter(object): 175 | """Formatter for generating usage messages and argument help strings. 176 | 177 | Only the name of this class is considered a public API. All the methods 178 | provided by the class are considered an implementation detail. 179 | """ 180 | 181 | def __init__(self, 182 | prog, 183 | indent_increment=2, 184 | max_help_position=24, 185 | width=None): 186 | 187 | # default setting for width 188 | if width is None: 189 | try: 190 | width = int(_os.environ['COLUMNS']) 191 | except (KeyError, ValueError): 192 | width = 80 193 | width -= 2 194 | 195 | self._prog = prog 196 | self._indent_increment = indent_increment 197 | self._max_help_position = max_help_position 198 | self._width = width 199 | 200 | self._current_indent = 0 201 | self._level = 0 202 | self._action_max_length = 0 203 | 204 | self._root_section = self._Section(self, None) 205 | self._current_section = self._root_section 206 | 207 | self._whitespace_matcher = _re.compile(r'\s+') 208 | self._long_break_matcher = _re.compile(r'\n\n\n+') 209 | 210 | # =============================== 211 | # Section and indentation methods 212 | # =============================== 213 | def _indent(self): 214 | self._current_indent += self._indent_increment 215 | self._level += 1 216 | 217 | def _dedent(self): 218 | self._current_indent -= self._indent_increment 219 | assert self._current_indent >= 0, 'Indent decreased below 0.' 220 | self._level -= 1 221 | 222 | class _Section(object): 223 | 224 | def __init__(self, formatter, parent, heading=None): 225 | self.formatter = formatter 226 | self.parent = parent 227 | self.heading = heading 228 | self.items = [] 229 | 230 | def format_help(self): 231 | # format the indented section 232 | if self.parent is not None: 233 | self.formatter._indent() 234 | join = self.formatter._join_parts 235 | for func, args in self.items: 236 | func(*args) 237 | item_help = join([func(*args) for func, args in self.items]) 238 | if self.parent is not None: 239 | self.formatter._dedent() 240 | 241 | # return nothing if the section was empty 242 | if not item_help: 243 | return '' 244 | 245 | # add the heading if the section was non-empty 246 | if self.heading is not SUPPRESS and self.heading is not None: 247 | current_indent = self.formatter._current_indent 248 | heading = '%*s%s:\n' % (current_indent, '', self.heading) 249 | else: 250 | heading = '' 251 | 252 | # join the section-initial newline, the heading and the help 253 | return join(['\n', heading, item_help, '\n']) 254 | 255 | def _add_item(self, func, args): 256 | self._current_section.items.append((func, args)) 257 | 258 | # ======================== 259 | # Message building methods 260 | # ======================== 261 | def start_section(self, heading): 262 | self._indent() 263 | section = self._Section(self, self._current_section, heading) 264 | self._add_item(section.format_help, []) 265 | self._current_section = section 266 | 267 | def end_section(self): 268 | self._current_section = self._current_section.parent 269 | self._dedent() 270 | 271 | def add_text(self, text): 272 | if text is not SUPPRESS and text is not None: 273 | self._add_item(self._format_text, [text]) 274 | 275 | def add_usage(self, usage, actions, groups, prefix=None): 276 | if usage is not SUPPRESS: 277 | args = usage, actions, groups, prefix 278 | self._add_item(self._format_usage, args) 279 | 280 | def add_argument(self, action): 281 | if action.help is not SUPPRESS: 282 | 283 | # find all invocations 284 | get_invocation = self._format_action_invocation 285 | invocations = [get_invocation(action)] 286 | for subaction in self._iter_indented_subactions(action): 287 | invocations.append(get_invocation(subaction)) 288 | 289 | # update the maximum item length 290 | invocation_length = max([len(s) for s in invocations]) 291 | action_length = invocation_length + self._current_indent 292 | self._action_max_length = max(self._action_max_length, 293 | action_length) 294 | 295 | # add the item to the list 296 | self._add_item(self._format_action, [action]) 297 | 298 | def add_arguments(self, actions): 299 | for action in actions: 300 | self.add_argument(action) 301 | 302 | # ======================= 303 | # Help-formatting methods 304 | # ======================= 305 | def format_help(self): 306 | help = self._root_section.format_help() 307 | if help: 308 | help = self._long_break_matcher.sub('\n\n', help) 309 | help = help.strip('\n') + '\n' 310 | return help 311 | 312 | def _join_parts(self, part_strings): 313 | return ''.join([part 314 | for part in part_strings 315 | if part and part is not SUPPRESS]) 316 | 317 | def _format_usage(self, usage, actions, groups, prefix): 318 | if prefix is None: 319 | prefix = _('usage: ') 320 | 321 | # if usage is specified, use that 322 | if usage is not None: 323 | usage = usage % dict(prog=self._prog) 324 | 325 | # if no optionals or positionals are available, usage is just prog 326 | elif usage is None and not actions: 327 | usage = '%(prog)s' % dict(prog=self._prog) 328 | 329 | # if optionals and positionals are available, calculate usage 330 | elif usage is None: 331 | prog = '%(prog)s' % dict(prog=self._prog) 332 | 333 | # split optionals from positionals 334 | optionals = [] 335 | positionals = [] 336 | for action in actions: 337 | if action.option_strings: 338 | optionals.append(action) 339 | else: 340 | positionals.append(action) 341 | 342 | # build full usage string 343 | format = self._format_actions_usage 344 | action_usage = format(optionals + positionals, groups) 345 | usage = ' '.join([s for s in [prog, action_usage] if s]) 346 | 347 | # wrap the usage parts if it's too long 348 | text_width = self._width - self._current_indent 349 | if len(prefix) + len(usage) > text_width: 350 | 351 | # break usage into wrappable parts 352 | part_regexp = r'\(.*?\)+|\[.*?\]+|\S+' 353 | opt_usage = format(optionals, groups) 354 | pos_usage = format(positionals, groups) 355 | opt_parts = _re.findall(part_regexp, opt_usage) 356 | pos_parts = _re.findall(part_regexp, pos_usage) 357 | assert ' '.join(opt_parts) == opt_usage 358 | assert ' '.join(pos_parts) == pos_usage 359 | 360 | # helper for wrapping lines 361 | def get_lines(parts, indent, prefix=None): 362 | lines = [] 363 | line = [] 364 | if prefix is not None: 365 | line_len = len(prefix) - 1 366 | else: 367 | line_len = len(indent) - 1 368 | for part in parts: 369 | if line_len + 1 + len(part) > text_width: 370 | lines.append(indent + ' '.join(line)) 371 | line = [] 372 | line_len = len(indent) - 1 373 | line.append(part) 374 | line_len += len(part) + 1 375 | if line: 376 | lines.append(indent + ' '.join(line)) 377 | if prefix is not None: 378 | lines[0] = lines[0][len(indent):] 379 | return lines 380 | 381 | # if prog is short, follow it with optionals or positionals 382 | if len(prefix) + len(prog) <= 0.75 * text_width: 383 | indent = ' ' * (len(prefix) + len(prog) + 1) 384 | if opt_parts: 385 | lines = get_lines([prog] + opt_parts, indent, prefix) 386 | lines.extend(get_lines(pos_parts, indent)) 387 | elif pos_parts: 388 | lines = get_lines([prog] + pos_parts, indent, prefix) 389 | else: 390 | lines = [prog] 391 | 392 | # if prog is long, put it on its own line 393 | else: 394 | indent = ' ' * len(prefix) 395 | parts = opt_parts + pos_parts 396 | lines = get_lines(parts, indent) 397 | if len(lines) > 1: 398 | lines = [] 399 | lines.extend(get_lines(opt_parts, indent)) 400 | lines.extend(get_lines(pos_parts, indent)) 401 | lines = [prog] + lines 402 | 403 | # join lines into usage 404 | usage = '\n'.join(lines) 405 | 406 | # prefix with 'usage:' 407 | return '%s%s\n\n' % (prefix, usage) 408 | 409 | def _format_actions_usage(self, actions, groups): 410 | # find group indices and identify actions in groups 411 | group_actions = set() 412 | inserts = {} 413 | for group in groups: 414 | try: 415 | start = actions.index(group._group_actions[0]) 416 | except ValueError: 417 | continue 418 | else: 419 | end = start + len(group._group_actions) 420 | if actions[start:end] == group._group_actions: 421 | for action in group._group_actions: 422 | group_actions.add(action) 423 | if not group.required: 424 | if start in inserts: 425 | inserts[start] += ' [' 426 | else: 427 | inserts[start] = '[' 428 | inserts[end] = ']' 429 | else: 430 | if start in inserts: 431 | inserts[start] += ' (' 432 | else: 433 | inserts[start] = '(' 434 | inserts[end] = ')' 435 | for i in range(start + 1, end): 436 | inserts[i] = '|' 437 | 438 | # collect all actions format strings 439 | parts = [] 440 | for i, action in enumerate(actions): 441 | 442 | # suppressed arguments are marked with None 443 | # remove | separators for suppressed arguments 444 | if action.help is SUPPRESS: 445 | parts.append(None) 446 | if inserts.get(i) == '|': 447 | inserts.pop(i) 448 | elif inserts.get(i + 1) == '|': 449 | inserts.pop(i + 1) 450 | 451 | # produce all arg strings 452 | elif not action.option_strings: 453 | part = self._format_args(action, action.dest) 454 | 455 | # if it's in a group, strip the outer [] 456 | if action in group_actions: 457 | if part[0] == '[' and part[-1] == ']': 458 | part = part[1:-1] 459 | 460 | # add the action string to the list 461 | parts.append(part) 462 | 463 | # produce the first way to invoke the option in brackets 464 | else: 465 | option_string = action.option_strings[0] 466 | 467 | # if the Optional doesn't take a value, format is: 468 | # -s or --long 469 | if action.nargs == 0: 470 | part = '%s' % option_string 471 | 472 | # if the Optional takes a value, format is: 473 | # -s ARGS or --long ARGS 474 | else: 475 | default = action.dest.upper() 476 | args_string = self._format_args(action, default) 477 | part = '%s %s' % (option_string, args_string) 478 | 479 | # make it look optional if it's not required or in a group 480 | if not action.required and action not in group_actions: 481 | part = '[%s]' % part 482 | 483 | # add the action string to the list 484 | parts.append(part) 485 | 486 | # insert things at the necessary indices 487 | for i in sorted(inserts, reverse=True): 488 | parts[i:i] = [inserts[i]] 489 | 490 | # join all the action items with spaces 491 | text = ' '.join([item for item in parts if item is not None]) 492 | 493 | # clean up separators for mutually exclusive groups 494 | open = r'[\[(]' 495 | close = r'[\])]' 496 | text = _re.sub(r'(%s) ' % open, r'\1', text) 497 | text = _re.sub(r' (%s)' % close, r'\1', text) 498 | text = _re.sub(r'%s *%s' % (open, close), r'', text) 499 | text = _re.sub(r'\(([^|]*)\)', r'\1', text) 500 | text = text.strip() 501 | 502 | # return the text 503 | return text 504 | 505 | def _format_text(self, text): 506 | if '%(prog)' in text: 507 | text = text % dict(prog=self._prog) 508 | text_width = self._width - self._current_indent 509 | indent = ' ' * self._current_indent 510 | return self._fill_text(text, text_width, indent) + '\n\n' 511 | 512 | def _format_action(self, action): 513 | # determine the required width and the entry label 514 | help_position = min(self._action_max_length + 2, 515 | self._max_help_position) 516 | help_width = self._width - help_position 517 | action_width = help_position - self._current_indent - 2 518 | action_header = self._format_action_invocation(action) 519 | 520 | # ho nelp; start on same line and add a final newline 521 | if not action.help: 522 | tup = self._current_indent, '', action_header 523 | action_header = '%*s%s\n' % tup 524 | 525 | # short action name; start on the same line and pad two spaces 526 | elif len(action_header) <= action_width: 527 | tup = self._current_indent, '', action_width, action_header 528 | action_header = '%*s%-*s ' % tup 529 | indent_first = 0 530 | 531 | # long action name; start on the next line 532 | else: 533 | tup = self._current_indent, '', action_header 534 | action_header = '%*s%s\n' % tup 535 | indent_first = help_position 536 | 537 | # collect the pieces of the action help 538 | parts = [action_header] 539 | 540 | # if there was help for the action, add lines of help text 541 | if action.help: 542 | help_text = self._expand_help(action) 543 | help_lines = self._split_lines(help_text, help_width) 544 | parts.append('%*s%s\n' % (indent_first, '', help_lines[0])) 545 | for line in help_lines[1:]: 546 | parts.append('%*s%s\n' % (help_position, '', line)) 547 | 548 | # or add a newline if the description doesn't end with one 549 | elif not action_header.endswith('\n'): 550 | parts.append('\n') 551 | 552 | # if there are any sub-actions, add their help as well 553 | for subaction in self._iter_indented_subactions(action): 554 | parts.append(self._format_action(subaction)) 555 | 556 | # return a single string 557 | return self._join_parts(parts) 558 | 559 | def _format_action_invocation(self, action): 560 | if not action.option_strings: 561 | metavar, = self._metavar_formatter(action, action.dest)(1) 562 | return metavar 563 | 564 | else: 565 | parts = [] 566 | 567 | # if the Optional doesn't take a value, format is: 568 | # -s, --long 569 | if action.nargs == 0: 570 | parts.extend(action.option_strings) 571 | 572 | # if the Optional takes a value, format is: 573 | # -s ARGS, --long ARGS 574 | else: 575 | default = action.dest.upper() 576 | args_string = self._format_args(action, default) 577 | for option_string in action.option_strings: 578 | parts.append('%s %s' % (option_string, args_string)) 579 | 580 | return ', '.join(parts) 581 | 582 | def _metavar_formatter(self, action, default_metavar): 583 | if action.metavar is not None: 584 | result = action.metavar 585 | elif action.choices is not None: 586 | choice_strs = [str(choice) for choice in action.choices] 587 | result = '{%s}' % ','.join(choice_strs) 588 | else: 589 | result = default_metavar 590 | 591 | def format(tuple_size): 592 | if isinstance(result, tuple): 593 | return result 594 | else: 595 | return (result, ) * tuple_size 596 | return format 597 | 598 | def _format_args(self, action, default_metavar): 599 | get_metavar = self._metavar_formatter(action, default_metavar) 600 | if action.nargs is None: 601 | result = '%s' % get_metavar(1) 602 | elif action.nargs == OPTIONAL: 603 | result = '[%s]' % get_metavar(1) 604 | elif action.nargs == ZERO_OR_MORE: 605 | result = '[%s [%s ...]]' % get_metavar(2) 606 | elif action.nargs == ONE_OR_MORE: 607 | result = '%s [%s ...]' % get_metavar(2) 608 | elif action.nargs == REMAINDER: 609 | result = '...' 610 | elif action.nargs == PARSER: 611 | result = '%s ...' % get_metavar(1) 612 | else: 613 | formats = ['%s' for _ in range(action.nargs)] 614 | result = ' '.join(formats) % get_metavar(action.nargs) 615 | return result 616 | 617 | def _expand_help(self, action): 618 | params = dict(vars(action), prog=self._prog) 619 | for name in list(params): 620 | if params[name] is SUPPRESS: 621 | del params[name] 622 | for name in list(params): 623 | if hasattr(params[name], '__name__'): 624 | params[name] = params[name].__name__ 625 | if params.get('choices') is not None: 626 | choices_str = ', '.join([str(c) for c in params['choices']]) 627 | params['choices'] = choices_str 628 | return self._get_help_string(action) % params 629 | 630 | def _iter_indented_subactions(self, action): 631 | try: 632 | get_subactions = action._get_subactions 633 | except AttributeError: 634 | pass 635 | else: 636 | self._indent() 637 | for subaction in get_subactions(): 638 | yield subaction 639 | self._dedent() 640 | 641 | def _split_lines(self, text, width): 642 | text = self._whitespace_matcher.sub(' ', text).strip() 643 | return _textwrap.wrap(text, width) 644 | 645 | def _fill_text(self, text, width, indent): 646 | text = self._whitespace_matcher.sub(' ', text).strip() 647 | return _textwrap.fill(text, width, initial_indent=indent, 648 | subsequent_indent=indent) 649 | 650 | def _get_help_string(self, action): 651 | return action.help 652 | 653 | 654 | class RawDescriptionHelpFormatter(HelpFormatter): 655 | """Help message formatter which retains any formatting in descriptions. 656 | 657 | Only the name of this class is considered a public API. All the methods 658 | provided by the class are considered an implementation detail. 659 | """ 660 | 661 | def _fill_text(self, text, width, indent): 662 | return ''.join([indent + line for line in text.splitlines(True)]) 663 | 664 | 665 | class RawTextHelpFormatter(RawDescriptionHelpFormatter): 666 | """Help message formatter which retains formatting of all help text. 667 | 668 | Only the name of this class is considered a public API. All the methods 669 | provided by the class are considered an implementation detail. 670 | """ 671 | 672 | def _split_lines(self, text, width): 673 | return text.splitlines() 674 | 675 | 676 | class ArgumentDefaultsHelpFormatter(HelpFormatter): 677 | """Help message formatter which adds default values to argument help. 678 | 679 | Only the name of this class is considered a public API. All the methods 680 | provided by the class are considered an implementation detail. 681 | """ 682 | 683 | def _get_help_string(self, action): 684 | help = action.help 685 | if '%(default)' not in action.help: 686 | if action.default is not SUPPRESS: 687 | defaulting_nargs = [OPTIONAL, ZERO_OR_MORE] 688 | if action.option_strings or action.nargs in defaulting_nargs: 689 | help += ' (default: %(default)s)' 690 | return help 691 | 692 | 693 | # ===================== 694 | # Options and Arguments 695 | # ===================== 696 | 697 | def _get_action_name(argument): 698 | if argument is None: 699 | return None 700 | elif argument.option_strings: 701 | return '/'.join(argument.option_strings) 702 | elif argument.metavar not in (None, SUPPRESS): 703 | return argument.metavar 704 | elif argument.dest not in (None, SUPPRESS): 705 | return argument.dest 706 | else: 707 | return None 708 | 709 | 710 | class ArgumentError(Exception): 711 | """An error from creating or using an argument (optional or positional). 712 | 713 | The string value of this exception is the message, augmented with 714 | information about the argument that caused it. 715 | """ 716 | 717 | def __init__(self, argument, message): 718 | self.argument_name = _get_action_name(argument) 719 | self.message = message 720 | 721 | def __str__(self): 722 | if self.argument_name is None: 723 | format = '%(message)s' 724 | else: 725 | format = 'argument %(argument_name)s: %(message)s' 726 | return format % dict(message=self.message, 727 | argument_name=self.argument_name) 728 | 729 | 730 | class ArgumentTypeError(Exception): 731 | """An error from trying to convert a command line string to a type.""" 732 | pass 733 | 734 | 735 | # ============== 736 | # Action classes 737 | # ============== 738 | 739 | class Action(_AttributeHolder): 740 | """Information about how to convert command line strings to Python objects. 741 | 742 | Action objects are used by an ArgumentParser to represent the information 743 | needed to parse a single argument from one or more strings from the 744 | command line. The keyword arguments to the Action constructor are also 745 | all attributes of Action instances. 746 | 747 | Keyword Arguments: 748 | 749 | - option_strings -- A list of command-line option strings which 750 | should be associated with this action. 751 | 752 | - dest -- The name of the attribute to hold the created object(s) 753 | 754 | - nargs -- The number of command-line arguments that should be 755 | consumed. By default, one argument will be consumed and a single 756 | value will be produced. Other values include: 757 | - N (an integer) consumes N arguments (and produces a list) 758 | - '?' consumes zero or one arguments 759 | - '*' consumes zero or more arguments (and produces a list) 760 | - '+' consumes one or more arguments (and produces a list) 761 | Note that the difference between the default and nargs=1 is that 762 | with the default, a single value will be produced, while with 763 | nargs=1, a list containing a single value will be produced. 764 | 765 | - const -- The value to be produced if the option is specified and the 766 | option uses an action that takes no values. 767 | 768 | - default -- The value to be produced if the option is not specified. 769 | 770 | - type -- The type which the command-line arguments should be converted 771 | to, should be one of 'string', 'int', 'float', 'complex' or a 772 | callable object that accepts a single string argument. If None, 773 | 'string' is assumed. 774 | 775 | - choices -- A container of values that should be allowed. If not None, 776 | after a command-line argument has been converted to the appropriate 777 | type, an exception will be raised if it is not a member of this 778 | collection. 779 | 780 | - required -- True if the action must always be specified at the 781 | command line. This is only meaningful for optional command-line 782 | arguments. 783 | 784 | - help -- The help string describing the argument. 785 | 786 | - metavar -- The name to be used for the option's argument with the 787 | help string. If None, the 'dest' value will be used as the name. 788 | """ 789 | 790 | def __init__(self, 791 | option_strings, 792 | dest, 793 | nargs=None, 794 | const=None, 795 | default=None, 796 | type=None, 797 | choices=None, 798 | required=False, 799 | help=None, 800 | metavar=None): 801 | self.option_strings = option_strings 802 | self.dest = dest 803 | self.nargs = nargs 804 | self.const = const 805 | self.default = default 806 | self.type = type 807 | self.choices = choices 808 | self.required = required 809 | self.help = help 810 | self.metavar = metavar 811 | 812 | def _get_kwargs(self): 813 | names = [ 814 | 'option_strings', 815 | 'dest', 816 | 'nargs', 817 | 'const', 818 | 'default', 819 | 'type', 820 | 'choices', 821 | 'help', 822 | 'metavar', 823 | ] 824 | return [(name, getattr(self, name)) for name in names] 825 | 826 | def __call__(self, parser, namespace, values, option_string=None): 827 | raise NotImplementedError(_('.__call__() not defined')) 828 | 829 | 830 | class _StoreAction(Action): 831 | 832 | def __init__(self, 833 | option_strings, 834 | dest, 835 | nargs=None, 836 | const=None, 837 | default=None, 838 | type=None, 839 | choices=None, 840 | required=False, 841 | help=None, 842 | metavar=None): 843 | if nargs == 0: 844 | raise ValueError('nargs for store actions must be > 0; if you ' 845 | 'have nothing to store, actions such as store ' 846 | 'true or store const may be more appropriate') 847 | if const is not None and nargs != OPTIONAL: 848 | raise ValueError('nargs must be %r to supply const' % OPTIONAL) 849 | super(_StoreAction, self).__init__( 850 | option_strings=option_strings, 851 | dest=dest, 852 | nargs=nargs, 853 | const=const, 854 | default=default, 855 | type=type, 856 | choices=choices, 857 | required=required, 858 | help=help, 859 | metavar=metavar) 860 | 861 | def __call__(self, parser, namespace, values, option_string=None): 862 | setattr(namespace, self.dest, values) 863 | 864 | 865 | class _StoreConstAction(Action): 866 | 867 | def __init__(self, 868 | option_strings, 869 | dest, 870 | const, 871 | default=None, 872 | required=False, 873 | help=None, 874 | metavar=None): 875 | super(_StoreConstAction, self).__init__( 876 | option_strings=option_strings, 877 | dest=dest, 878 | nargs=0, 879 | const=const, 880 | default=default, 881 | required=required, 882 | help=help) 883 | 884 | def __call__(self, parser, namespace, values, option_string=None): 885 | setattr(namespace, self.dest, self.const) 886 | 887 | 888 | class _StoreTrueAction(_StoreConstAction): 889 | 890 | def __init__(self, 891 | option_strings, 892 | dest, 893 | default=False, 894 | required=False, 895 | help=None): 896 | super(_StoreTrueAction, self).__init__( 897 | option_strings=option_strings, 898 | dest=dest, 899 | const=True, 900 | default=default, 901 | required=required, 902 | help=help) 903 | 904 | 905 | class _StoreFalseAction(_StoreConstAction): 906 | 907 | def __init__(self, 908 | option_strings, 909 | dest, 910 | default=True, 911 | required=False, 912 | help=None): 913 | super(_StoreFalseAction, self).__init__( 914 | option_strings=option_strings, 915 | dest=dest, 916 | const=False, 917 | default=default, 918 | required=required, 919 | help=help) 920 | 921 | 922 | class _AppendAction(Action): 923 | 924 | def __init__(self, 925 | option_strings, 926 | dest, 927 | nargs=None, 928 | const=None, 929 | default=None, 930 | type=None, 931 | choices=None, 932 | required=False, 933 | help=None, 934 | metavar=None): 935 | if nargs == 0: 936 | raise ValueError('nargs for append actions must be > 0; if arg ' 937 | 'strings are not supplying the value to append, ' 938 | 'the append const action may be more appropriate') 939 | if const is not None and nargs != OPTIONAL: 940 | raise ValueError('nargs must be %r to supply const' % OPTIONAL) 941 | super(_AppendAction, self).__init__( 942 | option_strings=option_strings, 943 | dest=dest, 944 | nargs=nargs, 945 | const=const, 946 | default=default, 947 | type=type, 948 | choices=choices, 949 | required=required, 950 | help=help, 951 | metavar=metavar) 952 | 953 | def __call__(self, parser, namespace, values, option_string=None): 954 | items = _copy.copy(_ensure_value(namespace, self.dest, [])) 955 | items.append(values) 956 | setattr(namespace, self.dest, items) 957 | 958 | 959 | class _AppendConstAction(Action): 960 | 961 | def __init__(self, 962 | option_strings, 963 | dest, 964 | const, 965 | default=None, 966 | required=False, 967 | help=None, 968 | metavar=None): 969 | super(_AppendConstAction, self).__init__( 970 | option_strings=option_strings, 971 | dest=dest, 972 | nargs=0, 973 | const=const, 974 | default=default, 975 | required=required, 976 | help=help, 977 | metavar=metavar) 978 | 979 | def __call__(self, parser, namespace, values, option_string=None): 980 | items = _copy.copy(_ensure_value(namespace, self.dest, [])) 981 | items.append(self.const) 982 | setattr(namespace, self.dest, items) 983 | 984 | 985 | class _CountAction(Action): 986 | 987 | def __init__(self, 988 | option_strings, 989 | dest, 990 | default=None, 991 | required=False, 992 | help=None): 993 | super(_CountAction, self).__init__( 994 | option_strings=option_strings, 995 | dest=dest, 996 | nargs=0, 997 | default=default, 998 | required=required, 999 | help=help) 1000 | 1001 | def __call__(self, parser, namespace, values, option_string=None): 1002 | new_count = _ensure_value(namespace, self.dest, 0) + 1 1003 | setattr(namespace, self.dest, new_count) 1004 | 1005 | 1006 | class _HelpAction(Action): 1007 | 1008 | def __init__(self, 1009 | option_strings, 1010 | dest=SUPPRESS, 1011 | default=SUPPRESS, 1012 | help=None): 1013 | super(_HelpAction, self).__init__( 1014 | option_strings=option_strings, 1015 | dest=dest, 1016 | default=default, 1017 | nargs=0, 1018 | help=help) 1019 | 1020 | def __call__(self, parser, namespace, values, option_string=None): 1021 | parser.print_help() 1022 | parser.exit() 1023 | 1024 | 1025 | class _VersionAction(Action): 1026 | 1027 | def __init__(self, 1028 | option_strings, 1029 | version=None, 1030 | dest=SUPPRESS, 1031 | default=SUPPRESS, 1032 | help="show program's version number and exit"): 1033 | super(_VersionAction, self).__init__( 1034 | option_strings=option_strings, 1035 | dest=dest, 1036 | default=default, 1037 | nargs=0, 1038 | help=help) 1039 | self.version = version 1040 | 1041 | def __call__(self, parser, namespace, values, option_string=None): 1042 | version = self.version 1043 | if version is None: 1044 | version = parser.version 1045 | formatter = parser._get_formatter() 1046 | formatter.add_text(version) 1047 | parser.exit(message=formatter.format_help()) 1048 | 1049 | 1050 | class _SubParsersAction(Action): 1051 | 1052 | class _ChoicesPseudoAction(Action): 1053 | 1054 | def __init__(self, name, aliases, help): 1055 | metavar = dest = name 1056 | if aliases: 1057 | metavar += ' (%s)' % ', '.join(aliases) 1058 | sup = super(_SubParsersAction._ChoicesPseudoAction, self) 1059 | sup.__init__(option_strings=[], dest=dest, help=help, 1060 | metavar=metavar) 1061 | 1062 | def __init__(self, 1063 | option_strings, 1064 | prog, 1065 | parser_class, 1066 | dest=SUPPRESS, 1067 | help=None, 1068 | metavar=None): 1069 | 1070 | self._prog_prefix = prog 1071 | self._parser_class = parser_class 1072 | self._name_parser_map = {} 1073 | self._choices_actions = [] 1074 | 1075 | super(_SubParsersAction, self).__init__( 1076 | option_strings=option_strings, 1077 | dest=dest, 1078 | nargs=PARSER, 1079 | choices=self._name_parser_map, 1080 | help=help, 1081 | metavar=metavar) 1082 | 1083 | def add_parser(self, name, **kwargs): 1084 | # set prog from the existing prefix 1085 | if kwargs.get('prog') is None: 1086 | kwargs['prog'] = '%s %s' % (self._prog_prefix, name) 1087 | 1088 | aliases = kwargs.pop('aliases', ()) 1089 | 1090 | # create a pseudo-action to hold the choice help 1091 | if 'help' in kwargs: 1092 | help = kwargs.pop('help') 1093 | choice_action = self._ChoicesPseudoAction(name, aliases, help) 1094 | self._choices_actions.append(choice_action) 1095 | 1096 | # create the parser and add it to the map 1097 | parser = self._parser_class(**kwargs) 1098 | self._name_parser_map[name] = parser 1099 | 1100 | # make parser available under aliases also 1101 | for alias in aliases: 1102 | self._name_parser_map[alias] = parser 1103 | 1104 | return parser 1105 | 1106 | def _get_subactions(self): 1107 | return self._choices_actions 1108 | 1109 | def __call__(self, parser, namespace, values, option_string=None): 1110 | parser_name = values[0] 1111 | arg_strings = values[1:] 1112 | 1113 | # set the parser name if requested 1114 | if self.dest is not SUPPRESS: 1115 | setattr(namespace, self.dest, parser_name) 1116 | 1117 | # select the parser 1118 | try: 1119 | parser = self._name_parser_map[parser_name] 1120 | except KeyError: 1121 | tup = parser_name, ', '.join(self._name_parser_map) 1122 | msg = _('unknown parser %r (choices: %s)' % tup) 1123 | raise ArgumentError(self, msg) 1124 | 1125 | # parse all the remaining options into the namespace 1126 | # store any unrecognized options on the object, so that the top 1127 | # level parser can decide what to do with them 1128 | namespace, arg_strings = parser.parse_known_args(arg_strings, namespace) 1129 | if arg_strings: 1130 | vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, []) 1131 | getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings) 1132 | 1133 | 1134 | # ============== 1135 | # Type classes 1136 | # ============== 1137 | 1138 | class FileType(object): 1139 | """Factory for creating file object types 1140 | 1141 | Instances of FileType are typically passed as type= arguments to the 1142 | ArgumentParser add_argument() method. 1143 | 1144 | Keyword Arguments: 1145 | - mode -- A string indicating how the file is to be opened. Accepts the 1146 | same values as the builtin open() function. 1147 | - bufsize -- The file's desired buffer size. Accepts the same values as 1148 | the builtin open() function. 1149 | """ 1150 | 1151 | def __init__(self, mode='r', bufsize=None): 1152 | self._mode = mode 1153 | self._bufsize = bufsize 1154 | 1155 | def __call__(self, string): 1156 | # the special argument "-" means sys.std{in,out} 1157 | if string == '-': 1158 | if 'r' in self._mode: 1159 | return _sys.stdin 1160 | elif 'w' in self._mode: 1161 | return _sys.stdout 1162 | else: 1163 | msg = _('argument "-" with mode %r' % self._mode) 1164 | raise ValueError(msg) 1165 | 1166 | try: 1167 | # all other arguments are used as file names 1168 | if self._bufsize: 1169 | return open(string, self._mode, self._bufsize) 1170 | else: 1171 | return open(string, self._mode) 1172 | except IOError: 1173 | err = _sys.exc_info()[1] 1174 | message = _("can't open '%s': %s") 1175 | raise ArgumentTypeError(message % (string, err)) 1176 | 1177 | def __repr__(self): 1178 | args = [self._mode, self._bufsize] 1179 | args_str = ', '.join([repr(arg) for arg in args if arg is not None]) 1180 | return '%s(%s)' % (type(self).__name__, args_str) 1181 | 1182 | # =========================== 1183 | # Optional and Positional Parsing 1184 | # =========================== 1185 | 1186 | class Namespace(_AttributeHolder): 1187 | """Simple object for storing attributes. 1188 | 1189 | Implements equality by attribute names and values, and provides a simple 1190 | string representation. 1191 | """ 1192 | 1193 | def __init__(self, **kwargs): 1194 | for name in kwargs: 1195 | setattr(self, name, kwargs[name]) 1196 | 1197 | __hash__ = None 1198 | 1199 | def __eq__(self, other): 1200 | return vars(self) == vars(other) 1201 | 1202 | def __ne__(self, other): 1203 | return not (self == other) 1204 | 1205 | def __contains__(self, key): 1206 | return key in self.__dict__ 1207 | 1208 | 1209 | class _ActionsContainer(object): 1210 | 1211 | def __init__(self, 1212 | description, 1213 | prefix_chars, 1214 | argument_default, 1215 | conflict_handler): 1216 | super(_ActionsContainer, self).__init__() 1217 | 1218 | self.description = description 1219 | self.argument_default = argument_default 1220 | self.prefix_chars = prefix_chars 1221 | self.conflict_handler = conflict_handler 1222 | 1223 | # set up registries 1224 | self._registries = {} 1225 | 1226 | # register actions 1227 | self.register('action', None, _StoreAction) 1228 | self.register('action', 'store', _StoreAction) 1229 | self.register('action', 'store_const', _StoreConstAction) 1230 | self.register('action', 'store_true', _StoreTrueAction) 1231 | self.register('action', 'store_false', _StoreFalseAction) 1232 | self.register('action', 'append', _AppendAction) 1233 | self.register('action', 'append_const', _AppendConstAction) 1234 | self.register('action', 'count', _CountAction) 1235 | self.register('action', 'help', _HelpAction) 1236 | self.register('action', 'version', _VersionAction) 1237 | self.register('action', 'parsers', _SubParsersAction) 1238 | 1239 | # raise an exception if the conflict handler is invalid 1240 | self._get_handler() 1241 | 1242 | # action storage 1243 | self._actions = [] 1244 | self._option_string_actions = {} 1245 | 1246 | # groups 1247 | self._action_groups = [] 1248 | self._mutually_exclusive_groups = [] 1249 | 1250 | # defaults storage 1251 | self._defaults = {} 1252 | 1253 | # determines whether an "option" looks like a negative number 1254 | self._negative_number_matcher = _re.compile(r'^-\d+$|^-\d*\.\d+$') 1255 | 1256 | # whether or not there are any optionals that look like negative 1257 | # numbers -- uses a list so it can be shared and edited 1258 | self._has_negative_number_optionals = [] 1259 | 1260 | # ==================== 1261 | # Registration methods 1262 | # ==================== 1263 | def register(self, registry_name, value, object): 1264 | registry = self._registries.setdefault(registry_name, {}) 1265 | registry[value] = object 1266 | 1267 | def _registry_get(self, registry_name, value, default=None): 1268 | return self._registries[registry_name].get(value, default) 1269 | 1270 | # ================================== 1271 | # Namespace default accessor methods 1272 | # ================================== 1273 | def set_defaults(self, **kwargs): 1274 | self._defaults.update(kwargs) 1275 | 1276 | # if these defaults match any existing arguments, replace 1277 | # the previous default on the object with the new one 1278 | for action in self._actions: 1279 | if action.dest in kwargs: 1280 | action.default = kwargs[action.dest] 1281 | 1282 | def get_default(self, dest): 1283 | for action in self._actions: 1284 | if action.dest == dest and action.default is not None: 1285 | return action.default 1286 | return self._defaults.get(dest, None) 1287 | 1288 | 1289 | # ======================= 1290 | # Adding argument actions 1291 | # ======================= 1292 | def add_argument(self, *args, **kwargs): 1293 | """ 1294 | add_argument(dest, ..., name=value, ...) 1295 | add_argument(option_string, option_string, ..., name=value, ...) 1296 | """ 1297 | 1298 | # if no positional args are supplied or only one is supplied and 1299 | # it doesn't look like an option string, parse a positional 1300 | # argument 1301 | chars = self.prefix_chars 1302 | if not args or len(args) == 1 and args[0][0] not in chars: 1303 | if args and 'dest' in kwargs: 1304 | raise ValueError('dest supplied twice for positional argument') 1305 | kwargs = self._get_positional_kwargs(*args, **kwargs) 1306 | 1307 | # otherwise, we're adding an optional argument 1308 | else: 1309 | kwargs = self._get_optional_kwargs(*args, **kwargs) 1310 | 1311 | # if no default was supplied, use the parser-level default 1312 | if 'default' not in kwargs: 1313 | dest = kwargs['dest'] 1314 | if dest in self._defaults: 1315 | kwargs['default'] = self._defaults[dest] 1316 | elif self.argument_default is not None: 1317 | kwargs['default'] = self.argument_default 1318 | 1319 | # create the action object, and add it to the parser 1320 | action_class = self._pop_action_class(kwargs) 1321 | if not _callable(action_class): 1322 | raise ValueError('unknown action "%s"' % action_class) 1323 | action = action_class(**kwargs) 1324 | 1325 | # raise an error if the action type is not callable 1326 | type_func = self._registry_get('type', action.type, action.type) 1327 | if not _callable(type_func): 1328 | raise ValueError('%r is not callable' % type_func) 1329 | 1330 | return self._add_action(action) 1331 | 1332 | def add_argument_group(self, *args, **kwargs): 1333 | group = _ArgumentGroup(self, *args, **kwargs) 1334 | self._action_groups.append(group) 1335 | return group 1336 | 1337 | def add_mutually_exclusive_group(self, **kwargs): 1338 | group = _MutuallyExclusiveGroup(self, **kwargs) 1339 | self._mutually_exclusive_groups.append(group) 1340 | return group 1341 | 1342 | def _add_action(self, action): 1343 | # resolve any conflicts 1344 | self._check_conflict(action) 1345 | 1346 | # add to actions list 1347 | self._actions.append(action) 1348 | action.container = self 1349 | 1350 | # index the action by any option strings it has 1351 | for option_string in action.option_strings: 1352 | self._option_string_actions[option_string] = action 1353 | 1354 | # set the flag if any option strings look like negative numbers 1355 | for option_string in action.option_strings: 1356 | if self._negative_number_matcher.match(option_string): 1357 | if not self._has_negative_number_optionals: 1358 | self._has_negative_number_optionals.append(True) 1359 | 1360 | # return the created action 1361 | return action 1362 | 1363 | def _remove_action(self, action): 1364 | self._actions.remove(action) 1365 | 1366 | def _add_container_actions(self, container): 1367 | # collect groups by titles 1368 | title_group_map = {} 1369 | for group in self._action_groups: 1370 | if group.title in title_group_map: 1371 | msg = _('cannot merge actions - two groups are named %r') 1372 | raise ValueError(msg % (group.title)) 1373 | title_group_map[group.title] = group 1374 | 1375 | # map each action to its group 1376 | group_map = {} 1377 | for group in container._action_groups: 1378 | 1379 | # if a group with the title exists, use that, otherwise 1380 | # create a new group matching the container's group 1381 | if group.title not in title_group_map: 1382 | title_group_map[group.title] = self.add_argument_group( 1383 | title=group.title, 1384 | description=group.description, 1385 | conflict_handler=group.conflict_handler) 1386 | 1387 | # map the actions to their new group 1388 | for action in group._group_actions: 1389 | group_map[action] = title_group_map[group.title] 1390 | 1391 | # add container's mutually exclusive groups 1392 | # NOTE: if add_mutually_exclusive_group ever gains title= and 1393 | # description= then this code will need to be expanded as above 1394 | for group in container._mutually_exclusive_groups: 1395 | mutex_group = self.add_mutually_exclusive_group( 1396 | required=group.required) 1397 | 1398 | # map the actions to their new mutex group 1399 | for action in group._group_actions: 1400 | group_map[action] = mutex_group 1401 | 1402 | # add all actions to this container or their group 1403 | for action in container._actions: 1404 | group_map.get(action, self)._add_action(action) 1405 | 1406 | def _get_positional_kwargs(self, dest, **kwargs): 1407 | # make sure required is not specified 1408 | if 'required' in kwargs: 1409 | msg = _("'required' is an invalid argument for positionals") 1410 | raise TypeError(msg) 1411 | 1412 | # mark positional arguments as required if at least one is 1413 | # always required 1414 | if kwargs.get('nargs') not in [OPTIONAL, ZERO_OR_MORE]: 1415 | kwargs['required'] = True 1416 | if kwargs.get('nargs') == ZERO_OR_MORE and 'default' not in kwargs: 1417 | kwargs['required'] = True 1418 | 1419 | # return the keyword arguments with no option strings 1420 | return dict(kwargs, dest=dest, option_strings=[]) 1421 | 1422 | def _get_optional_kwargs(self, *args, **kwargs): 1423 | # determine short and long option strings 1424 | option_strings = [] 1425 | long_option_strings = [] 1426 | for option_string in args: 1427 | # error on strings that don't start with an appropriate prefix 1428 | if not option_string[0] in self.prefix_chars: 1429 | msg = _('invalid option string %r: ' 1430 | 'must start with a character %r') 1431 | tup = option_string, self.prefix_chars 1432 | raise ValueError(msg % tup) 1433 | 1434 | # strings starting with two prefix characters are long options 1435 | option_strings.append(option_string) 1436 | if option_string[0] in self.prefix_chars: 1437 | if len(option_string) > 1: 1438 | if option_string[1] in self.prefix_chars: 1439 | long_option_strings.append(option_string) 1440 | 1441 | # infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x' 1442 | dest = kwargs.pop('dest', None) 1443 | if dest is None: 1444 | if long_option_strings: 1445 | dest_option_string = long_option_strings[0] 1446 | else: 1447 | dest_option_string = option_strings[0] 1448 | dest = dest_option_string.lstrip(self.prefix_chars) 1449 | if not dest: 1450 | msg = _('dest= is required for options like %r') 1451 | raise ValueError(msg % option_string) 1452 | dest = dest.replace('-', '_') 1453 | 1454 | # return the updated keyword arguments 1455 | return dict(kwargs, dest=dest, option_strings=option_strings) 1456 | 1457 | def _pop_action_class(self, kwargs, default=None): 1458 | action = kwargs.pop('action', default) 1459 | return self._registry_get('action', action, action) 1460 | 1461 | def _get_handler(self): 1462 | # determine function from conflict handler string 1463 | handler_func_name = '_handle_conflict_%s' % self.conflict_handler 1464 | try: 1465 | return getattr(self, handler_func_name) 1466 | except AttributeError: 1467 | msg = _('invalid conflict_resolution value: %r') 1468 | raise ValueError(msg % self.conflict_handler) 1469 | 1470 | def _check_conflict(self, action): 1471 | 1472 | # find all options that conflict with this option 1473 | confl_optionals = [] 1474 | for option_string in action.option_strings: 1475 | if option_string in self._option_string_actions: 1476 | confl_optional = self._option_string_actions[option_string] 1477 | confl_optionals.append((option_string, confl_optional)) 1478 | 1479 | # resolve any conflicts 1480 | if confl_optionals: 1481 | conflict_handler = self._get_handler() 1482 | conflict_handler(action, confl_optionals) 1483 | 1484 | def _handle_conflict_error(self, action, conflicting_actions): 1485 | message = _('conflicting option string(s): %s') 1486 | conflict_string = ', '.join([option_string 1487 | for option_string, action 1488 | in conflicting_actions]) 1489 | raise ArgumentError(action, message % conflict_string) 1490 | 1491 | def _handle_conflict_resolve(self, action, conflicting_actions): 1492 | 1493 | # remove all conflicting options 1494 | for option_string, action in conflicting_actions: 1495 | 1496 | # remove the conflicting option 1497 | action.option_strings.remove(option_string) 1498 | self._option_string_actions.pop(option_string, None) 1499 | 1500 | # if the option now has no option string, remove it from the 1501 | # container holding it 1502 | if not action.option_strings: 1503 | action.container._remove_action(action) 1504 | 1505 | 1506 | class _ArgumentGroup(_ActionsContainer): 1507 | 1508 | def __init__(self, container, title=None, description=None, **kwargs): 1509 | # add any missing keyword arguments by checking the container 1510 | update = kwargs.setdefault 1511 | update('conflict_handler', container.conflict_handler) 1512 | update('prefix_chars', container.prefix_chars) 1513 | update('argument_default', container.argument_default) 1514 | super_init = super(_ArgumentGroup, self).__init__ 1515 | super_init(description=description, **kwargs) 1516 | 1517 | # group attributes 1518 | self.title = title 1519 | self._group_actions = [] 1520 | 1521 | # share most attributes with the container 1522 | self._registries = container._registries 1523 | self._actions = container._actions 1524 | self._option_string_actions = container._option_string_actions 1525 | self._defaults = container._defaults 1526 | self._has_negative_number_optionals = \ 1527 | container._has_negative_number_optionals 1528 | 1529 | def _add_action(self, action): 1530 | action = super(_ArgumentGroup, self)._add_action(action) 1531 | self._group_actions.append(action) 1532 | return action 1533 | 1534 | def _remove_action(self, action): 1535 | super(_ArgumentGroup, self)._remove_action(action) 1536 | self._group_actions.remove(action) 1537 | 1538 | 1539 | class _MutuallyExclusiveGroup(_ArgumentGroup): 1540 | 1541 | def __init__(self, container, required=False): 1542 | super(_MutuallyExclusiveGroup, self).__init__(container) 1543 | self.required = required 1544 | self._container = container 1545 | 1546 | def _add_action(self, action): 1547 | if action.required: 1548 | msg = _('mutually exclusive arguments must be optional') 1549 | raise ValueError(msg) 1550 | action = self._container._add_action(action) 1551 | self._group_actions.append(action) 1552 | return action 1553 | 1554 | def _remove_action(self, action): 1555 | self._container._remove_action(action) 1556 | self._group_actions.remove(action) 1557 | 1558 | 1559 | class ArgumentParser(_AttributeHolder, _ActionsContainer): 1560 | """Object for parsing command line strings into Python objects. 1561 | 1562 | Keyword Arguments: 1563 | - prog -- The name of the program (default: sys.argv[0]) 1564 | - usage -- A usage message (default: auto-generated from arguments) 1565 | - description -- A description of what the program does 1566 | - epilog -- Text following the argument descriptions 1567 | - parents -- Parsers whose arguments should be copied into this one 1568 | - formatter_class -- HelpFormatter class for printing help messages 1569 | - prefix_chars -- Characters that prefix optional arguments 1570 | - fromfile_prefix_chars -- Characters that prefix files containing 1571 | additional arguments 1572 | - argument_default -- The default value for all arguments 1573 | - conflict_handler -- String indicating how to handle conflicts 1574 | - add_help -- Add a -h/-help option 1575 | """ 1576 | 1577 | def __init__(self, 1578 | prog=None, 1579 | usage=None, 1580 | description=None, 1581 | epilog=None, 1582 | version=None, 1583 | parents=[], 1584 | formatter_class=HelpFormatter, 1585 | prefix_chars='-', 1586 | fromfile_prefix_chars=None, 1587 | argument_default=None, 1588 | conflict_handler='error', 1589 | add_help=True): 1590 | 1591 | if version is not None: 1592 | import warnings 1593 | warnings.warn( 1594 | """The "version" argument to ArgumentParser is deprecated. """ 1595 | """Please use """ 1596 | """"add_argument(..., action='version', version="N", ...)" """ 1597 | """instead""", DeprecationWarning) 1598 | 1599 | superinit = super(ArgumentParser, self).__init__ 1600 | superinit(description=description, 1601 | prefix_chars=prefix_chars, 1602 | argument_default=argument_default, 1603 | conflict_handler=conflict_handler) 1604 | 1605 | # default setting for prog 1606 | if prog is None: 1607 | prog = _os.path.basename(_sys.argv[0]) 1608 | 1609 | self.prog = prog 1610 | self.usage = usage 1611 | self.epilog = epilog 1612 | self.version = version 1613 | self.formatter_class = formatter_class 1614 | self.fromfile_prefix_chars = fromfile_prefix_chars 1615 | self.add_help = add_help 1616 | 1617 | add_group = self.add_argument_group 1618 | self._positionals = add_group(_('positional arguments')) 1619 | self._optionals = add_group(_('optional arguments')) 1620 | self._subparsers = None 1621 | 1622 | # register types 1623 | def identity(string): 1624 | return string 1625 | self.register('type', None, identity) 1626 | 1627 | # add help and version arguments if necessary 1628 | # (using explicit default to override global argument_default) 1629 | if '-' in prefix_chars: 1630 | default_prefix = '-' 1631 | else: 1632 | default_prefix = prefix_chars[0] 1633 | if self.add_help: 1634 | self.add_argument( 1635 | default_prefix+'h', default_prefix*2+'help', 1636 | action='help', default=SUPPRESS, 1637 | help=_('show this help message and exit')) 1638 | if self.version: 1639 | self.add_argument( 1640 | default_prefix+'v', default_prefix*2+'version', 1641 | action='version', default=SUPPRESS, 1642 | version=self.version, 1643 | help=_("show program's version number and exit")) 1644 | 1645 | # add parent arguments and defaults 1646 | for parent in parents: 1647 | self._add_container_actions(parent) 1648 | try: 1649 | defaults = parent._defaults 1650 | except AttributeError: 1651 | pass 1652 | else: 1653 | self._defaults.update(defaults) 1654 | 1655 | # ======================= 1656 | # Pretty __repr__ methods 1657 | # ======================= 1658 | def _get_kwargs(self): 1659 | names = [ 1660 | 'prog', 1661 | 'usage', 1662 | 'description', 1663 | 'version', 1664 | 'formatter_class', 1665 | 'conflict_handler', 1666 | 'add_help', 1667 | ] 1668 | return [(name, getattr(self, name)) for name in names] 1669 | 1670 | # ================================== 1671 | # Optional/Positional adding methods 1672 | # ================================== 1673 | def add_subparsers(self, **kwargs): 1674 | if self._subparsers is not None: 1675 | self.error(_('cannot have multiple subparser arguments')) 1676 | 1677 | # add the parser class to the arguments if it's not present 1678 | kwargs.setdefault('parser_class', type(self)) 1679 | 1680 | if 'title' in kwargs or 'description' in kwargs: 1681 | title = _(kwargs.pop('title', 'subcommands')) 1682 | description = _(kwargs.pop('description', None)) 1683 | self._subparsers = self.add_argument_group(title, description) 1684 | else: 1685 | self._subparsers = self._positionals 1686 | 1687 | # prog defaults to the usage message of this parser, skipping 1688 | # optional arguments and with no "usage:" prefix 1689 | if kwargs.get('prog') is None: 1690 | formatter = self._get_formatter() 1691 | positionals = self._get_positional_actions() 1692 | groups = self._mutually_exclusive_groups 1693 | formatter.add_usage(self.usage, positionals, groups, '') 1694 | kwargs['prog'] = formatter.format_help().strip() 1695 | 1696 | # create the parsers action and add it to the positionals list 1697 | parsers_class = self._pop_action_class(kwargs, 'parsers') 1698 | action = parsers_class(option_strings=[], **kwargs) 1699 | self._subparsers._add_action(action) 1700 | 1701 | # return the created parsers action 1702 | return action 1703 | 1704 | def _add_action(self, action): 1705 | if action.option_strings: 1706 | self._optionals._add_action(action) 1707 | else: 1708 | self._positionals._add_action(action) 1709 | return action 1710 | 1711 | def _get_optional_actions(self): 1712 | return [action 1713 | for action in self._actions 1714 | if action.option_strings] 1715 | 1716 | def _get_positional_actions(self): 1717 | return [action 1718 | for action in self._actions 1719 | if not action.option_strings] 1720 | 1721 | # ===================================== 1722 | # Command line argument parsing methods 1723 | # ===================================== 1724 | def parse_args(self, args=None, namespace=None): 1725 | args, argv = self.parse_known_args(args, namespace) 1726 | if argv: 1727 | msg = _('unrecognized arguments: %s') 1728 | self.error(msg % ' '.join(argv)) 1729 | return args 1730 | 1731 | def parse_known_args(self, args=None, namespace=None): 1732 | # args default to the system args 1733 | if args is None: 1734 | args = _sys.argv[1:] 1735 | 1736 | # default Namespace built from parser defaults 1737 | if namespace is None: 1738 | namespace = Namespace() 1739 | 1740 | # add any action defaults that aren't present 1741 | for action in self._actions: 1742 | if action.dest is not SUPPRESS: 1743 | if not hasattr(namespace, action.dest): 1744 | if action.default is not SUPPRESS: 1745 | setattr(namespace, action.dest, action.default) 1746 | 1747 | # add any parser defaults that aren't present 1748 | for dest in self._defaults: 1749 | if not hasattr(namespace, dest): 1750 | setattr(namespace, dest, self._defaults[dest]) 1751 | 1752 | # parse the arguments and exit if there are any errors 1753 | try: 1754 | namespace, args = self._parse_known_args(args, namespace) 1755 | if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR): 1756 | args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR)) 1757 | delattr(namespace, _UNRECOGNIZED_ARGS_ATTR) 1758 | return namespace, args 1759 | except ArgumentError: 1760 | err = _sys.exc_info()[1] 1761 | self.error(str(err)) 1762 | 1763 | def _parse_known_args(self, arg_strings, namespace): 1764 | # replace arg strings that are file references 1765 | if self.fromfile_prefix_chars is not None: 1766 | arg_strings = self._read_args_from_files(arg_strings) 1767 | 1768 | # map all mutually exclusive arguments to the other arguments 1769 | # they can't occur with 1770 | action_conflicts = {} 1771 | for mutex_group in self._mutually_exclusive_groups: 1772 | group_actions = mutex_group._group_actions 1773 | for i, mutex_action in enumerate(mutex_group._group_actions): 1774 | conflicts = action_conflicts.setdefault(mutex_action, []) 1775 | conflicts.extend(group_actions[:i]) 1776 | conflicts.extend(group_actions[i + 1:]) 1777 | 1778 | # find all option indices, and determine the arg_string_pattern 1779 | # which has an 'O' if there is an option at an index, 1780 | # an 'A' if there is an argument, or a '-' if there is a '--' 1781 | option_string_indices = {} 1782 | arg_string_pattern_parts = [] 1783 | arg_strings_iter = iter(arg_strings) 1784 | for i, arg_string in enumerate(arg_strings_iter): 1785 | 1786 | # all args after -- are non-options 1787 | if arg_string == '--': 1788 | arg_string_pattern_parts.append('-') 1789 | for arg_string in arg_strings_iter: 1790 | arg_string_pattern_parts.append('A') 1791 | 1792 | # otherwise, add the arg to the arg strings 1793 | # and note the index if it was an option 1794 | else: 1795 | option_tuple = self._parse_optional(arg_string) 1796 | if option_tuple is None: 1797 | pattern = 'A' 1798 | else: 1799 | option_string_indices[i] = option_tuple 1800 | pattern = 'O' 1801 | arg_string_pattern_parts.append(pattern) 1802 | 1803 | # join the pieces together to form the pattern 1804 | arg_strings_pattern = ''.join(arg_string_pattern_parts) 1805 | 1806 | # converts arg strings to the appropriate and then takes the action 1807 | seen_actions = set() 1808 | seen_non_default_actions = set() 1809 | 1810 | def take_action(action, argument_strings, option_string=None): 1811 | seen_actions.add(action) 1812 | argument_values = self._get_values(action, argument_strings) 1813 | 1814 | # error if this argument is not allowed with other previously 1815 | # seen arguments, assuming that actions that use the default 1816 | # value don't really count as "present" 1817 | if argument_values is not action.default: 1818 | seen_non_default_actions.add(action) 1819 | for conflict_action in action_conflicts.get(action, []): 1820 | if conflict_action in seen_non_default_actions: 1821 | msg = _('not allowed with argument %s') 1822 | action_name = _get_action_name(conflict_action) 1823 | raise ArgumentError(action, msg % action_name) 1824 | 1825 | # take the action if we didn't receive a SUPPRESS value 1826 | # (e.g. from a default) 1827 | if argument_values is not SUPPRESS: 1828 | action(self, namespace, argument_values, option_string) 1829 | 1830 | # function to convert arg_strings into an optional action 1831 | def consume_optional(start_index): 1832 | 1833 | # get the optional identified at this index 1834 | option_tuple = option_string_indices[start_index] 1835 | action, option_string, explicit_arg = option_tuple 1836 | 1837 | # identify additional optionals in the same arg string 1838 | # (e.g. -xyz is the same as -x -y -z if no args are required) 1839 | match_argument = self._match_argument 1840 | action_tuples = [] 1841 | while True: 1842 | 1843 | # if we found no optional action, skip it 1844 | if action is None: 1845 | extras.append(arg_strings[start_index]) 1846 | return start_index + 1 1847 | 1848 | # if there is an explicit argument, try to match the 1849 | # optional's string arguments to only this 1850 | if explicit_arg is not None: 1851 | arg_count = match_argument(action, 'A') 1852 | 1853 | # if the action is a single-dash option and takes no 1854 | # arguments, try to parse more single-dash options out 1855 | # of the tail of the option string 1856 | chars = self.prefix_chars 1857 | if arg_count == 0 and option_string[1] not in chars: 1858 | action_tuples.append((action, [], option_string)) 1859 | char = option_string[0] 1860 | option_string = char + explicit_arg[0] 1861 | new_explicit_arg = explicit_arg[1:] or None 1862 | optionals_map = self._option_string_actions 1863 | if option_string in optionals_map: 1864 | action = optionals_map[option_string] 1865 | explicit_arg = new_explicit_arg 1866 | else: 1867 | msg = _('ignored explicit argument %r') 1868 | raise ArgumentError(action, msg % explicit_arg) 1869 | 1870 | # if the action expect exactly one argument, we've 1871 | # successfully matched the option; exit the loop 1872 | elif arg_count == 1: 1873 | stop = start_index + 1 1874 | args = [explicit_arg] 1875 | action_tuples.append((action, args, option_string)) 1876 | break 1877 | 1878 | # error if a double-dash option did not use the 1879 | # explicit argument 1880 | else: 1881 | msg = _('ignored explicit argument %r') 1882 | raise ArgumentError(action, msg % explicit_arg) 1883 | 1884 | # if there is no explicit argument, try to match the 1885 | # optional's string arguments with the following strings 1886 | # if successful, exit the loop 1887 | else: 1888 | start = start_index + 1 1889 | selected_patterns = arg_strings_pattern[start:] 1890 | arg_count = match_argument(action, selected_patterns) 1891 | stop = start + arg_count 1892 | args = arg_strings[start:stop] 1893 | action_tuples.append((action, args, option_string)) 1894 | break 1895 | 1896 | # add the Optional to the list and return the index at which 1897 | # the Optional's string args stopped 1898 | assert action_tuples 1899 | for action, args, option_string in action_tuples: 1900 | take_action(action, args, option_string) 1901 | return stop 1902 | 1903 | # the list of Positionals left to be parsed; this is modified 1904 | # by consume_positionals() 1905 | positionals = self._get_positional_actions() 1906 | 1907 | # function to convert arg_strings into positional actions 1908 | def consume_positionals(start_index): 1909 | # match as many Positionals as possible 1910 | match_partial = self._match_arguments_partial 1911 | selected_pattern = arg_strings_pattern[start_index:] 1912 | arg_counts = match_partial(positionals, selected_pattern) 1913 | 1914 | # slice off the appropriate arg strings for each Positional 1915 | # and add the Positional and its args to the list 1916 | for action, arg_count in zip(positionals, arg_counts): 1917 | args = arg_strings[start_index: start_index + arg_count] 1918 | start_index += arg_count 1919 | take_action(action, args) 1920 | 1921 | # slice off the Positionals that we just parsed and return the 1922 | # index at which the Positionals' string args stopped 1923 | positionals[:] = positionals[len(arg_counts):] 1924 | return start_index 1925 | 1926 | # consume Positionals and Optionals alternately, until we have 1927 | # passed the last option string 1928 | extras = [] 1929 | start_index = 0 1930 | if option_string_indices: 1931 | max_option_string_index = max(option_string_indices) 1932 | else: 1933 | max_option_string_index = -1 1934 | while start_index <= max_option_string_index: 1935 | 1936 | # consume any Positionals preceding the next option 1937 | next_option_string_index = min([ 1938 | index 1939 | for index in option_string_indices 1940 | if index >= start_index]) 1941 | if start_index != next_option_string_index: 1942 | positionals_end_index = consume_positionals(start_index) 1943 | 1944 | # only try to parse the next optional if we didn't consume 1945 | # the option string during the positionals parsing 1946 | if positionals_end_index > start_index: 1947 | start_index = positionals_end_index 1948 | continue 1949 | else: 1950 | start_index = positionals_end_index 1951 | 1952 | # if we consumed all the positionals we could and we're not 1953 | # at the index of an option string, there were extra arguments 1954 | if start_index not in option_string_indices: 1955 | strings = arg_strings[start_index:next_option_string_index] 1956 | extras.extend(strings) 1957 | start_index = next_option_string_index 1958 | 1959 | # consume the next optional and any arguments for it 1960 | start_index = consume_optional(start_index) 1961 | 1962 | # consume any positionals following the last Optional 1963 | stop_index = consume_positionals(start_index) 1964 | 1965 | # if we didn't consume all the argument strings, there were extras 1966 | extras.extend(arg_strings[stop_index:]) 1967 | 1968 | # if we didn't use all the Positional objects, there were too few 1969 | # arg strings supplied. 1970 | if positionals: 1971 | self.error(_('too few arguments')) 1972 | 1973 | # make sure all required actions were present, and convert defaults. 1974 | for action in self._actions: 1975 | if action not in seen_actions: 1976 | if action.required: 1977 | name = _get_action_name(action) 1978 | self.error(_('argument %s is required') % name) 1979 | else: 1980 | # Convert action default now instead of doing it before 1981 | # parsing arguments to avoid calling convert functions 1982 | # twice (which may fail) if the argument was given, but 1983 | # only if it was defined already in the namespace 1984 | if (action.default is not None and 1985 | isinstance(action.default, basestring) and 1986 | hasattr(namespace, action.dest) and 1987 | action.default is getattr(namespace, action.dest)): 1988 | setattr(namespace, action.dest, 1989 | self._get_value(action, action.default)) 1990 | 1991 | # make sure all required groups had one option present 1992 | for group in self._mutually_exclusive_groups: 1993 | if group.required: 1994 | for action in group._group_actions: 1995 | if action in seen_non_default_actions: 1996 | break 1997 | 1998 | # if no actions were used, report the error 1999 | else: 2000 | names = [_get_action_name(action) 2001 | for action in group._group_actions 2002 | if action.help is not SUPPRESS] 2003 | msg = _('one of the arguments %s is required') 2004 | self.error(msg % ' '.join(names)) 2005 | 2006 | # return the updated namespace and the extra arguments 2007 | return namespace, extras 2008 | 2009 | def _read_args_from_files(self, arg_strings): 2010 | # expand arguments referencing files 2011 | new_arg_strings = [] 2012 | for arg_string in arg_strings: 2013 | 2014 | # for regular arguments, just add them back into the list 2015 | if arg_string[0] not in self.fromfile_prefix_chars: 2016 | new_arg_strings.append(arg_string) 2017 | 2018 | # replace arguments referencing files with the file content 2019 | else: 2020 | try: 2021 | args_file = open(arg_string[1:]) 2022 | try: 2023 | arg_strings = [] 2024 | for arg_line in args_file.read().splitlines(): 2025 | for arg in self.convert_arg_line_to_args(arg_line): 2026 | arg_strings.append(arg) 2027 | arg_strings = self._read_args_from_files(arg_strings) 2028 | new_arg_strings.extend(arg_strings) 2029 | finally: 2030 | args_file.close() 2031 | except IOError: 2032 | err = _sys.exc_info()[1] 2033 | self.error(str(err)) 2034 | 2035 | # return the modified argument list 2036 | return new_arg_strings 2037 | 2038 | def convert_arg_line_to_args(self, arg_line): 2039 | return [arg_line] 2040 | 2041 | def _match_argument(self, action, arg_strings_pattern): 2042 | # match the pattern for this action to the arg strings 2043 | nargs_pattern = self._get_nargs_pattern(action) 2044 | match = _re.match(nargs_pattern, arg_strings_pattern) 2045 | 2046 | # raise an exception if we weren't able to find a match 2047 | if match is None: 2048 | nargs_errors = { 2049 | None: _('expected one argument'), 2050 | OPTIONAL: _('expected at most one argument'), 2051 | ONE_OR_MORE: _('expected at least one argument'), 2052 | } 2053 | default = _('expected %s argument(s)') % action.nargs 2054 | msg = nargs_errors.get(action.nargs, default) 2055 | raise ArgumentError(action, msg) 2056 | 2057 | # return the number of arguments matched 2058 | return len(match.group(1)) 2059 | 2060 | def _match_arguments_partial(self, actions, arg_strings_pattern): 2061 | # progressively shorten the actions list by slicing off the 2062 | # final actions until we find a match 2063 | result = [] 2064 | for i in range(len(actions), 0, -1): 2065 | actions_slice = actions[:i] 2066 | pattern = ''.join([self._get_nargs_pattern(action) 2067 | for action in actions_slice]) 2068 | match = _re.match(pattern, arg_strings_pattern) 2069 | if match is not None: 2070 | result.extend([len(string) for string in match.groups()]) 2071 | break 2072 | 2073 | # return the list of arg string counts 2074 | return result 2075 | 2076 | def _parse_optional(self, arg_string): 2077 | # if it's an empty string, it was meant to be a positional 2078 | if not arg_string: 2079 | return None 2080 | 2081 | # if it doesn't start with a prefix, it was meant to be positional 2082 | if not arg_string[0] in self.prefix_chars: 2083 | return None 2084 | 2085 | # if the option string is present in the parser, return the action 2086 | if arg_string in self._option_string_actions: 2087 | action = self._option_string_actions[arg_string] 2088 | return action, arg_string, None 2089 | 2090 | # if it's just a single character, it was meant to be positional 2091 | if len(arg_string) == 1: 2092 | return None 2093 | 2094 | # if the option string before the "=" is present, return the action 2095 | if '=' in arg_string: 2096 | option_string, explicit_arg = arg_string.split('=', 1) 2097 | if option_string in self._option_string_actions: 2098 | action = self._option_string_actions[option_string] 2099 | return action, option_string, explicit_arg 2100 | 2101 | # search through all possible prefixes of the option string 2102 | # and all actions in the parser for possible interpretations 2103 | option_tuples = self._get_option_tuples(arg_string) 2104 | 2105 | # if multiple actions match, the option string was ambiguous 2106 | if len(option_tuples) > 1: 2107 | options = ', '.join([option_string 2108 | for action, option_string, explicit_arg in option_tuples]) 2109 | tup = arg_string, options 2110 | self.error(_('ambiguous option: %s could match %s') % tup) 2111 | 2112 | # if exactly one action matched, this segmentation is good, 2113 | # so return the parsed action 2114 | elif len(option_tuples) == 1: 2115 | option_tuple, = option_tuples 2116 | return option_tuple 2117 | 2118 | # if it was not found as an option, but it looks like a negative 2119 | # number, it was meant to be positional 2120 | # unless there are negative-number-like options 2121 | if self._negative_number_matcher.match(arg_string): 2122 | if not self._has_negative_number_optionals: 2123 | return None 2124 | 2125 | # if it contains a space, it was meant to be a positional 2126 | if ' ' in arg_string: 2127 | return None 2128 | 2129 | # it was meant to be an optional but there is no such option 2130 | # in this parser (though it might be a valid option in a subparser) 2131 | return None, arg_string, None 2132 | 2133 | def _get_option_tuples(self, option_string): 2134 | result = [] 2135 | 2136 | # option strings starting with two prefix characters are only 2137 | # split at the '=' 2138 | chars = self.prefix_chars 2139 | if option_string[0] in chars and option_string[1] in chars: 2140 | if '=' in option_string: 2141 | option_prefix, explicit_arg = option_string.split('=', 1) 2142 | else: 2143 | option_prefix = option_string 2144 | explicit_arg = None 2145 | for option_string in self._option_string_actions: 2146 | if option_string.startswith(option_prefix): 2147 | action = self._option_string_actions[option_string] 2148 | tup = action, option_string, explicit_arg 2149 | result.append(tup) 2150 | 2151 | # single character options can be concatenated with their arguments 2152 | # but multiple character options always have to have their argument 2153 | # separate 2154 | elif option_string[0] in chars and option_string[1] not in chars: 2155 | option_prefix = option_string 2156 | explicit_arg = None 2157 | short_option_prefix = option_string[:2] 2158 | short_explicit_arg = option_string[2:] 2159 | 2160 | for option_string in self._option_string_actions: 2161 | if option_string == short_option_prefix: 2162 | action = self._option_string_actions[option_string] 2163 | tup = action, option_string, short_explicit_arg 2164 | result.append(tup) 2165 | elif option_string.startswith(option_prefix): 2166 | action = self._option_string_actions[option_string] 2167 | tup = action, option_string, explicit_arg 2168 | result.append(tup) 2169 | 2170 | # shouldn't ever get here 2171 | else: 2172 | self.error(_('unexpected option string: %s') % option_string) 2173 | 2174 | # return the collected option tuples 2175 | return result 2176 | 2177 | def _get_nargs_pattern(self, action): 2178 | # in all examples below, we have to allow for '--' args 2179 | # which are represented as '-' in the pattern 2180 | nargs = action.nargs 2181 | 2182 | # the default (None) is assumed to be a single argument 2183 | if nargs is None: 2184 | nargs_pattern = '(-*A-*)' 2185 | 2186 | # allow zero or one arguments 2187 | elif nargs == OPTIONAL: 2188 | nargs_pattern = '(-*A?-*)' 2189 | 2190 | # allow zero or more arguments 2191 | elif nargs == ZERO_OR_MORE: 2192 | nargs_pattern = '(-*[A-]*)' 2193 | 2194 | # allow one or more arguments 2195 | elif nargs == ONE_OR_MORE: 2196 | nargs_pattern = '(-*A[A-]*)' 2197 | 2198 | # allow any number of options or arguments 2199 | elif nargs == REMAINDER: 2200 | nargs_pattern = '([-AO]*)' 2201 | 2202 | # allow one argument followed by any number of options or arguments 2203 | elif nargs == PARSER: 2204 | nargs_pattern = '(-*A[-AO]*)' 2205 | 2206 | # all others should be integers 2207 | else: 2208 | nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs) 2209 | 2210 | # if this is an optional action, -- is not allowed 2211 | if action.option_strings: 2212 | nargs_pattern = nargs_pattern.replace('-*', '') 2213 | nargs_pattern = nargs_pattern.replace('-', '') 2214 | 2215 | # return the pattern 2216 | return nargs_pattern 2217 | 2218 | # ======================== 2219 | # Value conversion methods 2220 | # ======================== 2221 | def _get_values(self, action, arg_strings): 2222 | # for everything but PARSER args, strip out '--' 2223 | if action.nargs not in [PARSER, REMAINDER]: 2224 | arg_strings = [s for s in arg_strings if s != '--'] 2225 | 2226 | # optional argument produces a default when not present 2227 | if not arg_strings and action.nargs == OPTIONAL: 2228 | if action.option_strings: 2229 | value = action.const 2230 | else: 2231 | value = action.default 2232 | if isinstance(value, basestring): 2233 | value = self._get_value(action, value) 2234 | self._check_value(action, value) 2235 | 2236 | # when nargs='*' on a positional, if there were no command-line 2237 | # args, use the default if it is anything other than None 2238 | elif (not arg_strings and action.nargs == ZERO_OR_MORE and 2239 | not action.option_strings): 2240 | if action.default is not None: 2241 | value = action.default 2242 | else: 2243 | value = arg_strings 2244 | self._check_value(action, value) 2245 | 2246 | # single argument or optional argument produces a single value 2247 | elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]: 2248 | arg_string, = arg_strings 2249 | value = self._get_value(action, arg_string) 2250 | self._check_value(action, value) 2251 | 2252 | # REMAINDER arguments convert all values, checking none 2253 | elif action.nargs == REMAINDER: 2254 | value = [self._get_value(action, v) for v in arg_strings] 2255 | 2256 | # PARSER arguments convert all values, but check only the first 2257 | elif action.nargs == PARSER: 2258 | value = [self._get_value(action, v) for v in arg_strings] 2259 | self._check_value(action, value[0]) 2260 | 2261 | # all other types of nargs produce a list 2262 | else: 2263 | value = [self._get_value(action, v) for v in arg_strings] 2264 | for v in value: 2265 | self._check_value(action, v) 2266 | 2267 | # return the converted value 2268 | return value 2269 | 2270 | def _get_value(self, action, arg_string): 2271 | type_func = self._registry_get('type', action.type, action.type) 2272 | if not _callable(type_func): 2273 | msg = _('%r is not callable') 2274 | raise ArgumentError(action, msg % type_func) 2275 | 2276 | # convert the value to the appropriate type 2277 | try: 2278 | result = type_func(arg_string) 2279 | 2280 | # ArgumentTypeErrors indicate errors 2281 | except ArgumentTypeError: 2282 | name = getattr(action.type, '__name__', repr(action.type)) 2283 | msg = str(_sys.exc_info()[1]) 2284 | raise ArgumentError(action, msg) 2285 | 2286 | # TypeErrors or ValueErrors also indicate errors 2287 | except (TypeError, ValueError): 2288 | name = getattr(action.type, '__name__', repr(action.type)) 2289 | msg = _('invalid %s value: %r') 2290 | raise ArgumentError(action, msg % (name, arg_string)) 2291 | 2292 | # return the converted value 2293 | return result 2294 | 2295 | def _check_value(self, action, value): 2296 | # converted value must be one of the choices (if specified) 2297 | if action.choices is not None and value not in action.choices: 2298 | tup = value, ', '.join(map(repr, action.choices)) 2299 | msg = _('invalid choice: %r (choose from %s)') % tup 2300 | raise ArgumentError(action, msg) 2301 | 2302 | # ======================= 2303 | # Help-formatting methods 2304 | # ======================= 2305 | def format_usage(self): 2306 | formatter = self._get_formatter() 2307 | formatter.add_usage(self.usage, self._actions, 2308 | self._mutually_exclusive_groups) 2309 | return formatter.format_help() 2310 | 2311 | def format_help(self): 2312 | formatter = self._get_formatter() 2313 | 2314 | # usage 2315 | formatter.add_usage(self.usage, self._actions, 2316 | self._mutually_exclusive_groups) 2317 | 2318 | # description 2319 | formatter.add_text(self.description) 2320 | 2321 | # positionals, optionals and user-defined groups 2322 | for action_group in self._action_groups: 2323 | formatter.start_section(action_group.title) 2324 | formatter.add_text(action_group.description) 2325 | formatter.add_arguments(action_group._group_actions) 2326 | formatter.end_section() 2327 | 2328 | # epilog 2329 | formatter.add_text(self.epilog) 2330 | 2331 | # determine help from format above 2332 | return formatter.format_help() 2333 | 2334 | def format_version(self): 2335 | import warnings 2336 | warnings.warn( 2337 | 'The format_version method is deprecated -- the "version" ' 2338 | 'argument to ArgumentParser is no longer supported.', 2339 | DeprecationWarning) 2340 | formatter = self._get_formatter() 2341 | formatter.add_text(self.version) 2342 | return formatter.format_help() 2343 | 2344 | def _get_formatter(self): 2345 | return self.formatter_class(prog=self.prog) 2346 | 2347 | # ===================== 2348 | # Help-printing methods 2349 | # ===================== 2350 | def print_usage(self, file=None): 2351 | if file is None: 2352 | file = _sys.stdout 2353 | self._print_message(self.format_usage(), file) 2354 | 2355 | def print_help(self, file=None): 2356 | if file is None: 2357 | file = _sys.stdout 2358 | self._print_message(self.format_help(), file) 2359 | 2360 | def print_version(self, file=None): 2361 | import warnings 2362 | warnings.warn( 2363 | 'The print_version method is deprecated -- the "version" ' 2364 | 'argument to ArgumentParser is no longer supported.', 2365 | DeprecationWarning) 2366 | self._print_message(self.format_version(), file) 2367 | 2368 | def _print_message(self, message, file=None): 2369 | if message: 2370 | if file is None: 2371 | file = _sys.stderr 2372 | file.write(message) 2373 | 2374 | # =============== 2375 | # Exiting methods 2376 | # =============== 2377 | def exit(self, status=0, message=None): 2378 | if message: 2379 | self._print_message(message, _sys.stderr) 2380 | _sys.exit(status) 2381 | 2382 | def error(self, message): 2383 | """error(message: string) 2384 | 2385 | Prints a usage message incorporating the message to stderr and 2386 | exits. 2387 | 2388 | If you override this in a subclass, it should not return -- it 2389 | should either exit or raise an exception. 2390 | """ 2391 | self.print_usage(_sys.stderr) 2392 | self.exit(2, _('%s: error: %s\n') % (self.prog, message)) 2393 | -------------------------------------------------------------------------------- /opsmtools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # opsmtools.py - expose various MongoDB 4 | # Ops Manager API endpoint through a simple 5 | # command line script. 6 | # 7 | # Installation: 8 | # pip install requests,terminaltables 9 | # 10 | import sys, os 11 | import datetime 12 | import argparse 13 | import requests 14 | import json 15 | import copy 16 | from requests.auth import HTTPDigestAuth 17 | import time 18 | import tarfile 19 | import subprocess, signal 20 | import ast 21 | 22 | # verbose print message only if args.verbose=True 23 | def vprint(message,args): 24 | if args.verbose==True: 25 | print message 26 | 27 | # print out list of snapshots for a given cluster/replset id 28 | def get_snapshots(args): 29 | try: 30 | from terminaltables import AsciiTable 31 | except ImportError: 32 | AsciiTable = False 33 | pass 34 | response= requests.get(args.host+"/api/public/v1.0/groups/"+args.group+"/clusters/"+args.clusterId+"/snapshots" 35 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 36 | response.raise_for_status() 37 | vprint("============= response ==============",args) 38 | vprint( vars(response),args ) 39 | vprint("============= end response ==============",args) 40 | 41 | hosts_json = response.json() 42 | vprint(hosts_json,args) 43 | table_data = [ 44 | ['created','expires','complete','replicaSetName','id','parts'] 45 | ] 46 | for host in hosts_json['results']: 47 | row = [] 48 | part_data = [ 49 | [ 'replicaSetName', 'storageSizeBytes', 'mongodbVersion', 'typeName','fileSizeBytes','dataSizeBytes'] 50 | ] 51 | for column in table_data[0]: 52 | if column=='parts': 53 | parts = [] 54 | for part in host['parts']: 55 | for pcol in part_data[0]: 56 | parts.append( pcol + ":" + str( part.get(pcol) ) ) 57 | parts.append("++++++++++++++") 58 | row.append(str.join("\n",parts)) 59 | elif column=='created': 60 | row.append( str( host.get('created').get('date') ) ) 61 | else: 62 | row.append( str( host.get(column) ) ) 63 | table_data.append( row ) 64 | 65 | table_data.append(['','','Number of snapshots',str(hosts_json['totalCount'])]) 66 | 67 | host_info = 'Snapshots from ' + args.host + " for clusterId=" + args.clusterId 68 | 69 | if AsciiTable: 70 | table = AsciiTable( table_data, host_info ); 71 | table.inner_footing_row_border = True 72 | print table.table 73 | else: 74 | import pprint 75 | pprint.pprint(table_data) 76 | 77 | # create a restore job for a given snapshotId 78 | def create_restore(args): 79 | try: 80 | from terminaltables import AsciiTable 81 | except ImportError: 82 | AsciiTable = False 83 | pass 84 | headers = { "Content-Type" : "application/json" } 85 | if args.snapshotId: 86 | snapshotInfo = { "snapshotId" : args.snapshotId } 87 | elif args.snapshotTimestamp: 88 | snapshotInfo = { "timestamp" : { "date" : args.snapshotTimestamp, "increment" : 0 } } 89 | if args.snapshotIncrement: 90 | snapshotInfo['timestamp']['increment'] = args.snapshotIncrement 91 | else: 92 | raise Exception("ERROR no snapshotId or no snapshotTimestamp found, required for create_restore") 93 | vprint("============= POST data ==============",args) 94 | vprint( json.dumps(snapshotInfo),args ) 95 | vprint("============= end POST data ==============",args) 96 | url=args.host+"/api/public/v1.0/groups/"+args.group+"/clusters/"+args.clusterId+"/restoreJobs" 97 | vprint(url,args) 98 | response = requests.post(url, 99 | auth=HTTPDigestAuth(args.username,args.apikey), 100 | data=json.dumps(snapshotInfo), 101 | headers=headers) 102 | vprint( response.json(), args) 103 | response.raise_for_status() 104 | vprint("============= response ==============",args) 105 | vprint( vars(response),args ) 106 | vprint("============= end response ==============",args) 107 | # poll until restore Job is complete 108 | restore_json = response.json(); 109 | restoreUrl = restore_json.get('results')[0].get('links')[0].get('href') 110 | print("Restore job started: " + restoreUrl) 111 | restoreStatus = restore_json.get('statusName') 112 | while restoreStatus != "FINISHED": 113 | print("Restore of snapshot not complete.") 114 | time.sleep(5) 115 | response = requests.get(restoreUrl, 116 | auth=HTTPDigestAuth(args.username,args.apikey), 117 | headers=headers); 118 | restore_json = response.json() 119 | vprint("======= restore_json =========",args) 120 | vprint(restore_json,args) 121 | vprint("======= restore_json =========",args) 122 | restoreUrl = restore_json.get('links')[0].get('href') 123 | restoreStatus = restore_json.get('statusName') 124 | print "Restore complete." 125 | downloadUrl = restore_json.get('delivery').get("url") 126 | filename = downloadUrl.split('/')[-1] 127 | if hasattr(args,'outDirectory'): 128 | if not args.outDirectory.endswith(os.sep): 129 | args.outDirectory = args.outDirectory + os.sep 130 | vars(args)['outDirectory'] = args.outDirectory 131 | filename = args.outDirectory + filename 132 | print "Downloading from " + downloadUrl + " saving to " + filename 133 | response = requests.get(downloadUrl, 134 | auth=HTTPDigestAuth(args.username,args.apikey), 135 | stream=True) 136 | chunk_size = 2048 137 | with open(filename, 'wb') as fd: 138 | for chunk in response.iter_content(chunk_size): 139 | fd.write(chunk) 140 | print("Snapshot download complete.") 141 | vars(args)['create_restore_filename']=filename #save for possible later processing 142 | 143 | # download the respore 144 | 145 | def create_latest_restore(args): 146 | vprint("create_restore_latest",args) 147 | response= requests.get(args.host+"/api/public/v1.0/groups/"+args.group+"/clusters/"+args.clusterId+"/snapshots" 148 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 149 | response.raise_for_status() 150 | vprint("============= response ==============",args) 151 | vprint( vars(response),args ) 152 | vprint("============= end response ==============",args) 153 | 154 | snaps_json = response.json() 155 | vprint(snaps_json,args) 156 | snapshotId = snaps_json.get('results')[0].get('id') 157 | args.snapshotId = snapshotId 158 | create_restore(args) 159 | 160 | def create_restore_and_deploy(args): 161 | vprint("create_restore_and_deploy", args) 162 | create_restore(args) 163 | vprint("create_restore_and_deploy: args.create_restore_filename=" + args.create_restore_filename, args) 164 | total_size_bytes = 0 165 | restore_temp_dbpath = ""; 166 | restore_directory = os.path.dirname(args.create_restore_filename) 167 | restore_dir_stats = os.statvfs( restore_directory ) 168 | restore_dir_free_bytes = restore_dir_stats.f_bavail * restore_dir_stats.f_frsize 169 | with tarfile.open(args.create_restore_filename, "r:gz") as tfile: 170 | for tinfo in tfile: 171 | if total_size_bytes==0: 172 | restore_temp_dbpath = tinfo.name.split(os.sep)[0] 173 | vprint(tinfo.name + " size: " + str(tinfo.size),args) 174 | total_size_bytes += tinfo.size 175 | if ( total_size_bytes > restore_dir_free_bytes ): 176 | raise Exception('Snapshot requires ' + str(total_size_bytes) + 'bytes of free space. ' + 177 | 'Only ' + str(restore_dir_free_bytes) + 'bytes available in ' + restore_directory) 178 | tfile.extractall(args.outDirectory) 179 | 180 | vprint("total_size_bytes="+str(total_size_bytes),args); 181 | vprint("restore_dir_free_bytes=+"+str(restore_dir_free_bytes),args) 182 | restore_temp_dbpath = restore_directory + os.sep + restore_temp_dbpath 183 | vprint("snapshot extracted to "+restore_temp_dbpath,args) 184 | # start temp mongod 185 | vprint("args.restoreAndDeployTempPort="+args.restoreAndDeployTempPort,args) 186 | mongod_cmd = "mongod --port " + args.restoreAndDeployTempPort 187 | mongod_cmd += " --dbpath " + restore_temp_dbpath 188 | mongod_cmd += " --logpath " + restore_temp_dbpath + os.sep + 'mongod.log' 189 | mongod_cmd += " --fork" 190 | if args.restoreAndDeployTempMongodArgs: 191 | mongod_cmd += " " + args.restoreAndDeployTempMongodArgs 192 | vprint("mongod_cmd="+mongod_cmd,args) 193 | # TODO: not sure if I want shell=True??? 194 | subprocess.check_call(mongod_cmd, shell=True) 195 | vprint("temp mongod for restore started", args) 196 | with open(restore_temp_dbpath + os.sep + 'mongod.lock', 'r') as pidfile: 197 | temp_mongod_pid = pidfile.readline() 198 | vprint("temp_mongod_pid=" + temp_mongod_pid,args) 199 | #run mongoddump | mongorestore 200 | namespaces_to_restore = [] 201 | if args.restoreNamespace: 202 | restoreNSParts = args.restoreNamespace.split('.') 203 | if not restoreNSParts[0]=="*": 204 | db = restoreNSParts[0] 205 | if not restoreNSParts[1]=="*": 206 | namespaces_to_restore.append("--db " + db + " --collection " + restoreNSParts[1]) 207 | else: 208 | # need to find all the collections 209 | colls_s = subprocess.check_output(["mongo", "--port", args.restoreAndDeployTempPort 210 | , "--eval", "db.getSiblingDB('"+db+"').getCollectionNames()", "--quiet"]) 211 | colls = ast.literal_eval(colls_s) 212 | for coll in colls: 213 | namespaces_to_restore.append(" --db " + db + " --collection " + coll) 214 | 215 | else: 216 | #need to find all the dbs, than all the collections 217 | dbs_s = subprocess.check_output(["mongo", "--port", args.restoreAndDeployTempPort 218 | , "--eval" ,"db.getSiblingDB('admin').getMongo().getDBNames()", "--quiet"]) 219 | dbs = ast.literal_eval(dbs_s) 220 | for db in dbs: 221 | colls_s = subprocess.check_output(["mongo", "--port", args.restoreAndDeployTempPort 222 | , "--eval", "db.getSiblingDB('"+db+"').getCollectionNames()", "--quiet"]) 223 | colls = ast.literal_eval(colls_s) 224 | for coll in colls: 225 | namespaces_to_restore.append(" --db " + db + "--collection " + coll) 226 | 227 | 228 | mongodump_cmd = "mongodump --host localhost:" + args.restoreAndDeployTempPort 229 | mongodump_cmd += " --out - " # write to SDTOUT 230 | vprint("mongodump_cmd="+mongodump_cmd,args); 231 | 232 | mongorestore_cmd = "mongorestore --host " + args.targetHost 233 | if args.targetUsername: 234 | mongorestore_cmd += " --username " + args.targetUsername 235 | mongorestore_cmd += " --password " + args.targetPassword 236 | if args.targetAuthenticationDatabase: 237 | mongorestore_cmd += " --authenticationDatabase " + args.targetAuthenticationDatabase 238 | if args.targetAuthenticationMechanism: 239 | mongorestore_cmd += " --authenticationMechanism " + args.targetAuthenticationMechanism 240 | if args.restoreAndDeployDropFromTarget: 241 | mongorestore_cmd += " --drop" 242 | mongorestore_cmd += " --dir - " # read from STDIN 243 | 244 | 245 | 246 | vprint("mongorestore_cmd="+mongorestore_cmd,args) 247 | #spin through all the namespaces to restore since when reading & writing 248 | # to STDOUT/STDIN with mongodump and mongorestore and can only go 1 collection 249 | # at a time. 250 | for ns_to_restore in namespaces_to_restore: 251 | restore_cmd = mongodump_cmd + ns_to_restore + " | " + mongorestore_cmd + ns_to_restore 252 | vprint("restore_cmd="+restore_cmd,args) 253 | subprocess.check_call(restore_cmd, shell=True) 254 | 255 | 256 | #TODO: shutdown temp mongod & blow away dbpath? 257 | os.kill(int(temp_mongod_pid), signal.SIGKILL) 258 | 259 | # print out nice table of hosts & id's 260 | def get_hosts(args): 261 | try: 262 | from terminaltables import AsciiTable 263 | except ImportError: 264 | AsciiTable = False 265 | pass 266 | response= requests.get(args.host+"/api/public/v1.0/groups/"+args.group+"/hosts/" 267 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 268 | response.raise_for_status() 269 | vprint("============= response ==============",args) 270 | vprint( vars(response),args ) 271 | vprint("============= end response ==============",args) 272 | 273 | hosts_json = response.json() 274 | 275 | table_data = [ 276 | ['hostname','id','clusterId','version','typeName','replicaSetName','replicaStateName','lastPing'] 277 | ] 278 | 279 | for host in hosts_json['results']: 280 | row = [] 281 | for column in table_data[0]: 282 | row.append( str( host.get(column) ) ) 283 | table_data.append( row ); 284 | 285 | table_data.append(['','','Number of hosts',str(hosts_json['totalCount'])]) 286 | 287 | host_info = 'Hosts from ' + args.host 288 | 289 | if AsciiTable: 290 | table = AsciiTable( table_data, host_info ); 291 | table.inner_footing_row_border = True 292 | print table.table 293 | else: 294 | import pprint 295 | pprint.pprint(table_data) 296 | 297 | 298 | # print out list of clusters. 299 | def get_clusters(args): 300 | try: 301 | from terminaltables import AsciiTable 302 | except ImportError: 303 | AsciiTable = False 304 | pass 305 | response= requests.get(args.host+"/api/public/v1.0/groups/"+args.group+"/clusters/" 306 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 307 | response.raise_for_status() 308 | vprint("============= response ==============",args) 309 | vprint( vars(response),args ) 310 | vprint("============= end response ==============",args) 311 | 312 | hosts_json = response.json() 313 | vprint(hosts_json,args) 314 | table_data = [ 315 | ['clusterName','id','typeName','replicaSetName','lastHeartbeat'] 316 | ] 317 | 318 | for host in hosts_json['results']: 319 | row = [] 320 | for column in table_data[0]: 321 | row.append( str( host.get(column) ) ) 322 | table_data.append( row ); 323 | 324 | table_data.append(['','','Number of clusters',str(hosts_json['totalCount'])]) 325 | 326 | host_info = 'Clusters from ' + args.host 327 | 328 | if AsciiTable: 329 | table = AsciiTable( table_data, host_info ); 330 | table.inner_footing_row_border = True 331 | print table.table 332 | else: 333 | import pprint 334 | pprint.pprint(table_data) 335 | 336 | # retrieve information about the automation config 337 | def get_automation_config(args): 338 | response= requests.get(args.host+"/api/public/v1.0/groups/"+args.group+"/automationConfig/" 339 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 340 | response.raise_for_status() 341 | vprint("============= response ==============",args) 342 | vprint( vars(response),args ) 343 | vprint("============= end response ==============",args) 344 | 345 | hosts_json = response.json() 346 | 347 | print json.dumps(hosts_json, indent=4, sort_keys=True) 348 | 349 | # modify settings within the automation config 350 | def set_automation_config(args): 351 | with open(args.newAutomationConfigPath) as automation_conf_json: 352 | new_auto_conf = json.load(automation_conf_json) 353 | 354 | headers = {'content-type': 'application/json'} 355 | response= requests.put(args.host+"/api/public/v1.0/groups/"+args.group+"/automationConfig/" 356 | ,auth=HTTPDigestAuth(args.username,args.apikey) 357 | ,data=json.dumps(new_auto_conf) 358 | ,headers=headers) 359 | 360 | response.raise_for_status() 361 | vprint("============= response ==============",args) 362 | vprint( vars(response),args ) 363 | vprint("============= end response ==============",args) 364 | 365 | response_json = response.json() 366 | 367 | import pprint 368 | pprint.pprint(response_json) 369 | 370 | # print out nice table of hosts & id's 371 | def get_hosts(args): 372 | try: 373 | from terminaltables import AsciiTable 374 | except ImportError: 375 | AsciiTable = False 376 | pass 377 | response= requests.get(args.host+"/api/public/v1.0/groups/"+args.group+"/hosts/" 378 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 379 | response.raise_for_status() 380 | vprint("============= response ==============",args) 381 | vprint( vars(response),args ) 382 | vprint("============= end response ==============",args) 383 | 384 | hosts_json = response.json() 385 | 386 | table_data = [ 387 | ['hostname','id','clusterId','version','typeName','replicaSetName','replicaStateName','lastPing'] 388 | ] 389 | 390 | for host in hosts_json['results']: 391 | row = [] 392 | for column in table_data[0]: 393 | row.append( str( host.get(column) ) ) 394 | table_data.append( row ); 395 | 396 | table_data.append(['','','Number of hosts',str(hosts_json['totalCount'])]) 397 | 398 | host_info = 'Hosts from ' + args.host 399 | 400 | if AsciiTable: 401 | table = AsciiTable( table_data, host_info ); 402 | table.inner_footing_row_border = True 403 | print table.table 404 | else: 405 | import pprint 406 | pprint.pprint(table_data) 407 | 408 | 409 | def get_alerts(args): 410 | try: 411 | from terminaltables import AsciiTable 412 | except ImportError: 413 | AsciiTable = False 414 | pass 415 | if args.format=='json': 416 | AsciiTable = False 417 | host = args.host 418 | group_id = args.group 419 | user_name = args.username 420 | api_key = args.apikey 421 | response= requests.get(host+"/api/public/v1.0/groups/"+group_id+"/alerts/" 422 | ,auth=HTTPDigestAuth(user_name,api_key)) 423 | response.raise_for_status() 424 | vprint("============= response ==============",args) 425 | vprint( vars(response),args ) 426 | vprint("============= end response ==============",args) 427 | 428 | alerts_json = response.json() 429 | 430 | table_data = [ 431 | [ 'eventTypeName','status','created','replicaSetName'] 432 | ] 433 | 434 | for alert in alerts_json['results']: 435 | row = [ str(alert.get('eventTypeName')) 436 | , str(alert.get('status')) 437 | , str(alert.get('created')) 438 | , str(alert.get('replicaSetName',''))] 439 | table_data.append( row ); 440 | 441 | table_data.append(['','','Number alerts',str(alerts_json['totalCount'])]) 442 | 443 | host_info = 'Alerts from ' + host 444 | 445 | if AsciiTable: 446 | table = AsciiTable( table_data, host_info ); 447 | table.inner_footing_row_border = True 448 | print table.table 449 | else: 450 | if args.format=='json': 451 | print(json.dumps(alerts_json)) 452 | else: 453 | import pprint 454 | pprint.pprint(table_data) 455 | return alerts_json 456 | 457 | def process_alerts(args): 458 | alerts = get_alerts(args) 459 | for alert in alerts['results']: 460 | print(alert['id']) 461 | args.alertId = alert['id'] 462 | # 463 | # custom processing goes here! 464 | # 465 | acknowledge_alert(args) 466 | 467 | def acknowledge_alert(args): 468 | headers = { "Content-Type" : "application/json" } 469 | if args.ackUntil=="XXX": 470 | now = datetime.datetime.now() 471 | diff = datetime.timedelta(days=100*365) 472 | forever = now + diff 473 | args.ackUntil = forever.strftime("%Y-%m-%dT%H:%M:%SZ") 474 | ack_data = { "acknowledgedUntil" : args.ackUntil, 475 | "acknowledgementComment" : args.ackComment } 476 | response = requests.patch(args.host 477 | +"/api/public/v1.0/groups/" 478 | +args.group+"/alerts/"+args.alertId 479 | ,auth=HTTPDigestAuth(args.username,args.apikey) 480 | ,headers=headers, 481 | data=json.dumps(ack_data)) 482 | response.raise_for_status() 483 | result = json.dumps(response.json()) 484 | print(result) 485 | 486 | def get_alert_configs(args): 487 | response = requests.get(args.host 488 | +"/api/public/v1.0/groups/" 489 | +args.group+"/alertConfigs" 490 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 491 | response.raise_for_status() 492 | alert_configs = json.dumps(response.json()) 493 | print(alert_configs) 494 | 495 | 496 | def delete_alert_configs(args): 497 | print('delete_alert_configs') 498 | print( vars(args) ) 499 | deleted_alerts = 0 500 | failed_deletions = 0 501 | host = args.host 502 | group_id = args.group 503 | user_name = args.username 504 | api_key = args.apikey 505 | alert_configs_raw = requests.get(host+"/api/public/v1.0/groups/"+group_id+"/alertConfigs", 506 | auth=HTTPDigestAuth(user_name,api_key)) 507 | 508 | alert_configs = alert_configs_raw.json() 509 | vprint("============= SOURCE alert data ==============",args) 510 | vprint( json.dumps(alert_configs),args ) 511 | vprint("============= end SOURCE alert data ==============",args) 512 | 513 | for alert in alert_configs["results"]: 514 | #url = "http://requestb.in/15ftkhl1" 515 | url = host+"/api/public/v1.0/groups/"+args.group+"/alertConfigs/"+alert["id"] 516 | response = requests.delete(url, 517 | auth=HTTPDigestAuth(args.username,args.apikey)) 518 | vprint("============= response ==============",args) 519 | vprint( vars(response),args ) 520 | vprint("============= end response ==============",args) 521 | if args.continueOnError and (response.status_code != requests.codes.ok): 522 | print "ERROR %s %s" % (response.status_code,response.reason) 523 | print( "Failed migration alert JSON:" ) 524 | print json.dumps(new_alert) 525 | failed_deletions += 1 526 | else: 527 | response.raise_for_status() 528 | deleted_alerts += 1 529 | print "Deleted %d alerts to %s (%d failures)" % (deleted_alerts,args.targetHost,failed_deletions) 530 | 531 | print( vars(response) ) 532 | 533 | def post_alert_configs(args): 534 | print('post_alert_configs') 535 | print( vars(args) ) 536 | if ( args.alertConfigsSource == "-" ): 537 | data = sys.stdin.read() 538 | else: 539 | data = open( args.alertConfigsSource, 'r').read() 540 | print data 541 | alert_configs = json.loads(data) 542 | vprint("============= SOURCE alert data ==============",args) 543 | vprint( json.dumps(alert_configs),args ) 544 | vprint("============= end SOURCE alert data ==============",args) 545 | args.targetHost = args.host 546 | args.targetGroup = args.group 547 | args.targetApikey = args.apikey 548 | args.targetUsername = args.username 549 | __post_alert_configs(args,alert_configs) 550 | 551 | def migrate_alert_configs(args): 552 | print('migrate_alert_configs'); 553 | print( vars(args) ) 554 | response = requests.get(args.host+ 555 | "/api/public/v1.0/groups/"+args.group+"/alertConfigs" 556 | ,auth=HTTPDigestAuth(args.username,args.apikey)) 557 | response.raise_for_status() 558 | alert_configs = response.json() 559 | __post_alert_configs(args,alert_configs) 560 | 561 | def __post_alert_configs(args,alert_configs): 562 | migrated_alerts = 0 563 | failed_migrations = 0 564 | new_alert_configs = {} 565 | new_alert_configs['results']=[] 566 | for alert in alert_configs['results']: 567 | new_alert = copy.deepcopy(alert) 568 | vprint("============= SOURCE alert data ==============",args) 569 | vprint( json.dumps(alert),args ) 570 | vprint("============= end SOURCE alert data ==============",args) 571 | new_alert['groupId'] = args.targetGroup 572 | if ( alert.has_key('matchers') ): 573 | new_alert['matchers'] = [] 574 | del new_alert['links'] 575 | del new_alert['matchers'] 576 | del new_alert['id'] 577 | del new_alert['created'] 578 | del new_alert['updated'] 579 | #url = "http://requestb.in/11gd5mh1" 580 | url = args.targetHost+"/api/public/v1.0/groups/"+args.targetGroup+"/alertConfigs/" 581 | headers = { "Content-Type" : "application/json" } 582 | vprint("============= POST data ==============",args) 583 | vprint( json.dumps(new_alert),args ) 584 | vprint("============= end POST data ==============",args) 585 | 586 | response = requests.post(url, 587 | auth=HTTPDigestAuth(args.targetUsername,args.targetApikey), 588 | data=json.dumps(new_alert), 589 | headers=headers) 590 | vprint("============= response ==============",args) 591 | vprint( vars(response),args ) 592 | vprint("============= end response ==============",args) 593 | if args.continueOnError and (response.status_code != requests.codes.created): 594 | print "ERROR %s %s" % (response.status_code,response.reason) 595 | print( "Failed migration alert JSON:" ) 596 | print json.dumps(new_alert) 597 | failed_migrations += 1 598 | else: 599 | response.raise_for_status() 600 | migrated_alerts += 1 601 | print "Migrated %d alerts to %s (%d failures)" % (migrated_alerts,args.targetHost,failed_migrations) 602 | 603 | # "main" 604 | parser = argparse.ArgumentParser(description="Get alerts from MongoDB Ops/Cloud Manager") 605 | requiredNamed = parser.add_argument_group('required named arguments') 606 | requiredNamed.add_argument("--host" 607 | ,help='the OpsMgr host with protocol and port, e.g. http://server.com:8080' 608 | ,required=True) 609 | requiredNamed.add_argument("--group" 610 | ,help='the OpsMgr group id' 611 | ,required=True) 612 | requiredNamed.add_argument("--username" 613 | ,help='OpsMgr user name' 614 | ,required=True) 615 | requiredNamed.add_argument("--apikey" 616 | ,help='OpsMgr api key for the user' 617 | ,required=True) 618 | parser.add_argument("--getClusters",dest='action', action='store_const' 619 | ,const=get_clusters 620 | ,help='get cluster information') 621 | parser.add_argument("--getHosts",dest='action', action='store_const' 622 | ,const=get_hosts 623 | ,help='get host information') 624 | parser.add_argument("--getAlerts",dest='action', action='store_const' 625 | ,const=get_alerts 626 | ,help='get alerts') 627 | parser.add_argument("--ackAlert",dest='action', action='store_const' 628 | ,const=acknowledge_alert 629 | ,help='acknowledge an alert') 630 | parser.add_argument("--processAlerts",dest='action', action='store_const' 631 | ,const=process_alerts 632 | ,help='custom processing and acknowlegment of alerts') 633 | parser.add_argument("--getAlertConfigs",dest='action', action='store_const' 634 | ,const=get_alert_configs 635 | ,help='get alert configurations') 636 | parser.add_argument("--deleteAlertConfigs",dest='action', action='store_const' 637 | ,const=delete_alert_configs 638 | ,help='delete ALL alert configs from host') 639 | parser.add_argument("--postAlertConfigs",dest='action', action='store_const' 640 | ,const=post_alert_configs 641 | ,help='post ALL alert configs to host') 642 | parser.add_argument("--migrateAlertConfigs",dest='action', action='store_const' 643 | ,const=migrate_alert_configs 644 | ,help='migrate ALL alert configs from host to target') 645 | parser.add_argument("--getSnapshots",dest='action', action='store_const' 646 | ,const=get_snapshots 647 | ,help='get list of snapshots for a given --clusterId') 648 | parser.add_argument("--createRestore",dest='action', action='store_const' 649 | ,const=create_restore 650 | ,help='create a restore job from a given --clusterId for a given --snapshotId') 651 | parser.add_argument("--createRestoreLatest",dest='action', action='store_const' 652 | ,const=create_latest_restore 653 | ,help='create a restore job for the lastest snapshotId') 654 | parser.add_argument("--createRestoreAndDeploy",dest='action', action='store_const' 655 | ,const=create_restore_and_deploy 656 | ,help='create a restore job from a given --clusterId for a given --snapshotId'+ 657 | ' (or --snapshotTimestamp, --snapshotIncrement is optional)' 658 | ', download and unpack it, then deploy data in --restoreNamespace to --targetHost\n'+ 659 | 'NOTE: you must have the same or higher version of Mongo binaries installed on the machine '+ 660 | 'running this script as running on the --targetHost!') 661 | parser.add_argument("--getAutomationConfig", dest='action', action='store_const' 662 | ,const=get_automation_config 663 | ,help='get the current automation state of all hosts in the group') 664 | parser.add_argument("--setAutomationConfig", dest='action', action='store_const' 665 | ,const=set_automation_config 666 | ,help='update automation configuration through API endpoint') 667 | parser.add_argument("--newAutomationConfigPath" 668 | ,help='path to file with new configuration for automation') 669 | parser.add_argument("--targetHost" 670 | ,help='target OpsMgr/MongoDB host with protocol and port') 671 | parser.add_argument("--targetGroup" 672 | ,help='target OpsMgr group id') 673 | parser.add_argument("--targetUsername" 674 | ,help='target OpsMgr/MongoDB host user name') 675 | parser.add_argument("--targetApikey" 676 | ,help='target OpsMgr api key for target user') 677 | parser.add_argument("--targetPassword" 678 | ,help='target MongoDB instance password for target user') 679 | parser.add_argument("--targetAuthenticationDatabase" 680 | ,help='target MongoDB instance authentication database target user') 681 | parser.add_argument("--targetAuthenticationMechanism" 682 | ,help='target MongoDB instance authentication mechanism target user') 683 | parser.add_argument("--alertConfigsSource" 684 | ,help='A file containing JSON alert configs or "-" for STDIN') 685 | parser.add_argument("--clusterId" 686 | ,help='id of replica set or sharded cluster for snapshots') 687 | parser.add_argument("--snapshotId" 688 | ,help='id of a snapshot to restore') 689 | parser.add_argument("--snapshotTimestamp" 690 | ,help='point-in-time restore timestamp, format 2014-07-09T09:20:00Z') 691 | parser.add_argument("--snapshotIncrement" 692 | ,help='point-in-time restore increment, a positive integer') 693 | parser.add_argument("--restoreNamespace" 694 | ,help='Namespace(s) to restore from snapshot. Use "*" for all databases. '+ 695 | 'Use "foo.*" for all collections in the foo database. Use "foo.bar" for just the bar '+ 696 | 'collection in the foo database.') 697 | parser.add_argument("--outDirectory" 698 | ,help='optional directory to save downloaded snapshot') 699 | parser.add_argument("--restoreAndDeployTempPort" 700 | ,help='optional port number to run temporary mongod to restore snapshot from, '+ 701 | 'default is 27229',default='27229') 702 | parser.add_argument("--restoreAndDeployTempMongodArgs" 703 | ,help='optional arguments for temp mongod to restore snapshot from.') 704 | parser.add_argument("--restoreAndDeployDropFromTarget", action='store_true', default=False 705 | ,help='optional arguments for mongorestore process used to restore snapshot to --targetHost.' + 706 | ' For example "--drop" to force a drop of the collection(s) to be restored.') 707 | parser.add_argument("--continueOnError", action='store_true', default=False 708 | ,help='for operations that issue multiple API calls, set this flag to fail to report errors but keep going') 709 | parser.add_argument("--verbose", action='store_true', default=False 710 | ,help='enable versbose output for troubleshooting') 711 | parser.add_argument("--format", default='json' 712 | ,help='specify output format. Default: json') 713 | parser.add_argument("--alertId" 714 | ,help="id of alert to acknowledge") 715 | parser.add_argument("--ackUntil", default="XXX" 716 | ,help="datetime to ack alert default is 'forever' (100 years in the future)") 717 | parser.add_argument("--ackComment" 718 | ,help="comment to add for alert ack") 719 | 720 | parsed_args = parser.parse_args() 721 | 722 | vprint( vars(parsed_args), parsed_args ) 723 | 724 | if parsed_args.action is None: 725 | parser.parse_args(['-h']) 726 | else: 727 | parsed_args.action(parsed_args) 728 | --------------------------------------------------------------------------------