From de010241b4c9977d8e3df233f79cb10b7a720262 Mon Sep 17 00:00:00 2001 From: "Randy E. Oyarzabal" <32100282+randyoyarzabal@users.noreply.github.com> Date: Mon, 9 Oct 2017 15:46:26 -0500 Subject: [PATCH] Initial load into GitHub. --- .gitignore | 112 +++++ CHANGES.txt | 14 + CNAME | 1 + README.md | 138 +++++++ _config.yml | 1 + configs/config.sample | 61 +++ docs/KNOWN_ISSUES.md | 38 ++ docs/NOTES.txt | 35 ++ docs/installation/INSTALL.md | 76 ++++ docs/installation/sample_install_log.txt | 205 ++++++++++ docs/templates/commands_template.xlsx | Bin 0 -> 25227 bytes docs/templates/config_template.ini | 61 +++ docs/templates/hosts_file_sample.csv | 9 + docs/templates/sample_commands.csv | 7 + reach.py | 448 ++++++++++++++++++++ reachlib/BaseREOSSHWorker.py | 500 +++++++++++++++++++++++ reachlib/SSHWorkerClasses.py | 158 +++++++ reachlib/SSHWorkerConfig.py | 273 +++++++++++++ reachlib/__init__.py | 5 + reolib/REODelimitedFile.py | 144 +++++++ reolib/REORemoteHost.py | 489 ++++++++++++++++++++++ reolib/REOScript.py | 46 +++ reolib/REOUtility.py | 333 +++++++++++++++ reolib/__init__.py | 4 + 24 files changed, 3158 insertions(+) create mode 100755 .gitignore create mode 100755 CHANGES.txt create mode 100644 CNAME create mode 100755 README.md create mode 100644 _config.yml create mode 100755 configs/config.sample create mode 100755 docs/KNOWN_ISSUES.md create mode 100755 docs/NOTES.txt create mode 100755 docs/installation/INSTALL.md create mode 100755 docs/installation/sample_install_log.txt create mode 100755 docs/templates/commands_template.xlsx create mode 100755 docs/templates/config_template.ini create mode 100755 docs/templates/hosts_file_sample.csv create mode 100755 docs/templates/sample_commands.csv create mode 100755 reach.py create mode 100755 reachlib/BaseREOSSHWorker.py create mode 100755 reachlib/SSHWorkerClasses.py create mode 100755 reachlib/SSHWorkerConfig.py create mode 100755 reachlib/__init__.py create mode 100755 reolib/REODelimitedFile.py create mode 100755 reolib/REORemoteHost.py create mode 100755 reolib/REOScript.py create mode 100755 reolib/REOUtility.py create mode 100755 reolib/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..b72ee82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +# OSX and IDE related +.DS_Store +.idea + +# Project releated +logs +logs/* +testDriver.py +bashTests.sh +config.ini + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100755 index 0000000..d5f7e3b --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,14 @@ +Legend: + : added, - : removed, * : fixed + +https://github.com/randyoyarzabal/reach + +v1.0.1 [5-Dec-2016]: +------------ ++ Added prompt detection. +* Fixed a bug where Vyatta (configuration mode) prompt change was not being detected. +* Fixed "Host Duration" simulation display + +v1.0.0 [4-Dec-2016]: +------------ ++ Initial release. + diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..a22d847 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +reach.rbpsiu.com \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..b610611 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +### Developers + +- Randy Oyarzabal +- Francis Lan + +### Tested Use Cases + +- Firewall rules validation +- Network device configuration (e.g. Cisco, F5, Brocade etc. equipment) +- Firewall / host backups +- Middleware installation/support management +- Host package management +- User and Password management +- Multi-host File/State Search +- Monitoring + +### Installation + + See the [INSTALL.md](docs/installation/INSTALL.md) file in the `docs/installation/` folder. + +### Sample Prerequisite Files + + - [Inventory/hosts](docs/templates/hosts_file_sample.csv) file + - [Configuration](docs/templates/config_template.ini) file + - [Batch commands](docs/templates/sample_commands.csv) file + +### Synopsis + + ./reach.py -? + ./reach.py -v + ./reach.py -a + ./reach.py --cipher_text= + ./reach.py -b commands_file [-x] [-g] ... + ./reach.py -c command [-d search_string [-r report_string]] [-w wait_string -p response_string] ... + + Optionally, for any mode: + ./reach.py [--config=] [-i inventory_file] [-k column_key] [-x] [-g] + +### Help / Usage + + -? : This help screen + -v : Display version information only + +### Operation Modes + + -a : Access Check only + -b : Run Batch Commands (comma separated file, see template for format/options) + -c : Run Command + +### Optional for all Modes + + --config= Override the default config.ini + -x : Enable SIMULATION (no connection/commands invoked) + -g : Enable DEBUG + -i : Inventory (hosts) file (comma separated, define header key with -k) + -k : [Required with -i] Column header of keys + -e : Filter hosts to process. Operators are supported: = equal, | or, ~ contains, & and. + Note: Reach does not support mixed (& and |) in this release. + Example conditions: 'Build=WHC0122' , 'Build=WHC0122&Host~app, 'Build=WHC0122|Host~app|Host~dom' + +### Optional for Command (-c) Mode +##### *Note that for Batch Mode, these are internally defined in the commands file.* + + -o : Show command console output (ignored in batch (-b option) mode) + -s : Run command as root (run 'sudo su -' first) + -h : Halt looping through hosts when first done string (-d) is found + +##### The following can use hosts file column variables ($HF) delimited by '|': +##### *For example: '$HF_#' where # is the column number in the hosts file* + + --username= : Force user string instead of what is configured. + --password= : Force cipher-text password instead of what is configured. + --private_key= : Force private RSA key file instead of what is configured. + -d : Search for string in output (For example: 'Complete' or 'Nothing|Complete') + Can also use '$NF' to test for string is not found. + -r : [Optional with same length as -d] Matching string to print to screen when -d match + For example: 'Installed|Not Installed' + -w : Wait string + -p : [Required with same length as -w] Send a string when -w string is found + The following list of special markers can be used in -p: + $ENTER_KEY : '\n' + $RETURN_KEY : '\r' + $TAB_KEY : '\t' + $SPACE_KEY : ' ' + $CT= : Used for sending passwords to the terminal like changing passwords + or sending Cisco ASA passwords in "enable" mode. + +### Special mode only for changing passwords to cipher text + --cipher_text= return the password in cipher text to put in the password file + +### Examples + + - Run 'yum -y install gdb' as root, look for the strings: 'Nothing' or 'Complete', then display + 'Installed' or 'Not Installed' on the screen. Process hosts matching 'Build' column = 'WHC038' + ./reach.py -c 'yum -y install gdb' -s -d 'Nothing|Complete' -r 'Installed|Not Installed' -e 'Build=WHC038' + ------------------------------------------------------------------------------- + - Run 'sh ip route 10.143.92.134', look for the strings: 'bond0.' or 'bond0', then display + 'Found in: $HF_1' ($HF_1 is the first column of the inventory) or 'Not Here' then halt the hosts loop + ./reach.py -c "sh ip route 10.143.92.134" -d 'bond0.|bond0' -r 'Found in: $HF_1|Not Here' -h + ------------------------------------------------------------------------------- + - Check access against all hosts in the inventory file + ./reach.py -a + ------------------------------------------------------------------------------- + - Force read a different inventory file making sure to define the header key for the IP to use + ./reach.py -i 'vga_inventory.csv' -a -k 'Public IP' + ------------------------------------------------------------------------------- + - Run a series of commands defined in a file (see template for proper format) + ./reach.py -b 'vga_backups.csv' + ------------------------------------------------------------------------------- + - Change password for a user (run this in simulation mode for an explanation) + ./reach.py -c 'passwd randyo' -w 'New|Retype' -p 'mypass3|mypass3' -d 'successfully' -r 'Changed password' + +### Helpful Tips + + - Always be sure to run in SIMULATION (-x) mode first to see what the script is about to do! + NOTE: Some of the example below use specific details that may not pertain to your use and is + provided simply as a guide. + - Use the -r option in conjunction with -d to substitute results, optionally use: grep and/or sed to limit output. + Example: Find all hosts where 'vyatta' is found in the zones list. + ./reach.py -c 'show zone-policy zone' -d 'vyatta|$NF' -r 'yes|no' | grep -E 'yes|no' + Or used the output in the logs/last_run-log.txt + You may then paste the output as a new column in your inventory file. + - Use bash aliases for different hosts inventory or use, for example: + alias prjA-Utility='reach.py --config=/util_hosts/projectA.ini' + alias prjB-Utility='reach.py --config=/util_hosts/projectB.ini' + alias home-Utility='reach.py --config=/util_hosts/combined.ini -e "Location=Home" --username=\$HF_5' + alias work-Utility='reach.py --config=/util_hosts/combined.ini -e "Location=Work"' + - You can even use a bash function of your favorite use, for example, to find the firewall that an IP belongs to: + function find_firewall { whcUtility -c "sh ip route $1" -d 'bond0.|bond0' \ + -r 'Found in: $HF_1 - $HF_3 : $HF_5|Not Here' "${@:2}" -h; } + To use this: find_firewall + If you're wondering what "${@:2}" means, it is a neat bash specific notation for handling additional + arguments you may want to use, like: -x (simulation) or -g (debug) etc. + - Be sure to set SSH_COMMAND_TIMEOUT higher than the longest anticipated command duration. But not too high because + if a command hangs, it will wait for that duration before timing out. + +### Git Repository +[https://github.com/randyoyarzabal/reach](https://github.com/randyoyarzabal/reach) diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/configs/config.sample b/configs/config.sample new file mode 100755 index 0000000..30aa7ee --- /dev/null +++ b/configs/config.sample @@ -0,0 +1,61 @@ +; Change the settings below to suit your needs. +; No leading spaces are allowed and comments always start with a ";" (semi-colon). +; DO NOT change the name of the section header. + +; You may create copies of this file and pass to the tool like this: +; ./reach.py --config_file= + +[TOOL DEFAULTS] ; Do not change this! + +; Optional, comment this if you want to be prompted each time for a user name to use +; Define the rest of the defaults values below to suit. +SSH_USER_NAME : testuser + +; Optional, comment this if you want to be prompted each time for a password to use +; Cipher text rsa key passphrase or password +; To generate the cipher text: ./reach.py --cipher_text= +SSH_PASSWORD_CIPHER : XXXXXXXXXXX + +; Optional, comment this if no RSA keys will be used +SSH_PRIVATE_KEY : /Users/randyo/.ssh/id_rsa + +; Define default hosts inventory to use. It needs to be a comma separated file with a header row. +; Be sure to define the matching HOST_MARKER (text displayed when processing) +; and KEY_COLUMN (IP used for connections) below. +HOSTS_INPUT_FILE : /Users/randyo/dev/reach_hosts/my_inventory.csv + +; Optional, will log in "logs" directory if this is commented. +LOGS_DIRECTORY : /Users/randyo/dev/reach/logs/ + +; Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_LEVEL : DEBUG + +; Define default key column for the IP address to use for connections. +KEY_COLUMN : IP Address + +; Define the string format displayed when processing hosts. +; $HF_# where # is the column number from the file above. +HOST_MARKER : $HF_5 - $HF_1: $HF_3 + +; Safe to leave all as False or comment. Change to suit. +SHOW_HOST_DURATION : False +SHOW_CONSOLE_OUTPUT : False +RUN_IN_SIMULATION_MODE : False +DEBUG_FLAG : False +NO_DESTRUCTIVE_PROMPT : False + +; Set this to True if you want to blindly trust hosts or False to use the system known_hosts file +TRUST_HOSTS : False + +SSH_CONNECTION_TIMEOUT : 10 + +; Important to set this higher than your longest running command you plan to run. But set it too high +; Reach won't move to the next command if the command hangs. +SSH_COMMAND_TIMEOUT : 20 + +; Safe to leave commented. This is the prompt regex. These are characters to expect on hosts. +; Change only if you know what you're doing. Separate different prompt with | +; Example: +;PROMPT_REGEX : ([$#] |> )$ +; Default is: +;PROMPT_REGEX : [$#>]( )?$ diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md new file mode 100755 index 0000000..b6932c6 --- /dev/null +++ b/docs/KNOWN_ISSUES.md @@ -0,0 +1,38 @@ +Known Issues +------------ + +We've extensively tested Reach for production use for a variety of use cases against different target host +types such as Linux, Mac, Vyatta, and Cisco devices. However, we are aware of some issues and are continually +working towards enhancements. Here are known issues: + +- Reach sends remote commands to a host via an SSH connection over a virtual terminal session. There is no easy + way to detect command completion. What we did (and is the commonly accepted solution) is to set a + personalized prompt and detect the prompt in the virtual terminal's output. This works great for common + commands but we are aware that some situations may lead to unexpected outcomes. Examples: + + - Long running commands: Reach will timeout after SSH_COMMAND_TIMEOUT seconds. If you know some commands + are slow, make sure to set a high enough timeout value. + - Executing processes in the background. The command will immediately return but their output will + intertwine with the subsequent commands' output. + Possible solution: Redirect the background output to a file. + - Highly unlikely: the command's output matches the personalized prompt... and then hangs. In that case, + Reach wrongly assumes the command has completed and goes on to the next command or next host. + - The initial prompt is not '#', '$', '# ', or '$ ' and thus Reach will fail. + Solution: configure the PROMPT_REGEX value in config.ini + +- If you are using Excel or any other spreadsheet program to edit CSV files, be careful of invisible characters + that are introduced when pasting formatted text like that of from the terminal. You may not see it, but + when you paste formatted text to Excel, it preserved formatting and therefore introducing invisible characters + that will later haunt you in the output or worse yet, the results from Reach. Solution: Always paste to a + text editor first, then copy/paste into Excel before exporting as CSV. + +- Other potential issues: + - For commands that hang (e.g. telnet): + - Look for alternatives, for example, it is possible to force telnet to exit on successful connection: + like this: `echo B | telnet -e B 192.168.2.100 8443`, however this only works if the port is + listening. If it it's not, it will just hang at "Trying..." + - An alternative is to use nc. for example, `echo "QUIT" | nc -w 5 192.168.2.100 8443`, if + connected, it won't display any results, if not, it will display: "Ncat: Connection timed out." + You can then use the output as a done string (-d) condition. + +If in doubt, always run tests on a single host before running against many and use simulation mode (-x) to verify the actions that will be performed. diff --git a/docs/NOTES.txt b/docs/NOTES.txt new file mode 100755 index 0000000..e1b174f --- /dev/null +++ b/docs/NOTES.txt @@ -0,0 +1,35 @@ +Random Notes / Useful Commands: + +Check if a service is listening: +netstat -tnl | grep 50000 + +./reach.py -c 'netstat -tnl | grep 50000' -d 'LISTEN|$NF' -r 'DB2 is UP|DB2 is Down' + + +Remove spaces, tabs or - from beginning of the line: +sed -e 's/^[ \t-]*//' +sed -e 's/^[ -]*//' + + +Alternative port checking: +echo B | telnet -e B 10.143.180.206 8444 + +The problem is when it encounters a port that is closed. There it just says "Trying" and hangs. + +Better option: +echo "QUIT" | nc -w 5 10.143.180.206 8443 + +Find "vyatta" zones in any VGA: +-c 'sh zone-policy zone vyatta | grep local-zone' -d 'Interfaces: local-zone|$NF' -r 'Yes|No' + +Find VGA owning private IP (as an alias) +function find_firewall { whcReach -c "sh ip route $1" -d 'bond0.|bond0' -r \ + 'Found in: $HF_1 - $HF_3 : $HF_5|Not Here' "${@:2}" -h | grep -E 'Found in'; } + +Mass convert a text file pf passwords to cipher text: +cat test.pass | xargs -L 1 -I % reach.py --cipher_text=% | grep 'Cipher text:' | sed "s/Cipher text: '\(.*\)'/\1/" + +Remove config.ini amd history from git repo: +git filter-branch --force --index-filter \ +'git rm --cached --ignore-unmatch config.ini' \ +--prune-empty --tag-name-filter cat -- --all \ No newline at end of file diff --git a/docs/installation/INSTALL.md b/docs/installation/INSTALL.md new file mode 100755 index 0000000..ae7e03f --- /dev/null +++ b/docs/installation/INSTALL.md @@ -0,0 +1,76 @@ +Steps for installing/using Reach: +-------------------------------- + +Get the latest stable build of Reach by manually downloading the Reach tree +[zip file](https://github.com/randyoyarzabal/reach/archive/v1.0.2.zip) OR with git via SSH: + +> `git clone git@github.com:randyoyarzabal/reach.git` + +*Reach requires Python 2.7 (not compatible with 3.x) and the paramiko library module, if you already have it, skip to Step 5.* + +1. Install required packages: + + >`yum install gcc openssl-devel libffi-devel python-devel glibc python` + +2. Install EPEL (Extra Packages for Enterprise Linux) Repo for your OS. +Follow: http://www.tecmint.com/how-to-enable-epel-repository-for-rhel-centos-6-5/ + +3. Install python-pip: + + >`sudo install python-pip` + + OR (if above doesn't work) + + >`sudo yum --enablerepo=epel install python-pip` + +4. Install paramiko: + + >`pip install paramiko` + + *See [sample log file](sample_install_log.txt) for example sucessful Steps 1-4.* + +5. Create a copy of [docs/templates/config_template.ini](../templates/config_template.ini) +as `config.ini` (exact name required) in the `configs` directory by default. Or, create it with any name you choose +and pass it to Reach like this: `./reach.py --config_file=` + +5. Have your SSH hosts stored in a CSV files (at a minimum, you just need an IP Address column) +You can optionally have other columns so you can selectively process hosts, or use host specific +data like username, password (in cipher text), etc. See sample [docs/templates/hosts_file_sample.csv](../templates/hosts_file_sample.csv). The columns and +specific types are up to you, as long as it is in CSV (comma-delimited) format and you define a KEY_COLUMN (`-k`). +It is recommended you have categorical columns so you can use `-e` later to select a subset of your hosts. + +6. Generate your password cipher text + + >`./reach.py --cipher_text ` against all passwords you will use. Take the output and put + in `SSH_PASSWORD_CIPHER` of the `config.ini` file. + +7. Edit the rest of `config.ini` file variables to suit your needs. + +8. You can optionally do a quick test by checking access to your hosts or by running a simple command: + + - Check access: + > `./reach.py -a -x` to simulate. + + > `./reach.py -a` to check your access against all the hosts. + + - Run a simple command like `whoami` on all hosts, optionally append a filter condition against the hosts on + an available column like `Type` example: `-e 'Type=Linux'` to run only against Linux hosts only: + + > `./reach.py -c 'whoami' -o` (append `-x` to simulate like above) + +That's it! + +Notes +------- + +Note that you may create copies of the config.ini file and pass to the tool like this: + + >`./reach.py --config_file=` + +Optionally, you can also pass the hosts file: + + >`./reach.py -i docs/templates/hosts_file_sample.csv -k 'IP Address' ...` + + +Remember -x for simulation! Useful for checking to make sure the commands and filtering (-e) + is correct before actually executing. diff --git a/docs/installation/sample_install_log.txt b/docs/installation/sample_install_log.txt new file mode 100755 index 0000000..74f23d4 --- /dev/null +++ b/docs/installation/sample_install_log.txt @@ -0,0 +1,205 @@ +Step 1 +------ + +[user@-laptop ~]$ sudo yum install gcc openssl-devel libffi-devel python-devel glibc python python-pip +[sudo] password for user: +Loaded plugins: downloadkvmonly-background, downloadonly-background, -check- + : lotus-updates, -repo-checker, -repository, refresh- + : packagekit, security, versionlock +Setting up Install Process +Package glibc-2.12-1.166.el6_7.7.x86_64 already installed and latest version +Package python-2.6.6-66.el6_8.x86_64 already installed and latest version +No package python-pip available. +Resolving Dependencies +--> Running transaction check +---> Package gcc.x86_64 0:4.4.7-16.el6 will be installed +--> Processing Dependency: cpp = 4.4.7-16.el6 for package: gcc-4.4.7-16.el6.x86_64 +--> Processing Dependency: cloog-ppl >= 0.15 for package: gcc-4.4.7-16.el6.x86_64 +---> Package libffi-devel.x86_64 0:3.0.5-3.2.el6 will be installed +---> Package openssl-devel.x86_64 0:1.0.1e-48.el6_8.3 will be installed +---> Package python-devel.x86_64 0:2.6.6-66.el6_8 will be installed +--> Running transaction check +---> Package cloog-ppl.x86_64 0:0.15.7-1.2.el6 will be installed +--> Processing Dependency: libppl_c.so.2()(64bit) for package: cloog-ppl-0.15.7-1.2.el6.x86_64 +--> Processing Dependency: libppl.so.7()(64bit) for package: cloog-ppl-0.15.7-1.2.el6.x86_64 +---> Package cpp.x86_64 0:4.4.7-16.el6 will be installed +--> Processing Dependency: lpfr.so.1()(64bit) for package: cpp-4.4.7-16.el6.x86_64 +--> Running transaction check +---> Package mpfr.x86_64 0:2.4.1-6.el6 will be installed +---> Package ppl.x86_64 0:0.10.2-11.el6 will be installed +--> Finished Dependency Resolution + +Dependencies Resolved + +================================================================================ + Package Arch Version Repository Size +================================================================================ +Installing: + gcc x86_64 4.4.7-16.el6 RHEL-67-x86_64 10 M + libffi-devel x86_64 3.0.5-3.2.el6 RHEL-67-x86_64 18 k + openssl-devel x86_64 1.0.1e-48.el6_8.3 RHEL-67-x86_64-updates 1.2 M + python-devel x86_64 2.6.6-66.el6_8 RHEL-67-x86_64-updates 173 k +Installing for dependencies: + cloog-ppl x86_64 0.15.7-1.2.el6 RHEL-67-x86_64 93 k + cpp x86_64 4.4.7-16.el6 RHEL-67-x86_64 3.7 M + mpfr x86_64 2.4.1-6.el6 RHEL-67-x86_64 156 k + ppl x86_64 0.10.2-11.el6 RHEL-67-x86_64 1.3 M + +Transaction Summary +================================================================================ +Install 8 Package(s) + +Total download size: 17 M +Installed size: 36 M +Is this ok [y/N]: Y +Downloading Packages: +(1/8): cloog-ppl-0.15.7-1.2.el6.x86_64.rpm | 93 kB 00:01 +(2/8): cpp-4.4.7-16.el6.x86_64.rpm | 3.7 MB 00:19 +(3/8): gcc-4.4.7-16.el6.x86_64.rpm | 10 MB 00:08 +(4/8): libffi-devel-3.0.5-3.2.el6.x86_64.rpm | 18 kB 00:00 +(5/8): mpfr-2.4.1-6.el6.x86_64.rpm | 156 kB 00:01 +(6/8): openssl-devel-1.0.1e-48.el6_8.3.x86_64.rpm | 1.2 MB 00:05 +(7/8): ppl-0.10.2-11.el6.x86_64.rpm | 1.3 MB 00:02 +(8/8): python-devel-2.6.6-66.el6_8.x86_64.rpm | 173 kB 00:00 +----------------------------------------------------------------------------------------------------------------------------------------------- +Total 277 kB/s | 17 MB 01:01 +Running rpm_check_debug +Running Transaction Test +Transaction Test Succeeded +Running Transaction + Installing : ppl-0.10.2-11.el6.x86_64 1/8 + Installing : cloog-ppl-0.15.7-1.2.el6.x86_64 2/8 + Installing : mpfr-2.4.1-6.el6.x86_64 3/8 + Installing : cpp-4.4.7-16.el6.x86_64 4/8 + Installing : gcc-4.4.7-16.el6.x86_64 5/8 + Installing : libffi-devel-3.0.5-3.2.el6.x86_64 6/8 + Installing : openssl-devel-1.0.1e-48.el6_8.3.x86_64 7/8 + Installing : python-devel-2.6.6-66.el6_8.x86_64 8/8 + Verifying : gcc-4.4.7-16.el6.x86_64 1/8 + Verifying : python-devel-2.6.6-66.el6_8.x86_64 2/8 + Verifying : openssl-devel-1.0.1e-48.el6_8.3.x86_64 3/8 + Verifying : mpfr-2.4.1-6.el6.x86_64 4/8 + Verifying : libffi-devel-3.0.5-3.2.el6.x86_64 5/8 + Verifying : cpp-4.4.7-16.el6.x86_64 6/8 + Verifying : ppl-0.10.2-11.el6.x86_64 7/8 + Verifying : cloog-ppl-0.15.7-1.2.el6.x86_64 8/8 + +Installed: + gcc.x86_64 0:4.4.7-16.el6 libffi-devel.x86_64 0:3.0.5-3.2.el6 openssl-devel.x86_64 0:1.0.1e-48.el6_8.3 python-devel.x86_64 0:2.6.6-66.el6_8 + +Dependency Installed: + cloog-ppl.x86_64 0:0.15.7-1.2.el6 cpp.x86_64 0:4.4.7-16.el6 mpfr.x86_64 0:2.4.1-6.el6 ppl.x86_64 0:0.10.2-11.el6 + +Complete! + + +Step 2 +------ + +[user@-laptop ~]$ wget http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm +--2016-12-01 18:42:42-- http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm +Resolving download.fedoraproject.org... 67.203.2.67, 152.19.134.198, 67.219.144.68, ... +Connecting to download.fedoraproject.org|67.203.2.67|:80... connected. +HTTP request sent, awaiting response... 302 Found +Location: http://mirror.globo.com/epel/6/x86_64/epel-release-6-8.noarch.rpm [following] +--2016-12-01 18:42:43-- http://mirror.globo.com/epel/6/x86_64/epel-release-6-8.noarch.rpm +Resolving mirror.globo.com... 131.0.25.51 +Connecting to mirror.globo.com|131.0.25.51|:80... connected. +HTTP request sent, awaiting response... 200 OK +Length: 14540 (14K) [application/x-redhat-package-manager] +Saving to: “epel-release-6-8.noarch.rpm” + +100%[=====================================================================================================>] 14,540 --.-K/s in 0.05s + +2016-12-01 18:42:43 (274 KB/s) - “epel-release-6-8.noarch.rpm” saved [14540/14540] + +[user@-laptop ~]$ # rpm -ivh epel-release-6-8.noarch.rpm + + +Step 3 +------ + +[user@-laptop ~]$ sudo yum --enablerepo=epel install python-pip +Loaded plugins: downloadkvmonly-background, downloadonly-background, -check-lotus-updates, -repo-checker, -repository, refresh- + : packagekit, security, versionlock +Setting up Install Process +Resolving Dependencies +--> Running transaction check +---> Package python-pip.noarch 0:7.1.0-1.el6 will be installed +--> Finished Dependency Resolution + +Dependencies Resolved + +=============================================================================================================================================== + Package Arch Version Repository Size +=============================================================================================================================================== +Installing: + python-pip noarch 7.1.0-1.el6 epel 1.5 M + +Transaction Summary +=============================================================================================================================================== +Install 1 Package(s) + +Total download size: 1.5 M +Installed size: 6.6 M +Is this ok [y/N]: y +Downloading Packages: +python-pip-7.1.0-1.el6.noarch.rpm | 1.5 MB 00:00 +Running rpm_check_debug +Running Transaction Test +Transaction Test Succeeded +Running Transaction + Installing : python-pip-7.1.0-1.el6.noarch 1/1 + Verifying : python-pip-7.1.0-1.el6.noarch 1/1 + +Installed: + python-pip.noarch 0:7.1.0-1.el6 + +Complete! + + +Step 4 +------ + +[user@-laptop ~]$ sudo pip install paramiko +/usr/lib/python2.6/site-packages/pip/_vendor/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. + InsecurePlatformWarning +You are using pip version 7.1.0, however version 9.0.1 is available. +You should consider upgrading via the 'pip install --upgrade pip' command. +Collecting paramiko +/usr/lib/python2.6/site-packages/pip/_vendor/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. + InsecurePlatformWarning + Downloading paramiko-2.0.2-py2.py3-none-any.whl (171kB) + 100% |████████████████████████████████| 172kB 579kB/s +Collecting pyasn1>=0.1.7 (from paramiko) + Downloading pyasn1-0.1.9-py2.py3-none-any.whl +Collecting cryptography>=1.1 (from paramiko) + Downloading cryptography-1.6.tar.gz (410kB) + 100% |████████████████████████████████| 413kB 433kB/s +Collecting idna>=2.0 (from cryptography>=1.1->paramiko) + Downloading idna-2.1-py2.py3-none-any.whl (54kB) + 100% |████████████████████████████████| 57kB 759kB/s +Requirement already satisfied (use --upgrade to upgrade): six>=1.4.1 in /usr/lib/python2.6/site-packages (from cryptography>=1.1->paramiko) +Collecting setuptools>=11.3 (from cryptography>=1.1->paramiko) + Downloading setuptools-30.0.0-py2.py3-none-any.whl (472kB) + 100% |████████████████████████████████| 475kB 382kB/s +Collecting enum34 (from cryptography>=1.1->paramiko) + Downloading enum34-1.1.6-py2-none-any.whl +Collecting ipaddress (from cryptography>=1.1->paramiko) + Downloading ipaddress-1.0.17-py2-none-any.whl +Collecting cffi>=1.4.1 (from cryptography>=1.1->paramiko) + Downloading cffi-1.9.1.tar.gz (407kB) + 100% |████████████████████████████████| 409kB 558kB/s +Collecting pycparser (from cffi>=1.4.1->cryptography>=1.1->paramiko) + Downloading pycparser-2.17.tar.gz (231kB) + 100% |████████████████████████████████| 233kB 834kB/s +Installing collected packages: pyasn1, idna, setuptools, enum34, ipaddress, pycparser, cffi, cryptography, paramiko + Found existing installation: setuptools 0.6rc11 + DEPRECATION: Uninstalling a distutils installed project (setuptools) has been deprecated and will be removed in a future version. This is due to the fact that uninstalling a distutils project will only partially uninstall the project. + Uninstalling setuptools-0.6rc11: + Successfully uninstalled setuptools-0.6rc11 + Running setup.py install for pycparser + Running setup.py install for cffi + Running setup.py install for cryptography +Successfully installed cffi-1.9.1 cryptography-1.6 enum34-1.1.6 idna-2.1 ipaddress-1.0.17 paramiko-2.0.2 pyasn1-0.1.9 pycparser-2.17 setuptools-30.0.0 + diff --git a/docs/templates/commands_template.xlsx b/docs/templates/commands_template.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..f3a61f027aeb45ba1e41363638ae8698a7d002e3 GIT binary patch literal 25227 zcmeIa2Ut_vwl=(wUPJE?k*WwNB1jF0QbYtSfFMMgG$AT2NFYe>CwE!Yjiqb+y zMWlCuB&c*r6k!P=`Ir0Ld(S@m?DPCzdG2?VXM0RZ>_mVNeyC|`d!Uw`Dq;2Um!4$5es>-&os_9+wq`=IOp zb^Jd(1269Po$EZpeR|u3V)#kv%*PsimgDOYy}XC4^xC@8yGB)lKMqrM`Le88qA!-r zb|v)sZ9Kf!n?BXEbe)qfM@WA`pE&&v>HkyXvcW@Bar@8Y6;vz? z)~~|s);N4Vyu`62*TD46mjhkyPI0*?HJZQEº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 literal 0 HcmV?d00001 diff --git a/docs/templates/config_template.ini b/docs/templates/config_template.ini new file mode 100755 index 0000000..acd7f29 --- /dev/null +++ b/docs/templates/config_template.ini @@ -0,0 +1,61 @@ +; Change the settings below to suit your needs. +; No leading spaces are allowed and comments always start with a ";" (semi-colon). +; DO NOT change the name of the section header. + +; You may create copies of this file and pass to the tool like this: +; ./reach.py --config_file= + +[TOOL DEFAULTS] ; Do not change this! + +; Optional, comment this if you want to be prompted each time for a user name to use +; Define the rest of the defaults values below to suit. +SSH_USER_NAME : testuser + +; Optional, comment this if you want to be prompted each time for a password to use +; Cipher text rsa key passphrase or password +; To generate the cipher text: ./reach.py --cipher_text= +SSH_PASSWORD_CIPHER : XXXXXXXXXXX + +; Optional, comment this if no RSA keys will be used +SSH_PRIVATE_KEY : /Users/randyo/.ssh/id_rsa + +; Define default hosts inventory to use. It needs to be a comma separated file with a header row. +; Be sure to define the matching HOST_MARKER (text displayed when processing) +; and KEY_COLUMN (IP used for connections) below. +HOSTS_INPUT_FILE : /Users/randyo/dev/reach_hosts/my_inventory.csv + +; Optional, will log in "logs" directory if this is commented. +LOGS_DIRECTORY : /Users/randyo/dev/Reach/logs/ + +; Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_LEVEL : DEBUG + +; Define default key column for the IP address to use for connections. +KEY_COLUMN : IP Address + +; Define the string format displayed when processing hosts. +; $HF_# where # is the column number from the file above. +HOST_MARKER : $HF_5 - $HF_1: $HF_3 + +; Safe to leave all as False or comment. Change to suit. +SHOW_HOST_DURATION : False +SHOW_CONSOLE_OUTPUT : False +RUN_IN_SIMULATION_MODE : False +DEBUG_FLAG : False +NO_DESTRUCTIVE_PROMPT : False + +; Set this to True if you want to blindly trust hosts or False to use the system known_hosts file +TRUST_HOSTS : False + +SSH_CONNECTION_TIMEOUT : 10 + +; Important to set this higher than your longest running command you plan to run. But set it too high +; Reach won't move to the next command if the command hangs. +SSH_COMMAND_TIMEOUT : 20 + +; Safe to leave commented. This is the prompt regex. These are characters to expect on hosts. +; Change only if you know what you're doing. Separate different prompt with | +; Example: +;PROMPT_REGEX : ([$#] |> )$ +; Default is: +;PROMPT_REGEX : [$#>]( )?$ diff --git a/docs/templates/hosts_file_sample.csv b/docs/templates/hosts_file_sample.csv new file mode 100755 index 0000000..05af0ad --- /dev/null +++ b/docs/templates/hosts_file_sample.csv @@ -0,0 +1,9 @@ +Location,Hostname,Type,IP Address,User,Password,Version,Comments,Backup File +Dallas,host1.mydomain.com,Cisco Switch,10.25.1.10,randyo,YWUldXYMe2YpISU=,r. 5,, +Dallas,host3.mydomain.com,Linux,10.25.1.11,root,YWUldXYMe2YpISUQ,r. 6,, +Southbury,host5.mydomain.com,Vyatta,10.25.1.12,techno,YWUldXYMe2YpISUT,r. 7,,host5_config.txt +Raleigh,host2.mydomain.com,Vyatta,10.25.1.13,root,YWUldXYMe2YpISUS,r. 8,,host2_config.txt +Las Vegas,host4.mydomain.com,Cisco Firewall,10.25.1.14,tool_user,YWUldXYMe2YpISUQ,r. 9,, +Las Vegas,host10.mydomain.com,Linux,10.25.1.15,randyo,YWUldXYMe2YpISUQ,r. 10,, +Las Vegas,host8.mydomain.com,Linux,10.25.1.16,tool_user,YWUldXYMe2YpISUQ,r. 11,, +Las Vegas,host6.mydomain.com,Linux,10.25.1.17,test_user,YWUldXYMe2YpISUQ,r. 12,, \ No newline at end of file diff --git a/docs/templates/sample_commands.csv b/docs/templates/sample_commands.csv new file mode 100755 index 0000000..e9d0023 --- /dev/null +++ b/docs/templates/sample_commands.csv @@ -0,0 +1,7 @@ +Command (Can use host file column $HF_#. For example: $HF_8 (use data from 8th column of hosts file),Show Output (-o) (Default = no),Local Command (Default=no),Expect (-w) String(s) ,Send (-p) String(s),Search (-d) String(s),Print Status (-r),Halt (-h) hosts loop when found Search String (Default = no) +show configuration commands > $HF_9; ls -al,no,,,,total,Success, +conf,no,,,,total,, +save /home/randyo/$HF_9.boot,,,,,,, +scp randyo@$HF_5:$HF_9 /Users/randyo/dev/sandbox/vga_backups/,,yes,,,,, +scp randyo@$HF_5:$HF_9.boot /Users/randyo/dev/sandbox/vga_backups/$HF_8.boot,,yes,,,,, +rm -rf $HF_9 $HF_9.boot,,,,,,, \ No newline at end of file diff --git a/reach.py b/reach.py new file mode 100755 index 0000000..26f5daa --- /dev/null +++ b/reach.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python + +import ConfigParser +import errno +import getopt +import os +import re +import sys + +from reolib import * +from reachlib import * + + +class Reach(REOScript): + """ + Main tool driver class. + """ + + def __init__(self): + """ + Class constructor. + """ + super(self.__class__, self).__init__() + self.sshworker = None + """Main worker instance""" + + self.util = REOUtility() + """Utility instance""" + + self.logger = None + """Optional logger (from logging module) instance""" + + self.main_config = ConfigParser.SafeConfigParser(allow_no_value=True) + """Config main_config instance""" + + self.full_path = os.path.realpath(__file__) + """Full path of running script""" + + self.dir_path = os.path.dirname(self.full_path) + """Directory only of running script""" + + self.SCRIPT_NAME = os.path.basename(self.full_path) + self.SCRIPT_VERSION = 'v1.0.2' + self.SCRIPT_DATE = '05-Jan-2017' + self.SCRIPT_DESCRIPTION = "Lightweight tool for executing remote commands on multiple hosts via SSH." + self.SCRIPT_SYNTAX_OR_INFO = "Git Repository: https://github.com/randyoyarzabal/reach" + self.SCRIPT_HELP = "Help/usage: reach.py -?" + self.SCRIPT_USAGE = """ +Synopsis: + ./reach.py -? + ./reach.py -v + ./reach.py -a + ./reach.py --cipher_text= + ./reach.py -b commands_file [-x] [-g] ... + ./reach.py -c command [-d search_string [-r report_string]] [-w wait_string -p response_string] ... + + Optionally, for any mode: + ./reach.py [--config=] [-i inventory_file] [-k column_key] [-x] [-g] + +Help / Usage: + -? : This help screen + -v : Display version information only + +Operation Modes: + -a : Access Check only + -b : Run Batch Commands (comma separated file, see template for format/options) + -c : Run Command + +Optional for all modes: + --config= Override the default config.ini + -x : Enable SIMULATION (no connection/commands invoked) + -g : Enable DEBUG + -i : Inventory (hosts) file (comma separated, define header key with -k) + -k : [Required with -i] Column header of keys + -e : Filter hosts to process. Operators are supported: = equal, | or, ~ contains, & and. + Note: Reach does not support mixed (& and |) in this release yet. + Example conditions: 'Build=WHC0122' , 'Build=WHC0122&Host~app, 'Build=WHC0122|Host~app|Host~dom' + +Optional for Command (-c) mode. Note that for Batch (-b) Mode, these are internally defined in the commands file. + -o : Show command console output (ignored in batch (-b option) mode) + -s : Run command as root (run 'sudo su -' first) + -h : Halt looping through hosts when first done string (-d) is found + +Special mode only for changing passwords to cipher text: + --cipher_text= return the password in cipher text to put in the password file + + The following can use hosts file column variables ($HF_#) delimited by '|': + For example: '$HF_#' where # is the column number in the hosts file: + --username= : Force user string instead of what is configured. + --password= : Force cipher-text password instead of what is configured. + --private_key= : Force private RSA key file instead of what is configured. + -d : Search for string in output (For example: 'Complete' or 'Nothing|Complete') + Can also use '$NF' to test for string is not found. + -r : [Optional with same length as -d] Matching string to print to screen when -d match + For example: 'Installed|Not Installed' + -w : Wait string + -p : [Required with same length as -w] Send a string when -w string is found + The following list of special markers can be used in -p: + $ENTER_KEY : '\\n' + $RETURN_KEY : '\\r' + $TAB_KEY : '\\t' + $SPACE_KEY : ' ' + $CT= : Used for sending passwords to the terminal like changing passwords + or sending Cisco ASA passwords in "enable" mode. + +Examples: + - Run 'yum -y install gdb' as root, look for the strings: 'Nothing' or 'Complete', then display + 'Installed' or 'Not Installed' on the screen. Process hosts matching 'Build' column = 'WHC038' + ./reach.py -c 'yum -y install gdb' -s -d 'Nothing|Complete' -r 'Installed|Not Installed' -e 'Build=WHC038' + ------------------------------------------------------------------------------- + - Run 'sh ip route 10.143.92.134', look for the strings: 'bond0.' or 'bond0', then display + 'Found in: $HF_1' ($HF_1 is the first column of the inventory) or 'Not Here' then halt the hosts loop + ./reach.py -c "sh ip route 10.143.92.134" -d 'bond0.|bond0' -r 'Found in: $HF_1|Not Here' -h + ------------------------------------------------------------------------------- + - Check access against all hosts in the inventory file + ./reach.py -a + ------------------------------------------------------------------------------- + - Force read a different inventory file making sure to define the header key for the IP to use + ./reach.py -i 'vga_inventory.csv' -a -k 'Public IP' + ------------------------------------------------------------------------------- + - Run a series of commands defined in a file (see template for proper format) + ./reach.py -b 'vga_backups.csv' + ------------------------------------------------------------------------------- + - Change password for a user (run this in simulation mode for an explanation) + ./reach.py -c 'passwd testuser' -w 'New|Retype' -p 'mypass3|mypass3' -d 'successfully' -r 'Changed password successfully!' + +Tips: + - Always be sure to run in SIMULATION mode first to see what the script is about to do! + - Use the -r option in conjunction with -d to substitute results, optionally use: grep and sed to limit output. + Example: Find all hosts where 'vyatta' is found in the zones list. (Assumes you have access to all servers in file) + ./reach.py -c 'show zone-policy zone' -d 'vyatta|$NF' -r 'yes|no' | grep -E 'yes|no' | sed -e 's/^[ \t-]*//' + You may then paste the output as a new column in your inventory file. + """ + + def read_switches(self, argv): + """ + Read command-line switches. + :param argv: Argument list + :return: None + """ + opts = None + try: + opts, args = getopt.getopt(argv, 'ab:c:xgvoe:i:k:shd:w:p:r:?', + ["config=", "username=", "password=", "private_key=", "cipher_text="]) + except KeyboardInterrupt: + REOUtility.key_interrupt() + except getopt.GetoptError: + self.author(show_help=True) + sys.exit(2) + if len(opts) == 0 or len(args) > 0: + self.author(show_desc=True, show_help=True) + sys.exit(2) + + for opt, arg in opts: + # Optional switches + if opt in BOOL_OPTS: + set_cli_config(opt, True) + user_opts[opt] = True + if opt in STRING_OPTS: + if SWITCH_KEYS[opt] == CONDITION_STRING and CONDITION_STRING in cli_config.keys(): + concat_condition = cli_config[CONDITION_STRING] + "&" + arg + set_cli_config(opt, concat_condition) + user_opts[opt] = concat_condition + else: + set_cli_config(opt, arg) + user_opts[opt] = arg + if opt in NUM_OPTS: + set_cli_config(opt, float(arg)) + user_opts[opt] = float(arg) + + def check_prerequisites(self): + """ + Check tool pre-conditions and raise an exception if a problem is found. + :return: None + """ + # Required switch, only allow one of these + operations = [option for option in cli_config if option in EXCLUSIVE_OPTS] + if len(operations) == 0: + self.author(show_help=True) + sys.exit(2) + elif len(operations) > 1: + raise ValueError("One of these options is required: " + str(EXCLUSIVE_OPTS_KEYS) + " (but only one!)") + + config[OPERATION] = operations[0] + + # Special operations + if config[OPERATION] == SHOW_AUTHOR: + self.author(show_desc=False, show_help=True) + sys.exit(2) + + if config[OPERATION] == SHOW_USAGE: + self.usage() + sys.exit(2) + + if config[OPERATION] == CIPHER: + self.author() + print ("Warning: this cipher text form is only useful in Reach. It is by no means secure.\n" + "It is simply meant to conceal and prevent passwords from being displayed in clear-text.") + print "\nCipher text: '" + REOUtility.encrypt_str(cli_config[CIPHER]) + "'" + sys.exit(0) + + # Forbidden opts when using -b + if config[OPERATION] == OPERATION_BATCH: + conflicting_opts = set(BAD_BATCH_OPTS) & set(cli_config.keys()) + conflicting_opts_keys = [SWITCH_VALUE[opt] for opt in conflicting_opts] + if len(conflicting_opts) > 0: + raise ValueError("Option(s) " + ", ".join( + conflicting_opts_keys) + " not allowed in batch (-b) mode! Specify the option in the commands file.") + + # Forbidden opts when using -a + if config[OPERATION] == OPERATION_ACCESS: + conflicting_opts = set(BAD_ACCESS_OPTS) & set(cli_config.keys()) + conflicting_opts_keys = [SWITCH_VALUE[opt] for opt in conflicting_opts] + if len(conflicting_opts) > 0: + raise ValueError("Option(s) " + ", ".join( + conflicting_opts_keys) + " not allowed in access (-a) mode!") + + # Check HOSTS_IMPUT_FILE + if config[HOSTS_INPUT_FILE] == '': + raise IOError("HOSTS_INPUT_FILE must be defined either in " + + config[CONFIG_FILE] + " or with option '" + + SWITCH_VALUE[HOSTS_INPUT_FILE] + "'.") + + # Files existence detection + # CONFIG_FILE already checked + keys_with_files = [COMMANDS_FILE, SSH_PRIVATE_KEY, HOSTS_INPUT_FILE] + keys_with_files = [key for key in keys_with_files if config[key]] + for file_key in keys_with_files: + if not os.path.isfile(config[file_key]): + raise IOError(config[file_key] + " doesn't exist.") + + # -k must be used with -i + if HOSTS_INPUT_FILE in cli_config.keys(): + if KEY_COLUMN not in cli_config.keys(): + raise ValueError("'" + SWITCH_VALUE[HOSTS_INPUT_FILE] + "' can only be used in conjunction with '" + + SWITCH_VALUE[KEY_COLUMN] + "'.") + # Check hosts file + hosts_file = REODelimitedFile(config[HOSTS_INPUT_FILE], has_header=True) + max_column = len(hosts_file.header_list) + if config[KEY_COLUMN] not in hosts_file.header_list: + raise ValueError("Column '" + config[KEY_COLUMN] + "' cannot be found in hosts file.") + if config[CONDITION_STRING]: + if STRINGS_MULTI_CONDITION in config[CONDITION_STRING] and STRINGS_DELIMITER in config[CONDITION_STRING]: + raise ValueError( + "Condition cannot contain both '" + STRINGS_MULTI_CONDITION + "' and '" + STRINGS_DELIMITER + "'") + conditions = re.split("[" + STRINGS_MULTI_CONDITION + STRINGS_DELIMITER + "]", + config[CONDITION_STRING]) + conditions = [{'cond': cond, 'splits': re.split("[~=]", cond)} for cond in conditions] + for cond in conditions: + if len(cond['splits']) != 2: + raise ValueError( + "Condition '" + cond['cond'] + "' invalid, please specify '~' or '=' for each condition.") + if cond['splits'][0] not in hosts_file.header_list: + raise ValueError("Column '" + cond['splits'][0] + "' cannot be found in hosts file.") + + del hosts_file + + # List of commands read from file + commands = [] + + # This if/elif statement creates the commands array that will contain + # the list of commands to be executed. Then each command will be + # checked for validity + if config[OPERATION] == OPERATION_BATCH: + commands_file = REODelimitedFile(config[COMMANDS_FILE]) + row = 0 + for command in commands_file: + row += 1 + if row == 1: + continue + temp = {} + for i in range(len(command)): + temp[BATCH_COMMANDS_COLUMN_ORDER[i]] = command[i] + commands.append(temp) + del commands_file + elif config[OPERATION] == OPERATION_COMMAND: + temp = {} + for var_name in BATCH_COMMANDS_COLUMN_ORDER: + temp[var_name] = str(config[var_name]).strip() if var_name in config else '' + commands.append(temp) + + # Check whether each command is valid + for command in commands: + if command[COMMAND_STRING] == '': + raise ValueError("Command cannot be empty.") + if command[SHOW_CONSOLE_OUTPUT].lower() not in VALID_YES_NO: + raise ValueError("'" + SWITCH_VALUE[ + SHOW_CONSOLE_OUTPUT] + "' must be 'yes', 'no' or be left empty (meaning no).") + if command[LOCAL_COMMAND].lower() not in VALID_YES_NO: + raise ValueError( + "'" + SWITCH_VALUE[LOCAL_COMMAND] + "' must be 'yes', 'no' or be left empty (meaning no).") + if command[HALT_ON_STRING].lower() not in VALID_YES_NO: + raise ValueError( + "'" + SWITCH_VALUE[HALT_ON_STRING] + "' must be 'yes', 'no' or be left empty (meaning no).") + + temp = {} + temp[COMMAND_WAIT_STRING] = len(command[COMMAND_WAIT_STRING].split(STRINGS_DELIMITER)) if command[ + COMMAND_WAIT_STRING] != '' else 0 + temp[COMMAND_SEND_STRING] = len(command[COMMAND_SEND_STRING].split(STRINGS_DELIMITER)) if command[ + COMMAND_SEND_STRING] != '' else 0 + temp[COMMAND_SEARCH_STRING] = len(command[COMMAND_SEARCH_STRING].split(STRINGS_DELIMITER)) if command[ + COMMAND_SEARCH_STRING] != '' else 0 + temp[COMMAND_REPORT_STRING] = len(command[COMMAND_REPORT_STRING].split(STRINGS_DELIMITER)) if command[ + COMMAND_REPORT_STRING] != '' else 0 + if temp[COMMAND_WAIT_STRING] != temp[COMMAND_SEND_STRING]: + raise ValueError("Option '" + SWITCH_VALUE[ + COMMAND_SEND_STRING] + "' must be used in conjunction with option '" + + SWITCH_VALUE[COMMAND_WAIT_STRING] + "' with the same length.") + if temp[COMMAND_REPORT_STRING] > 0 and temp[COMMAND_SEARCH_STRING] == 0: + raise ValueError("Option '" + SWITCH_VALUE[ + COMMAND_REPORT_STRING] + "' cannot be used without option '" + + SWITCH_VALUE[COMMAND_SEARCH_STRING] + "'.") + if temp[COMMAND_REPORT_STRING] > 0 and (temp[COMMAND_SEARCH_STRING] != temp[COMMAND_REPORT_STRING]): + raise ValueError("When option '" + SWITCH_VALUE[ + COMMAND_REPORT_STRING] + "' is used in conjunction with option '" + + SWITCH_VALUE[COMMAND_SEARCH_STRING] + "', they must have the same length.") + if command[HALT_ON_STRING].lower() in ['yes', 'true'] and command[COMMAND_SEARCH_STRING] == '': + raise ValueError( + "Option '" + SWITCH_VALUE[HALT_ON_STRING] + "' must be used in conjunction with option '" + + SWITCH_VALUE[COMMAND_SEARCH_STRING] + "'.") + + command_raw = ','.join(command.values()) + hvars = re.findall(re.escape(COLUMN_VARIABLE) + '\d*', command_raw) + for hvar in hvars: + if int(hvar.split("_")[1]) > max_column: + raise ValueError( + "Host File Column variable '" + hvar + "' must be between 1 and " + str(max_column) + ".") + + def run_util(self, argv): + """ + Main driver method. + :param argv: Argument list + :return: None + """ + # Read switches first, into temp dict and get the config file if any + self.read_switches(argv) + + # Read our current path + config[CONFIG_FILE] = self.dir_path + "/" + config[CONFIG_FILE] + + # Parse config file as defaults + config_file = cli_config[CONFIG_FILE] if (CONFIG_FILE in cli_config) else config[CONFIG_FILE] + + if not os.path.isfile(config_file): + print("Aborted...Config File defined: " + config_file + " doesn't exist.") + sys.exit(1) + + # Override system defaults with user defaults + self.main_config.read(config_file) + config_defaults = REOUtility.get_parser_config(CONFIG_SECTION, self.main_config) + for conf in config_defaults: + if conf in STRING_DEFAULTS: + + defaults[conf] = self.main_config.get(CONFIG_SECTION, conf) + + elif conf in BOOLEAN_DEFAULTS: + defaults[conf] = self.main_config.getboolean(CONFIG_SECTION, conf) + + elif conf in NUMBER_DEFAULTS: + defaults[conf] = self.main_config.getfloat(CONFIG_SECTION, conf) + + # Prepend our current dir to default log directory if not defined by user + if LOGS_DIRECTORY not in config_defaults: + defaults[LOGS_DIRECTORY] = self.dir_path + "/" + defaults[LOGS_DIRECTORY] + + # Apply system defaults + set_defaults() + + # Apply command line switches + set_cli_to_config() + + # Set a slash if not already there + if not config[LOGS_DIRECTORY].endswith('/'): + config[LOGS_DIRECTORY] += '/' + + # Create logs directory if non existent + if not os.path.isdir(config[LOGS_DIRECTORY]): + try: + os.makedirs(os.path.dirname(config[LOGS_DIRECTORY])) + except OSError as exc: # Guard against race condition + if exc.errno != errno.EEXIST: + print "Unable to create log directory defined: " + config[LOGS_DIRECTORY] + sys.exit(1) + + # Set logging file and level + self.logger = REOUtility.get_logger(config[LOGS_DIRECTORY] + config[LOG_FILE], + LOG_LEVELS[config[LOG_LEVEL]]) + self.util.toggle_debug(config[DEBUG_FLAG]) + + self.log(logging.INFO, "Reach Process Started", False) + + try: + self.check_prerequisites() + except Exception as error: + print("Aborted..." + error.message) + print "Use: 'reach.py -?' for usage/help." + sys.exit(1) + + self.author() + + if config[OPERATION] == OPERATION_ACCESS: + print ("== | Operation: Access Check" + self.get_simulation_str() + " | ==\n") + self.log(logging.INFO, "Access Mode Started", False) + self.sshworker = CheckAccessWorker(logger=self.logger) + self.sshworker.SHOW_HOST_DURATION = False # Force to false, this is never needed in this mode + if config[OPERATION] == OPERATION_BATCH: + print ("== | Operation: Run Batch Commands from File" + self.get_simulation_str() + " | ==\n") + self.sshworker = RunBatchCommandsWorker(config[COMMANDS_FILE], logger=self.logger) + self.log(logging.INFO, "Batch Mode Started", False) + self.log(logging.INFO, "Processing batch file: " + config[COMMANDS_FILE], False) + if config[OPERATION] == OPERATION_COMMAND: + print ("== | Operation: Run Command" + self.get_simulation_str() + " | ==\n") + self.log(logging.INFO, "Command Mode Started", False) + self.sshworker = RunCommandWorker(logger=self.logger) + self.sshworker.str_vars_exist = check_for_vars() + + # Process hosts + self.sshworker.hosts_worker() # Loop through hosts + + self.log(logging.INFO, "Reach Process Ended", False) + + def log(self, level, message, print_to_screen=False): + """ + Logging mechanism if defined. + :param print_to_screen: True to print message to screen as well + :param level: Log level + :param message: Message + :return: + """ + if print_to_screen: + print message + if self.util.debug and level == logging.DEBUG: + self.util.print_debug(message) + if self.logger: + self.logger.log(level, message) + + @classmethod + def get_simulation_str(cls): + return ' - SIMULATION MODE ONLY' if config[RUN_IN_SIMULATION_MODE] else '' + + +def main(argv): + myscript = Reach() + myscript.run_util(argv) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/reachlib/BaseREOSSHWorker.py b/reachlib/BaseREOSSHWorker.py new file mode 100755 index 0000000..6c4af36 --- /dev/null +++ b/reachlib/BaseREOSSHWorker.py @@ -0,0 +1,500 @@ +from reolib.REODelimitedFile import REODelimitedFile +from reolib.REOUtility import REOUtility +from reolib.REORemoteHost import REORemoteHost +from reachlib.SSHWorkerConfig import * +import re +import pdb +import sys + + +class BaseREOSSHWorker(object): + """ + This class is a semi-abstract class defining basic tasks for an SSH activity. + Logging on, iterating through the hosts file, and processing it. How the hosts are processed + is defined in the work() method that concrete classes need to implement. + """ + + def __init__(self, logger=None): + """ + Class constructor + """ + self.rhost = None + """SSH Host""" + + self.current_ip = '' + """Current IP being processed""" + + self.current_host = None + """Current host being processed""" + + self.hosts = REODelimitedFile(config[HOSTS_INPUT_FILE], ',', has_header=True) + """Main hosts file for processing""" + + self.util = REOUtility(config[DEBUG_FLAG]) + """Utility instance""" + + self.last_run_log = open(config[LOGS_DIRECTORY] + config[LAST_RUN_OUTPUT], 'w') + """File containing results strings only for the last run""" + + self.command_string = '' + """Command string (vars replaced)""" + + self.search_string = '' + """Search string delimited (vars replaced)""" + + self.report_string = '' + """Report (for search) delimited (vars replaced) string""" + + self.wait_string = '' + """Wait string delimited (vars replaced)""" + + self.response_string = '' + """Send/put string delimited (vars replaced)""" + + self.search_strings = None + """Search strings (vars replaced) list""" + + self.report_strings = None + """Report/put strings (vars replaced) list""" + + self.wait_strings = None + """Wait strings list (vars replaced) list""" + + self.response_strings = None + """Send/put strings list (vars replaced) list""" + + self.response_strings_display = None + """Send string for screen display list""" + + self.str_vars_exist = False + """String vars existence flag""" + + self.destr_cmds_exist = False + """Destructive commands flag""" + + self.phost_count = 0 # Processed hosts + """Number of processed hosts""" + + self.logger = logger + """Optional logger (from logging module) instance""" + + def hosts_worker(self): + """ + The is the main entry point for an instance of the concrete class. + :return: None + """ + + self.phost_count = 0 # Processed hosts + + self.show_header_confirmation() + + log_str = ("# This file is only relevant/useful for a single command (-c) with a (-d or -r) defined.\n" + "# It is meant to be pasted in a new column spreadsheet of the hosts file.\n") + + if config[CONDITION_STRING]: + log_str += "# Filtered results: " + config[CONDITION_STRING] + "\n" + self.log(logging.WARNING, "Filtered results: " + config[CONDITION_STRING], False) + + self.last_run_log.write(log_str) + + for host in self.hosts: + self.current_host = host + self.current_ip = self.hosts.get_row_val(name=config[KEY_COLUMN]) + + if config[CONDITION_STRING]: + try: + # TODO: Handle complex conditions with mixed & and | + if STRINGS_MULTI_CONDITION in config[CONDITION_STRING]: + conditions = config[CONDITION_STRING].split(STRINGS_MULTI_CONDITION) + alltrue = True + elif STRINGS_DELIMITER in config[CONDITION_STRING]: + conditions = config[CONDITION_STRING].split(STRINGS_DELIMITER) + alltrue = False + else: + conditions = [config[CONDITION_STRING]] + + conditions_values = map(lambda condition: self.__eval_condition(condition), conditions) + skip_host = not reduce(lambda x, y: x & y if alltrue else x | y, conditions_values) + + if skip_host: + self.log(logging.DEBUG, self.current_ip + ' - does not meet condition: ' + config[ + CONDITION_STRING] + ' == Skipping', False) + continue # Skipping host... + except KeyboardInterrupt: + self.util.key_interrupt() + + self.phost_count += 1 + if not config[RUN_IN_SIMULATION_MODE]: + if not self.host_worker(): + break # Stop loop if concrete method returns False + else: + # Simulated run + self.__process_host_simulation() + + if self.phost_count % 10: # Don't delay writing to file for every 10 hosts processed. + self.last_run_log.flush() + + print "" + self.log(logging.INFO, + "Script Duration: " + str(self.util.get_current_duration()) + " " + STRINGS_DELIMITER + " " + str( + self.phost_count) + " out of " + str(len(self.hosts)) + " hosts processed.", True) + + def host_worker(self): + """ + Common code defining prerequisite actions per host like making the remote connection. + :return: True signal continuation of hosts iteration, False otherwise. + """ + retval = True + self.util.start_timer() + self.rhost = REORemoteHost(self.current_ip, config[PROMPT_REGEX], config[NEW_PROMPT_REGEX], + config[NEW_PROMPT], logger=self.logger) + self.rhost.ssh_lib_log = config[LOGS_DIRECTORY] + config[SSH_LOG_FILE] + self.rhost.util.toggle_debug(config[DEBUG_FLAG]) + self.rhost.usr = self.replace_column_vars(config[SSH_USER_NAME]) + + if config[SSH_PASSWORD_CIPHER]: + self.rhost.pwd = REOUtility.decrypt_str(self.replace_column_vars(config[SSH_PASSWORD_CIPHER])) + else: + self.rhost.pwd = config[SSH_PASSWORD] + + self.rhost.key_file = self.replace_column_vars(config[SSH_PRIVATE_KEY]) + self.rhost.str_vars_exist = self.str_vars_exist + + if config[HOST_MARKER]: + print self.__get_process_str() + self.replace_column_vars(config[HOST_MARKER]) + " ...", + self.log(logging.INFO, "Processing host: " + self.replace_column_vars(config[HOST_MARKER])) + else: + print self.__get_process_str() + self.current_ip + " ...", + self.log(logging.INFO, "Processing host: " + self.current_ip) + sys.stdout.flush() + + connected = self.rhost.connect_host(set_prompt=config[OPERATION] != OPERATION_ACCESS, + conn_timeout=config[SSH_CONNECTION_TIMEOUT], + cmd_timeout=config[SSH_COMMAND_TIMEOUT], trust_hosts=config[TRUST_HOSTS]) + + self.log(logging.INFO, self.rhost.connect_status_string, True) + if config[OPERATION] == OPERATION_ACCESS: + self.last_run_log.write(self.rhost.connect_status_string + "\n") + else: + if self.rhost.connect_status_string != self.rhost.SERVER_STATUS_CONNECTED: + self.last_run_log.write(self.rhost.connect_status_string + "\n") + + if connected: + # Implemented in different ways by concrete classes + retval = self.host_work() + if config[SHOW_HOST_DURATION] and config[OPERATION] != OPERATION_ACCESS: + print (" Host Duration: " + str(self.util.stop_timer())) + + return retval + + def host_work(self): + """ + Abstract method to be implemented by concrete class + This method defines how the hosts are processed + """ + raise NotImplementedError + + def run_command(self): + """ + Process a single command with its options stored in the config[] dict. + :return: True signal continuation of hosts iteration, False otherwise (halt). + """ + retval = True + + self.__replace_vars_in_strings() + + if config[RUN_SUDO_FIRST]: + sudo_cmd = 'sudo su -' + if self.str_vars_exist: + print (" Switching to root user with: \'" + sudo_cmd + "\'") + self.log(logging.INFO, 'Sudo switch to root user', False) + output, error_msg = self.rhost.send_cmd_wait_respond(sudo_cmd, last_run_log=self.last_run_log) + + if error_msg: + if config[SHOW_CONSOLE_OUTPUT]: + print (" - Console Output: \n" + output) + return retval, error_msg + + if self.str_vars_exist: + print (" Running command: \'" + self.command_string + "\'") + self.log(logging.INFO, "Running command: \'" + self.command_string + "\'", False) + + if self.search_string or self.wait_string: + # Send the command to the server and wait for a string + output, error_msg = self.rhost.send_cmd_wait_respond(self.command_string, self.search_string, + self.wait_string, self.response_string, + self.last_run_log) + # skip the rest of the if statement if error_msg != '' ? + w = self.rhost.search_string_isfound + + if self.report_string and (w != ''): + result = self.report_strings[self.search_strings.index(w)] + print (" - " + result) + self.log(logging.INFO, "Report String: " + result, False) + self.last_run_log.write(result + "\n") + self.log(logging.DEBUG, "Command Wait String Found: " + w, False) + else: + if w != '': + self.log(logging.INFO, " - Found: " + w, True) + self.last_run_log.write("Found: " + w + "\n") + + if config[HALT_ON_STRING]: + if w == self.search_strings[0]: + print (" - Found: \'" + w + "\' halting as requested.") + self.log(logging.INFO, "Found \'" + w + "\' halting as requested.", True) + retval = False # Stop hosts loop, we found what we are looking for + + else: + # Send the command to the server and wait for a string + output, error_msg = self.rhost.send_cmd_wait_respond(self.command_string, last_run_log=self.last_run_log) + + if config[SHOW_CONSOLE_OUTPUT]: + print (" - Console Output: \n" + output) + + return retval, error_msg + + def show_header_confirmation(self): + """ + Method to display confirmation information before execution. + :return: + """ + # Display for any mode + print "Hosts File: " + config[HOSTS_INPUT_FILE] + + if not self.str_vars_exist: + if config[RUN_SUDO_FIRST]: + print ("- Will switch to root user with: \'sudo su -\'") + + if config[OPERATION] != OPERATION_ACCESS: + print "- Command string to run: '" + config[COMMAND_STRING] + "'" + + if config[LOCAL_COMMAND]: + print "- Will run command locally" + + if config[COMMAND_SEARCH_STRING]: + print "- Search String: '" + config[COMMAND_SEARCH_STRING] + "'" + + if config[COMMAND_REPORT_STRING]: + print " - Report String: '" + config[COMMAND_REPORT_STRING] + "'" + + if config[COMMAND_WAIT_STRING]: + print "- Wait String: '" + config[COMMAND_WAIT_STRING] + "'" + + if config[COMMAND_SEND_STRING]: + tmp_list = config[COMMAND_SEND_STRING].split(STRINGS_DELIMITER) + tmp_list = STRINGS_DELIMITER.join( + self.__replace_vars_in_list(tmp_list, replace_column=False, display_only=True)) + print " - Send/Response String: '" + tmp_list + "'" + + if config[SHOW_CONSOLE_OUTPUT]: + print "- Show console output" + + if config[HALT_ON_STRING] and config[COMMAND_SEARCH_STRING]: + print ("- If " + config[COMMAND_SEARCH_STRING] + " is found, hosts loop will halt") + + if config[SHOW_HOST_DURATION] and config[OPERATION] != OPERATION_ACCESS: + print ("- Calculate and show host processing duration") + + # Display the rest for specific modes + if config[OPERATION] == OPERATION_BATCH: + print "- Commands File: " + config[COMMANDS_FILE] + if config[CONDITION_STRING]: + print ("Condition found. Filtering processing to: '" + config[CONDITION_STRING]) + "'" + + print ("--------------------------------------------------------------------") + + if not config[NO_DESTRUCTIVE_PROMPT]: + if config[OPERATION] == OPERATION_COMMAND: + # Check for destructive commands in the command string or if running sudo + if True in [(s in config[COMMAND_STRING]) for s in DESTRUCTIVE_COMMANDS] or config[RUN_SUDO_FIRST]: + self.destr_cmds_exist = True + + if self.destr_cmds_exist and not config[RUN_IN_SIMULATION_MODE]: + if not self.util.query_yes_no("\nYour command(s) contains one or more destructive commands: " + str( + DESTRUCTIVE_COMMANDS) + ".\nAre you sure you want to continue?", default='no'): + sys.exit(2) + else: + self.log(logging.WARNING, "Destructive commands execution confirmed.", False) + + if not config[RUN_IN_SIMULATION_MODE]: + + if not config[SSH_USER_NAME] and not config[SSH_PASSWORD_CIPHER]: + config[SSH_USER_NAME], config[SSH_PASSWORD] = self.util.prompt_user_password(desc="SSH") + + elif not config[SSH_USER_NAME]: + config[SSH_USER_NAME], none = self.util.prompt_user_password(password_prompt=False, desc="SSH") + + elif not config[SSH_PASSWORD_CIPHER]: + none, config[SSH_PASSWORD] = self.util.prompt_user_password(user_prompt=False, desc="SSH") + + def simulate_command(self): + """ + Display simulation confirmation per host + :return: None + """ + if not self.str_vars_exist: + return + + self.__replace_vars_in_strings() + + if config[RUN_SUDO_FIRST]: + print (" Switch to root user with: \'sudo su -\'") + + if config[LOCAL_COMMAND]: + print (" Run command locally: \'" + self.command_string + "\'") + else: + print (" Run command: \'" + self.command_string + "\'") + + if self.search_string: + print (" - Search for string(s): " + self.search_string) + + if self.report_string: + print " - Display string(s): '" + self.report_string + "'" + + if self.wait_string: + print (" - Wait for string(s) in sequence: " + self.wait_string) + print (" - Send string(s) in sequence: " + self.response_string_display) + + if config[HALT_ON_STRING] and self.search_string: + print (" - If '" + self.search_string + "' is found, hosts loop will halt.") + + if config[SHOW_CONSOLE_OUTPUT]: + print (" - Print console output") + + if config[SHOW_HOST_DURATION] and config[OPERATION] != OPERATION_ACCESS: + print (" - Calculate and show host processing duration") + + def __process_host_simulation(self): + """ + Display per-host progress to screen. + :return: True signal continuation of hosts iteration, False otherwise (halt). + """ # Running in simulation mode + if config[HOST_MARKER]: + print (self.__get_process_str() + self.replace_column_vars(config[HOST_MARKER])) + else: + print (self.__get_process_str() + self.current_ip) + + # Implemented in different ways by concrete classes + self.run_simulation() + + def run_simulation(self): + """ + Abstract method to be implemented by concrete class + This method defines how the hosts are simulated + """ + raise NotImplementedError + + def __eval_condition(self, condition): + """ + Evaluate a single condition string. For example: 'Build=WHC058' or 'Hostname~app' + :param condition: condition to be evaluated + :return: True if condition is met, False otherwise. + """ + t_cond = re.split("[~=]", condition) + if len(t_cond) != 2: + raise KeyError('Invalid condition: ' + condition) + field = t_cond[0] + fvalue = t_cond[1] + try: + rvalue = self.current_host[field] + except KeyError: + # shouldn't happen, condition is already checked + raise KeyError('Invalid field "' + field + '" in condition ' + condition) + return (fvalue in rvalue) if '~' in condition else (fvalue == rvalue) + + def __replace_vars_in_list(self, l, replace_column=True, display_only=False): + """ + Replace and column vars ($HF_#) with real values. + :param l: List potentially containing vars. + :return: New list with vars replaced with real values. + """ + new_list = l + for i, r in enumerate(new_list): + x = r + if replace_column: + x = self.replace_column_vars(r) + # Replace key strokes for display if exists + if display_only: + for k_str in REORemoteHost.KEY_STROKE_DISPLAY: + x = x.replace(k_str, REORemoteHost.KEY_STROKE_DISPLAY[k_str]) + cipher_text = re.search(re.escape(CIPHER_TEXT_MARKER) + '[^|]*', x) + if cipher_text is not None: + x = x.replace(cipher_text.group(0), '**********') + new_list[i] = x + return new_list + + def __replace_vars_in_strings(self): + """ + Replace vars ($HF_#) in switch strings + :return: None + """ + # Turn strings into list delimited by | + self.command_string = [self.util.trim_quotes(config[COMMAND_STRING])] + self.search_strings = config[COMMAND_SEARCH_STRING].split(STRINGS_DELIMITER) + self.report_strings = config[COMMAND_REPORT_STRING].split(STRINGS_DELIMITER) + self.wait_strings = config[COMMAND_WAIT_STRING].split(STRINGS_DELIMITER) + self.response_strings = config[COMMAND_SEND_STRING].split(STRINGS_DELIMITER) + + # Replace column/string and key stroke variables + self.command_string = STRINGS_DELIMITER.join(self.__replace_vars_in_list(self.command_string)) + self.report_string = STRINGS_DELIMITER.join(self.__replace_vars_in_list(self.report_strings)) + self.wait_string = STRINGS_DELIMITER.join(self.__replace_vars_in_list(self.wait_strings)) + self.search_string = STRINGS_DELIMITER.join(self.__replace_vars_in_list(self.search_strings)) + self.response_string = STRINGS_DELIMITER.join(self.__replace_vars_in_list(self.response_strings)) + + # Special case for showing response_strings with key stroke variables + self.response_string_display = STRINGS_DELIMITER.join( + self.__replace_vars_in_list(self.response_strings, display_only=True)) + + def replace_column_vars(self, s): + """ + Replace a column var with its corresponding value. + :param s: Column variable + :return: Column value + """ + cmd_str = s + if COLUMN_VARIABLE in cmd_str: + hvars = re.findall(re.escape(COLUMN_VARIABLE) + '\d*', cmd_str) + for hvar in hvars: # Replace all variables found + col_num = int(hvar.split("_")[1]) # Read integer after $HF_ + cmd_str = cmd_str.replace(hvar, self.hosts.get_row_val(self.hosts.current_row, col_idx=col_num - 1)) + return cmd_str + + def log(self, level, message, print_to_screen=False): + """ + Logging mechanism if defined. + :param level: Log level + :param message: Message + :param print_to_screen: True to print message to screen as well + :return: + """ + if print_to_screen: + print message + + if self.util.debug and level == logging.DEBUG: + self.util.print_debug(message) + + if self.logger: + self.logger.log(level, message) + + def __get_process_str(self): + """ + Add new-lines to string depending on conditions for better reporting output + :return: Proper string to display + """ + p_str = 'Processing host: ' + if config[OPERATION] in (OPERATION_COMMAND, OPERATION_ACCESS): + if self.phost_count == 1 or self.str_vars_exist: + p_str = "\n" + p_str + if config[OPERATION] == OPERATION_BATCH: + if not self.destr_cmds_exist or self.phost_count > 1: + p_str = "\n" + p_str + + return p_str + + def __del__(self): + """ + Class destructor. Close log file. + :return: + """ + self.last_run_log.close() diff --git a/reachlib/SSHWorkerClasses.py b/reachlib/SSHWorkerClasses.py new file mode 100755 index 0000000..0be766a --- /dev/null +++ b/reachlib/SSHWorkerClasses.py @@ -0,0 +1,158 @@ +from reachlib.BaseREOSSHWorker import BaseREOSSHWorker +from reolib.REODelimitedFile import REODelimitedFile +from reachlib.SSHWorkerConfig import * +import pdb + + +class CheckAccessWorker(BaseREOSSHWorker): + """ + Concrete class to process access (-a) checks. + """ + + def host_work(self): + return True # Don't stop at 1 host + + def run_simulation(self): + pass + + +class RunCommandWorker(BaseREOSSHWorker): + """ + Concrete class to process individual commands (-c). + """ + + def host_work(self): + retval, error_msg = self.run_command() + if error_msg: + print(" - Error: " + error_msg) + return retval + + def run_simulation(self): + self.simulate_command() + + +class RunBatchCommandsWorker(BaseREOSSHWorker): + """ + Concrete class to process batch commands (-b). + """ + + def __init__(self, commands_file='', logger=None): + """ + Class Constructor + :param commands_file: Commands file to process. + """ + super(self.__class__, self).__init__(logger) + + self.commands_file = commands_file # Future use + """File path containing batch commands""" + + self.commands = REODelimitedFile(self.commands_file, ',') + """File containing batch commands""" + + self.str_vars_exist = True # Assume true no matter what + config[COMMANDS_FILE] = commands_file + self.process_commands(check_only=True) # Check for destructive commands, and wait time + + def run_simulation(self): + self.process_commands(False, True) + + def process_commands(self, check_only=False, simulation=False): + """ + Loop through commands file for simulation + :param simulation: True if running in simulation, False otherwise + :param check_only: True if only checking for destructive commands, False otherwise + :return: None + """ + retval = True + row_num = 0 + for cmd in self.commands: + row_num += 1 + + if row_num == 1: + continue + + """ + Keys for cmd[X] + 0 - Command + 1 - Show Output Flag + 2 - Local Command + 3 - Wait String(s) + 4 - Send String + 5 - Done String + 6 - Report String(s) + 7 - Halt Loop Flag + """ + + # Trim any Excel generated quotes + for i, s in enumerate(cmd): + cmd[i] = self.util.trim_quotes(cmd[i]) + + cmd_str = cmd[0] + if check_only: + # Check for destructive commands + config[COMMAND_STRING] = cmd_str + + # Check for destructive commands in the command string or if running sudo + if True in [(s in config[COMMAND_STRING]) for s in DESTRUCTIVE_COMMANDS]: + self.destr_cmds_exist = True # Once true, always true + + continue + + # 0 - Command: Check/replace if command has vars to replace + cmd_str = self.replace_column_vars(cmd_str) + + config[COMMAND_STRING] = cmd_str + + # 1 - Show Output Flag + if cmd[1].lower() in VALID_YES: + config[SHOW_CONSOLE_OUTPUT] = True + else: + config[SHOW_CONSOLE_OUTPUT] = False + + # 2 - Show Output Flag + if str(cmd[2]).lower() in VALID_YES: + config[LOCAL_COMMAND] = True + else: + config[LOCAL_COMMAND] = False + + # 3 - Wait String(s) + config[COMMAND_WAIT_STRING] = cmd[3] + + # 4 - Send String + config[COMMAND_SEND_STRING] = cmd[4] + + # 5 - Done String + config[COMMAND_SEARCH_STRING] = cmd[5] + + # 6 - Report String(s) + config[COMMAND_REPORT_STRING] = cmd[6] + + # 7 - Halt Loop Flag + if str(cmd[7]).lower() in VALID_YES: + config[HALT_ON_STRING] = True + else: + config[HALT_ON_STRING] = False + + if simulation: + self.simulate_command() + else: + if config[LOCAL_COMMAND]: + print (" Running command locally: " + config[COMMAND_STRING]) + cmd_output = self.util.run_os_command(config[COMMAND_STRING]) + if config[HALT_ON_STRING]: + retval = False + if config[SHOW_CONSOLE_OUTPUT]: + print(" Console Output: \n" + cmd_output) + else: + new_retval, error_msg = self.run_command() + retval &= new_retval + # If not continue_commands (ie a command timed out), end + # this host and move on to the next + # TODO: ask user to retry the batch of commands on this host or not + if error_msg: + print(" - Error: " + error_msg) + break + return retval + + def host_work(self): + return self.process_commands(False, config[RUN_IN_SIMULATION_MODE]) diff --git a/reachlib/SSHWorkerConfig.py b/reachlib/SSHWorkerConfig.py new file mode 100755 index 0000000..6af99b8 --- /dev/null +++ b/reachlib/SSHWorkerConfig.py @@ -0,0 +1,273 @@ +import collections +import logging +import pdb + +# Configuration Keys +ACCESS_CHECK = 'ACCESS_CHECK' +CONFIG_SECTION = 'TOOL DEFAULTS' +COMMANDS_FILE = 'COMMANDS_FILE' +COMMAND_STRING = 'COMMAND_STRING' +LOCAL_COMMAND = 'LOCAL_COMMAND' +RUN_SUDO_FIRST = 'RUN_SUDO_FIRST' +HALT_ON_STRING = 'HALT_ON_STRING' +COMMAND_SEARCH_STRING = 'COMMAND_SEARCH_STRING' +COMMAND_WAIT_STRING = 'COMMAND_WAIT_STRING' +COMMAND_SEND_STRING = 'COMMAND_SEND_STRING' +CONDITION_STRING = 'CONDITION_STRING' +COMMAND_REPORT_STRING = 'COMMAND_REPORT_STRING' +OPERATION = 'OPERATION' +SHOW_AUTHOR = 'SHOW_AUTHOR' +SHOW_USAGE = 'SHOW_USAGE' +CIPHER = 'CIPHER' +CONFIG_FILE = 'CONFIG_FILE' + +# Logging levels +DEBUG = 'DEBUG' +INFO = 'INFO' +WARNING = 'WARNING' +ERROR = 'ERROR' +CRITICAL = 'CRITICAL' + +LOG_LEVELS = { + DEBUG: logging.DEBUG, + INFO: logging.INFO, + WARNING: logging.WARNING, + ERROR: logging.ERROR, + CRITICAL: logging.CRITICAL, +} + +# Delimiters +STRINGS_DELIMITER = '|' + +STRINGS_EQUAL = '=' +STRINGS_CONTAINS = '~' +STRINGS_MULTI_CONDITION = '&' + +# Markers / Variables +COLUMN_VARIABLE = '$HF_' +NOT_FOUND_MARKER = '$NF' +CIPHER_TEXT_MARKER = '$CT=' + +# Operations +OPERATION_ACCESS = ACCESS_CHECK +OPERATION_BATCH = COMMANDS_FILE +OPERATION_COMMAND = COMMAND_STRING + +# User Default Keys +SSH_CONNECTION_TIMEOUT = 'SSH_CONNECTION_TIMEOUT' +SSH_COMMAND_TIMEOUT = 'SSH_COMMAND_TIMEOUT' +SSH_USER_NAME = 'SSH_USER_NAME' +SSH_PASSWORD_CIPHER = 'SSH_PASSWORD_CIPHER' +SSH_PASSWORD = 'SSH_PASSWORD' +SSH_PRIVATE_KEY = 'SSH_PRIVATE_KEY' +HOSTS_INPUT_FILE = 'HOSTS_INPUT_FILE' +KEY_COLUMN = 'KEY_COLUMN' +SHOW_HOST_DURATION = 'SHOW_HOST_DURATION' +HOST_MARKER = 'HOST_MARKER' +DEBUG_FLAG = 'DEBUG_FLAG' +RUN_IN_SIMULATION_MODE = 'RUN_IN_SIMULATION_MODE' +SHOW_CONSOLE_OUTPUT = 'SHOW_CONSOLE_OUTPUT' +LAST_RUN_OUTPUT = 'LAST_RUN_OUTPUT' +NO_DESTRUCTIVE_PROMPT = 'NO_DESTRUCTIVE_PROMPT' +TRUST_HOSTS = 'TRUST_HOSTS' +LOGS_DIRECTORY = 'LOGS_DIRECTORY' +LOG_FILE = 'LOG_FILE' +SSH_LOG_FILE = 'SSH_LOG_FILE' +LOG_LEVEL = 'LOG_LEVEL' +PROMPT_REGEX = 'PROMPT_REGEX' +NEW_PROMPT_REGEX = 'NEW_PROMPT_REGEX' +NEW_PROMPT = 'NEW_PROMPT' + +# Destructive commands +DESTRUCTIVE_COMMANDS = ('rm -rf', 'sudo') + +# Options type +BOOL_OPTS = ('-o', '-s', '-h', '-g', '-x', '-a', '-v', '-?') +STRING_OPTS = ( + '-b', '-c', '-w', '-i', '-k', '-e', '-r', '-p', '-d', '--config', '--username', '--password', '--private_key', + '--cipher_text') +NUM_OPTS = () +COMMAND_OPTS = ('-c', '-w', '-r', '-p', '-d') + +# Options dependencies +EXCLUSIVE_OPTS = (ACCESS_CHECK, COMMANDS_FILE, COMMAND_STRING, SHOW_AUTHOR, SHOW_USAGE, CIPHER) + +EXCLUSIVE_OPTS_KEYS = ('-a', '-b', '-c', '-v', '-?', '--cipher_text') + +BAD_BATCH_OPTS = (SHOW_CONSOLE_OUTPUT, RUN_SUDO_FIRST, HALT_ON_STRING, COMMAND_SEARCH_STRING, + COMMAND_WAIT_STRING, COMMAND_SEND_STRING, COMMAND_REPORT_STRING) + +BAD_ACCESS_OPTS = (RUN_SUDO_FIRST, HALT_ON_STRING, COMMAND_SEARCH_STRING, COMMAND_REPORT_STRING, + COMMAND_WAIT_STRING, COMMAND_SEND_STRING) + +BATCH_COMMANDS_COLUMN_ORDER = [COMMAND_STRING, SHOW_CONSOLE_OUTPUT, LOCAL_COMMAND, COMMAND_WAIT_STRING, + COMMAND_SEND_STRING, + COMMAND_SEARCH_STRING, COMMAND_REPORT_STRING, HALT_ON_STRING] + +VALID_YES = ['yes', 'true', 'y'] +VALID_NO = ['no', 'false', 'n', ''] + +VALID_YES_NO = sum([VALID_YES, VALID_NO], []) + +# Command-line Switches +SWITCH_KEYS = collections.OrderedDict() +SWITCH_KEYS['-a'] = ACCESS_CHECK +SWITCH_KEYS['-b'] = COMMANDS_FILE +SWITCH_KEYS['-c'] = COMMAND_STRING +SWITCH_KEYS['-x'] = RUN_IN_SIMULATION_MODE +SWITCH_KEYS['-o'] = SHOW_CONSOLE_OUTPUT +SWITCH_KEYS['-s'] = RUN_SUDO_FIRST +SWITCH_KEYS['-h'] = HALT_ON_STRING +SWITCH_KEYS['-i'] = HOSTS_INPUT_FILE +SWITCH_KEYS['-k'] = KEY_COLUMN +SWITCH_KEYS['-d'] = COMMAND_SEARCH_STRING +SWITCH_KEYS['-w'] = COMMAND_WAIT_STRING +SWITCH_KEYS['-p'] = COMMAND_SEND_STRING +SWITCH_KEYS['-e'] = CONDITION_STRING +SWITCH_KEYS['-r'] = COMMAND_REPORT_STRING +SWITCH_KEYS['-g'] = DEBUG_FLAG +SWITCH_KEYS['-v'] = SHOW_AUTHOR +SWITCH_KEYS['-?'] = SHOW_USAGE +SWITCH_KEYS['--config'] = CONFIG_FILE +SWITCH_KEYS['--username'] = SSH_USER_NAME +SWITCH_KEYS['--password'] = SSH_PASSWORD_CIPHER +SWITCH_KEYS['--private_key'] = SSH_PRIVATE_KEY +SWITCH_KEYS['--cipher_text'] = CIPHER + +# Reverse of above +SWITCH_VALUE = {} +for key, value in SWITCH_KEYS.iteritems(): + SWITCH_VALUE[value] = key + +user_opts = collections.OrderedDict() + +# String defaults +STRING_DEFAULTS = (LOGS_DIRECTORY, HOSTS_INPUT_FILE, KEY_COLUMN, HOST_MARKER, SSH_USER_NAME, + SSH_PASSWORD_CIPHER, SSH_PRIVATE_KEY, LAST_RUN_OUTPUT, PROMPT_REGEX, LOG_LEVEL) + +BOOLEAN_DEFAULTS = (SHOW_HOST_DURATION, SHOW_CONSOLE_OUTPUT, RUN_IN_SIMULATION_MODE, DEBUG_FLAG, + NO_DESTRUCTIVE_PROMPT, TRUST_HOSTS) + +NUMBER_DEFAULTS = (SSH_CONNECTION_TIMEOUT, SSH_COMMAND_TIMEOUT) + +# System user-defined defaults +defaults = collections.OrderedDict() +defaults[LOGS_DIRECTORY] = 'logs' +defaults[SSH_LOG_FILE] = 'reach_ssh_lib.log' +defaults[LOG_FILE] = 'reach_main.log' +defaults[LAST_RUN_OUTPUT] = 'reach_last_run.log' +defaults[LOG_LEVEL] = INFO +defaults[SSH_CONNECTION_TIMEOUT] = 10 +defaults[SSH_COMMAND_TIMEOUT] = 20 +defaults[SSH_USER_NAME] = '' +defaults[SSH_PASSWORD_CIPHER] = '' +defaults[SSH_PRIVATE_KEY] = '' +defaults[CONFIG_FILE] = 'configs/config.ini' +defaults[HOSTS_INPUT_FILE] = '' +defaults[KEY_COLUMN] = '' +defaults[SHOW_HOST_DURATION] = False +defaults[DEBUG_FLAG] = False +defaults[RUN_IN_SIMULATION_MODE] = False +defaults[SHOW_CONSOLE_OUTPUT] = False +defaults[HOST_MARKER] = '' +defaults[NO_DESTRUCTIVE_PROMPT] = False +defaults[TRUST_HOSTS] = False +defaults[PROMPT_REGEX] = '[$#>]( )?$' +defaults[NEW_PROMPT_REGEX] = '\[REACH\]# $' +defaults[NEW_PROMPT] = '[REACH]# ' + +config = collections.OrderedDict() +# Pre-define configs so they are ordered +config[LOGS_DIRECTORY] = defaults[LOGS_DIRECTORY] +config[SSH_LOG_FILE] = defaults[SSH_LOG_FILE] +config[LOG_FILE] = defaults[LOG_FILE] +config[LOG_LEVEL] = defaults[LOG_LEVEL] +config[LAST_RUN_OUTPUT] = defaults[LAST_RUN_OUTPUT] +config[CONFIG_FILE] = defaults[CONFIG_FILE] +config[OPERATION] = '' +config[COMMANDS_FILE] = '' +config[COMMAND_STRING] = '' +config[RUN_IN_SIMULATION_MODE] = False +config[SHOW_CONSOLE_OUTPUT] = defaults[SHOW_CONSOLE_OUTPUT] +config[LOCAL_COMMAND] = False +config[RUN_SUDO_FIRST] = False +config[HALT_ON_STRING] = False +config[HOSTS_INPUT_FILE] = '' +config[COMMAND_SEARCH_STRING] = '' +config[COMMAND_WAIT_STRING] = '' +config[COMMAND_SEND_STRING] = '' +config[CONDITION_STRING] = '' +config[COMMAND_REPORT_STRING] = '' +config[DEBUG_FLAG] = defaults[DEBUG_FLAG] +config[RUN_IN_SIMULATION_MODE] = defaults[RUN_IN_SIMULATION_MODE] +config[SSH_CONNECTION_TIMEOUT] = defaults[SSH_CONNECTION_TIMEOUT] +config[SSH_COMMAND_TIMEOUT] = defaults[SSH_COMMAND_TIMEOUT] +config[SSH_USER_NAME] = defaults[SSH_USER_NAME] +config[SSH_PASSWORD_CIPHER] = defaults[SSH_PASSWORD_CIPHER] +config[SSH_PASSWORD] = '' +config[SSH_PRIVATE_KEY] = defaults[SSH_PRIVATE_KEY] +config[HOSTS_INPUT_FILE] = defaults[HOSTS_INPUT_FILE] +config[KEY_COLUMN] = defaults[KEY_COLUMN] +config[SHOW_HOST_DURATION] = defaults[SHOW_HOST_DURATION] +config[NO_DESTRUCTIVE_PROMPT] = defaults[NO_DESTRUCTIVE_PROMPT] +config[TRUST_HOSTS] = defaults[TRUST_HOSTS] +config[PROMPT_REGEX] = defaults[PROMPT_REGEX] +config[NEW_PROMPT_REGEX] = defaults[NEW_PROMPT_REGEX] +config[NEW_PROMPT] = defaults[NEW_PROMPT] + +cli_config = collections.OrderedDict() + + +def set_defaults(): + """ + Set all configs equal to defaults + :return: None + """ + for key in defaults: + config[key] = defaults[key] + + +def set_cli_to_config(): + """ + Write user defined options to main config + :return: None + """ + for key in cli_config: + config[key] = cli_config[key] + + +def set_config(switch, switch_val): + """ + Read commands switches straight to the config + :param switch: Switch string (i.e. -c -r etc.) + :param switch_val: Switch value + :return: None + """ + if switch in SWITCH_KEYS: + config[SWITCH_KEYS[switch]] = switch_val + + +def set_cli_config(switch, switch_val): + """ + Save command-line switches to a temporary dict + :param switch: Switch string (i.e. -c -r etc.) + :param switch_val: Switch value + :return: None + """ + if switch in SWITCH_KEYS: + cli_config[SWITCH_KEYS[switch]] = switch_val + + +def check_for_vars(): + """ + Check any string variables in STRING_OPTS for any column variables. + :return: True if variables found, False otherwise. + """ + retval = False + + # Check if any of the strings have a variable + for i in COMMAND_OPTS: + if COLUMN_VARIABLE in config[SWITCH_KEYS[i]]: + retval = True + + return retval diff --git a/reachlib/__init__.py b/reachlib/__init__.py new file mode 100755 index 0000000..2d64840 --- /dev/null +++ b/reachlib/__init__.py @@ -0,0 +1,5 @@ +from reachlib import BaseREOSSHWorker +from SSHWorkerClasses import CheckAccessWorker +from SSHWorkerClasses import RunCommandWorker +from SSHWorkerClasses import RunBatchCommandsWorker +from SSHWorkerConfig import * diff --git a/reolib/REODelimitedFile.py b/reolib/REODelimitedFile.py new file mode 100755 index 0000000..d137e4b --- /dev/null +++ b/reolib/REODelimitedFile.py @@ -0,0 +1,144 @@ +import pdb +import sys +import logging +from REOUtility import REOUtility + + +class REODelimitedFile(object): + """ + This class is an abstraction of a delimited file and implements and iterator. + Iterator automatically ignores white spaces as well as return a dict if headers + are defined, or a list of values otherwise. + """ + + def __init__(self, file_src='', delimiter=",", has_header=False, logger=None): + """ + Class constructor + :param file_src: Source file + :param delimiter: Delimiter character(s) + :param has_header: True if headers exist, False otherwise + """ + self.delimiter = delimiter + """Delimiter character used. Default is a comma.""" + + self.file_name = file_src + """File path of delimited file""" + + self.infile = None + """File handle for delimited file""" + + self.current_row = None + """Current row (list of dict) being processed""" + + self.has_header = has_header + """Header existence indicator flag""" + + self.row_count = 0 + """Number of rows in file""" + + self.header_list = None + """Header row as a list""" + + self.logger = logger + """Optional logger (from logging module) for this class """ + + try: + self.infile = open(self.file_name, 'r') + except KeyboardInterrupt: + REOUtility.key_interrupt() + except: + self.log(logging.ERROR, "Aborting...File: " + self.file_name + " not found.", True) + sys.exit(1) + + if self.has_header: + self.header_list = self.infile.next().split(self.delimiter) + # Strip any leading/trailing spaces from headers + self.header_list = [header.strip() for header in self.header_list] + + # Count hosts once right away + self.row_count = 1 if not self.header_list else 0 + for i in self.infile: + self.row_count += 1 + + def __iter__(self): + """ + Iterator over lines in the file + :return: self instance + """ + self.infile.seek(0) + if self.header_list: + self.infile.next() + return self + + def __len__(self): + """ + Return the number of rows + :return: Number of rows + """ + return self.row_count + + def next(self): + """ + Get the next row in the file. + :return: If headers exist, a dict, a list otherwise. + """ + # Move read head past blank lines and implicitly stops iteration when EOL is reached. + rowdata = self.infile.next() + while not rowdata.strip(): + rowdata = self.infile.next() + + # Strip any leading/trailing spaces from data fields + host_values = [val.strip() for val in rowdata.split(self.delimiter)] + + if self.header_list: + # Headers found return row as a dict + self.current_row = dict(zip(self.header_list, host_values)) + else: + # No headers found, return row as a list + self.current_row = host_values + return self.current_row + + def get_row_val(self, row=None, col_idx=0, name=''): + """ + Get host row value by index or name + :param row: Row being affected + :param col_idx: index + :param name: column name + :return: row value + """ + col_name = '' + + try: + if name == '': + col_name = self.header_list[col_idx] + else: + col_name = name + if not row: + row = self.current_row + except: + self.log(logging.ERROR, "Invalid file key column: \'" + col_name + "\' or column index: " + str( + col_idx) + ". Check input file.", True) + sys.exit(2) + + return row[col_name] + + def log(self, level, message, print_to_screen=False): + """ + Logging mechanism if defined. + :param print_to_screen: True to print message to screen as well + :param level: Log level + :param message: Message + :return: + """ + if print_to_screen: + print message + if self.logger: + self.logger.log(level, message) + + def __del__(self): + """ + Class destructor. Close the file. + :return: + """ + if self.infile: + self.infile.close() diff --git a/reolib/REORemoteHost.py b/reolib/REORemoteHost.py new file mode 100755 index 0000000..063a8c1 --- /dev/null +++ b/reolib/REORemoteHost.py @@ -0,0 +1,489 @@ +import paramiko +import socket +import re +from REOUtility import REOUtility +from paramiko.ssh_exception import * +import logging +import time +import sys +import pdb + + +class REORemoteHost(object): + """ + This class is wrapper for remote hosts currently implemented using paramiko for SSH + which could in the future implemented as any type of host, SSH, telnet etc. + """ + SERVER_STATUS_NO_ACCESS = 'No access to server' + SERVER_STATUS_UNABLE_TO_REACH = 'Unable to reach server' + SERVER_STATUS_CONNECTED = 'Connected' + SERVER_STATUS_ERROR_PROMPT = 'Unable to detect initial prompt' + STRINGS_DELIMITER = '|' + NOT_FOUND_MARKER = '$NF' + DEFAULT_PROMPT_REGEX = '[$#>]( )?$' + DEFAULT_NEW_PROMPT_REGEX = '\[REACH\]# $' + DEFAULT_NEW_PROMPT = '[REACH]# ' + PROMPT_DETECTION_TIMEOUT = 10 + PROMPT_SET_TIMEOUT = 5 + + # Key markers + ENTER_KEY = '$ENTER_KEY' + RETURN_KEY = '$RETURN_KEY' + TAB_KEY = '$TAB_KEY' + SPACE_KEY = '$SPACE_KEY' + CIPHER_TEXT_MARKER = '$CT=' + + KEY_STROKE = {ENTER_KEY: '\n', RETURN_KEY: '\n', TAB_KEY: '\t', SPACE_KEY: ' '} + """Dictionary of key strokes and their corresponding real character(s)""" + + KEY_STROKE_DISPLAY = {ENTER_KEY: '', RETURN_KEY: '', TAB_KEY: '', + SPACE_KEY: ''} + """Dictionary of key strokes and their corresponding screen display text""" + + def __init__(self, ip, prompt_regex=DEFAULT_PROMPT_REGEX, new_prompt_regex=DEFAULT_NEW_PROMPT_REGEX, + new_prompt=DEFAULT_NEW_PROMPT, usr=None, pwd=None, logger=None): + """ + Class constructor + :param ip: IP address of host + :param usr: User name + :param pwd: Password + """ + self.ip = ip + """IP Address of host""" + + self.usr = usr + """User Name""" + + self.pwd = pwd + """Password""" + + self.client = None + """Virtual SSH client connection""" + + self.util = REOUtility() + """Utility instance""" + + self.connect_status_string = '' + """Connection status string""" + + self.key_file = None + """RSA private key file""" + + self.shell = None + """Virtual shell session""" + + self.connected = False + """Connection flag""" + + self.console_output_buffer = '' + """String container of console output buffer being processed""" + + self.search_string_isfound = '' + """Search string found flag""" + + self.cmd_timeout = 0 + """SSH command timeout value""" + + self.str_vars_exist = False + """String variable existence flag""" + + self.ssh_lib_log = '' + """Paramiko logger instance""" + + self.logger = logger + """Optional logger (from logging module) for this class""" + + self.original_prompt_regex = prompt_regex + """Regex of the initial prompt at login""" + + self.new_prompt_regex = new_prompt_regex + """Regex of the personalized prompt to be set""" + + self.new_prompt = new_prompt + """Personalized prompt to be set""" + + self.expected_prompt_regex = prompt_regex + """Regex of the prompt used to detect command completion""" + + def connect_host(self, set_prompt=True, u=None, p=None, f=None, conn_timeout=10, cmd_timeout=5, trust_hosts=False): + """ + Establish connection with host + :param set_prompt: Set a personalized prompt + :param u: User name + :param p: Password + :param f: File containing password + :param conn_timeout: Connection time-out + :param cmd_timeout: Command time-out + :param trust_hosts: True to blindly trust hosts, False to use system known_hosts + :return: True if successfully connected, False otherwise + """ + self.connected = False + if self.util.debug: + print ' ' + self.log(logging.DEBUG, "Connecting to: " + self.ip, False) + if u: self.usr = u + if f: self.key_file = f + if p: self.pwd = p + + self.cmd_timeout = cmd_timeout + self.client = paramiko.SSHClient() + + if trust_hosts: + # Blindly trust hosts + self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + else: + # Load system "known_hosts" file + self.client.load_system_host_keys() + + paramiko.util.log_to_file(self.ssh_lib_log) + + self.log(logging.DEBUG, "CONNECTION_TIMEOUT " + str(conn_timeout), False) + + self.log(logging.DEBUG, "COMMAND_TIMEOUT " + str(cmd_timeout), False) + + self.log(logging.DEBUG, "User Name: " + self.usr, False) + + try: + if self.key_file: + self.log(logging.DEBUG, "RSA key file: " + self.key_file, False) + self.log(logging.DEBUG, "Using user-defined key file", False) + self.client.connect(self.ip, username=self.usr, timeout=conn_timeout, + allow_agent=True, key_filename=self.key_file, + password=self.pwd) + else: + self.log(logging.DEBUG, "Using password", False) + self.client.connect(self.ip, username=self.usr, password=self.pwd, + timeout=conn_timeout, + allow_agent=False) + + self.connected = True + except KeyboardInterrupt: + self.util.key_interrupt() + except (BadAuthenticationType, AuthenticationException, PartialAuthentication, SSHException): + self.connect_status_string = self.SERVER_STATUS_NO_ACCESS + self.log(logging.DEBUG, self.connect_status_string, False) + except: + self.connect_status_string = self.SERVER_STATUS_UNABLE_TO_REACH + self.log(logging.DEBUG, self.connect_status_string, False) + + if self.connected: + self.shell = self.client.invoke_shell() + self.shell.settimeout(cmd_timeout) + self.connect_status_string = self.SERVER_STATUS_CONNECTED + if not self.detect_initial_prompt(): + self.connect_status_string = self.SERVER_STATUS_ERROR_PROMPT + self.log(logging.DEBUG, self.connect_status_string, False) + return False + if set_prompt: + # Variable output not used, but may be in the future + prompt_set, output = self.set_prompt() + if not prompt_set: + # Continue with old prompt for now and not fail + self.connect_status_string = self.SERVER_STATUS_CONNECTED + self.log(logging.DEBUG, self.connect_status_string, False) + + return self.connected + + def set_password_from_file(self, f): + """ + Read and set the password from a file. + :param f: Password file + :return: None + """ + infile = open(f, 'r') + lines = infile.readlines() + + for line in lines: + tpwd = line.strip() + if not tpwd: + continue + else: + self.pwd = tpwd + break # found the password + + infile.close() + + def detect_initial_prompt(self): + """ + Detect initial prompt + :return: True/False for success/failure + """ + self.log(logging.DEBUG, "Detect initial prompt start", False) + self.shell.settimeout(self.PROMPT_DETECTION_TIMEOUT) + match = None + output = '' + while match is None: + try: + output += self.shell.recv(1024) + match = re.search(self.original_prompt_regex, output) + except KeyboardInterrupt: + self.util.key_interrupt() + except socket.timeout: + self.shell.settimeout(self.cmd_timeout) + return False + except: + self.util.print_stack() + self.shell.settimeout(self.cmd_timeout) + self.log(logging.DEBUG, "Detect initial prompt success", False) + return True + + def set_prompt(self): + """ + Try to set the prompt to a personalized one. + :return: True/False for success/failure, output + """ + self.log(logging.DEBUG,"Set prompt start", False) + # Setting the timeout for the shell to a lower value for this function only + self.shell.settimeout(self.PROMPT_SET_TIMEOUT) + self.shell.send("PS1=$PS1'" + self.new_prompt + "'\n") # In case of sh-style + output = '' + matches = [] + while len(matches) != 2: + try: + output += self.shell.recv(1024) + matches = re.findall(self.new_prompt_regex[:-1], output) + except KeyboardInterrupt: + self.util.key_interrupt() + except socket.timeout: + self.shell.settimeout(self.cmd_timeout) + self.expected_prompt_regex = self.original_prompt_regex + return False, output + except: + self.util.print_stack() + self.shell.settimeout(self.cmd_timeout) + self.expected_prompt_regex = self.new_prompt_regex + self.log(logging.DEBUG,"Set prompt success", False) + + return True, output + + def send_cmd_wait_respond(self, command, search_string='', wait_string='', response_string='', last_run_log=None): + """ + This method will send a command to the server and search for a "search" string. + It can optionally, wait for subsequent strings and send a response string. + :param last_run_log: Log file for results of the last run + :param command: Command to send + :param search_string: Search string(s) + :param wait_string: String(s) to expect + :param response_string: String(s) to use as a response for wait_strings + :return: Output of command, error_msg (or '') + """ + # Check for wait conditions + will_wait_respond = False + wait_response_pair = {} + search_strings = None + wait_strings = None + response_strings = None + response_display = None + search_strings_display = None + self.console_output_buffer = '' + self.search_string_isfound = '' + + if search_string: + search_strings = search_string.split(self.STRINGS_DELIMITER) + + # Remove not found marker from search_strings list + if self.NOT_FOUND_MARKER in search_strings: + search_strings.remove(self.NOT_FOUND_MARKER) + + search_strings_display = '\'' + "\' or \'".join(search_strings) + '\'' + + if self.str_vars_exist: + print (" - Waiting for string(s): " + search_strings_display) + + if wait_string: + wait_strings = wait_string.split(self.STRINGS_DELIMITER) + wait_strings_display = '\'' + "\',\'".join(wait_strings) + '\'' + self.log(logging.DEBUG, "Wait String: " + wait_string, False) + if self.str_vars_exist: + print (" - Waiting for string(s) in sequence: " + wait_strings_display) + + if response_string: + response_strings = response_string.split(self.STRINGS_DELIMITER) + # Replace key stroke markers with real chars and create a display-only list + response_display = response_strings[:] # Create a copy of the response list + for i, rs in enumerate(response_strings): + for key in self.KEY_STROKE: + response_strings[i] = response_strings[i].replace(key, self.KEY_STROKE[key]) + response_display[i] = response_display[i].replace(key, self.KEY_STROKE_DISPLAY[key]) + + # Allow sending password in cipher text (Format: $CT=), + # useful for sudo requiring passwords or Cisco ASA 'enable' passwords + cipher_text = re.search(re.escape(self.CIPHER_TEXT_MARKER) + '([^|]*)', rs) + if cipher_text is not None: + response_strings[i] = response_strings[i].replace(cipher_text.group(0), + REOUtility.decrypt_str(cipher_text.group(1))) + response_display[i] = response_display[i].replace(cipher_text.group(0), '**********') + wait_response_pair = zip(wait_strings, response_strings, response_display) + if len(wait_response_pair) > 0: + will_wait_respond = True + + # Send the command to the host + self.shell.send(command + "\n") + + command_completed = False + error_msg = '' + output_last = '' + buffer_index = 0 + if self.__detect_special_commands(command): + self.expected_prompt_regex = self.original_prompt_regex + if self.__detect_sudo(command): + self.expected_prompt_regex = self.original_prompt_regex + + while not command_completed: + # Continuously read output lines until wait strings are found or timeout reached + try: + self.log(logging.DEBUG, "Expected prompt regex: " + self.expected_prompt_regex, False) + self.console_output_buffer += self.shell.recv(1024) + if buffer_index == 0: + temp = self.console_output_buffer.replace(' \r', '').split(command, 1) + if len(temp) < 2: + continue + output_last = temp[1] + else: + output_last = self.console_output_buffer[buffer_index:] + command_completed = (re.search(self.expected_prompt_regex, output_last) is not None) + if command_completed: + break + + except KeyboardInterrupt: + self.util.key_interrupt() + except socket.timeout: + self.util.print_debug("Timeout, console_output_buffer: " + + self.console_output_buffer) + if will_wait_respond: + # If wait_strings not all consumed and the shell timed out, + # it may mean that the wait strings are incorrect + print (" - Wait string(s) not found and command timeout. Timeout value: " + str(self.cmd_timeout)) + last_run_log.write("Not Found\n") + self.log(logging.ERROR, 'Command timeout. No more commands will be sent to this host.', False) + error_msg = 'Command timeout. No more commands will be sent to this host.' + break + + # command not completed, check for wait strings + if will_wait_respond: + # Look for wait_string keywords in the output and send response + for (wait, response, response_display) in wait_response_pair: + self.log(logging.DEBUG, "Looking for wait key: " + wait, False) + if wait in output_last: + # Check if wait is a special key stroke character(s) + key_stroke_isfound = True in [self.KEY_STROKE[ks] in response for ks in self.KEY_STROKE] + + # This is stripping the space when sending + # $SPACE_KEY (or any other special char keystroke) + if key_stroke_isfound: + rstring = response + else: + rstring = response.strip() + + self.log(logging.DEBUG, "Found wait key: " + wait, False) + print (" - Found \'" + wait + "\'") + self.log(logging.DEBUG, "Sending response: " + rstring, False) + print (" - Sent response \'" + response_display + "\'") + + if key_stroke_isfound: + self.shell.send(rstring) + else: + self.shell.send(rstring + "\n") + + wait_response_pair.remove((wait, response, response_display)) + buffer_index = len(self.console_output_buffer) + break # Found a match in output + + # end of while not command_completed + + if search_strings: + # Look for search strings + for search_key in search_strings: + if search_key == self.NOT_FOUND_MARKER: continue # Ignore "not found" marker + self.log(logging.DEBUG, "Looking for search key: " + search_key, False) + self.util.print_debug("Current output: " + self.console_output_buffer) + if search_key in self.console_output_buffer.replace(' \r', '').split(command, 1)[1]: + self.log(logging.DEBUG, "Found search key: " + search_key, False) + self.search_string_isfound = search_key + break + else: # if for loop terminates without breaking (ie, no search_string found) + if self.NOT_FOUND_MARKER not in search_string: + print (" - Search string(s) " + search_strings_display + " not found.") + last_run_log.write("Not Found: " + search_strings_display + "\n") + if self.NOT_FOUND_MARKER in search_string: + self.search_string_isfound = self.NOT_FOUND_MARKER + else: + self.search_string_isfound = '' # Exited on timeout, not a wait keyword + if len(wait_response_pair) > 0: + keys_not_found = ", ".join( + ["'" + pair[0] + "'" for pair in wait_response_pair if pair[0] not in self.console_output_buffer]) + keys_found = ", ".join( + ["'" + pair[0] + "'" for pair in wait_response_pair if pair[0] in self.console_output_buffer]) + if keys_not_found != '': + self.log(logging.DEBUG, "Wait keys not found: " + keys_not_found, False) + print (" - Wait strings not found: " + keys_not_found + "") + if keys_found != '': + self.log(logging.DEBUG, + "Wait keys found but response not sent because prompt immediately returned: " + keys_found, + False) + print ( + " - Wait strings found but response not sent because prompt immediately returned: " + keys_found + "") + + if self.__detect_sudo(command): + prompt_set, output = self.set_prompt() + # self.console_buffer += output + if not prompt_set: + # error_msg = "Unable to set personalized prompt in su mode" + pass + + self.util.print_debug("Output:\n" + self.console_output_buffer.replace(' \r', '')) + + return re.sub(self.new_prompt_regex, '', self.console_output_buffer).replace(' \r', ''), error_msg + + def __detect_special_commands(self, command): + """ + Detect if a special command is used + :param command: Command to test + """ + # TODO: Better detect special commands (use devices.ini) + return re.match('conf$', command) is not None + + def __detect_sudo(self, command): + """ + Detect if command is using sudo + :param command: Command to test + :return: True if command contains sudo, False otherwise + """ + # TODO: Better detect sudo root equivalent commands + return re.match('(sudo (root|su).*)$', command) is not None + + def log(self, level, message, print_to_screen=False): + """ + Logging mechanism if defined. + :param print_to_screen: True to print message to screen as well + :param level: Log level + :param message: Message + :return: + """ + if print_to_screen: + print message + if self.util.debug and level == logging.DEBUG: + self.util.print_debug(message) + if self.logger: + self.logger.log(level, message) + + def close(self): + """ + Close the connection to host if open. + :return: None + """ + if self.connected: + self.connected = False + self.client.close() + + def __del__(self): + """ + Class destructor. Close the connection. + :return: + """ + closed = False + if self.connected: + self.client.close() + closed = True + if closed: + self.log(logging.DEBUG, 'Connection to ' + self.ip + ' closed', False) diff --git a/reolib/REOScript.py b/reolib/REOScript.py new file mode 100755 index 0000000..d2ffee8 --- /dev/null +++ b/reolib/REOScript.py @@ -0,0 +1,46 @@ +import os + + +class REOScript(object): + """ + This class is a base class for all scripts using reolib. + """ + + SCRIPT_VERSION = '0.0' + SCRIPT_DATE = '' + SCRIPT_DESCRIPTION = '' + SCRIPT_AUTHOR = 'Randy Oyarzabal, Francis Lan - http://reach.rbpsiu.com' + SCRIPT_SYNTAX_OR_INFO = '' + SCRIPT_USAGE = '' + SCRIPT_NAME = '' + SCRIPT_HELP = '' + + def __init__(self): + """ + Class constructor. + """ + pass + + def author(self, show_desc=False, show_help=False): + """ + Print author information. + :return: None + """ + print ( + self.SCRIPT_NAME + " " + self.SCRIPT_VERSION + " (" + self.SCRIPT_DATE + ")\n" + + self.SCRIPT_AUTHOR + "\n" + ) + if show_desc: + print (self.SCRIPT_DESCRIPTION) + + if show_help: + print (self.SCRIPT_HELP) + + def usage(self): + """ + Print usage information. + :return: None + """ + self.author(show_desc=True) + print (self.SCRIPT_SYNTAX_OR_INFO) + print (self.SCRIPT_USAGE) diff --git a/reolib/REOUtility.py b/reolib/REOUtility.py new file mode 100755 index 0000000..7cc483f --- /dev/null +++ b/reolib/REOUtility.py @@ -0,0 +1,333 @@ +import socket +import subprocess +import datetime +import re +import traceback +from Crypto.Cipher import XOR +import base64 +import json +import getpass +import logging +from logging.handlers import RotatingFileHandler + + +class REOUtility: + """ + This class is a collection of utility methods static and otherwise. + """ + CIPHER_KEY = '#$a%7_(1fsa!@WsfjYU<>' + str(s) + '<==') + else: + print ("[ DEBUG ] " + s) + + @classmethod + def is_valid_ipv4_address(cls, address): + """ + Check if address is a valid IPV4 IP address format + :param address: IP address + :return: True if valid, False otherwise + """ + try: + socket.inet_pton(socket.AF_INET, address) + except KeyboardInterrupt: + REOUtility.key_interrupt() + except AttributeError: # no inet_pton here, sorry + try: + socket.inet_aton(address) + except KeyboardInterrupt: + REOUtility.key_interrupt() + except socket.error: + return False + return address.count('.') == 3 + except socket.error: # not a valid address + return False + + return True + + @classmethod + def is_valid_ipv6_address(cls, address): + """ + Check if address is a valid IPV6 IP address format + :param address: IP address + :return: True if valid, False otherwise + """ + try: + socket.inet_pton(socket.AF_INET6, address) + except KeyboardInterrupt: + REOUtility.key_interrupt() + except socket.error: # not a valid address + return False + return True + + @classmethod + def print_stack(cls): + """ + Print the stack trace. + :return: None + """ + traceback.print_stack() + + @classmethod + def key_interrupt(cls): + """ + Display keyboard interrupt message + :return: None + """ + print ("\n\nAw shucks, Ctrl-C detected. Exiting...\n") + exit() + + def run_os_command(self, c): + """ + Execute a system command + :param c: Command to execute + :return: Output of command + """ + self.print_debug(c, 'Command') + temp = subprocess.Popen([c], stdout=subprocess.PIPE, shell=True, stderr=subprocess.STDOUT, ) + (output, errput) = temp.communicate() + temp.wait() + self.print_debug(output, 'Output') + self.last_command = c + self.last_command_output = output + return output.strip() + + @classmethod + def is_number(cls, s): + """ + Check if string is a valid number. + :param s: String to check + :return: True if is a number, False otherwise + """ + return re.match(r"[-+]?\d+", s) is not None + + @classmethod + def query_yes_no(cls, question, default=None): + """Ask a yes/no question via raw_input() and return their answer. + + "question" is a string that is presented to the user. + "default" is the presumed answer if the user just hits . + It must be "yes" (the default), "no" or None (meaning + an answer is required of the user). + + The "answer" return value is True for "yes" or False for "no". + """ + """ Code copied from: + http://code.activestate.com/recipes/577058 + with minor modifications + """ + + valid = {"yes": True, "y": True, "no": False, "n": False} + if default is None: + prompt = " [y/n] " + elif default == "yes": + prompt = " [Y/n] " + elif default == "no": + prompt = " [y/N] " + else: + raise ValueError("Invalid default answer: '%s'" % default) + + while True: + print question + prompt, + choice = raw_input().lower() + if default is not None and choice == '': + print "" # Display blank line + return valid[default] + elif choice in valid: + print "" # Display blank line + return valid[choice] + else: + print "Please respond with 'yes' or 'no' (or 'y' or 'n')" + + @classmethod + def encrypt_str(cls, i_str): + """ + Change text to cipher text. Not meant to be secure but at least prevent opportunity + theft of sensitive text such as passwords. + :param i_str: String to encrypt + :return: Encrypted string + """ + cipher = XOR.new(REOUtility.CIPHER_KEY) + return base64.b64encode(cipher.encrypt(i_str)) + + @classmethod + def decrypt_str(cls, e_str): + """ + Decrypt string. Only works if strng was encrypted by this class + :param e_str: Encrypted string + :return: Decrypted string + """ + cipher = XOR.new(REOUtility.CIPHER_KEY) + return cipher.decrypt(base64.b64decode(e_str)) + + @classmethod + def escape_string(cls, e_str): + """ + Escape special characters in string + :param e_str: String to process + :return: Escaped string + """ + return json.dumps(e_str) + + @classmethod + def trim_quotes(cls, q_str): + """ + This is primarily used for strings quoted in Excel. If double quotes encountered, replace + with one, and trim outer quotes. + :param q_str: String to process + :return: Processed string + """ + retval = q_str + double_quotes = "\"\"" + if double_quotes in q_str: + retval = q_str.replace(double_quotes, '"') + if retval.startswith('"'): + retval = retval[1:] # Remove first character + if retval.endswith('"'): + retval = retval[:-1] # Remove last character + return retval + + @classmethod + def prompt_user_password(cls, user_prompt=True, password_prompt=True, desc=''): + """ + Prompt for username and password. + :return: None + """ + username = '' + password = '' + if user_prompt: + while not username: + print desc + " User Name: ", + username = raw_input() + + if password_prompt: + pwdprompt = lambda: (getpass.getpass(prompt=desc + ' Password: '), getpass.getpass('Retype password: ')) + + p1, p2 = pwdprompt() + while p1 != p2: + print('Passwords do not match. Try again') + p1, p2 = pwdprompt() + password = p1 + + return username, password + + @classmethod + def get_logger(cls, log_file, level, module_info=False): + """ + Get/create a logger instance. + :param module_info: True to set the module info in the file, False otherwise + :param log_file: Log file. + :param level: Logger level (verbosity) + :return: Logger instance + """ + if module_info: + log_formatter = logging.Formatter("%(asctime)s,%(levelname)s,%(funcName)s(%(lineno)d),%(message)s", + "%Y-%m-%d %H:%M:%S") + else: + log_formatter = logging.Formatter("%(asctime)s,%(levelname)s,%(message)s", + "%Y-%m-%d %H:%M:%S") + + my_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, + backupCount=10, encoding=None, delay=0) + my_handler.setFormatter(log_formatter) + my_handler.setLevel(level) + app_log = logging.getLogger("logger") + app_log.setLevel(level) + app_log.addHandler(my_handler) + return app_log + + @classmethod + def get_parser_config(cls, section, parser): + """ + Get a section from a config main_config + :param section: Section header + :param parser: Parser to get from + :return: Dict contained in configuration section + """ + dict1 = {} + options = parser.options(section) + for option in options: + option = option.upper() + try: + dict1[option] = parser.get(section, option) + if dict1[option] == -1: + print "skip: %s" % option + except: + print("exception on %s!" % option) + dict1[option] = None + return dict1 diff --git a/reolib/__init__.py b/reolib/__init__.py new file mode 100755 index 0000000..1ba1855 --- /dev/null +++ b/reolib/__init__.py @@ -0,0 +1,4 @@ +from REODelimitedFile import REODelimitedFile +from REORemoteHost import REORemoteHost +from REOScript import REOScript +from REOUtility import REOUtility