1#!/usr/bin/env python 2from __future__ import print_function 3from builtins import input 4from builtins import str 5from builtins import range 6# Copyright (C) 2007-2010 Samuel Abels. 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License version 2, as 10# published by the Free Software Foundation. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20import sys 21import os 22import re 23import imp 24import getpass 25import Exscript.protocols.drivers 26from optparse import OptionParser, Option, OptionValueError 27from Exscript import Queue, Account, PrivateKey, __version__ 28from Exscript.protocols import protocol_map 29from Exscript.util import template 30from Exscript.util.sigint import SigIntWatcher 31from Exscript.util.cast import to_list, to_host 32from Exscript.util.interact import get_login 33from Exscript.util.log import log_to_file 34from Exscript.util.file import get_accounts_from_file, \ 35 get_hosts_from_file, \ 36 get_hosts_from_csv, \ 37 load_lib 38from Exscript.util.decorator import autologin 39 40bracket_expression_re = re.compile(r'^\{([^\]]*)\}$') 41 42def expand_bracket(value_name, value): 43 match = bracket_expression_re.match(value) 44 if match is None: 45 return value 46 prompt = match.group(1) or 'a value for "%s"' % value_name 47 if prompt.startswith('!'): 48 value = getpass.getpass('Please enter %s: ' % prompt[1:]) 49 else: 50 value = eval(input('Please enter %s: ' % prompt)) 51 return value 52 53def expand_host_variables(host): 54 # Define host-specific variables. 55 for key, value in list(host.get_all().items()): 56 if isinstance(value, str): 57 value = expand_bracket(key, value) 58 elif hasattr(value, '__iter__'): 59 for n in range(len(value)): 60 if not isinstance(value[n], str): 61 continue 62 value[n] = expand_bracket(key, value[n]) 63 host.set(key, value) 64 65def run(): 66 # Parse options. 67 options, args = parser.parse_args(sys.argv) 68 args.pop(0) 69 70 def mkhost(name): 71 return to_host(name, 72 default_protocol = options.protocol, 73 default_domain = options.default_domain) 74 75 def mkhosts_from_file(filename): 76 return get_hosts_from_file(options.hosts, 77 default_protocol = options.protocol, 78 default_domain = options.default_domain) 79 80 def mkhosts_from_csv(filename): 81 return get_hosts_from_csv(options.csv_hosts, 82 default_protocol = options.protocol, 83 default_domain = options.default_domain) 84 85 # Extract the hostnames out of the command line arguments. 86 filename = None 87 if not options.execute: 88 try: 89 filename = args.pop(0) 90 except IndexError: 91 parser.error('Missing filename') 92 hosts = [mkhost(h) for h in args] 93 94 # If a filename containing hostnames AND VARIABLES was given, read it. 95 if options.csv_hosts: 96 try: 97 csv_hosts = mkhosts_from_csv(options.csv_hosts) 98 except IOError as e: 99 parser.error(str(e)) 100 if not csv_hosts: 101 print('Warning: %s is empty.' % options.csv_hosts) 102 hosts += csv_hosts 103 104 # If a filename containing hostnames was given, read it. 105 if options.hosts: 106 try: 107 txt_hosts = mkhosts_from_file(options.hosts) 108 except IOError as e: 109 parser.error(str(e)) 110 except ValueError as e: 111 parser.error(e) 112 if not txt_hosts: 113 print('Warning: %s is empty.' % options.hosts) 114 hosts += txt_hosts 115 116 # If a filename of an Exscript driver was given, import it. 117 if options.load_driver: 118 print('Searching drivers in %s...' % options.load_driver) 119 name = os.path.splitext(os.path.basename(options.load_driver))[0] 120 try: 121 driver_module = imp.load_source(name, options.load_driver) 122 except IOError as e: 123 parser.error(str(e)) 124 except Exception as e: 125 sys.stderr.write('Error in driver ' + options.load_driver + '\n') 126 raise 127 for name, obj in list(driver_module.__dict__.items()): 128 if Exscript.protocols.drivers.isdriver(obj): 129 Exscript.protocols.drivers.add_driver(obj) 130 print('Driver', repr(name), 'added.') 131 132 # Make sure that all mandatory options are present. 133 if not hosts: 134 parser.error('No hosts to connect to') 135 136 # Set host options. 137 for host in hosts: 138 host.set_option('debug', options.protocol_verbose) 139 host.set_option('verify_fingerprint', not options.ssh_auto_verify) 140 141 # Read the Exscript. 142 if options.execute: 143 script_content = options.execute 144 else: 145 with open(filename, 'r') as fp: 146 script_content = fp.read() 147 148 # Prepare the code that is executed after the user script has completed. 149 #FIXME: Move into the library, then use read_template_from_file instead of read_template(). 150 if not options.no_auto_logout: 151 script_content += r''' 152 ## Exscript generated commands. ## 153 {if connection.guess_os() is "vrp"} 154 {connection.sendline("quit")} 155 {else} 156 {connection.sendline("exit")} 157 {end}''' 158 159 # Load extra functions. 160 functions = {} 161 if options.lib: 162 try: 163 functions = load_lib(options.lib) 164 except IOError as e: 165 parser.error(str(e)) 166 167 # Test whether the template compiles. 168 vars = functions.copy() 169 vars.update(options.define) 170 if filename: 171 vars['__filename__'] = filename 172 vars.update(hosts[0].get_all()) 173 try: 174 template.test(script_content, **vars) 175 except Exception as e: 176 if options.parser_verbose > 0: 177 raise 178 parser.error(str(e)) 179 180 # Create Exscript. 181 queue = Queue(domain = options.default_domain, 182 mode = 'multiprocessing', 183 host_driver = options.use_driver, 184 verbose = options.verbose, 185 max_threads = options.connections) 186 default_pool = queue.account_manager.default_pool 187 188 # Read the account pool file. 189 try: 190 if options.account_pool: 191 accounts = get_accounts_from_file(options.account_pool) 192 default_pool.add_account(accounts) 193 except IOError as e: 194 parser.error(str(e)) 195 if options.account_pool and default_pool.n_accounts() == 0: 196 msg = r'WARNING: No accounts found in account pool file (%s)!' 197 print(msg % options.account_pool) 198 199 # Read username and password. 200 if options.non_interactive or default_pool.n_accounts() > 0: 201 user = None 202 password = None 203 password2 = None 204 else: 205 try: 206 user, password = get_login() 207 except KeyboardInterrupt: 208 sys.exit(1) 209 if options.authorization: 210 prompt = 'Please enter your authorization password: ' 211 password2 = getpass.getpass(prompt) 212 else: 213 password2 = password 214 215 # Read the SSH key. 216 if options.ssh_key: 217 print("Reading key from", options.ssh_key) 218 try: 219 key = PrivateKey.from_file(options.ssh_key, password) 220 except ValueError as e: 221 parser.error(str(e)) 222 if user is None: 223 user = getpass.getuser() 224 else: 225 key = None 226 227 # Add the account to the pool. 228 if user is not None: 229 account = Account(user, password, password2, key = key) 230 default_pool.add_account(account) 231 232 # Ask for values of marked variables. 233 for host in hosts: 234 expand_host_variables(host) 235 for key, val in list(options.define.items()): 236 options.define[key] = expand_bracket(key, val) 237 238 # Choose the template processing type. 239 tmpl_vars = options.define 240 if options.no_prompt: 241 def function(job, host, conn, **kwargs): 242 kwargs.update(functions) 243 kwargs.update(tmpl_vars) 244 kwargs.update(host.get_all()) 245 return template.paste(conn, script_content, **kwargs) 246 else: 247 strip = not options.no_strip 248 def function(job, host, conn, **kwargs): 249 kwargs.update(functions) 250 kwargs.update(tmpl_vars) 251 kwargs.update(host.get_all()) 252 return template.eval(conn, script_content, strip, **kwargs) 253 254 # Wrap the template processor such that the login procedure is automated. 255 if not options.no_authentication: 256 attempts = options.retry_login + 1 257 decorator = autologin(attempts = attempts) 258 function = decorator(function) 259 260 # Wrap the template processor such that logging is enabled. 261 if options.logdir: 262 # Create the log directory. 263 if not os.path.exists(options.logdir): 264 print('Creating log directory (%s)...' % options.logdir) 265 try: 266 os.makedirs(options.logdir) 267 except IOError as e: 268 parser.error(str(e)) 269 270 # Enable logging. 271 mode = options.overwrite_logs and 'w' or 'a' 272 decorator = log_to_file(options.logdir, mode, options.delete_logs) 273 function = decorator(function) 274 275 # Wait until the specified time. We are using os.system because 276 # it allows for specifying absolute times, not just the number 277 # of seconds like Python's time.sleep(). 278 if options.sleep: 279 print("Waiting for %s..." % options.sleep, end='', flush=True) 280 os.system('sleep %s' % options.sleep) 281 print("time expired, starting script.") 282 283 # Run the template. 284 for host in hosts: 285 queue.run(host, function, attempts = options.retry + 1) 286 queue.join() 287 failed = queue.failed 288 queue.destroy() 289 return failed 290 291# Define command line option value types. 292def check_assignment(option, opt, value): 293 if not re.match(r'\S+=\S+', value): 294 raise OptionValueError('option %s: invalid value: %r' % (opt, value)) 295 return value 296 297def check_protocol(option, opt, value): 298 if value not in protocol_map: 299 raise OptionValueError('option %s: invalid value: %r' % (opt, value)) 300 return value 301 302class AssignmentOption(Option): 303 # Define a new 'assignment' type for the '--define' command line option. 304 TYPES = Option.TYPES + ('assignment', 'protocol') 305 TYPE_CHECKER = Option.TYPE_CHECKER.copy() 306 TYPE_CHECKER['assignment'] = check_assignment 307 TYPE_CHECKER['protocol'] = check_protocol 308 309 # Define a new store action that parses 'assignment' values. 310 ACTIONS = Option.ACTIONS + ('store_dict',) 311 STORE_ACTIONS = Option.STORE_ACTIONS + ('store_dict',) 312 TYPED_ACTIONS = Option.TYPED_ACTIONS + ('store_dict',) 313 314 def take_action(self, action, dest, opt, value, values, parser): 315 if action == "store_dict": 316 left, right = value.split("=") 317 values.ensure_value(dest, {})[left] = right 318 return 319 Option.take_action(self, 320 action, 321 dest, 322 opt, 323 value, 324 values, 325 parser) 326 327# Define the command line syntax. 328usage = '%prog [options] filename [hostname [hostname ...]]' 329usage += '\nCopyright (C) 2007-2010 by Samuel Abels.' 330parser = OptionParser(usage = usage, 331 version = __version__, 332 option_class = AssignmentOption) 333 334parser.add_option('--account-pool', 335 dest = 'account_pool', 336 metavar = 'FILE', 337 help = ''' 338Reads the user/password combination from the given file 339instead of prompting on the command line. The file may 340also contain more than one user/password combination, in 341which case the accounts are used round robin. 342'''.strip()) 343 344parser.add_option('--connections', '-c', 345 dest = 'connections', 346 type = 'int', 347 metavar = 'NUM', 348 default = 1, 349 help = ''' 350Maximum number of concurrent connections. 351NUM is a number between 1 and 20, default is 1. 352'''.strip()) 353 354parser.add_option('--csv-hosts', 355 dest = 'csv_hosts', 356 metavar = 'FILE', 357 help = ''' 358Loads a list of hostnames and definitions from the given file. 359The first line of the file must have the column headers in the 360following syntax: "hostname [variable] [variable] ...", where 361the fields are separated by tabs, "hostname" is the keyword 362"hostname" and "variable" is a unique name under which the 363column is accessed in the script. 364The following lines contain the hostname in the first column, 365and the values of the variables in the following columns. 366'''.strip()) 367 368parser.add_option('--define', '-d', 369 dest = 'define', 370 type = 'assignment', 371 action = 'store_dict', 372 default = {}, 373 metavar = 'PAIR', 374 help = ''' 375Defines a variable that is passed to the script. 376PAIR has the following syntax: STRING=STRING. 377'''.strip()) 378 379parser.add_option('--default-domain', 380 dest = 'default_domain', 381 metavar = 'STRING', 382 default = '', 383 help = ''' 384The IP domain name that is used if a given hostname has no domain appended. 385'''.strip()) 386 387parser.add_option('--delete-logs', 388 dest = 'delete_logs', 389 action = 'store_true', 390 default = False, 391 help = 'Delete logs of successful operations when done.') 392 393parser.add_option('--lib', 394 dest = 'lib', 395 metavar = 'FILE', 396 help = ''' 397A python file containing a __lib__ dictionary that points to extra 398functions to be made available in the Exscript template. 399'''.strip()) 400 401parser.add_option('--load-driver', 402 dest = 'load_driver', 403 metavar = 'FILE', 404 help = ''' 405A python file containing an Exscript driver class. 406'''.strip()) 407 408parser.add_option('--use-driver', 409 dest = 'use_driver', 410 default = None, 411 help = ''' 412The name of an exscript driver to use for all connections (overriding os detection). 413'''.strip()) 414 415parser.add_option('--execute', '-e', 416 dest = 'execute', 417 metavar = 'EXSCRIPT', 418 help = 'Interprets the given string as the script.') 419 420parser.add_option('--hosts', 421 dest = 'hosts', 422 metavar = 'FILE', 423 help = ''' 424Loads a list of hostnames from the given file (one host per line). 425'''.strip()) 426 427parser.add_option('--non-interactive', '-i', 428 dest = 'non_interactive', 429 action = 'store_true', 430 default = False, 431 help = ''' 432Do not ask for a username or password, but still try to authenticate. 433May be used if the login credentials are passed as part of a URI formatted 434hostname. 435'''.strip()) 436 437parser.add_option('--logdir', '-l', 438 dest = 'logdir', 439 metavar = 'DIR', 440 help = ''' 441Logs any communication into the directory with the given name. 442Each filename consists of the hostname with ".log" appended. 443Errors are written to a separate file, where the filename 444consists of the hostname with ".log.error" appended. 445'''.strip()) 446 447parser.add_option('--no-authentication', '-n', 448 dest = 'no_authentication', 449 action = 'store_true', 450 default = False, 451 help = ''' 452When given, the automatic authentication procedure is skipped. Implies -i. 453'''.strip()) 454 455parser.add_option('--authorization', 456 dest = 'authorization', 457 action = 'store_true', 458 default = False, 459 help = ''' 460Ask for an authorization password in addition to the authentication password. 461'''.strip()) 462 463parser.add_option('--no-auto-logout', 464 dest = 'no_auto_logout', 465 action = 'store_true', 466 default = False, 467 help = ''' 468Do not attempt to execute the exit or quit command at the end of a script. 469'''.strip()) 470 471parser.add_option('--no-prompt', 472 dest = 'no_prompt', 473 action = 'store_true', 474 default = False, 475 help = ''' 476Do not wait for a prompt anywhere (except during the authentication 477procedure). Note that this will also cause Exscript to disable commands 478that require a prompt, such as "extract". 479'''.strip()) 480 481parser.add_option('--no-strip', 482 dest = 'no_strip', 483 action = 'store_true', 484 default = False, 485 help = 'Do not strip the first line of each response.') 486 487parser.add_option('--overwrite-logs', 488 dest = 'overwrite_logs', 489 action = 'store_true', 490 default = False, 491 help = ''' 492Instructs Exscript to overwrite existing logfiles. The default 493is to append the output if a log already exists. 494'''.strip()) 495 496protocols = list(protocol_map.keys()) 497protocols.sort() 498parser.add_option('--protocol', '-p', 499 dest = 'protocol', 500 type = 'protocol', 501 metavar = 'STRING', 502 default = 'telnet', 503 help = ''' 504Specify which protocol to use to connect to the remote host. 505Allowed values for STRING include: %s. 506The default protocol is telnet. 507'''.strip() % ', '.join(protocols)) 508 509parser.add_option('--retry', 510 dest = 'retry', 511 type = 'int', 512 metavar = 'NUM', 513 default = 0, 514 help = ''' 515Defines the number of retries per host on failure. Default is 0. 516'''.strip()) 517 518parser.add_option('--retry-login', 519 dest = 'retry_login', 520 type = 'int', 521 metavar = 'NUM', 522 default = 0, 523 help = ''' 524Defines the number of retries per host on login failure. Default is 0. 525'''.strip()) 526 527parser.add_option('--sleep', 528 dest = 'sleep', 529 metavar = 'TIME', 530 default = '', 531 help = ''' 532Waits for the specified time before running the script. 533TIME is a timespec as specified by the 'sleep' Unix command. 534'''.strip()) 535 536parser.add_option('--ssh-auto-verify', 537 dest = 'ssh_auto_verify', 538 action = 'store_true', 539 default = False, 540 help = ''' 541Automatically confirms the 'Host key changed' SSH error 542message with 'yes'. Highly insecure and not recommended. 543'''.strip()) 544 545parser.add_option('--ssh-key', 546 dest = 'ssh_key', 547 metavar = 'FILE', 548 help = ''' 549Specify a key file that is passed to the SSH client. 550This is equivalent to using the "-i" parameter of the 551openssh command line client. 552'''.strip()) 553 554parser.add_option('--verbose', '-v', 555 dest = 'verbose', 556 type = 'int', 557 metavar = 'NUM', 558 default = 1, 559 help = ''' 560Print out debug information about the queue. 561NUM is a number between 0 (min) and 5 (max). Default is 1. 562'''.strip()) 563 564parser.add_option('--parser-verbose', '-V', 565 dest = 'parser_verbose', 566 type = 'int', 567 metavar = 'NUM', 568 default = 0, 569 help = ''' 570Print out debug information about the Exscript template parser. 571NUM is a number between 0 (min) and 5 (max). Default is 0. 572'''.strip()) 573 574parser.add_option('--protocol-verbose', 575 dest = 'protocol_verbose', 576 type = 'int', 577 metavar = 'NUM', 578 default = 0, 579 help = ''' 580Print out debug information about the network activity. 581NUM is a number between 0 (min) and 5 (max). Default is 1. 582'''.strip()) 583 584if __name__ == '__main__': 585 SigIntWatcher() 586 failed = run() 587 if failed != 0: 588 sys.exit('error: %d actions failed.' % failed) 589