From afe1d590ea5471cf0d666bf0df8211ab6fb6d56a Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 08:34:27 -0700 Subject: [PATCH 01/35] Update README.rst --- README.rst | 59 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index cafb847..c5427b1 100644 --- a/README.rst +++ b/README.rst @@ -5,14 +5,11 @@ List, start, stop and ssh to AWS instances using Name or Instance-ID --------------------------------------------------------------------------------- -|TRAVIS| |Code Climate| |GitHub issues| |PyPi release| |lang| |license| - +|TRAVIS| |AppVeyor| |Codacy Grade| |Codacy Cov| |PyPi release| |lang| |license| -------------- -AWS Shortcuts (awss) allows listing, starting, stopping and connecting to instances by Name or ID. Future versions will also allow referencing instances with any ``Tag`` : ``Value`` combination. - -Note: This utility requires Python 2.7 or newer. There is a similar utility written in Bash called `aws-quick-cli `_. +AWS Shortcuts (awss) allows listing, starting, stopping and connecting to instances by name or instance-id. Future versions will also allow referencing instances with any ``Tag`` : ``Value`` combination. Overview @@ -43,12 +40,12 @@ Details - List Instances: ``awss list`` - - list all instances (default) + - list all instances (default). - list running instances ``-r`` or ``--running`` - list stopped instances ``-s`` or ``--stopped`` - list instances with specified name ``awss list NAME`` - list instance with specified instance-id ``awss list -i ID`` - - state, NAME, and instance-id may be combined in queries + - instance-state and NAME may be combined in queries. - ex: list instances with NAME currently running: ``awss list NAME -r`` @@ -67,25 +64,25 @@ Details Target Instance Verification ---------------------------- -The ``start``, ``stop``, and ``ssh`` commands verify that their action will apply to only one instance +The ``start``, ``stop``, and ``ssh`` commands verify that their action will apply to only one instance. - This check is performed by looking for other instances that match: - - the instance-specification given (name or ID) - - the running-state appropriate for the command + - the instance-specification given (name or ID). + - the running-state appropriate for the command. - If multiple instances match these conditions, they are listed and the user selects the intended target. The **running-state** appropriate for each command is as follows: -- The ``ssh`` command looks for **running** instances (it cannot connect to stopped instanced) -- The ``stop`` command looks for **running** instances (it cannot stop instances that are already stopped) -- The ``start`` command looks for **stopped** instances (it cannot start instances that are already started) -- The ``list`` command looks at all instances, unless optional parameters have been specified to narrow its search to **running**, **stopped** or specific instances. +- The ``ssh`` command looks for **running** instances (it cannot connect to stopped instanced). +- The ``stop`` command looks for **running** instances (it cannot stop instances that are already stopped). +- The ``start`` command looks for **stopped** instances (it cannot start instances that are already started). +- The ``list`` command looks at all instances, unless optional parameters have been specified to narrow its search to **running**, **stopped** instances. -Supported Versions & Platforms ------------------------------- +Platforms & Python Versions Tested +---------------------------------- Python 2.7, 3.3, 3.4, 3.5, 3.6 @@ -105,17 +102,27 @@ This utility can be installed with ``pip``: pip install awss -.. |Code Climate| image:: https://codeclimate.com/github/robertpeteuil/aws-shortcuts/badges/gpa.svg?style=flat-square - :target: https://codeclimate.com/github/robertpeteuil/aws-shortcuts -.. |GitHub issues| image:: https://img.shields.io/github/issues/robertpeteuil/aws-shortcuts.svg - :target: https://github.com/robertpeteuil/aws-shortcuts -.. |GitHub release| image:: https://img.shields.io/github/release/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf - :target: https://github.com/robertpeteuil/aws-shortcuts -.. |lang| image:: https://img.shields.io/badge/language-python-3572A5.svg?style=flat-square - :target: https://github.com/robertpeteuil/aws-shortcuts -.. |license| image:: https://img.shields.io/github/license/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf - :target: https://github.com/robertpeteuil/aws-shortcuts .. |PyPi release| image:: https://img.shields.io/pypi/v/awss.svg :target: https://pypi.python.org/pypi/awss + .. |Travis| image:: https://travis-ci.org/robertpeteuil/aws-shortcuts.svg?branch=master :target: https://travis-ci.org/robertpeteuil/aws-shortcuts + +.. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/1meclb632h49sik7/branch/master?svg=true + :target: https://ci.appveyor.com/project/robertpeteuil/aws-shortcuts/branch/master + +.. |Codacy Grade| image:: https://api.codacy.com/project/badge/Grade/477279a80d31407a99fb3c3551e066cb + :target: https://www.codacy.com/app/robertpeteuil/aws-shortcuts?utm_source=github.com&utm_medium=referral&utm_content=robertpeteuil/aws-shortcuts&utm_campaign=Badge_Grade +.. |Codacy Cov| image:: https://api.codacy.com/project/badge/Coverage/477279a80d31407a99fb3c3551e066cb + :target: https://www.codacy.com/app/robertpeteuil/aws-shortcuts?utm_source=github.com&utm_medium=referral&utm_content=robertpeteuil/aws-shortcuts&utm_campaign=Badge_Coverage +.. |license| image:: https://img.shields.io/github/license/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf + :target: https://github.com/robertpeteuil/aws-shortcuts +.. |lang| image:: https://img.shields.io/badge/language-python-3572A5.svg?style=flat-square + :target: https://github.com/robertpeteuil/aws-shortcuts + +.. |GitHub issues| image:: https://img.shields.io/github/issues/robertpeteuil/aws-shortcuts.svg + :target: https://github.com/robertpeteuil/aws-shortcuts +.. |GitHub release| image:: https://img.shields.io/github/release/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf + :target: https://github.com/robertpeteuil/aws-shortcuts +.. |Code Climate| image:: https://codeclimate.com/github/robertpeteuil/aws-shortcuts/badges/gpa.svg?style=flat-square + :target: https://codeclimate.com/github/robertpeteuil/aws-shortcuts From 90ea29a5dee88b17f56294f8e9fa0d9376af1521 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 08:51:45 -0700 Subject: [PATCH 02/35] Remove code climate and requirements.txt and references --- .codeclimate.yml | 36 ------------------------------------ .travis.yml | 1 - requirements.txt | 9 --------- tox.ini | 2 +- 4 files changed, 1 insertion(+), 47 deletions(-) delete mode 100644 .codeclimate.yml delete mode 100644 requirements.txt diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index e817433..0000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,36 +0,0 @@ ---- -engines: - fixme: - enabled: true - pep8: - enabled: true - checks: - E501: - enabled: false - E401: - enabled: false - F401: - enabled: false - duplication: - enabled: true - checks: - Similar Code: - enabled: true - config: - languages: - python: - radon: - enabled: true - config: - threshold: "A" -ratings: - paths: - - "awss/*" - - "setup.py" -exclude_paths: - - "test/*" - - ".tox/*" - - "build/*" - - "dist/*" - - "*.egg-info/*" - - "zref/*" diff --git a/.travis.yml b/.travis.yml index 605e356..8b4c1a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,4 +51,3 @@ after_success: - coverage combine - coverage xml - python-codacy-coverage -r coverage.xml - - codeclimate-test-reporter diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5009a30..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -readme_renderer -doctools -flake8 -radon -coverage -mock -pytest -mando -codeclimate-test-reporter diff --git a/tox.ini b/tox.ini index 38e51ec..a2f6ffb 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ deps = commands = python setup.py check -m -r -s doc8 README.rst - check-manifest --ignore tox.ini,.codeclimate.yml,requirements.txt,.coveragerc,.pylintrc,.appveyor.yml + check-manifest --ignore tox.ini,.coveragerc,.pylintrc,.appveyor.yml [testenv:pylint] basepython = python3.5 From 99a494fb9b07ec7be876790b6d7c822e59a49490 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 10:36:23 -0700 Subject: [PATCH 03/35] Avoid known linter exceptions in specific code --- .pylintrc | 9 +++++---- awss/awsc.py | 6 +++--- awss/debg.py | 4 ++-- awss/getchar.py | 49 +++++++++++++++++++++++++++++-------------------- setup.cfg | 3 --- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/.pylintrc b/.pylintrc index 3b0465d..77b28f8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -43,7 +43,8 @@ optimize-ast=no # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence=INFERENCE_FAILURE +# confidence=INFERENCE_FAILURE +confidence= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -59,7 +60,7 @@ confidence=INFERENCE_FAILURE # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable= +disable=redefined-builtin #intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin @@ -189,7 +190,7 @@ max-nested-blocks=5 [FORMAT] # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=79 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ @@ -202,7 +203,7 @@ single-line-if-stmt=no # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator +# no-space-check=trailing-comma,dict-separator # Maximum number of lines in a module max-module-lines=1000 diff --git a/awss/awsc.py b/awss/awsc.py index f2cea1a..1f2be1b 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -23,8 +23,8 @@ def init(): any other function in this module, or they won't function. """ - global EC2C - global EC2R + global EC2C # pylint: disable=global-statement + global EC2R # pylint: disable=global-statement EC2C = boto3.client('ec2') EC2R = boto3.resource('ec2') @@ -46,7 +46,7 @@ def getids(qry_string=None): if qry_string is None: qry_string = 'EC2C.describe_instances()' - summary_data = eval(qry_string) + summary_data = eval(qry_string) # pylint: disable=eval-used i_info = {} for i, j in enumerate(summary_data['Reservations']): i_info[i] = {'id': j['Instances'][0]['InstanceId']} diff --git a/awss/debg.py b/awss/debg.py index 56cde54..d9d7d98 100644 --- a/awss/debg.py +++ b/awss/debg.py @@ -25,8 +25,8 @@ def init(deb1=False, deb2=False): will not generate output. """ - global DEBUG - global DEBUGALL + global DEBUG # pylint: disable=global-statement + global DEBUGALL # pylint: disable=global-statement DEBUG = deb1 DEBUGALL = deb2 diff --git a/awss/getchar.py b/awss/getchar.py index 4caeb38..fe7e7f1 100644 --- a/awss/getchar.py +++ b/awss/getchar.py @@ -1,6 +1,13 @@ -'''Module holding cross-platform class for reading keys from the keyboard, -without requiring the enter key. -''' +"""Getchar Module holding cross-platform class for reading single keypresses + +When the object is instantiated, it configures itself for the operating +system in use (windows or linux/mac). + +Called directly - the object returns the key pressed. +Called via the int method - the object coverted the key pressed into +int and returns it. If the key pressed cannot be converted to int, +then the string "999" is returned. +""" from builtins import object @@ -12,10 +19,11 @@ def __init__(self): except ImportError: self.impl = _GetchUnix() - def __call__(self): # pragma: no cover + def __call__(self): # pragma: no cover return self.impl() - def int(self): # pragma: no cover + def int(self): # pragma: no cover + """ Coverts a read keystroke to int or returns "999" """ try: value = int(self.impl()) except ValueError: @@ -25,27 +33,28 @@ def int(self): # pragma: no cover class _GetchUnix(object): def __init__(self): - import tty # noqa: F401 - import sys # noqa: F401 - - def __call__(self): # pragma: no cover - import sys # noqa: F401 - import tty # noqa: F401 - import termios # noqa: F401 - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) + import tty # noqa: F401 pylint: disable=unused-variable + import sys # noqa: F401 pylint: disable=unused-variable + + def __call__(self): # pragma: no cover + import sys # noqa: F401 + import tty # noqa: F401 + import termios # noqa: F401 + file_desc = sys.stdin.fileno() + old_settings = termios.tcgetattr(file_desc) try: tty.setraw(sys.stdin.fileno()) - ch = sys.stdin.read(1) + chr_read = sys.stdin.read(1) finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return ch + termios.tcsetattr(file_desc, termios.TCSADRAIN, old_settings) + return chr_read class _GetchWindows(object): def __init__(self): - import msvcrt # noqa: F401 + # pylint: disable=unused-variable,import-error + import msvcrt # noqa: F401 - def __call__(self): # pragma: no cover - import msvcrt + def __call__(self): # pragma: no cover + import msvcrt # pylint: disable=import-error return msvcrt.getch() diff --git a/setup.cfg b/setup.cfg index 3d7fa7f..7c964b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,2 @@ [wheel] universal=1 - -[pycodestyle] -ignore = F401 From 8bb464f3dbed5f37639c489a3583a543889d7dbf Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 12:46:39 -0700 Subject: [PATCH 04/35] Updating pylint config file [ci skip] --- .pylintrc | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.pylintrc b/.pylintrc index 77b28f8..7eafc73 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,11 +60,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=redefined-builtin - -#intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin - -#map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,redefined-builtin,redefined-variable-type,R0204 [REPORTS] @@ -290,7 +286,7 @@ generated-members= [VARIABLES] # Tells whether we should check for unused import in __init__ files. -init-import=no +init-import=yes # A regular expression matching the name of dummy variables (i.e. expectedly # not used). @@ -349,7 +345,7 @@ max-parents=7 max-attributes=10 # Minimum number of public methods for a class (see R0903). -min-public-methods=1 +min-public-methods=0 # Maximum number of public methods for a class (see R0904). max-public-methods=20 From 2f9fe39002f837fffc77a2a0ebc984a0d6fa3fe7 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 12:48:59 -0700 Subject: [PATCH 05/35] Updating main-module, getchar and test_picklist, to remove unnecessary str to int conversions. [ci skip] --- awss/__init__.py | 15 +++++++-------- awss/getchar.py | 6 +++--- test/test_picklist.py | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index ec70d64..98e3960 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -26,7 +26,7 @@ import awss.awsc as awsc import awss.debg as debg -__version__ = '0.9.6' +__version__ = '0.9.6.1' def main(): @@ -410,8 +410,8 @@ def user_picklist(title_out, i_info, command): len(i_info), C_NORM, C_TI, C_NORM)) entry_raw = getch.int() - keyconvert = {"999": "invalid entry"} - entry_display = keyconvert.get(str(entry_raw), entry_raw) + keyconvert = {999: "invalid entry"} + entry_display = keyconvert.get(entry_raw, entry_raw) sys.stdout.write(str(entry_display)) (tar_idx, entry_valid) = user_entry(entry_raw, command, len(i_info)) print() @@ -432,18 +432,17 @@ def user_entry(entry_raw, command, maxqty): """ entry_valid = "False" - entry_int = int(entry_raw) - if entry_int == 0: + if entry_raw == 0: print("\n\n%saborting%s - %s instance\n" % (C_ERR, C_NORM, command)) sys.exit() - elif entry_int >= 1 and entry_int <= maxqty: - entry_idx = entry_int - 1 + elif entry_raw >= 1 and entry_raw <= maxqty: + entry_idx = entry_raw - 1 entry_valid = "True" else: sys.stdout.write("\n%sInvalid entry:%s enter a number between 1" " and %s.\n" % (C_ERR, C_NORM, maxqty)) - entry_idx = entry_int + entry_idx = entry_raw return (entry_idx, entry_valid) diff --git a/awss/getchar.py b/awss/getchar.py index fe7e7f1..5ae221a 100644 --- a/awss/getchar.py +++ b/awss/getchar.py @@ -5,8 +5,8 @@ Called directly - the object returns the key pressed. Called via the int method - the object coverted the key pressed into -int and returns it. If the key pressed cannot be converted to int, -then the string "999" is returned. +int and returns it. If the key pressed cannot be converted to int +then 999 is returned. """ from builtins import object @@ -27,7 +27,7 @@ def int(self): # pragma: no cover try: value = int(self.impl()) except ValueError: - value = "999" + value = 999 return value diff --git a/test/test_picklist.py b/test/test_picklist.py index f881a8d..6e4bef3 100644 --- a/test/test_picklist.py +++ b/test/test_picklist.py @@ -69,7 +69,7 @@ def RetKey(item1): try: value = int(keye) except ValueError: - value = "999" + value = 999 return value with mock.patch('awss.awsc.getdetails', getlocaldetails, create=True): From 26ff855fbd5e6f9ffb2e47c0c89dd8f8f12cd816 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 12:49:53 -0700 Subject: [PATCH 06/35] Minor Code Refinement in getdetails function --- awss/awsc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awss/awsc.py b/awss/awsc.py index 1f2be1b..4292d28 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -70,7 +70,7 @@ def getdetails(i_info=None): if i_info is None: i_info = getids() - for i in range(len(i_info)): + for i in i_info: instance_data = EC2R.Instance(i_info[i]['id']) i_info[i]['state'] = instance_data.state['Name'] i_info[i]['ami'] = instance_data.image_id From 9e07a73b111dbf5a46c737e33808c25021bae016 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 13:49:02 -0700 Subject: [PATCH 07/35] Updating PyLint config --- .pylintrc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 7eafc73..64b65c5 100644 --- a/.pylintrc +++ b/.pylintrc @@ -43,8 +43,7 @@ optimize-ast=no # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -# confidence=INFERENCE_FAILURE -confidence= +confidence=INFERENCE_FAILURE # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -60,7 +59,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,redefined-builtin,redefined-variable-type,R0204 +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,redefined-builtin,redefined-variable-type,R0204,W0603,W0123,W0612,E0401 [REPORTS] From 5f2a4a0d8822d4acbad8296a30462eab6e34ce65 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 15:58:06 -0700 Subject: [PATCH 08/35] Adjusting tox.ini to workaround bug in current flake8 versions flake8 3.3.0 was ignoring items that failed the test with --radon-max-cc=5 reverted to 3.0.4, stopped checking the CC on test scripts --- tox.ini | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/tox.ini b/tox.ini index a2f6ffb..0b350ec 100644 --- a/tox.ini +++ b/tox.ini @@ -18,10 +18,10 @@ basepython = python3.5 skip_install = true deps = radon - flake8 + flake8<=3.0.4 commands = flake8 awss/ test/ setup.py - flake8 . --radon-max-cc=5 + flake8 awss --radon-max-cc=5 # DOC TESTS [testenv:readme] @@ -87,19 +87,7 @@ commands = [flake8] ignore = D203 select = E,W,F -max-complexity = 10 -exclude = - .tox, - .git, - *.pyc, - .cache, - .eggs, - *.egg, - build, - dist, - test.py, - zref, - __pycache__ +exclude = .tox,.git,*.pyc,.cache,.eggs,*.egg,build,dist,test.py,zref,__pycache__ [pytest] python_files = test_*.py From 3184460b0192e17bfef01c33efba7b35c235628b Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 20:12:43 -0700 Subject: [PATCH 09/35] DocString Mods [ci skip] --- awss/__init__.py | 178 +++++++++++++++++++++++++++++++---------------- awss/awsc.py | 39 +++++------ awss/debg.py | 14 ++-- 3 files changed, 140 insertions(+), 91 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index 98e3960..581f082 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -20,21 +20,28 @@ from builtins import range import argparse import sys -from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT +from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT from awss.getchar import _Getch import awss.awsc as awsc import awss.debg as debg -__version__ = '0.9.6.1' +__version__ = '0.9.6.2' def main(): - """Prepare environment, collect args and call command funct. + """Orchestrate collecting user args and call function for + command specified. This functions sets up the environment, collects relevant agrs and cals the function specific to the command the user has specified. + + Args: + None + + Returns: + None """ parser = parser_setup() options = parser.parse_args() @@ -51,11 +58,17 @@ def main(): def parser_setup(): - """ - Sets up the command line parser and four subparsers, one for each command: - list, start, stop and ssh. - """ + """Create ArgumentParser object to parse command line arguments. + Args: + None + + Returns: + parser (object): containing ArgumentParser data and methods. + + Raises: + SystemExit: if the user enters invalid args. + """ parser = argparse.ArgumentParser(description="Control AWS instances from" " the command line with: list, start," " stop or ssh.", prog='awss', @@ -145,12 +158,11 @@ def parser_setup(): def cmd_list(options): - """ - 'list' executer: input: object - created by the parser + """Gather data for instances matching args and call display func. - Finds instances that match the user specified args and displays them. + Args: + options (object): contains args and data from parser """ - (qry_string, title_out) = qry_create(options) i_info = awsc.getids(qry_string) items = len(i_info) @@ -163,15 +175,16 @@ def cmd_list(options): def cmd_startstop(options): - """ - 'start' and 'stop' executer: input: object - created by the parser + """Start or Stop the specified instance. Finds instances that match the user specified args plus the command specific args. The target instance is determined and the specified action is applied to the instance. The action return information is retreived and displayed. - """ + Args: + options (object): contains args and data from parser + """ statelu = {"start": "stopped", "stop": "running"} options.inState = statelu[options.command] debg.dprint("toggle set state: ", options.inState) @@ -192,15 +205,16 @@ def cmd_startstop(options): def cmd_ssh(options): - """ - 'ssh' executer: input: object - created by the parser + """Connect to the specified instance via ssh. Finds instances that match the user specified args that are also in the 'running' state. The target instance is determined, the required connection information is retreived (IP, key used, ssh user-name), and an 'ssh' connection is made to the instance. - """ + Args: + options (object): contains args and data from parser + """ import os import subprocess options.inState = "running" @@ -236,14 +250,18 @@ def cmd_ssh(options): def qry_check(qry_string): - """ - Query String Validator: input: query string - in aws ec2 format + """Validate that the query string is functional. Check if the generated query string is empty, and if so exits. This is executed by the 'start', 'stop', and 'ssh' command as they must target a specific instance. - """ + Args: + qry_string (str): the query to be used against the aws ec2 client. + + Raises: + SystemExit: if the query string is empty. + """ if qry_string == "ec2C.describe_instances()": print("%sError%s - instance identifier not specified" % (C_ERR, C_NORM)) @@ -253,9 +271,7 @@ def qry_check(qry_string): def qry_create(options): - """ - Query Creator: input: object - created by the parser - returns: Query_String, Report_Title + """Create query from the args specified and command chosen. Creates aws ec2 formatted query string that incorporates the args in the options object. Generation of this query on the fly allows for queries @@ -264,8 +280,14 @@ def qry_create(options): This function also generates the report output title for the 'list' function as the creation of it uses the exact same algoruthm as creating the query. - """ + Args: + options (object): contains args and data from parser + + Returns: + qry_string (str): the query to be used against the aws ec2 client. + title_out (str): the title to display before the list. + """ qry_string = "EC2C.describe_instances(" filt_st = "Filters=[" filt_end = "" @@ -303,16 +325,27 @@ def qry_create(options): def qry_helper(flag_filt, qry_string, title_out, flag_id=False, filt_st=""): - """ - Query helper: input: filter_set_flag, query_string, report_title, - id_set_flag (optional), string_flag (option) - returns: query_string, report_title + """Dynamically add syntaxtical elements to query. This functions adds syntactical elements to the query string, and report title, based on the types and number of items added thus far. It is broken-out into a seperate function to eliminate duplication. - """ + Args: + flag_filt (bool): indicates that at least one filter item specified. + qry_string (str): the portion of the query that has been constructed + up to this point. + title_out (str): the title to display before the list. + flag_id (bool): optional param that specifies if the instance-id + has been specified in the args. + filt_st (str): optional param to allow adding syntactical elements + if a filter has been specified. + + Returns: + qry_string (str): the portion of the query that was passed in with + the appropriate syntactical elements added. + title_out (str): the title to display before the list. + """ if flag_id or flag_filt: qry_string += ", " title_out += ", " @@ -322,10 +355,7 @@ def qry_helper(flag_filt, qry_string, title_out, flag_id=False, filt_st=""): def list_instances(title_out, i_info, numbered="no"): - """ - Displays Instance Information: - input: report_title, dict of inst_info, and - special_case_flag(optional) + """Display a list of all instances and their details. This function iterates through all the instances contained in the i_info dict, displayed the information contained, and also obtained @@ -336,8 +366,15 @@ def list_instances(title_out, i_info, numbered="no"): If the special_case flag is set, it means this function is being called to display a list for a user to select from. In this case, a colored number is displayed before each instances data. - """ + Args: + title_out (str): the title to display before the list + i_info (dict): information on instances and details + numbered (bool): indicates wheter the list should be displayed + with numbers before each instance. This is used + when this function is called from the + user_picklist function. + """ if numbered == "no": print("\n%s\n" % (title_out)) for i in range(len(i_info)): @@ -356,19 +393,25 @@ def list_instances(title_out, i_info, numbered="no"): def det_instance(command, i_info, title_out): - """ - Determine Target Instance ID: - input: command, dict of instance info, report_title - returns: instance-id-of-target-instance, dict-index-of-target - - This functions inspects the dict of instance-ids: - if it is empty, it displays a message that no instances were found that - matched the query conditions specified, then exits. - If it contains one item, then the instance-id of that item is returned. - If it contains more than one item, then the picklist function is called. - note: command, and report_title are only used for user display purposes. - """ + """Determine the instance-id of the target instance. + + Inspect the number of instance-ids collected: + If none, displays a message and exits. + If one item, the instance-id is returned as the target. + Ifmore than one, the user_picklist function is called. + + Args: + command (str): command specified on the command line. + i_info (dict): information on instances and details. + title_out (str): the title to display before the list. + + Returns: + tar_inst (str): the AWS instance-id of the target instance + Raises: + SystemExit: if no instances were found that match the + parameters specified in the args. + """ qty_instances = len(i_info) if qty_instances == 0: print("No instances found with parameters: %s" % (title_out)) @@ -386,19 +429,22 @@ def det_instance(command, i_info, title_out): def user_picklist(title_out, i_info, command): - """ - Picklist Function: - input: report_title, dict of instance info, command - returns: dictionary-index-of-target - - Display Matching Instances and askd user t0 select target. The list is - displayed by calling the list_instances func with the special_flag set. - Once the list is displayed, the user will be requires to enter a number - between 1 and the number of matchign instances (or a '0' to abort). + """Display list of instances matching args and ask user to select target. + + Once the list is displayed, the user will be requires to enter the number + of the instance they are targeting. + matching instances (or a '0' to abort). Entering a number outside of this range generates an invalid selection error message, and the user is asked again. - """ + Args: + title_out (str): the title to display before the list. + i_info (dict): information on instances and details. + command (str): command specified on the command line. + + Returns: + tar_idx (int): the dictionary index number of the targeted instance + """ getch = _Getch() entry_valid = "False" i_info = awsc.getdetails(i_info) @@ -419,18 +465,30 @@ def user_picklist(title_out, i_info, command): def user_entry(entry_raw, command, maxqty): - """ - User Entry Validation: input: user_entry, command, max_valid_entry + """Checks validity of number entered by user and returns index + or invalid-entry flag. - This function validates the user entry: If it is 0, an abort message is diaplyed and the program exits. If the entry is between 1 and max_valid_entry, then one is subtracted (because its a zero based index) and it is set as the dict-index-of- target and the entry_valid flag is set. Otherwise the entry is invlid, and the functions returns the invalid_ value and the entry_valid flag remains False. - """ + Args: + entry_raw (int): contains either the number the user entered or + the value 999 if the user entered a non-number. + command (str): command specified on the command line. + maxqty (int): the number of instances the user is choosing from. + + Returns: + entry_idx(int): the dictionary index number of the targeted instance + entry_valid (bool): specifies if entry_idx is valid. + + Raises: + SystemExit: if the user enters 0 when they are choosing from the + list it triggers the "abort" option offered to the user. + """ entry_valid = "False" if entry_raw == 0: print("\n\n%saborting%s - %s instance\n" % diff --git a/awss/awsc.py b/awss/awsc.py index 4292d28..e2ad589 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -17,12 +17,11 @@ def init(): - """ - initializes this module for use by attaching global vars EC2C, - and EC2R to the AWS service. This must be called once before - any other function in this module, or they won't function. - """ + """Attach global vars EC2C, and EC2R to the AWS service. + This must be called once before any other function in this module + or they won't function. + """ global EC2C # pylint: disable=global-statement global EC2R # pylint: disable=global-statement EC2C = boto3.client('ec2') @@ -30,8 +29,8 @@ def init(): def getids(qry_string=None): - """ - Get All Instance-Ids that match the qry_string provided + """Get All Instance-Ids that match the qry_string provided. + input: qry_string (optional) returns: dict containing Instance-Ids @@ -43,7 +42,6 @@ def getids(qry_string=None): to searching for all EC2 instances in the default-data-center as defined in the user's AWS config file. """ - if qry_string is None: qry_string = 'EC2C.describe_instances()' summary_data = eval(qry_string) # pylint: disable=eval-used @@ -57,8 +55,8 @@ def getids(qry_string=None): def getdetails(i_info=None): - """ - Get Details for Each Instance-Id in the dict provided. + """Get Details for Each Instance-Id in the dict provided. + input: dict containing Instance-Ids (optional) output: dict containing additional key:value pairs for each instance, containing: execution_state, @@ -67,7 +65,6 @@ def getdetails(i_info=None): Note: if no dict was provided, it calls the getids func to create one, then proceeds with the dict returned. """ - if i_info is None: i_info = getids() for i in i_info: @@ -81,14 +78,13 @@ def getdetails(i_info=None): def gettagvalue(inst_id, tag_title="Name"): - """ - Get Tag Value fpr Specified Instance-Id, and Tag + """Get Tag Value fpr Specified Instance-Id, and Tag. + input: instance-id, and tag_title (optional) If a tag_title is not provided, it defaults to retrieving the value for the 'Name' tag. """ - instance_tags = EC2R.Instance(inst_id).tags qty_tags = len(instance_tags) if qty_tags: @@ -102,8 +98,8 @@ def gettagvalue(inst_id, tag_title="Name"): def getaminame(inst_img_id): - """ - Get Image_Name for the Image_Id specified + """Get Image_Name for the Image_Id specified. + input: Instance_Image_Id The Instance_Image_Id is easily retrieved from the @@ -113,18 +109,16 @@ def getaminame(inst_img_id): retriving the Image_Name, this function is only called when this information is specifically needed. """ - aminame = EC2R.Image(inst_img_id).name return aminame def getsshinfo(inst_id): - """ - Get Instance Information Needed for SSH + """Get Instance Information Needed for SSH. + input: instance-id return: public_ip, login_key, image_id """ - tar_inst = EC2R.Instance(inst_id) inst_ip = tar_inst.public_ip_address inst_key = tar_inst.key_name @@ -133,12 +127,11 @@ def getsshinfo(inst_id): def startstop(inst_id, cmdtodo): - """ - Start or Stop the Specified Instance + """Start or Stop the Specified Instance. + input: instance-id, command (start or stop) return: dict containing the reponse text from AWS """ - tar_inst = EC2R.Instance(inst_id) thecmd = getattr(tar_inst, cmdtodo) response = thecmd() diff --git a/awss/debg.py b/awss/debg.py index d9d7d98..3163782 100644 --- a/awss/debg.py +++ b/awss/debg.py @@ -1,13 +1,11 @@ -""" -This is part of the AWSS Utility located here: -https://github.com/robertpeteuil/aws-shortcuts +"""Module containing functions for printing debug information. -This file contains debug print functions that only print -if this module's init() function is called in the form: - init(DEBUG, DEBUGALL) +The debug print functions only print if one of the debug-modes +was set by a previous call to this module's init() function. -DEBUG allows calls to the dprint function to print -DEBUGALL allows calls to the dprintx function to print +There are two debug-modes: + DEBUG allows calls to the dprint function to print + DEBUGALL allows calls to the dprintx function to print """ from __future__ import print_function From c9ff25662ba11ef42f0568c6c69c307942780399 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 20:13:11 -0700 Subject: [PATCH 10/35] README.rst tweaks [ci skip] --- README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c5427b1..30d2f7f 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ List, start, stop and ssh to AWS instances using Name or Instance-ID --------------------------------------------------------------------------------- -|TRAVIS| |AppVeyor| |Codacy Grade| |Codacy Cov| |PyPi release| |lang| |license| +|TRAVIS| |AppVeyor| |Codacy Grade| |Codacy Cov| |PyPi release| |Py ver| |license s| -------------- @@ -117,9 +117,15 @@ This utility can be installed with ``pip``: :target: https://www.codacy.com/app/robertpeteuil/aws-shortcuts?utm_source=github.com&utm_medium=referral&utm_content=robertpeteuil/aws-shortcuts&utm_campaign=Badge_Coverage .. |license| image:: https://img.shields.io/github/license/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf :target: https://github.com/robertpeteuil/aws-shortcuts +.. |license s| image:: https://img.shields.io/badge/license-MIT-1c64bf.svg?style=flat-square + :target: https://github.com/robertpeteuil/aws-shortcuts .. |lang| image:: https://img.shields.io/badge/language-python-3572A5.svg?style=flat-square :target: https://github.com/robertpeteuil/aws-shortcuts +.. |Py ver| image:: https://img.shields.io/pypi/pyversions/awss.svg + :target: https://pypi.python.org/pypi/bandit/ + :alt: Python Versions + .. |GitHub issues| image:: https://img.shields.io/github/issues/robertpeteuil/aws-shortcuts.svg :target: https://github.com/robertpeteuil/aws-shortcuts .. |GitHub release| image:: https://img.shields.io/github/release/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf From 67207e565cf7e0ab39974b4196d6e244c35f3f5c Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Fri, 28 Apr 2017 20:14:00 -0700 Subject: [PATCH 11/35] Adding bandit to testing matrix in tox, travis, and appveyor --- .appveyor.yml | 1 + .travis.yml | 2 ++ tox.ini | 34 ++++++++++++++++++++++------------ 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index f1943c3..8960f93 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -18,6 +18,7 @@ environment: - TOXENV: py35 - TOXENV: py36 - TOXENV: flake8 + - TOXENV: bandit - TOXENV: readme cache: .tox build_script: diff --git a/.travis.yml b/.travis.yml index 8b4c1a0..da20d1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,8 @@ matrix: env: TOXENV=py36 - python: 3.5 env: TOXENV=flake8 + - python: 2.7 + env: TOXENV=bandit - python: 2.7 env: TOXENV=readme - os: osx diff --git a/tox.ini b/tox.ini index 0b350ec..1070ffe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ # tox testing configuration [tox] -envlist = py27,py33,py34,py35,py36,flake8,readme +envlist = py27,py33,py34,py35,py36,flake8,bandit,readme skip_missing_interpreters=true [testenv] @@ -12,7 +12,6 @@ deps = commands = coverage run -p --source awss -m py.test -# Linter Tests [testenv:flake8] basepython = python3.5 skip_install = true @@ -23,6 +22,15 @@ commands = flake8 awss/ test/ setup.py flake8 awss --radon-max-cc=5 +# Security Test +[testenv:bandit] +basepython = python2.7 +skip_install = true +deps = + bandit +commands = + bandit awss -r --ini tox.ini + # DOC TESTS [testenv:readme] basepython = python3.5 @@ -30,29 +38,27 @@ skip_install = true deps = readme_renderer doc8 - check-manifest commands = python setup.py check -m -r -s doc8 README.rst - check-manifest --ignore tox.ini,.coveragerc,.pylintrc,.appveyor.yml -[testenv:pylint] +# DEV Linters +[testenv:pydoc] basepython = python3.5 skip_install = true deps = - pyflakes - pylint + pydocstyle commands = - pylint awss + pydocstyle awss -# DEV Linters -[testenv:pydoc] +[testenv:pylint] basepython = python3.5 skip_install = true deps = - pydocstyle + pyflakes + pylint commands = - pydocstyle awss + pylint awss [testenv:linters] basepython = python3.5 @@ -101,3 +107,7 @@ ignore = D400 [pycodestyle] ignore = F401 + +[bandit] +exclude: /test +tests: B101,B102,B103,B104,B105,B106,B107,B108,B109,B110,B111,B112,B201,B301,B302,B303,B304,B305,B306,B308,B309,B310,B311,B312,B313,B314,B315,B316,B317,B318,B319,B320,B321,B322,B401,B402,B403,B405,B406,B407,B408,B409,B410,B411,B412,B501,B502,B503,B504,B505,B506,B601,B603,B604,B605,B606,B607,B608,B609,B701,B702 From 63f1ddef5f38b4a1d4c4fc8928cf33db99efe469 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 11:15:45 -0700 Subject: [PATCH 12/35] Update tox.ini [ci skip] --- tox.ini | 61 ++++++++++++++++++++++++++------------------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/tox.ini b/tox.ini index 1070ffe..dc16e10 100644 --- a/tox.ini +++ b/tox.ini @@ -7,68 +7,62 @@ skip_missing_interpreters=true [testenv] deps = mock - pytest + pytest <= 3.0.7 coverage commands = coverage run -p --source awss -m py.test +# Linters [testenv:flake8] basepython = python3.5 skip_install = true deps = - radon - flake8<=3.0.4 + radon <= 1.4.2 + flake8 <= 3.0.4 commands = flake8 awss/ test/ setup.py flake8 awss --radon-max-cc=5 -# Security Test -[testenv:bandit] -basepython = python2.7 -skip_install = true -deps = - bandit -commands = - bandit awss -r --ini tox.ini - -# DOC TESTS -[testenv:readme] +[testenv:pylint] basepython = python3.5 skip_install = true deps = - readme_renderer - doc8 + pyflakes <= 0.8.1 + pylint <= 1.71 commands = - python setup.py check -m -r -s - doc8 README.rst + pylint awss -# DEV Linters -[testenv:pydoc] -basepython = python3.5 +# Security Linter +[testenv:bandit] +basepython = python2.7 skip_install = true deps = - pydocstyle + bandit <= 1.4.0 commands = - pydocstyle awss + bandit awss -r --ini tox.ini -[testenv:pylint] +[testenv:linters] basepython = python3.5 skip_install = true deps = - pyflakes - pylint + {[testenv:flake8]deps} + {[testenv:pylint]deps} + {[testenv:bandit]deps} commands = - pylint awss + {[testenv:flake8]commands} + {[testenv:pylint]commands} + {[testenv:bandit]commands} -[testenv:linters] +# DOC TESTS +[testenv:readme] basepython = python3.5 skip_install = true deps = - {[testenv:pylint]deps} - {[testenv:pydoc]deps} + readme_renderer + doc8 <= 0.8.0 commands = - {[testenv:pylint]commands} - {[testenv:pydoc]commands} + python setup.py check -m -r -s + doc8 README.rst # RELEASE tooling [testenv:build] @@ -105,8 +99,9 @@ ignore=D001 [pydocstyle] ignore = D400 +# previously 'pep8' [pycodestyle] -ignore = F401 +ignore = [bandit] exclude: /test From bfe746ae69165555265604cae2a88277378b2678 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 11:17:58 -0700 Subject: [PATCH 13/35] Docstring Corrections and Minor Code Adjustment for all Modules and Tests --- awss/__init__.py | 148 +++++++++++++++++++++++------------------- awss/awsc.py | 95 ++++++++++++++------------- awss/colors.py | 13 ++-- awss/debg.py | 64 +++++++++--------- awss/getchar.py | 11 ++-- test/test_display.py | 9 +-- test/test_parser.py | 14 +++- test/test_picklist.py | 3 +- test/test_querygen.py | 9 ++- 9 files changed, 207 insertions(+), 159 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index 581f082..816df8b 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -1,23 +1,20 @@ -"""Control AWS instances from command line: list, start, stop or ssh. +"""Control and connect to AWS EC2 instances from command line. -The AWS Shortcuts (awss) package is a utility that allows listing, -starting, stopping and connecting to instances by Name or ID. +The AWS Shortcuts (awss) library is a CLI utility allowing listing, +starting, stopping and connecting to AWS EC2 instances by Name or ID. -The modules in this package are: +Modules in this library: -__init__ - Main module providing entry point and core algorythms. -awsc - AWS Connectivity module that performs all communications - to the AWS service -colors - Determines color capabilities and defined color vars. -debg - Debug module that performs two debug print functions. -getchar - Module holding cross-platform class for reading keys - from the keyboard, without requiring the enter key. +__init__ - Main module providing entry point and core code. +awsc - Communicates with AWS services. +colors - Determine color capability, define color vars and theme. +debg - Debug print functions that execute if debug mode initialized. +getchar - Cross-platform object that reads single keypress. URL: https://github.com/robertpeteuil/aws-shortcuts Author: Robert Peteuil @RobertPeteuil """ from __future__ import print_function -from builtins import range import argparse import sys @@ -26,22 +23,21 @@ import awss.awsc as awsc import awss.debg as debg -__version__ = '0.9.6.2' +__version__ = '0.9.6.4' def main(): - """Orchestrate collecting user args and call function for - command specified. + """Collect user args and call command funct. - This functions sets up the environment, collects relevant - agrs and cals the function specific to the command the user - has specified. + Setup environment, collect relevant args then call function + specific to the command specified in args. Args: None Returns: None + """ parser = parser_setup() options = parser.parse_args() @@ -49,7 +45,6 @@ def main(): debug = bool(options.debug > 0) debugall = bool(options.debug > 1) - awsc.init() debg.init(debug, debugall) options.func(options) @@ -68,6 +63,7 @@ def parser_setup(): Raises: SystemExit: if the user enters invalid args. + """ parser = argparse.ArgumentParser(description="Control AWS instances from" " the command line with: list, start," @@ -161,7 +157,11 @@ def cmd_list(options): """Gather data for instances matching args and call display func. Args: - options (object): contains args and data from parser + options (object): contains args and data from parser. + + Returns: + None + """ (qry_string, title_out) = qry_create(options) i_info = awsc.getids(qry_string) @@ -177,13 +177,16 @@ def cmd_list(options): def cmd_startstop(options): """Start or Stop the specified instance. - Finds instances that match the user specified args plus the command - specific args. The target instance is determined and the specified - action is applied to the instance. The action return information is - retreived and displayed. + Finds instances that match args and instance-state expected by the + command. Then, the target instance is determined, the action is + performed on the instance, and the eturn information is displayed. Args: - options (object): contains args and data from parser + options (object): contains args and data from parser. + + Returns: + None + """ statelu = {"start": "stopped", "stop": "running"} options.inState = statelu[options.command] @@ -209,11 +212,15 @@ def cmd_ssh(options): Finds instances that match the user specified args that are also in the 'running' state. The target instance is determined, the - required connection information is retreived (IP, key used, ssh - user-name), and an 'ssh' connection is made to the instance. + required connection information is retreived (IP, key and ssh + user-name), then an 'ssh' connection is made to the instance. Args: options (object): contains args and data from parser + + Returns: + None + """ import os import subprocess @@ -233,6 +240,7 @@ def cmd_ssh(options): debg.dprint("loginuser Calculated: ", options.user) else: debg.dprint("LoginUser set by user: ", options.user) + if options.nopem: debg.dprint("Connect string: ", "ssh %s@%s" % (options.user, inst_ip)) @@ -259,8 +267,12 @@ def qry_check(qry_string): Args: qry_string (str): the query to be used against the aws ec2 client. + Returns: + None + Raises: SystemExit: if the query string is empty. + """ if qry_string == "ec2C.describe_instances()": print("%sError%s - instance identifier not specified" % @@ -287,6 +299,7 @@ def qry_create(options): Returns: qry_string (str): the query to be used against the aws ec2 client. title_out (str): the title to display before the list. + """ qry_string = "EC2C.describe_instances(" filt_st = "Filters=[" @@ -295,11 +308,13 @@ def qry_create(options): out_end = "All" flag_id = False flag_filt = False + if options.id: qry_string += "InstanceIds=['%s']" % (options.id) title_out += "id: '%s'" % (options.id) flag_id = True out_end = "" + if options.instname: (qry_string, title_out) = qry_helper(flag_id, qry_string, title_out) flag_filt = True @@ -308,6 +323,7 @@ def qry_create(options): qry_string += filt_st + ("{'Name': 'tag:Name', 'Values': ['%s']}" % (options.instname)) title_out += "name: '%s'" % (options.instname) + if options.inState: (qry_string, title_out) = qry_helper(flag_filt, qry_string, title_out, flag_id, filt_st) @@ -316,6 +332,7 @@ def qry_create(options): title_out += "state: '%s'" % (options.inState) filt_end = "]" out_end = "" + qry_string += filt_end + ")" title_out += out_end debg.dprintx("\nQuery String") @@ -345,27 +362,23 @@ def qry_helper(flag_filt, qry_string, title_out, flag_id=False, filt_st=""): qry_string (str): the portion of the query that was passed in with the appropriate syntactical elements added. title_out (str): the title to display before the list. + """ if flag_id or flag_filt: qry_string += ", " title_out += ", " + if not flag_filt: qry_string += filt_st return (qry_string, title_out) -def list_instances(title_out, i_info, numbered="no"): +def list_instances(title_out, i_info, numbered=False): """Display a list of all instances and their details. This function iterates through all the instances contained in the - i_info dict, displayed the information contained, and also obtained - the name of the EC2 image that was used to create the instance. The - image name is not retreived until it is certain it is needed because - retrieving it is relatively slow. - - If the special_case flag is set, it means this function is being called - to display a list for a user to select from. In this case, a colored - number is displayed before each instances data. + i_info dict, displayed the information contained and the name of the + EC2 image that was used to create the instance. Args: title_out (str): the title to display before the list @@ -374,12 +387,18 @@ def list_instances(title_out, i_info, numbered="no"): with numbers before each instance. This is used when this function is called from the user_picklist function. + + Returns: + None + """ - if numbered == "no": + if not numbered: print("\n%s\n" % (title_out)) - for i in range(len(i_info)): - if numbered == "yes": + + for i in i_info: + if numbered: print("Instance %s#%s%s" % (C_WARN, i + 1, C_NORM)) + i_info[i]['aminame'] = awsc.getaminame(i_info[i]['ami']) print("\tName: %s%s%s\t\tID: %s%s%s\t\tStatus: %s%s%s" % (C_TI, i_info[i]['name'], C_NORM, C_TI, @@ -388,6 +407,7 @@ def list_instances(title_out, i_info, numbered="no"): print("\tAMI: %s%s%s\tAMI Name: %s%s%s\n" % (C_TI, i_info[i]['ami'], C_NORM, C_TI, i_info[i]['aminame'], C_NORM)) + debg.dprintx("All Data") debg.dprintx(i_info, True) @@ -395,10 +415,9 @@ def list_instances(title_out, i_info, numbered="no"): def det_instance(command, i_info, title_out): """Determine the instance-id of the target instance. - Inspect the number of instance-ids collected: - If none, displays a message and exits. - If one item, the instance-id is returned as the target. - Ifmore than one, the user_picklist function is called. + Inspect the number of instance-ids collected and take the + appropriate action: exit if no ids, return if single id, + and call user_picklist function if multiple ids exist. Args: command (str): command specified on the command line. @@ -411,19 +430,20 @@ def det_instance(command, i_info, title_out): Raises: SystemExit: if no instances were found that match the parameters specified in the args. + """ qty_instances = len(i_info) if qty_instances == 0: print("No instances found with parameters: %s" % (title_out)) sys.exit() + if qty_instances > 1: print("\n%s instances match these parameters:\n" % (qty_instances)) tar_idx = user_picklist(title_out, i_info, command) else: tar_idx = 0 tar_inst = i_info[tar_idx]['id'] - print("\n%s%sing%s instance id %s%s%s" % (C_STAT[command], - command, C_NORM, + print("\n%s%sing%s instance id %s%s%s" % (C_STAT[command], command, C_NORM, C_TI, tar_inst, C_NORM)) return tar_inst @@ -431,11 +451,8 @@ def det_instance(command, i_info, title_out): def user_picklist(title_out, i_info, command): """Display list of instances matching args and ask user to select target. - Once the list is displayed, the user will be requires to enter the number - of the instance they are targeting. - matching instances (or a '0' to abort). - Entering a number outside of this range generates an invalid selection - error message, and the user is asked again. + Instance list displayed and user asked to enter the number corresponding + to the desired target instance, or '0' to abort. Args: title_out (str): the title to display before the list. @@ -444,17 +461,17 @@ def user_picklist(title_out, i_info, command): Returns: tar_idx (int): the dictionary index number of the targeted instance + """ getch = _Getch() - entry_valid = "False" + entry_valid = False i_info = awsc.getdetails(i_info) - list_instances(title_out, i_info, "yes") - while entry_valid != "True": + list_instances(title_out, i_info, True) + while not entry_valid: sys.stdout.write("Enter %s#%s of instance to %s (%s1%s-%s%i%s) [%s0" - " aborts%s]: " % (C_WARN, C_NORM, command, - C_WARN, C_NORM, C_WARN, - len(i_info), C_NORM, C_TI, - C_NORM)) + " aborts%s]: " % (C_WARN, C_NORM, command, C_WARN, + C_NORM, C_WARN, len(i_info), + C_NORM, C_TI, C_NORM)) entry_raw = getch.int() keyconvert = {999: "invalid entry"} entry_display = keyconvert.get(entry_raw, entry_raw) @@ -465,15 +482,11 @@ def user_picklist(title_out, i_info, command): def user_entry(entry_raw, command, maxqty): - """Checks validity of number entered by user and returns index - or invalid-entry flag. + """Validate user entry and returns index and validity flag. - If it is 0, an abort message is diaplyed and the program exits. - If the entry is between 1 and max_valid_entry, then one is subtracted - (because its a zero based index) and it is set as the dict-index-of- - target and the entry_valid flag is set. - Otherwise the entry is invlid, and the functions returns the invalid_ - value and the entry_valid flag remains False. + Processes the user entry and take the appropriate action: abort + if '0' entered, set validity flag and index is valid entry, else + return invalid index and the still unset validity flag. Args: entry_raw (int): contains either the number the user entered or @@ -488,15 +501,16 @@ def user_entry(entry_raw, command, maxqty): Raises: SystemExit: if the user enters 0 when they are choosing from the list it triggers the "abort" option offered to the user. + """ - entry_valid = "False" + entry_valid = False if entry_raw == 0: print("\n\n%saborting%s - %s instance\n" % (C_ERR, C_NORM, command)) sys.exit() elif entry_raw >= 1 and entry_raw <= maxqty: entry_idx = entry_raw - 1 - entry_valid = "True" + entry_valid = True else: sys.stdout.write("\n%sInvalid entry:%s enter a number between 1" " and %s.\n" % (C_ERR, C_NORM, maxqty)) diff --git a/awss/awsc.py b/awss/awsc.py index e2ad589..58aef6d 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -1,9 +1,5 @@ -""" -This is part of the AWSS Utility located here: -https://github.com/robertpeteuil/aws-shortcuts +"""Communicates with AWS services. -This file contains all functions which talk to AWS EC2. -They communicate via the AWS boto3 libraries. Functions exist to to search for instances, gather certain information about instances, and start / stop instances. """ @@ -12,28 +8,13 @@ import boto3 import awss.debg as debg -EC2C = "" -EC2R = "" - - -def init(): - """Attach global vars EC2C, and EC2R to the AWS service. - - This must be called once before any other function in this module - or they won't function. - """ - global EC2C # pylint: disable=global-statement - global EC2R # pylint: disable=global-statement - EC2C = boto3.client('ec2') - EC2R = boto3.resource('ec2') +EC2C = boto3.client('ec2') +EC2R = boto3.resource('ec2') def getids(qry_string=None): """Get All Instance-Ids that match the qry_string provided. - input: qry_string (optional) - returns: dict containing Instance-Ids - the dict will contain one indexed line, in the format: "0: {'id': }" for each instance-id that matched the query parameters. @@ -41,6 +22,13 @@ def getids(qry_string=None): Note: If no qry_string is provided, it will default to searching for all EC2 instances in the default-data-center as defined in the user's AWS config file. + + Args: + qry_string (str): the query to be used against the aws ec2 client. + + Returns: + i_info (dict): contains all instance-ids returned from query. + """ if qry_string is None: qry_string = 'EC2C.describe_instances()' @@ -57,13 +45,15 @@ def getids(qry_string=None): def getdetails(i_info=None): """Get Details for Each Instance-Id in the dict provided. - input: dict containing Instance-Ids (optional) - output: dict containing additional key:value pairs - for each instance, containing: execution_state, - image_id, and instance 'name' if it has one. - Note: if no dict was provided, it calls the getids func to create one, then proceeds with the dict returned. + + Args: + i_info (dict): contains all instance-ids returned from query. + + Returns: + i_info (dict): information on instances and details. + """ if i_info is None: i_info = getids() @@ -78,12 +68,16 @@ def getdetails(i_info=None): def gettagvalue(inst_id, tag_title="Name"): - """Get Tag Value fpr Specified Instance-Id, and Tag. + """Get value for tag in specified instance.. - input: instance-id, and tag_title (optional) + Args: + inst_id (str): instance-id to get tag value from. + tag_title (str): (optional) name of tag to get + value from. defaults to 'Name'. + + Returns: + tagvalue (str): value of the tag and instance specified - If a tag_title is not provided, it defaults to retrieving - the value for the 'Name' tag. """ instance_tags = EC2R.Instance(inst_id).tags qty_tags = len(instance_tags) @@ -98,26 +92,33 @@ def gettagvalue(inst_id, tag_title="Name"): def getaminame(inst_img_id): - """Get Image_Name for the Image_Id specified. + """Get Image_Name for the image_id specified. + + Connects to ec2 resource.Image object, which is slower + than retrieving info from the ec2 resource.Instance object. - input: Instance_Image_Id + Args: + inst_img_id (str): image_id to get name value from. + + Returns: + aminame (str): name of the image. - The Instance_Image_Id is easily retrieved from the - instance object. But, retrieving the corresponding Name - of the Image requires connecting to the Image object, which - is substantially slower. Because of the time penalty in - retriving the Image_Name, this function is only called when - this information is specifically needed. """ aminame = EC2R.Image(inst_img_id).name return aminame def getsshinfo(inst_id): - """Get Instance Information Needed for SSH. + """Get instance information needed for ssh action. + + Args: + inst_id (str): instance-id to get ssh info for + + Returns: + inst_ip (str): public ip-address of specified instance. + inst_key (str): keyname for specified instance. + inst_img_id (str): name of image for specified instance. - input: instance-id - return: public_ip, login_key, image_id """ tar_inst = EC2R.Instance(inst_id) inst_ip = tar_inst.public_ip_address @@ -129,8 +130,14 @@ def getsshinfo(inst_id): def startstop(inst_id, cmdtodo): """Start or Stop the Specified Instance. - input: instance-id, command (start or stop) - return: dict containing the reponse text from AWS + Args: + inst_id (str): instance-id to perform command against + cmdtodo (str): command to perform (start or stop) + + Returns: + response (dict): reponse returned from AWS after + performing specified action. + """ tar_inst = EC2R.Instance(inst_id) thecmd = getattr(tar_inst, cmdtodo) diff --git a/awss/colors.py b/awss/colors.py index dbb0830..6afbfb2 100644 --- a/awss/colors.py +++ b/awss/colors.py @@ -1,8 +1,7 @@ -'''Module holding constants used to format lines that are printed to the -terminal. -''' +"""Determine color capability, define color vars and theme.""" import sys + try: import colorama colorama.init(strip=(not sys.stdout.isatty())) @@ -11,9 +10,14 @@ BLUE, CYAN, WHITE = (colorama.Fore.BLUE, colorama.Fore.CYAN, colorama.Fore.WHITE) except ImportError: # pragma: no cover - # No colorama, so let's fallback to no-color mode + # No colorama, fallback to no-color mode GREEN = YELLOW = RED = BLUE = CYAN = WHITE = '' +# Create 'color theme' by aliasing color vars to vars that +# are imported and used by other modules. +# User can change colors used in modules by simply changing +# their assignment here, thus avoiding having to change +# the color var-name throughout the module. C_NORM = WHITE C_HEAD = GREEN C_HEAD2 = BLUE @@ -23,6 +27,7 @@ C_WARN = YELLOW C_ERR = RED +# Color dictionary for setting color names for item status of commands C_STAT = {"running": C_GOOD, "start": C_GOOD, "ssh": C_GOOD, "stopped": C_ERR, "stop": C_ERR, "stopping": C_WARN, "pending": C_WARN, "starting": C_WARN} diff --git a/awss/debg.py b/awss/debg.py index 3163782..4b440a8 100644 --- a/awss/debg.py +++ b/awss/debg.py @@ -1,4 +1,4 @@ -"""Module containing functions for printing debug information. +"""Debug print functions that execute if debug mode initialized. The debug print functions only print if one of the debug-modes was set by a previous call to this module's init() function. @@ -15,14 +15,21 @@ DEBUGALL = False -def init(deb1=False, deb2=False): - """ - initialize the values of DEBUG and DEBUGALL - if this init is not called, they default to False, and - all calls to the debug print functions, dprint and dprintx - will not generate output. - """ +def init(deb1, deb2=False): + """Initialize DEBUG and DEBUGALL. + + Allows other modules to set DEBUG and DEBUGALL, so their + call to dprint or dprintx generate output. + + Args: + deb1 (bool): value of DEBUG to set + deb2 (bool): optional - value of DEBUGALL to set, + defaults to False. + Returns: + None + + """ global DEBUG # pylint: disable=global-statement global DEBUGALL # pylint: disable=global-statement DEBUG = deb1 @@ -30,35 +37,32 @@ def init(deb1=False, deb2=False): def dprint(item1, item2=""): - """ - Debug Print - only print is debug mode is set - input: item_to_print, 2nd_item (optional) - - This prints the first paramter sent without change, so color - strings can be inserted before calling. - If the optional 2nd paramter is passed, it will print in CYAN. - This allows for easy printing of variable names (in WHITE) and - their values (in CYAN) like this: dprint(item, value) + """Print Text if DEBUG set. + + Args: + item1 (str): item to print + item2 (str): optional 2nd item to print + + Returns: + None + """ if DEBUG: print(item1, "%s%s%s" % (C_TI, item2, C_NORM)) def dprintx(passeditem, special=False): - """ - Extra Debug Print with optional PrettyPrint - input: item_to_print, pretty_print_mode (optional) - - If DEBUGALL is set, the item passed is printed with the - normal pritn command, or PrettyPrint if a 2nd 'True' param - is passed. This provides a method of printing out entire - dictionaries on occasion, without making them part of the - normal debug display. - Calling without passing a 2nd param is ideal for printing - titles that you want to display before a pprint output of - a dict. - """ + """Print Text if DEBUGALL set, optionally with PrettyPrint. + Args: + passeditem (str): item to print + special (bool): determines if item prints with PrettyPrint + or regular print. + + Returns: + None + + """ if DEBUGALL: if special: from pprint import pprint diff --git a/awss/getchar.py b/awss/getchar.py index 5ae221a..740bc64 100644 --- a/awss/getchar.py +++ b/awss/getchar.py @@ -1,12 +1,13 @@ -"""Getchar Module holding cross-platform class for reading single keypresses +"""Cross-platform object that reads single keypress. -When the object is instantiated, it configures itself for the operating +When the object is instantiated, it initializes for the operating system in use (windows or linux/mac). Called directly - the object returns the key pressed. -Called via the int method - the object coverted the key pressed into -int and returns it. If the key pressed cannot be converted to int +Called via the int method - the object coverted the key pressed +into int and returns it. If the key cannot be converted to int then 999 is returned. + """ from builtins import object @@ -23,7 +24,7 @@ def __call__(self): # pragma: no cover return self.impl() def int(self): # pragma: no cover - """ Coverts a read keystroke to int or returns "999" """ + """Return read keystroke as int else return "999".""" try: value = int(self.impl()) except ValueError: diff --git a/test/test_display.py b/test/test_display.py index b14b171..3bde21e 100644 --- a/test/test_display.py +++ b/test/test_display.py @@ -1,8 +1,4 @@ -#!/usr/bin/env python - -''' -This runs the display list function using sample data. -''' +"""Test module for list_instances function in awss.""" from __future__ import print_function import mock @@ -20,6 +16,7 @@ def getlocalaminame(ami): + """Return name for ami-number passed as arg.""" amiName = amiNameList[ami] return amiName @@ -47,7 +44,7 @@ def getlocalaminame(ami): @mock.patch('awss.awsc.getaminame', getlocalaminame, create=True) def test_display_list(capsys): - + """Test list_instances function in awss.""" debg.init(False, False) outputTitle = "Test Report" list_instances(outputTitle, infoNoAmiName) diff --git a/test/test_parser.py b/test/test_parser.py index 8b845e9..269fa96 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +"""Test module for parser functions in awss.""" from __future__ import print_function import pytest @@ -8,46 +8,55 @@ @pytest.fixture(params=["start ", "stop "]) def cmdname(request): + """Provide command params to test function.""" return request.param @pytest.fixture(params=[("server ", "server"), ("", None)]) def inname(request): + """Provide name and expected result params to test function.""" return request.param @pytest.fixture(params=[("-i 123456 ", "123456"), ("", None)]) def innum(request): + """Provide instance-id and expected result params to test function.""" return request.param @pytest.fixture(params=[("", None), ("-r ", "running"), ("-s ", "stopped")]) def instate(request): + """Provide instance-state and expected result params to test function.""" return request.param @pytest.fixture(params=[("", 0), ("-d ", 1), ("-dd ", 2)]) def debugstate(request): + """Provide debug-state and expected result params to test function.""" return request.param @pytest.fixture(params=[("", None), ("-p ", True)]) def pemstate(request): + """Provide pem-mode and expected result params to test function.""" return request.param @pytest.fixture(params=[("", None), ("-u TestUser ", "TestUser")]) def username(request): + """Provide user-name and expected result params to test function.""" return request.param @pytest.fixture(params=[("", "-h"), ("list ", "-h"), ("start ", "-h"), ("stop ", "-h"), ("ssh ", "-h")]) def inhelp(request): + """Provide help params for each parser and subparser to test function.""" return request.param def test_list_parse_valid(inname, innum, instate, debugstate): + """Test all valid variations of the list command.""" print("test - list_parse_valid for: name: %s, id: %s, state: %s," " debug: %s" % (inname[0], innum[0], instate[0], debugstate[0])) @@ -63,6 +72,7 @@ def test_list_parse_valid(inname, innum, instate, debugstate): def test_startstop_parse_valid(cmdname, inname, innum, debugstate): + """Test all valid variations of the start and stop command.""" print("test - start/stop_parse_valid for: command: %s, name: %s, id: %s," " debug: %s" % (cmdname, inname[0], innum[0], debugstate[0])) @@ -77,6 +87,7 @@ def test_startstop_parse_valid(cmdname, inname, innum, debugstate): def test_ssh_parse_valid(inname, innum, debugstate): + """Test all valid variations of the ssh command.""" print("test - ssh_parse_valid for: name: %s, id: %s, debug: %s" % (inname[0], innum[0], debugstate[0])) @@ -91,6 +102,7 @@ def test_ssh_parse_valid(inname, innum, debugstate): def test_list_parse_help(inhelp): + """Test help for each parser and subparser.""" print("test - list_parse_help for: command: %s" % (inhelp[0])) args = inhelp[0] + inhelp[1] diff --git a/test/test_picklist.py b/test/test_picklist.py index 6e4bef3..4ec623b 100644 --- a/test/test_picklist.py +++ b/test/test_picklist.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +"""Test module for det_instance function in awss.""" from __future__ import print_function import pytest @@ -50,6 +50,7 @@ 'state': 'running'}}, 0), ({}, ['1'], {}, {}, 0)]) def test_det_target(ids, kys, anames, ilist, ide): + """Test valid and invalid params with det_instance function in awss.""" global counter counter = 0 diff --git a/test/test_querygen.py b/test/test_querygen.py index af80a2a..9cb6e77 100644 --- a/test/test_querygen.py +++ b/test/test_querygen.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +"""Test module for qry_create function in awss.""" from __future__ import print_function import pytest @@ -10,21 +10,27 @@ @pytest.fixture(params=["-i 123456", ""]) def genid(request): + """Provide instance-id params to test function.""" return request.param @pytest.fixture(params=["server", ""]) def genname(request): + """Provide name params to test function.""" return request.param @pytest.fixture(params=["running", "stopped", ""]) def genstate(request): + """Provide instance-state params to test function.""" return request.param class holdOptions(): + """Hold options used by qry_create function.""" + def __init__(self, idnum, instname, inState): + """Initialize options to specified values.""" self.id = idnum self.instname = instname self.inState = inState @@ -76,6 +82,7 @@ def __init__(self, idnum, instname, inState): def test_query_generation(genid, genname, genstate): + """Test all valid variations of params with qry_create function in awss.""" print("TEST - Query_Parser - id: %s, name: %s, state: %s" % (genid, genname, genstate)) From a98928d7c001fbdaf0353b6b23591f7d58049278 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 11:52:06 -0700 Subject: [PATCH 14/35] Reverting awsc back to requiring init so testing can occur on hosts without AWS config files --- awss/__init__.py | 3 ++- awss/awsc.py | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index 816df8b..b28a5b3 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -23,7 +23,7 @@ import awss.awsc as awsc import awss.debg as debg -__version__ = '0.9.6.4' +__version__ = '0.9.6.5' def main(): @@ -45,6 +45,7 @@ def main(): debug = bool(options.debug > 0) debugall = bool(options.debug > 1) + awsc.init() debg.init(debug, debugall) options.func(options) diff --git a/awss/awsc.py b/awss/awsc.py index 58aef6d..5905f33 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -8,8 +8,21 @@ import boto3 import awss.debg as debg -EC2C = boto3.client('ec2') -EC2R = boto3.resource('ec2') +EC2C = "" +EC2R = "" + + +def init(): + """Attach global vars EC2C, and EC2R to the AWS service. + + This must be called once before any other function in this module + or they won't function. + + """ + global EC2C # pylint: disable=global-statement + global EC2R # pylint: disable=global-statement + EC2C = boto3.client('ec2') + EC2R = boto3.resource('ec2') def getids(qry_string=None): From c9e3f7ab918c04d7bc2b3620e8548c657dc9f892 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 12:13:05 -0700 Subject: [PATCH 15/35] Updated .coveragerc [ci skip] --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 21192ef..a57a5bd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ [run] branch = True -omit = awss/awsc.py +omit = awss/awsc.py,awss/getchar.py [report] exclude_lines = From c258be381253c19d2a5d41e9f8e9023871d5df2f Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 15:15:57 -0700 Subject: [PATCH 16/35] Revised tox.ini [ci skip] --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index dc16e10..0c501ab 100644 --- a/tox.ini +++ b/tox.ini @@ -85,7 +85,7 @@ commands = twine upload --skip-existing dist/* [flake8] -ignore = D203 +ignore = D203,H301,H306 select = E,W,F exclude = .tox,.git,*.pyc,.cache,.eggs,*.egg,build,dist,test.py,zref,__pycache__ From 80b37eb3cfc31dedd3532636527f7996ce7aafa2 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 15:16:25 -0700 Subject: [PATCH 17/35] Minor docstring cleanup [ci skip] --- awss/debg.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/awss/debg.py b/awss/debg.py index 4b440a8..ed19fa9 100644 --- a/awss/debg.py +++ b/awss/debg.py @@ -26,9 +26,6 @@ def init(deb1, deb2=False): deb2 (bool): optional - value of DEBUGALL to set, defaults to False. - Returns: - None - """ global DEBUG # pylint: disable=global-statement global DEBUGALL # pylint: disable=global-statement @@ -43,9 +40,6 @@ def dprint(item1, item2=""): item1 (str): item to print item2 (str): optional 2nd item to print - Returns: - None - """ if DEBUG: print(item1, "%s%s%s" % (C_TI, item2, C_NORM)) @@ -59,9 +53,6 @@ def dprintx(passeditem, special=False): special (bool): determines if item prints with PrettyPrint or regular print. - Returns: - None - """ if DEBUGALL: if special: From e60fa4f96fb5ad514518cc62d94a4155d97b2bd5 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 15:18:46 -0700 Subject: [PATCH 18/35] Change the handling of queries in qry_create and get_ids, updated qrygen test --- awss/__init__.py | 94 ++++------------------- awss/awsc.py | 38 +++------ test/{test_querygen.py => test_qrygen.py} | 49 ++++++------ 3 files changed, 52 insertions(+), 129 deletions(-) rename test/{test_querygen.py => test_qrygen.py} (62%) diff --git a/awss/__init__.py b/awss/__init__.py index b28a5b3..2ca0efb 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -18,25 +18,19 @@ import argparse import sys -from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT -from awss.getchar import _Getch import awss.awsc as awsc import awss.debg as debg +from awss.getchar import _Getch +from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.5' +__version__ = '0.9.6.6' -def main(): +def main(): # pragma: no cover """Collect user args and call command funct. - Setup environment, collect relevant args then call function - specific to the command specified in args. - - Args: - None - - Returns: - None + Collect command line args and setup environment then call + function for command specified in args. """ parser = parser_setup() @@ -56,12 +50,8 @@ def main(): def parser_setup(): """Create ArgumentParser object to parse command line arguments. - Args: - None - Returns: parser (object): containing ArgumentParser data and methods. - Raises: SystemExit: if the user enters invalid args. @@ -160,9 +150,6 @@ def cmd_list(options): Args: options (object): contains args and data from parser. - Returns: - None - """ (qry_string, title_out) = qry_create(options) i_info = awsc.getids(qry_string) @@ -185,15 +172,11 @@ def cmd_startstop(options): Args: options (object): contains args and data from parser. - Returns: - None - """ statelu = {"start": "stopped", "stop": "running"} options.inState = statelu[options.command] debg.dprint("toggle set state: ", options.inState) (qry_string, title_out) = qry_create(options) - qry_check(qry_string) i_info = awsc.getids(qry_string) tar_inst = det_instance(options.command, i_info, title_out) response = awsc.startstop(tar_inst, options.command) @@ -219,15 +202,11 @@ def cmd_ssh(options): Args: options (object): contains args and data from parser - Returns: - None - """ import os import subprocess options.inState = "running" (qry_string, title_out) = qry_create(options) - qry_check(qry_string) i_info = awsc.getids(qry_string) tar_inst = det_instance(options.command, i_info, title_out) (inst_ip, inst_key, inst_img_id) = awsc.getsshinfo(tar_inst) @@ -258,31 +237,6 @@ def cmd_ssh(options): inst_ip)], shell=True) -def qry_check(qry_string): - """Validate that the query string is functional. - - Check if the generated query string is empty, and if so exits. - This is executed by the 'start', 'stop', and 'ssh' command as they must - target a specific instance. - - Args: - qry_string (str): the query to be used against the aws ec2 client. - - Returns: - None - - Raises: - SystemExit: if the query string is empty. - - """ - if qry_string == "ec2C.describe_instances()": - print("%sError%s - instance identifier not specified" % - (C_ERR, C_NORM)) - sys.exit(1) - else: - return - - def qry_create(options): """Create query from the args specified and command chosen. @@ -296,16 +250,13 @@ def qry_create(options): Args: options (object): contains args and data from parser - Returns: qry_string (str): the query to be used against the aws ec2 client. title_out (str): the title to display before the list. """ - qry_string = "EC2C.describe_instances(" + qry_string = filt_end = title_out = "" filt_st = "Filters=[" - filt_end = "" - title_out = "" out_end = "All" flag_id = False flag_filt = False @@ -334,7 +285,7 @@ def qry_create(options): filt_end = "]" out_end = "" - qry_string += filt_end + ")" + qry_string += filt_end title_out += out_end debg.dprintx("\nQuery String") debg.dprintx(qry_string, True) @@ -358,7 +309,6 @@ def qry_helper(flag_filt, qry_string, title_out, flag_id=False, filt_st=""): has been specified in the args. filt_st (str): optional param to allow adding syntactical elements if a filter has been specified. - Returns: qry_string (str): the portion of the query that was passed in with the appropriate syntactical elements added. @@ -377,20 +327,15 @@ def qry_helper(flag_filt, qry_string, title_out, flag_id=False, filt_st=""): def list_instances(title_out, i_info, numbered=False): """Display a list of all instances and their details. - This function iterates through all the instances contained in the - i_info dict, displayed the information contained and the name of the - EC2 image that was used to create the instance. + Iterates through all the instances in the dict, and displays + information for each instance. Args: title_out (str): the title to display before the list i_info (dict): information on instances and details - numbered (bool): indicates wheter the list should be displayed - with numbers before each instance. This is used - when this function is called from the - user_picklist function. - - Returns: - None + numbered (bool): optional - indicates wheter the list should be + displayed with numbers before each instance. + This is used when called from user_picklist. """ if not numbered: @@ -424,10 +369,8 @@ def det_instance(command, i_info, title_out): command (str): command specified on the command line. i_info (dict): information on instances and details. title_out (str): the title to display before the list. - Returns: tar_inst (str): the AWS instance-id of the target instance - Raises: SystemExit: if no instances were found that match the parameters specified in the args. @@ -435,8 +378,8 @@ def det_instance(command, i_info, title_out): """ qty_instances = len(i_info) if qty_instances == 0: - print("No instances found with parameters: %s" % (title_out)) - sys.exit() + print("No instances found with parameters: {0}".format(title_out)) + sys.exit(1) if qty_instances > 1: print("\n%s instances match these parameters:\n" % (qty_instances)) @@ -459,7 +402,6 @@ def user_picklist(title_out, i_info, command): title_out (str): the title to display before the list. i_info (dict): information on instances and details. command (str): command specified on the command line. - Returns: tar_idx (int): the dictionary index number of the targeted instance @@ -494,11 +436,9 @@ def user_entry(entry_raw, command, maxqty): the value 999 if the user entered a non-number. command (str): command specified on the command line. maxqty (int): the number of instances the user is choosing from. - Returns: entry_idx(int): the dictionary index number of the targeted instance entry_valid (bool): specifies if entry_idx is valid. - Raises: SystemExit: if the user enters 0 when they are choosing from the list it triggers the "abort" option offered to the user. @@ -506,8 +446,8 @@ def user_entry(entry_raw, command, maxqty): """ entry_valid = False if entry_raw == 0: - print("\n\n%saborting%s - %s instance\n" % - (C_ERR, C_NORM, command)) + print("\n\n{0}aborting{1} - {2} instance\n". + format(C_ERR, C_NORM, command)) sys.exit() elif entry_raw >= 1 and entry_raw <= maxqty: entry_idx = entry_raw - 1 diff --git a/awss/awsc.py b/awss/awsc.py index 5905f33..b967dc9 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -4,9 +4,9 @@ information about instances, and start / stop instances. """ -from builtins import range -import boto3 import awss.debg as debg +import boto3 +from builtins import range EC2C = "" EC2R = "" @@ -25,27 +25,23 @@ def init(): EC2R = boto3.resource('ec2') -def getids(qry_string=None): - """Get All Instance-Ids that match the qry_string provided. - - the dict will contain one indexed line, in the format: - "0: {'id': }" for each instance-id that matched - the query parameters. +def getids(qry_string): + """Get All Instance-Ids that match the qry_string. - Note: If no qry_string is provided, it will default - to searching for all EC2 instances in the default-data-center - as defined in the user's AWS config file. + Execute a query against the AWS EC2 client object, that is + based on the contents of qry_string. The raw qry_string + passed to this function is combined with the command prefix + and syntactical elements before the query executes. Args: qry_string (str): the query to be used against the aws ec2 client. - Returns: i_info (dict): contains all instance-ids returned from query. """ - if qry_string is None: - qry_string = 'EC2C.describe_instances()' - summary_data = eval(qry_string) # pylint: disable=eval-used + qry_prefix = "EC2C.describe_instances(" + qry_real = qry_prefix + qry_string + ")" + summary_data = eval(qry_real) # pylint: disable=eval-used i_info = {} for i, j in enumerate(summary_data['Reservations']): i_info[i] = {'id': j['Instances'][0]['InstanceId']} @@ -55,21 +51,15 @@ def getids(qry_string=None): return i_info -def getdetails(i_info=None): +def getdetails(i_info): """Get Details for Each Instance-Id in the dict provided. - Note: if no dict was provided, it calls the getids func - to create one, then proceeds with the dict returned. - Args: i_info (dict): contains all instance-ids returned from query. - Returns: i_info (dict): information on instances and details. """ - if i_info is None: - i_info = getids() for i in i_info: instance_data = EC2R.Instance(i_info[i]['id']) i_info[i]['state'] = instance_data.state['Name'] @@ -87,7 +77,6 @@ def gettagvalue(inst_id, tag_title="Name"): inst_id (str): instance-id to get tag value from. tag_title (str): (optional) name of tag to get value from. defaults to 'Name'. - Returns: tagvalue (str): value of the tag and instance specified @@ -112,7 +101,6 @@ def getaminame(inst_img_id): Args: inst_img_id (str): image_id to get name value from. - Returns: aminame (str): name of the image. @@ -126,7 +114,6 @@ def getsshinfo(inst_id): Args: inst_id (str): instance-id to get ssh info for - Returns: inst_ip (str): public ip-address of specified instance. inst_key (str): keyname for specified instance. @@ -146,7 +133,6 @@ def startstop(inst_id, cmdtodo): Args: inst_id (str): instance-id to perform command against cmdtodo (str): command to perform (start or stop) - Returns: response (dict): reponse returned from AWS after performing specified action. diff --git a/test/test_querygen.py b/test/test_qrygen.py similarity index 62% rename from test/test_querygen.py rename to test/test_qrygen.py index 9cb6e77..afdaaed 100644 --- a/test/test_querygen.py +++ b/test/test_qrygen.py @@ -42,43 +42,40 @@ def __init__(self, idnum, instname, inState): expected_results = { 0: {'title': "All", - 'query': "EC2C.describe_instances()"}, + 'query': ""}, 1: {'title': "id: '-i 123456'", - 'query': "EC2C.describe_instances(InstanceIds=['-i 123456'])"}, + 'query': "InstanceIds=['-i 123456']"}, 2: {'title': "name: 'server'", - 'query': "EC2C.describe_instances(Filters=[{'Name': 'tag:Name'," - " 'Values': ['server']}])"}, + 'query': "Filters=[{'Name': 'tag:Name', 'Values': ['server']}]"}, 3: {'title': "id: '-i 123456', name: 'server'", - 'query': "EC2C.describe_instances(InstanceIds=['-i 123456']," - " Filters=[{'Name': 'tag:Name', 'Values': ['server']}])"}, + 'query': "InstanceIds=['-i 123456'], Filters=[{'Name': 'tag:Name'," + " 'Values': ['server']}]"}, 4: {'title': "state: 'stopped'", - 'query': "EC2C.describe_instances(Filters=[{'Name':" - " 'instance-state-name','Values': ['stopped']}])"}, + 'query': "Filters=[{'Name': 'instance-state-name'," + "'Values': ['stopped']}]"}, 5: {'title': "id: '-i 123456', state: 'stopped'", - 'query': "EC2C.describe_instances(InstanceIds=['-i 123456'], Filters" - "=[{'Name': 'instance-state-name','Values': ['stopped']}])"}, + 'query': "InstanceIds=['-i 123456'], Filters=[{'Name':" + " 'instance-state-name','Values': ['stopped']}]"}, 6: {'title': "name: 'server', state: 'stopped'", - 'query': "EC2C.describe_instances(Filters=[{'Name': 'tag:Name'," - " 'Values': ['server']}, {'Name': 'instance-state-name'," - "'Values': ['stopped']}])"}, + 'query': "Filters=[{'Name': 'tag:Name', 'Values': ['server']}," + " {'Name': 'instance-state-name','Values': ['stopped']}]"}, 7: {'title': "id: '-i 123456', name: 'server', state: 'stopped'", - 'query': "EC2C.describe_instances(InstanceIds=['-i 123456'], Filters" - "=[{'Name': 'tag:Name', 'Values': ['server']}, {'Name':" - " 'instance-state-name','Values': ['stopped']}])"}, + 'query': "InstanceIds=['-i 123456'], Filters=[{'Name': 'tag:Name'," + " 'Values': ['server']}, {'Name': 'instance-state-name'," + "'Values': ['stopped']}]"}, 8: {'title': "state: 'running'", - 'query': "EC2C.describe_instances(Filters=[{'Name':" - " 'instance-state-name','Values': ['running']}])"}, + 'query': "Filters=[{'Name': 'instance-state-name'," + "'Values': ['running']}]"}, 9: {'title': "id: '-i 123456', state: 'running'", - 'query': "EC2C.describe_instances(InstanceIds=['-i 123456'], Filters" - "=[{'Name': 'instance-state-name','Values': ['running']}])"}, + 'query': "InstanceIds=['-i 123456'], Filters=[{'Name':" + " 'instance-state-name','Values': ['running']}]"}, 10: {'title': "name: 'server', state: 'running'", - 'query': "EC2C.describe_instances(Filters=[{'Name': 'tag:Name'," - " 'Values': ['server']}, {'Name': 'instance-state-name'," - "'Values': ['running']}])"}, + 'query': "Filters=[{'Name': 'tag:Name', 'Values': ['server']}," + " {'Name': 'instance-state-name','Values': ['running']}]"}, 11: {'title': "id: '-i 123456', name: 'server', state: 'running'", - 'query': "EC2C.describe_instances(InstanceIds=['-i 123456'], Filters" - "=[{'Name': 'tag:Name', 'Values': ['server']}, {'Name':" - " 'instance-state-name','Values': ['running']}])"}} + 'query': "InstanceIds=['-i 123456'], Filters=[{'Name': 'tag:Name'," + " 'Values': ['server']}, {'Name': 'instance-state-name'," + "'Values': ['running']}]"}} def test_query_generation(genid, genname, genstate): From c56442c01364490d92ea6e7e6b9a06de51ecf1dc Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 18:21:33 -0700 Subject: [PATCH 19/35] Adjusting Appveyor Config [ci skip] --- .appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 8960f93..82d46dc 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -25,7 +25,8 @@ build_script: - cmd: pip install -e . test_script: - cmd: tox -deploy_script: +deploy: off +on_success: - cmd: >- echo Test Success From f72af4be82e8362a3bf0bdfcfe1727bcb914cf8a Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 18:26:55 -0700 Subject: [PATCH 20/35] Changed user_picklist to use input instead of getch Created a stub function to run the input command so the function can be mocked in tests. Mocking the builtin.input command did not work Updated test for new changes Old code is still present, but commented out. Once finalized, the gutter module will no longer be necessary. --- test/test_picklist.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/test_picklist.py b/test/test_picklist.py index 4ec623b..79c871b 100644 --- a/test/test_picklist.py +++ b/test/test_picklist.py @@ -65,17 +65,18 @@ def RetKey(item1): global counter keye = kys[counter] counter += 1 - if counter > len(kys): - counter = 0 - try: - value = int(keye) - except ValueError: - value = 999 - return value + # if counter > len(kys): + # counter = 0 + # try: + # value = int(keye) + # except ValueError: + # value = 999 + # return value + return keye with mock.patch('awss.awsc.getdetails', getlocaldetails, create=True): with mock.patch('awss.awsc.getaminame', getlocalaminame, create=True): - with mock.patch('awss.getchar._Getch.int', RetKey, create=True): + with mock.patch('awss.ob_in', RetKey, create=True): if ide: debg.init(True, True) tar_inst = det_instance("ssh", ids, "TEST") From 1ef98af471c49627099bebca23084229d2555258 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sat, 29 Apr 2017 18:27:25 -0700 Subject: [PATCH 21/35] Changed user_picklist to use input instead of getch Created a stub function to run the input command so the function can be mocked in tests. Mocking the builtin.input command did not work Updated test for new changes Old code is still present, but commented out. Once finalized, the gutter module will no longer be necessary. --- awss/__init__.py | 68 +++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index 2ca0efb..4ca2de2 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -15,15 +15,16 @@ Author: Robert Peteuil @RobertPeteuil """ from __future__ import print_function +from builtins import input import argparse import sys import awss.awsc as awsc import awss.debg as debg -from awss.getchar import _Getch +# from awss.getchar import _Getch from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.6' +__version__ = '0.9.6.7' def main(): # pragma: no cover @@ -240,13 +241,9 @@ def cmd_ssh(options): def qry_create(options): """Create query from the args specified and command chosen. - Creates aws ec2 formatted query string that incorporates the args in the - options object. Generation of this query on the fly allows for queries - that search and/or filter on multiple properties in the same query. - - This function also generates the report output title for the 'list' - function as the creation of it uses the exact same algoruthm as creating - the query. + Creates a query string that incorporates the args in the options + object. Also Creates the title for the 'list' function, as it's + done in parallel by the same algorythm. Args: options (object): contains args and data from parser @@ -403,27 +400,42 @@ def user_picklist(title_out, i_info, command): i_info (dict): information on instances and details. command (str): command specified on the command line. Returns: - tar_idx (int): the dictionary index number of the targeted instance + tar_idx (int): the dictionary index number of the targeted instance. """ - getch = _Getch() + # getch = _Getch() entry_valid = False i_info = awsc.getdetails(i_info) list_instances(title_out, i_info, True) while not entry_valid: - sys.stdout.write("Enter %s#%s of instance to %s (%s1%s-%s%i%s) [%s0" - " aborts%s]: " % (C_WARN, C_NORM, command, C_WARN, - C_NORM, C_WARN, len(i_info), - C_NORM, C_TI, C_NORM)) - entry_raw = getch.int() - keyconvert = {999: "invalid entry"} - entry_display = keyconvert.get(entry_raw, entry_raw) - sys.stdout.write(str(entry_display)) + # sys.stdout.write("Enter %s#%s of instance to %s (%s1%s-%s%i%s) [%s0" + # " aborts%s]: " % (C_WARN, C_NORM, command, C_WARN, + # C_NORM, C_WARN, len(i_info), + # C_NORM, C_TI, C_NORM)) + entry_base = ob_in("Enter %s#%s of instance to %s (%s1%s-%s%s%s) [%s0" + " aborts%s]: " % (C_WARN, C_NORM, command, + C_WARN, C_NORM, C_WARN, + len(i_info), C_NORM, C_TI, + C_NORM)) + # entry_raw = getch.int() + # keyconvert = {999: "invalid entry"} + # entry_display = keyconvert.get(entry_raw, entry_raw) + # sys.stdout.write(str(entry_display)) + try: + entry_raw = int(entry_base) + except ValueError: + # print("invalid input") + entry_raw = 999 (tar_idx, entry_valid) = user_entry(entry_raw, command, len(i_info)) print() return tar_idx +def ob_in(message_text): + """Perform input command as a function so it can be mocked.""" + return (input(message_text)) + + def user_entry(entry_raw, command, maxqty): """Validate user entry and returns index and validity flag. @@ -432,10 +444,9 @@ def user_entry(entry_raw, command, maxqty): return invalid index and the still unset validity flag. Args: - entry_raw (int): contains either the number the user entered or - the value 999 if the user entered a non-number. - command (str): command specified on the command line. - maxqty (int): the number of instances the user is choosing from. + entry_raw (int): a number entered or 999 if a non-int was entered. + command (str): program command to display in prompt. + maxqty (int): the largest valid number that can be entered. Returns: entry_idx(int): the dictionary index number of the targeted instance entry_valid (bool): specifies if entry_idx is valid. @@ -445,16 +456,19 @@ def user_entry(entry_raw, command, maxqty): """ entry_valid = False - if entry_raw == 0: - print("\n\n{0}aborting{1} - {2} instance\n". + # if entry_raw == 0: + if not entry_raw: + print("{0}aborting{1} - {2} instance\n". format(C_ERR, C_NORM, command)) sys.exit() elif entry_raw >= 1 and entry_raw <= maxqty: entry_idx = entry_raw - 1 entry_valid = True else: - sys.stdout.write("\n%sInvalid entry:%s enter a number between 1" - " and %s.\n" % (C_ERR, C_NORM, maxqty)) + # sys.stdout.write("\n%sInvalid entry:%s enter a number between 1" + # " and %s.\n" % (C_ERR, C_NORM, maxqty)) + print("%sInvalid entry:%s enter a number between 1" + " and %s." % (C_ERR, C_NORM, maxqty)) entry_idx = entry_raw return (entry_idx, entry_valid) From 5d4b6d40bbf12eb4cb4dade23e3934639fa9fa46 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sun, 30 Apr 2017 12:30:44 -0700 Subject: [PATCH 22/35] Major change in instance data retrieval & picklist input changed from single-char to input command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit all data (except ami-name) is derived from the results of the initial query - i_info is complete from the onset, and name is keyed as ‘tag:Name’ - this eliminated many functions in awsc: getids, getdetails, and getsshinfo - the display function adjusted to read the Name in the new key ‘tag:Name’ - the cmd functions for each command minor adjustments - picklist adjusted not to call getdetails - tests for display and picklist adjusted data entry was changed from a single character to input for scenarios when there are more then 9 instances to choose from - this eliminated the need for the getchar module - piccoloist test adjusted for this change --- .coveragerc | 2 +- awss/__init__.py | 134 +++++++++++++++++------------------------- awss/awsc.py | 64 ++++++-------------- awss/getchar.py | 61 ------------------- test/test_display.py | 20 +++---- test/test_picklist.py | 82 ++++++++++---------------- 6 files changed, 117 insertions(+), 246 deletions(-) delete mode 100644 awss/getchar.py diff --git a/.coveragerc b/.coveragerc index a57a5bd..21192ef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ [run] branch = True -omit = awss/awsc.py,awss/getchar.py +omit = awss/awsc.py [report] exclude_lines = diff --git a/awss/__init__.py b/awss/__init__.py index 4ca2de2..fd10c31 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -9,7 +9,6 @@ awsc - Communicates with AWS services. colors - Determine color capability, define color vars and theme. debg - Debug print functions that execute if debug mode initialized. -getchar - Cross-platform object that reads single keypress. URL: https://github.com/robertpeteuil/aws-shortcuts Author: Robert Peteuil @RobertPeteuil @@ -21,10 +20,9 @@ import awss.awsc as awsc import awss.debg as debg -# from awss.getchar import _Getch from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.7' +__version__ = '0.9.6.8' def main(): # pragma: no cover @@ -153,14 +151,12 @@ def cmd_list(options): """ (qry_string, title_out) = qry_create(options) - i_info = awsc.getids(qry_string) - items = len(i_info) - if items: - i_info = awsc.getdetails(i_info) + i_info = awsc.get_inst_info(qry_string) + if i_info: title_out = "Instance List - " + title_out list_instances(title_out, i_info) else: - print("No instances found with parameters: %s" % (title_out)) + print("No instances found with parameters: {}".format(title_out)) def cmd_startstop(options): @@ -178,8 +174,8 @@ def cmd_startstop(options): options.inState = statelu[options.command] debg.dprint("toggle set state: ", options.inState) (qry_string, title_out) = qry_create(options) - i_info = awsc.getids(qry_string) - tar_inst = det_instance(options.command, i_info, title_out) + i_info = awsc.get_inst_info(qry_string) + (tar_inst, tar_idx) = det_instance(options.command, i_info, title_out) response = awsc.startstop(tar_inst, options.command) responselu = {"start": "StartingInstances", "stop": "StoppingInstances"} filt = responselu[options.command] @@ -187,9 +183,9 @@ def cmd_startstop(options): state_term = ('CurrentState', 'PreviousState') for i, j in enumerate(state_term): resp[i] = response["{0}".format(filt)][0]["{0}".format(j)]['Name'] - print("\tCurrent State: %s%s%s - Previous State: %s%s%s\n" % - (C_STAT[resp[0]], resp[0], C_NORM, - C_STAT[resp[1]], resp[1], C_NORM)) + print("\tCurrent State: {}{}{} - Previous State: {}{}{}\n". + format(C_STAT[resp[0]], resp[0], C_NORM, + C_STAT[resp[1]], resp[1], C_NORM)) def cmd_ssh(options): @@ -208,12 +204,11 @@ def cmd_ssh(options): import subprocess options.inState = "running" (qry_string, title_out) = qry_create(options) - i_info = awsc.getids(qry_string) - tar_inst = det_instance(options.command, i_info, title_out) - (inst_ip, inst_key, inst_img_id) = awsc.getsshinfo(tar_inst) + i_info = awsc.get_inst_info(qry_string) + (tar_inst, tar_idx) = det_instance(options.command, i_info, title_out) home_dir = os.environ['HOME'] if options.user is None: - tar_aminame = awsc.getaminame(inst_img_id) + tar_aminame = awsc.getaminame(i_info[tar_idx]['ami']) # only first 5 chars of AMI-name used to avoid version numbers userlu = {"ubunt": "ubuntu", "debia": "admin", "fedor": "fedora", "cento": "centos", "openB": "root"} @@ -223,27 +218,28 @@ def cmd_ssh(options): debg.dprint("LoginUser set by user: ", options.user) if options.nopem: - debg.dprint("Connect string: ", "ssh %s@%s" % - (options.user, inst_ip)) - print("%sNo PEM mode%s - connecting without PEM key\n" % (C_HEAD, - C_NORM)) - subprocess.call(["ssh {0}@{1}".format(options.user, inst_ip)], - shell=True) + debg.dprint("Connect string: ", "ssh {}@{}". + format(options.user, i_info[tar_idx]['pub_dns_name'])) + print("{0}No PEM mode{1} - connecting without PEM key\n". + format(C_HEAD, C_NORM)) + subprocess.call(["ssh {0}@{1}".format(options.user, + i_info[tar_idx]['pub_dns_name'])], shell=True) else: - debg.dprint("Connect string: ", "ssh -i %s/.aws/%s.pem %s@%s" % - (home_dir, inst_key, options.user, inst_ip)) + debg.dprint("Connect string: ", "ssh -i {}/.aws/{}.pem {}@{}". + format(home_dir, i_info[tar_idx]['ssh_key'], options.user, + i_info[tar_idx]['pub_dns_name'])) print("") subprocess.call(["ssh -i {0}/.aws/{1}.pem {2}@{3}". - format(home_dir, inst_key, options.user, - inst_ip)], shell=True) + format(home_dir, i_info[tar_idx]['ssh_key'], + options.user, i_info[tar_idx]['pub_dns_name'])], + shell=True) def qry_create(options): """Create query from the args specified and command chosen. Creates a query string that incorporates the args in the options - object. Also Creates the title for the 'list' function, as it's - done in parallel by the same algorythm. + object, and creates the title for the 'list' function. Args: options (object): contains args and data from parser @@ -295,17 +291,13 @@ def qry_helper(flag_filt, qry_string, title_out, flag_id=False, filt_st=""): This functions adds syntactical elements to the query string, and report title, based on the types and number of items added thus far. - It is broken-out into a seperate function to eliminate duplication. Args: - flag_filt (bool): indicates that at least one filter item specified. - qry_string (str): the portion of the query that has been constructed - up to this point. + flag_filt (bool): at least one filter item specified. + qry_string (str): portion of the query constructed thus far. title_out (str): the title to display before the list. - flag_id (bool): optional param that specifies if the instance-id - has been specified in the args. - filt_st (str): optional param to allow adding syntactical elements - if a filter has been specified. + flag_id (bool): optional - instance-id was specified. + filt_st (str): optional - syntax to add on end if filter specified. Returns: qry_string (str): the portion of the query that was passed in with the appropriate syntactical elements added. @@ -336,20 +328,19 @@ def list_instances(title_out, i_info, numbered=False): """ if not numbered: - print("\n%s\n" % (title_out)) + print("\n{}\n".format(title_out)) for i in i_info: if numbered: - print("Instance %s#%s%s" % (C_WARN, i + 1, C_NORM)) + print("Instance {}#{}{}".format(C_WARN, i + 1, C_NORM)) i_info[i]['aminame'] = awsc.getaminame(i_info[i]['ami']) - print("\tName: %s%s%s\t\tID: %s%s%s\t\tStatus: %s%s%s" % - (C_TI, i_info[i]['name'], C_NORM, C_TI, - i_info[i]['id'], C_NORM, C_STAT[i_info[i]['state']], - i_info[i]['state'], C_NORM)) - print("\tAMI: %s%s%s\tAMI Name: %s%s%s\n" % - (C_TI, i_info[i]['ami'], C_NORM, C_TI, - i_info[i]['aminame'], C_NORM)) + print("\tName: {0}{3}{1}\t\tID: {0}{4}{1}\t\tStatus: {2}{5}{1}". + format(C_TI, C_NORM, C_STAT[i_info[i]['state']], + i_info[i]['tag:Name'], i_info[i]['id'], + i_info[i]['state'])) + print("\tAMI: {0}{2}{1}\tAMI Name: {0}{3}{1}\n". + format(C_TI, C_NORM, i_info[i]['ami'], i_info[i]['aminame'])) debg.dprintx("All Data") debg.dprintx(i_info, True) @@ -364,29 +355,29 @@ def det_instance(command, i_info, title_out): Args: command (str): command specified on the command line. - i_info (dict): information on instances and details. - title_out (str): the title to display before the list. + i_info (dict): information and details for instances. + title_out (str): the title to display in the listing. Returns: - tar_inst (str): the AWS instance-id of the target instance + tar_inst (str): the AWS instance-id of the target. Raises: - SystemExit: if no instances were found that match the - parameters specified in the args. + SystemExit: if no instances are match parameters specified. """ qty_instances = len(i_info) if qty_instances == 0: - print("No instances found with parameters: {0}".format(title_out)) + print("No instances found with parameters: {}".format(title_out)) sys.exit(1) if qty_instances > 1: - print("\n%s instances match these parameters:\n" % (qty_instances)) + print("\n{} instances match these parameters:\n".format(qty_instances)) tar_idx = user_picklist(title_out, i_info, command) + else: tar_idx = 0 tar_inst = i_info[tar_idx]['id'] - print("\n%s%sing%s instance id %s%s%s" % (C_STAT[command], command, C_NORM, - C_TI, tar_inst, C_NORM)) - return tar_inst + print("\n{0}{3}ing{1} instance id {2}{4}{1}". + format(C_STAT[command], C_NORM, C_TI, command, tar_inst)) + return (tar_inst, tar_idx) def user_picklist(title_out, i_info, command): @@ -403,35 +394,23 @@ def user_picklist(title_out, i_info, command): tar_idx (int): the dictionary index number of the targeted instance. """ - # getch = _Getch() entry_valid = False - i_info = awsc.getdetails(i_info) list_instances(title_out, i_info, True) + msg_txt = ("Enter {0}#{1} of instance to {3} ({0}1{1}-{0}{4}{1})" + " [{2}0 aborts{1}]: ".format(C_WARN, C_NORM, C_TI, + command, len(i_info))) while not entry_valid: - # sys.stdout.write("Enter %s#%s of instance to %s (%s1%s-%s%i%s) [%s0" - # " aborts%s]: " % (C_WARN, C_NORM, command, C_WARN, - # C_NORM, C_WARN, len(i_info), - # C_NORM, C_TI, C_NORM)) - entry_base = ob_in("Enter %s#%s of instance to %s (%s1%s-%s%s%s) [%s0" - " aborts%s]: " % (C_WARN, C_NORM, command, - C_WARN, C_NORM, C_WARN, - len(i_info), C_NORM, C_TI, - C_NORM)) - # entry_raw = getch.int() - # keyconvert = {999: "invalid entry"} - # entry_display = keyconvert.get(entry_raw, entry_raw) - # sys.stdout.write(str(entry_display)) + entry_base = obtain_input(msg_txt) try: entry_raw = int(entry_base) except ValueError: - # print("invalid input") entry_raw = 999 (tar_idx, entry_valid) = user_entry(entry_raw, command, len(i_info)) print() return tar_idx -def ob_in(message_text): +def obtain_input(message_text): # pragma: no cover """Perform input command as a function so it can be mocked.""" return (input(message_text)) @@ -456,19 +435,16 @@ def user_entry(entry_raw, command, maxqty): """ entry_valid = False - # if entry_raw == 0: if not entry_raw: - print("{0}aborting{1} - {2} instance\n". + print("{}aborting{} - {} instance\n". format(C_ERR, C_NORM, command)) sys.exit() elif entry_raw >= 1 and entry_raw <= maxqty: entry_idx = entry_raw - 1 entry_valid = True else: - # sys.stdout.write("\n%sInvalid entry:%s enter a number between 1" - # " and %s.\n" % (C_ERR, C_NORM, maxqty)) - print("%sInvalid entry:%s enter a number between 1" - " and %s." % (C_ERR, C_NORM, maxqty)) + print("{}Invalid entry:{} enter a number between 1" + " and {}.".format(C_ERR, C_NORM, maxqty)) entry_idx = entry_raw return (entry_idx, entry_valid) diff --git a/awss/awsc.py b/awss/awsc.py index b967dc9..0f4623a 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -15,8 +15,13 @@ def init(): """Attach global vars EC2C, and EC2R to the AWS service. - This must be called once before any other function in this module - or they won't function. + Must be called before any other functions in this module + will work in production mode. + + To allow testing on CI servers without AWS credentials, + this assignment is done in this function instead of the + module itself - as the boto3 methods below require AWS + credentials on the host. """ global EC2C # pylint: disable=global-statement @@ -25,18 +30,16 @@ def init(): EC2R = boto3.resource('ec2') -def getids(qry_string): - """Get All Instance-Ids that match the qry_string. +def get_inst_info(qry_string): + """Get details for instances that match the qry_string. Execute a query against the AWS EC2 client object, that is - based on the contents of qry_string. The raw qry_string - passed to this function is combined with the command prefix - and syntactical elements before the query executes. + based on the contents of qry_string. Args: qry_string (str): the query to be used against the aws ec2 client. Returns: - i_info (dict): contains all instance-ids returned from query. + i_info (dict): information on instances and details. """ qry_prefix = "EC2C.describe_instances(" @@ -45,26 +48,15 @@ def getids(qry_string): i_info = {} for i, j in enumerate(summary_data['Reservations']): i_info[i] = {'id': j['Instances'][0]['InstanceId']} + i_info[i]['state'] = j['Instances'][0]['State']['Name'] + i_info[i]['ami'] = j['Instances'][0]['ImageId'] + i_info[i]['ssh_key'] = j['Instances'][0]['KeyName'] + i_info[i]['pub_dns_name'] = j['Instances'][0]['PublicDnsName'] + inst_tags = j['Instances'][0]['Tags'] + for k in range(len(inst_tags)): + tagname = inst_tags[k]['Key'] + i_info[i]["tag:" + tagname] = inst_tags[k]['Value'] debg.dprint("numInstances: ", len(i_info)) - debg.dprintx("InstanceIds Only") - debg.dprintx(i_info, True) - return i_info - - -def getdetails(i_info): - """Get Details for Each Instance-Id in the dict provided. - - Args: - i_info (dict): contains all instance-ids returned from query. - Returns: - i_info (dict): information on instances and details. - - """ - for i in i_info: - instance_data = EC2R.Instance(i_info[i]['id']) - i_info[i]['state'] = instance_data.state['Name'] - i_info[i]['ami'] = instance_data.image_id - i_info[i]['name'] = gettagvalue(i_info[i]['id']) debg.dprintx("Details except AMI-name") debg.dprintx(i_info, True) return i_info @@ -109,24 +101,6 @@ def getaminame(inst_img_id): return aminame -def getsshinfo(inst_id): - """Get instance information needed for ssh action. - - Args: - inst_id (str): instance-id to get ssh info for - Returns: - inst_ip (str): public ip-address of specified instance. - inst_key (str): keyname for specified instance. - inst_img_id (str): name of image for specified instance. - - """ - tar_inst = EC2R.Instance(inst_id) - inst_ip = tar_inst.public_ip_address - inst_key = tar_inst.key_name - inst_img_id = tar_inst.image_id - return (inst_ip, inst_key, inst_img_id) - - def startstop(inst_id, cmdtodo): """Start or Stop the Specified Instance. diff --git a/awss/getchar.py b/awss/getchar.py deleted file mode 100644 index 740bc64..0000000 --- a/awss/getchar.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Cross-platform object that reads single keypress. - -When the object is instantiated, it initializes for the operating -system in use (windows or linux/mac). - -Called directly - the object returns the key pressed. -Called via the int method - the object coverted the key pressed -into int and returns it. If the key cannot be converted to int -then 999 is returned. - -""" - -from builtins import object - - -class _Getch(object): - def __init__(self): - try: - self.impl = _GetchWindows() - except ImportError: - self.impl = _GetchUnix() - - def __call__(self): # pragma: no cover - return self.impl() - - def int(self): # pragma: no cover - """Return read keystroke as int else return "999".""" - try: - value = int(self.impl()) - except ValueError: - value = 999 - return value - - -class _GetchUnix(object): - def __init__(self): - import tty # noqa: F401 pylint: disable=unused-variable - import sys # noqa: F401 pylint: disable=unused-variable - - def __call__(self): # pragma: no cover - import sys # noqa: F401 - import tty # noqa: F401 - import termios # noqa: F401 - file_desc = sys.stdin.fileno() - old_settings = termios.tcgetattr(file_desc) - try: - tty.setraw(sys.stdin.fileno()) - chr_read = sys.stdin.read(1) - finally: - termios.tcsetattr(file_desc, termios.TCSADRAIN, old_settings) - return chr_read - - -class _GetchWindows(object): - def __init__(self): - # pylint: disable=unused-variable,import-error - import msvcrt # noqa: F401 - - def __call__(self): # pragma: no cover - import msvcrt # pylint: disable=import-error - return msvcrt.getch() diff --git a/test/test_display.py b/test/test_display.py index 3bde21e..a401c5c 100644 --- a/test/test_display.py +++ b/test/test_display.py @@ -30,16 +30,16 @@ def getlocalaminame(ami): infoNoAmiName = { - 0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', 'name': 'Ubuntu', - 'state': 'running'}, - 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', 'name': 'Amazon', - 'state': 'running'}, - 2: {'ami': 'ami-e09acc80', 'id': 'i-0014d16e7f68ce746', 'name': 'Suse', - 'state': 'stopped'}, - 3: {'ami': 'ami-3e21725e', 'id': 'i-0c459a77e113c6c9c', 'name': 'Ubuntu', - 'state': 'stopping'}, - 4: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', 'name': 'RHEL', - 'state': 'stopped'}} + 0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', + 'tag:Name': 'Ubuntu', 'state': 'running'}, + 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', + 'tag:Name': 'Amazon', 'state': 'running'}, + 2: {'ami': 'ami-e09acc80', 'id': 'i-0014d16e7f68ce746', + 'tag:Name': 'Suse', 'state': 'stopped'}, + 3: {'ami': 'ami-3e21725e', 'id': 'i-0c459a77e113c6c9c', + 'tag:Name': 'Ubuntu', 'state': 'stopping'}, + 4: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', + 'tag:Name': 'RHEL', 'state': 'stopped'}} @mock.patch('awss.awsc.getaminame', getlocalaminame, create=True) diff --git a/test/test_picklist.py b/test/test_picklist.py index 79c871b..dd8077d 100644 --- a/test/test_picklist.py +++ b/test/test_picklist.py @@ -10,53 +10,42 @@ debg.init(False, False) -@pytest.mark.parametrize(("ids", "kys", "anames", "ilist", "ide"), [ - ({0: {'id': 'i-0c875fafa1e71327b'}, - 1: {'id': 'i-03120c2544fdf5b6f'}, - 2: {'id': 'i-0014d16e7f68ce746'}, - 3: {'id': 'i-0c459a77e113c6c9c'}, - 4: {'id': 'i-0341963e139617c75'}}, +@pytest.mark.parametrize(("ids", "kys", "anames", "ide", "tidx"), [ + ({0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', + 'tag:Name': 'Ubuntu', 'state': 'running'}, + 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', + 'tag:Name': 'Amazon', 'state': 'running'}, + 2: {'ami': 'ami-e09acc80', 'id': 'i-0014d16e7f68ce746', + 'tag:Name': 'Suse', 'state': 'running'}, + 3: {'ami': 'ami-3e21725e', 'id': 'i-0c459a77e113c6c9c', + 'tag:Name': 'Ubuntu', 'state': 'running'}, + 4: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', + 'tag:Name': 'RHEL', 'state': 'running'}}, ['a', 'P', '.', '9', '3'], {'ami-16efb076': 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64', 'ami-3e21725e': 'ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64', 'ami-e09acc80': 'suse-sles-12-sp2-v20161214-hvm-ssd-x86_64', 'ami-165a0876': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2', 'ami-2cade64c': 'RHEL-7.3_HVM_GA-20161026-x86_64-1-Hourly2-GP2'}, - {0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', 'name': 'Ubuntu', - 'state': 'running'}, - 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', 'name': 'Amazon', - 'state': 'running'}, - 2: {'ami': 'ami-e09acc80', 'id': 'i-0014d16e7f68ce746', 'name': 'Suse', - 'state': 'running'}, - 3: {'ami': 'ami-3e21725e', 'id': 'i-0c459a77e113c6c9c', 'name': 'Ubuntu', - 'state': 'running'}, - 4: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', 'name': 'RHEL', - 'state': 'running'}}, - 'i-0014d16e7f68ce746'), - ({0: {'id': 'i-0341963e139617c75'}}, + 'i-0014d16e7f68ce746', 2), + ({0: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', + 'tag:Name': 'RHEL', 'state': 'running'}}, ['1'], {'ami-2cade64c': 'RHEL-7.3_HVM_GA-20161026-x86_64-1-Hourly2-GP2'}, - {0: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', 'name': 'RHEL', - 'state': 'running'}}, - 'i-0341963e139617c75'), - ({0: {'id': 'i-0c875fafa1e71327b'}, - 1: {'id': 'i-03120c2544fdf5b6f'}}, + 'i-0341963e139617c75', 0), + ({0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', + 'tag:Name': 'Ubuntu', 'state': 'running'}, + 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', + 'tag:Name': 'Amazon', 'state': 'running'}}, ['a', '7', '0'], {'ami-16efb076': 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64', - 'ami-165a0876': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2'}, - {0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', 'name': 'Ubuntu', - 'state': 'running'}, - 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', 'name': 'Amazon', - 'state': 'running'}}, 0), - ({}, ['1'], {}, {}, 0)]) -def test_det_target(ids, kys, anames, ilist, ide): + 'ami-165a0876': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2'}, 0, 0), + ({}, ['1'], {}, 0, 0)]) +def test_det_target(ids, kys, anames, ide, tidx): """Test valid and invalid params with det_instance function in awss.""" global counter counter = 0 - def getlocaldetails(info): - return ilist - def getlocalaminame(ami): amiName = anames[ami] return amiName @@ -65,24 +54,17 @@ def RetKey(item1): global counter keye = kys[counter] counter += 1 - # if counter > len(kys): - # counter = 0 - # try: - # value = int(keye) - # except ValueError: - # value = 999 - # return value return keye - with mock.patch('awss.awsc.getdetails', getlocaldetails, create=True): - with mock.patch('awss.awsc.getaminame', getlocalaminame, create=True): - with mock.patch('awss.ob_in', RetKey, create=True): - if ide: + with mock.patch('awss.awsc.getaminame', getlocalaminame, create=True): + with mock.patch('awss.obtain_input', RetKey, create=True): + if ide: + debg.init(True, True) + (tar_inst, tar_idx) = det_instance("ssh", ids, "TEST") + assert tar_inst == ide + assert tar_idx == tidx + else: + with pytest.raises(SystemExit): debg.init(True, True) tar_inst = det_instance("ssh", ids, "TEST") - assert tar_inst == ide - else: - with pytest.raises(SystemExit): - debg.init(True, True) - tar_inst = det_instance("ssh", ids, "TEST") - pass + pass From 8440ffdb3762ea58ba4394033f6c5262c44b08f0 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sun, 30 Apr 2017 14:21:51 -0700 Subject: [PATCH 23/35] =?UTF-8?q?Added=20=E2=80=9Cdecode=5Fresults?= =?UTF-8?q?=E2=80=9D=20func=20in=20main=20module=20for=20logic=20to=20deco?= =?UTF-8?q?de=20raw=20aws=20query=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Query-results-decode logic was moved from the get_inst_info func in awsc - easy to test since it’s now detached from AWS calls - this further limits logic code in awsc, which is desirable because the awsc module requires AWS credentials Added “get_data” fun to run code set of function ran by all commands - it can’t be moved to main yet as some command need to change settings first - next step is to move command specific settings to main along with the code in this func --- awss/__init__.py | 65 ++++++++++++++++++++++++++++++++++++++++++------ awss/awsc.py | 52 ++++++-------------------------------- 2 files changed, 65 insertions(+), 52 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index fd10c31..9675202 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -15,6 +15,7 @@ """ from __future__ import print_function from builtins import input +from builtins import range import argparse import sys @@ -22,7 +23,7 @@ import awss.debg as debg from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.8' +__version__ = '0.9.6.9' def main(): # pragma: no cover @@ -150,8 +151,10 @@ def cmd_list(options): options (object): contains args and data from parser. """ - (qry_string, title_out) = qry_create(options) - i_info = awsc.get_inst_info(qry_string) + # (qry_string, title_out) = qry_create(options) + # qry_results = awsc.get_inst_info(qry_string) + # i_info = decode_results(qry_results) + (i_info, title_out) = get_data(options) if i_info: title_out = "Instance List - " + title_out list_instances(title_out, i_info) @@ -173,8 +176,10 @@ def cmd_startstop(options): statelu = {"start": "stopped", "stop": "running"} options.inState = statelu[options.command] debg.dprint("toggle set state: ", options.inState) - (qry_string, title_out) = qry_create(options) - i_info = awsc.get_inst_info(qry_string) + # (qry_string, title_out) = qry_create(options) + # qry_results = awsc.get_inst_info(qry_string) + # i_info = decode_results(qry_results) + (i_info, title_out) = get_data(options) (tar_inst, tar_idx) = det_instance(options.command, i_info, title_out) response = awsc.startstop(tar_inst, options.command) responselu = {"start": "StartingInstances", "stop": "StoppingInstances"} @@ -203,8 +208,10 @@ def cmd_ssh(options): import os import subprocess options.inState = "running" - (qry_string, title_out) = qry_create(options) - i_info = awsc.get_inst_info(qry_string) + # (qry_string, title_out) = qry_create(options) + # qry_results = awsc.get_inst_info(qry_string) + # i_info = decode_results(qry_results) + (i_info, title_out) = get_data(options) (tar_inst, tar_idx) = det_instance(options.command, i_info, title_out) home_dir = os.environ['HOME'] if options.user is None: @@ -235,6 +242,50 @@ def cmd_ssh(options): shell=True) +def get_data(options): + """Get Data specific for command selected. + + Args: + options (object): contains args and data from parser. + Returns: + i_info (dict): information on instances and details. + title_out (str): the title to display before the list. + + """ + (qry_string, title_out) = qry_create(options) + qry_results = awsc.get_inst_info(qry_string) + i_info = decode_results(qry_results) + return (i_info, title_out) + + +def decode_results(qry_results): + """Generate dictionary of results from query. + + Decodes the large dict recturned from the AWS query. + + Args: + qry_results (dict): results from awsc.get_inst_info + Returns: + i_info (dict): information on instances and details. + + """ + i_info = {} + for i, j in enumerate(qry_results['Reservations']): + i_info[i] = {'id': j['Instances'][0]['InstanceId']} + i_info[i]['state'] = j['Instances'][0]['State']['Name'] + i_info[i]['ami'] = j['Instances'][0]['ImageId'] + i_info[i]['ssh_key'] = j['Instances'][0]['KeyName'] + i_info[i]['pub_dns_name'] = j['Instances'][0]['PublicDnsName'] + inst_tags = j['Instances'][0]['Tags'] + for k in range(len(inst_tags)): + tagname = inst_tags[k]['Key'] + i_info[i]["tag:" + tagname] = inst_tags[k]['Value'] + debg.dprint("numInstances: ", len(i_info)) + debg.dprintx("Details except AMI-name") + debg.dprintx(i_info, True) + return i_info + + def qry_create(options): """Create query from the args specified and command chosen. diff --git a/awss/awsc.py b/awss/awsc.py index 0f4623a..3a7cad9 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -1,12 +1,11 @@ -"""Communicates with AWS services. +"""Communicate with AWS EC2 to get data and interact with instances. -Functions exist to to search for instances, gather certain -information about instances, and start / stop instances. +Functions for retrieving data for queried instances, retrieving +the name of the image of an instance (AMI Name), and for starting +or stopping instances. """ -import awss.debg as debg import boto3 -from builtins import range EC2C = "" EC2R = "" @@ -39,50 +38,13 @@ def get_inst_info(qry_string): Args: qry_string (str): the query to be used against the aws ec2 client. Returns: - i_info (dict): information on instances and details. + qry_results (dict): raw information returned from AWS. """ qry_prefix = "EC2C.describe_instances(" qry_real = qry_prefix + qry_string + ")" - summary_data = eval(qry_real) # pylint: disable=eval-used - i_info = {} - for i, j in enumerate(summary_data['Reservations']): - i_info[i] = {'id': j['Instances'][0]['InstanceId']} - i_info[i]['state'] = j['Instances'][0]['State']['Name'] - i_info[i]['ami'] = j['Instances'][0]['ImageId'] - i_info[i]['ssh_key'] = j['Instances'][0]['KeyName'] - i_info[i]['pub_dns_name'] = j['Instances'][0]['PublicDnsName'] - inst_tags = j['Instances'][0]['Tags'] - for k in range(len(inst_tags)): - tagname = inst_tags[k]['Key'] - i_info[i]["tag:" + tagname] = inst_tags[k]['Value'] - debg.dprint("numInstances: ", len(i_info)) - debg.dprintx("Details except AMI-name") - debg.dprintx(i_info, True) - return i_info - - -def gettagvalue(inst_id, tag_title="Name"): - """Get value for tag in specified instance.. - - Args: - inst_id (str): instance-id to get tag value from. - tag_title (str): (optional) name of tag to get - value from. defaults to 'Name'. - Returns: - tagvalue (str): value of the tag and instance specified - - """ - instance_tags = EC2R.Instance(inst_id).tags - qty_tags = len(instance_tags) - if qty_tags: - for j in range(qty_tags): - if instance_tags[j]['Key'] == tag_title: - tagvalue = instance_tags[j]['Value'] - break - else: - tagvalue = "" - return tagvalue + qry_results = eval(qry_real) # pylint: disable=eval-used + return qry_results def getaminame(inst_img_id): From 83dd7eb95a7514ece93360848bd8e376c43d570a Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Sun, 30 Apr 2017 15:37:00 -0700 Subject: [PATCH 24/35] Changed Tag Storage to a Tag sub-dict for each Instance changed names for det_instance to determine_inst to make its purpose more clear also changed test scripts impacted by this change - test-display, and test-picklist --- awss/__init__.py | 46 ++++++++++++------------- test/test_display.py | 40 ++++++++++++++++------ test/test_picklist.py | 78 +++++++++++++++++++++++++++++++------------ 3 files changed, 109 insertions(+), 55 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index 9675202..c45350f 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -23,7 +23,7 @@ import awss.debg as debg from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.9' +__version__ = '0.9.6.10' def main(): # pragma: no cover @@ -151,10 +151,7 @@ def cmd_list(options): options (object): contains args and data from parser. """ - # (qry_string, title_out) = qry_create(options) - # qry_results = awsc.get_inst_info(qry_string) - # i_info = decode_results(qry_results) - (i_info, title_out) = get_data(options) + (i_info, title_out) = gather_data(options) if i_info: title_out = "Instance List - " + title_out list_instances(title_out, i_info) @@ -176,11 +173,8 @@ def cmd_startstop(options): statelu = {"start": "stopped", "stop": "running"} options.inState = statelu[options.command] debg.dprint("toggle set state: ", options.inState) - # (qry_string, title_out) = qry_create(options) - # qry_results = awsc.get_inst_info(qry_string) - # i_info = decode_results(qry_results) - (i_info, title_out) = get_data(options) - (tar_inst, tar_idx) = det_instance(options.command, i_info, title_out) + (i_info, title_out) = gather_data(options) + (tar_inst, tar_idx) = determine_inst(options.command, i_info, title_out) response = awsc.startstop(tar_inst, options.command) responselu = {"start": "StartingInstances", "stop": "StoppingInstances"} filt = responselu[options.command] @@ -208,11 +202,8 @@ def cmd_ssh(options): import os import subprocess options.inState = "running" - # (qry_string, title_out) = qry_create(options) - # qry_results = awsc.get_inst_info(qry_string) - # i_info = decode_results(qry_results) - (i_info, title_out) = get_data(options) - (tar_inst, tar_idx) = det_instance(options.command, i_info, title_out) + (i_info, title_out) = gather_data(options) + (tar_inst, tar_idx) = determine_inst(options.command, i_info, title_out) home_dir = os.environ['HOME'] if options.user is None: tar_aminame = awsc.getaminame(i_info[tar_idx]['ami']) @@ -242,11 +233,18 @@ def cmd_ssh(options): shell=True) -def get_data(options): +def gather_data(options): """Get Data specific for command selected. + Create ec2 specific query and output title based on + options specified, retrieves the raw response data + from aws, then processes it into the i_info dict, + which is used throughout this module. + Args: - options (object): contains args and data from parser. + options (object): contains args and data from parser, + that has been adjusted by the command + specific functions as appropriate. Returns: i_info (dict): information on instances and details. title_out (str): the title to display before the list. @@ -254,11 +252,11 @@ def get_data(options): """ (qry_string, title_out) = qry_create(options) qry_results = awsc.get_inst_info(qry_string) - i_info = decode_results(qry_results) + i_info = process_results(qry_results) return (i_info, title_out) -def decode_results(qry_results): +def process_results(qry_results): """Generate dictionary of results from query. Decodes the large dict recturned from the AWS query. @@ -277,9 +275,11 @@ def decode_results(qry_results): i_info[i]['ssh_key'] = j['Instances'][0]['KeyName'] i_info[i]['pub_dns_name'] = j['Instances'][0]['PublicDnsName'] inst_tags = j['Instances'][0]['Tags'] + tag_dict = {} for k in range(len(inst_tags)): - tagname = inst_tags[k]['Key'] - i_info[i]["tag:" + tagname] = inst_tags[k]['Value'] + tag_dict[inst_tags[k]['Key']] = inst_tags[k]['Value'] + i_info[i]['tag'] = tag_dict + # reference the name tag as: i_info[i]['tag']['Name'] debg.dprint("numInstances: ", len(i_info)) debg.dprintx("Details except AMI-name") debg.dprintx(i_info, True) @@ -388,7 +388,7 @@ def list_instances(title_out, i_info, numbered=False): i_info[i]['aminame'] = awsc.getaminame(i_info[i]['ami']) print("\tName: {0}{3}{1}\t\tID: {0}{4}{1}\t\tStatus: {2}{5}{1}". format(C_TI, C_NORM, C_STAT[i_info[i]['state']], - i_info[i]['tag:Name'], i_info[i]['id'], + i_info[i]['tag']['Name'], i_info[i]['id'], i_info[i]['state'])) print("\tAMI: {0}{2}{1}\tAMI Name: {0}{3}{1}\n". format(C_TI, C_NORM, i_info[i]['ami'], i_info[i]['aminame'])) @@ -397,7 +397,7 @@ def list_instances(title_out, i_info, numbered=False): debg.dprintx(i_info, True) -def det_instance(command, i_info, title_out): +def determine_inst(command, i_info, title_out): """Determine the instance-id of the target instance. Inspect the number of instance-ids collected and take the diff --git a/test/test_display.py b/test/test_display.py index a401c5c..3ba031c 100644 --- a/test/test_display.py +++ b/test/test_display.py @@ -30,16 +30,36 @@ def getlocalaminame(ami): infoNoAmiName = { - 0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', - 'tag:Name': 'Ubuntu', 'state': 'running'}, - 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', - 'tag:Name': 'Amazon', 'state': 'running'}, - 2: {'ami': 'ami-e09acc80', 'id': 'i-0014d16e7f68ce746', - 'tag:Name': 'Suse', 'state': 'stopped'}, - 3: {'ami': 'ami-3e21725e', 'id': 'i-0c459a77e113c6c9c', - 'tag:Name': 'Ubuntu', 'state': 'stopping'}, - 4: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', - 'tag:Name': 'RHEL', 'state': 'stopped'}} + 0: {'ami': 'ami-16efb076', + 'id': 'i-0c875fafa1e71327b', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Ubuntu', 'Role': 'Test'}}, + 1: {'ami': 'ami-3e21725e', + 'id': 'i-0c459a77e113c6c9c', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Ubuntu'}}, + 2: {'ami': 'ami-e09acc80', + 'id': 'i-0014d16e7f68ce746', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Suse', 'Role': 'Test'}}, + 3: {'ami': 'ami-165a0876', + 'id': 'i-03120c2544fdf5b6f', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Amazon', 'Role': 'Test'}}, + 4: {'ami': 'ami-2cade64c', + 'id': 'i-0341963e139617c75', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'RHEL', 'Role': 'Test'}}} @mock.patch('awss.awsc.getaminame', getlocalaminame, create=True) diff --git a/test/test_picklist.py b/test/test_picklist.py index dd8077d..6e716b5 100644 --- a/test/test_picklist.py +++ b/test/test_picklist.py @@ -4,23 +4,43 @@ import pytest import mock -from awss import det_instance +from awss import determine_inst import awss.debg as debg debg.init(False, False) @pytest.mark.parametrize(("ids", "kys", "anames", "ide", "tidx"), [ - ({0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', - 'tag:Name': 'Ubuntu', 'state': 'running'}, - 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', - 'tag:Name': 'Amazon', 'state': 'running'}, - 2: {'ami': 'ami-e09acc80', 'id': 'i-0014d16e7f68ce746', - 'tag:Name': 'Suse', 'state': 'running'}, - 3: {'ami': 'ami-3e21725e', 'id': 'i-0c459a77e113c6c9c', - 'tag:Name': 'Ubuntu', 'state': 'running'}, - 4: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', - 'tag:Name': 'RHEL', 'state': 'running'}}, + ({0: {'ami': 'ami-16efb076', + 'id': 'i-0c875fafa1e71327b', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Ubuntu', 'Role': 'Test'}}, + 1: {'ami': 'ami-3e21725e', + 'id': 'i-0c459a77e113c6c9c', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Ubuntu'}}, + 2: {'ami': 'ami-e09acc80', + 'id': 'i-0014d16e7f68ce746', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Suse', 'Role': 'Test'}}, + 3: {'ami': 'ami-165a0876', + 'id': 'i-03120c2544fdf5b6f', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Amazon', 'Role': 'Test'}}, + 4: {'ami': 'ami-2cade64c', + 'id': 'i-0341963e139617c75', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'RHEL', 'Role': 'Test'}}}, ['a', 'P', '.', '9', '3'], {'ami-16efb076': 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64', 'ami-3e21725e': 'ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64', @@ -28,20 +48,34 @@ 'ami-165a0876': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2', 'ami-2cade64c': 'RHEL-7.3_HVM_GA-20161026-x86_64-1-Hourly2-GP2'}, 'i-0014d16e7f68ce746', 2), - ({0: {'ami': 'ami-2cade64c', 'id': 'i-0341963e139617c75', - 'tag:Name': 'RHEL', 'state': 'running'}}, + ({0: {'ami': 'ami-2cade64c', + 'id': 'i-0341963e139617c75', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'RHEL', 'Role': 'Test'}}}, ['1'], {'ami-2cade64c': 'RHEL-7.3_HVM_GA-20161026-x86_64-1-Hourly2-GP2'}, 'i-0341963e139617c75', 0), - ({0: {'ami': 'ami-16efb076', 'id': 'i-0c875fafa1e71327b', - 'tag:Name': 'Ubuntu', 'state': 'running'}, - 1: {'ami': 'ami-165a0876', 'id': 'i-03120c2544fdf5b6f', - 'tag:Name': 'Amazon', 'state': 'running'}}, + ({0: {'ami': 'ami-16efb076', + 'id': 'i-0c875fafa1e71327b', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Ubuntu', 'Role': 'Test'}}, + 1: {'ami': 'ami-165a0876', + 'id': 'i-03120c2544fdf5b6f', + 'pub_dns_name': '', + 'ssh_key': 'robert', + 'state': 'stopped', + 'tag': {'Name': 'Amazon', 'Role': 'Test'}}}, ['a', '7', '0'], {'ami-16efb076': 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64', - 'ami-165a0876': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2'}, 0, 0), - ({}, ['1'], {}, 0, 0)]) -def test_det_target(ids, kys, anames, ide, tidx): + 'ami-165a0876': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2'}, + 0, 0), + ({}, ['1'], {}, + 0, 0)]) +def test_determine_inst(ids, kys, anames, ide, tidx): """Test valid and invalid params with det_instance function in awss.""" global counter counter = 0 @@ -60,11 +94,11 @@ def RetKey(item1): with mock.patch('awss.obtain_input', RetKey, create=True): if ide: debg.init(True, True) - (tar_inst, tar_idx) = det_instance("ssh", ids, "TEST") + (tar_inst, tar_idx) = determine_inst("ssh", ids, "TEST") assert tar_inst == ide assert tar_idx == tidx else: with pytest.raises(SystemExit): debg.init(True, True) - tar_inst = det_instance("ssh", ids, "TEST") + tar_inst = determine_inst("ssh", ids, "TEST") pass From 3cdd84ce09789f75087b3fde2d3c51a96dea5fe0 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 08:38:07 -0700 Subject: [PATCH 25/35] Changed Retrieval of AMI-Names and Adjusted Tests for New Usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed to isolate list_instance function from extraneous calls cmd_ssh functions the same - it gets the aminame (for login-user lookup) before connection from renamed func - get_one_aminame list_instances now expects a complete i_info - new func “get_all_aminames” is called in cmd_list or user_picklist, before calling list_instances --- awss/__init__.py | 21 ++++++++++++++------- awss/awsc.py | 20 ++++++++++++++++---- test/test_display.py | 33 +++++++-------------------------- test/test_picklist.py | 9 +++++---- 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index c45350f..b8db1f5 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -23,7 +23,7 @@ import awss.debg as debg from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.10' +__version__ = '0.9.6.11' def main(): # pragma: no cover @@ -153,6 +153,7 @@ def cmd_list(options): """ (i_info, title_out) = gather_data(options) if i_info: + awsc.get_all_aminames(i_info) title_out = "Instance List - " + title_out list_instances(title_out, i_info) else: @@ -206,7 +207,7 @@ def cmd_ssh(options): (tar_inst, tar_idx) = determine_inst(options.command, i_info, title_out) home_dir = os.environ['HOME'] if options.user is None: - tar_aminame = awsc.getaminame(i_info[tar_idx]['ami']) + tar_aminame = awsc.get_one_aminame(i_info[tar_idx]['ami']) # only first 5 chars of AMI-name used to avoid version numbers userlu = {"ubunt": "ubuntu", "debia": "admin", "fedor": "fedora", "cento": "centos", "openB": "root"} @@ -371,8 +372,8 @@ def list_instances(title_out, i_info, numbered=False): information for each instance. Args: - title_out (str): the title to display before the list - i_info (dict): information on instances and details + title_out (str): the title to display before the list. + i_info (dict): information on instances and details. numbered (bool): optional - indicates wheter the list should be displayed with numbers before each instance. This is used when called from user_picklist. @@ -385,12 +386,17 @@ def list_instances(title_out, i_info, numbered=False): if numbered: print("Instance {}#{}{}".format(C_WARN, i + 1, C_NORM)) - i_info[i]['aminame'] = awsc.getaminame(i_info[i]['ami']) - print("\tName: {0}{3}{1}\t\tID: {0}{4}{1}\t\tStatus: {2}{5}{1}". + # print("\tName: {0}{3}{1}\t\tID: {0}{4}{1}\t\tStatus: {2}{5}{1}". + # format(C_TI, C_NORM, C_STAT[i_info[i]['state']], + # i_info[i]['tag']['Name'], i_info[i]['id'], + # i_info[i]['state'])) + # print("\tAMI: {0}{2}{1}\tAMI Name: {0}{3}{1}\n". + # format(C_TI, C_NORM, i_info[i]['ami'], i_info[i]['aminame'])) + print(" Name: {0}{3:<20}{1}ID: {0}{4:<20}{1:<18}Status: {2}{5}{1}". format(C_TI, C_NORM, C_STAT[i_info[i]['state']], i_info[i]['tag']['Name'], i_info[i]['id'], i_info[i]['state'])) - print("\tAMI: {0}{2}{1}\tAMI Name: {0}{3}{1}\n". + print(" AMI: {0}{2:<21}{1}AMI Name: {0}{3}{1}\n". format(C_TI, C_NORM, i_info[i]['ami'], i_info[i]['aminame'])) debg.dprintx("All Data") @@ -446,6 +452,7 @@ def user_picklist(title_out, i_info, command): """ entry_valid = False + awsc.get_all_aminames(i_info) list_instances(title_out, i_info, True) msg_txt = ("Enter {0}#{1} of instance to {3} ({0}1{1}-{0}{4}{1})" " [{2}0 aborts{1}]: ".format(C_WARN, C_NORM, C_TI, diff --git a/awss/awsc.py b/awss/awsc.py index 3a7cad9..2924daa 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -47,11 +47,23 @@ def get_inst_info(qry_string): return qry_results -def getaminame(inst_img_id): - """Get Image_Name for the image_id specified. +def get_all_aminames(i_info): + """Get Image_Name for each instance in i_info. + + Args: + i_info (dict): information on instances and details. + Returns: + i_info (dict): i_info is returned with the aminame + added for each instance. - Connects to ec2 resource.Image object, which is slower - than retrieving info from the ec2 resource.Instance object. + """ + for i in i_info: + i_info[i]['aminame'] = EC2R.Image(i_info[i]['ami']).name + return i_info + + +def get_one_aminame(inst_img_id): + """Get Image_Name for the image_id specified. Args: inst_img_id (str): image_id to get name value from. diff --git a/test/test_display.py b/test/test_display.py index 3ba031c..1695c1e 100644 --- a/test/test_display.py +++ b/test/test_display.py @@ -1,60 +1,42 @@ """Test module for list_instances function in awss.""" from __future__ import print_function -import mock from awss import list_instances import awss.debg as debg -amiNameList = { - 'ami-16efb076': 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64', - 'ami-3e21725e': 'ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64', - 'ami-e09acc80': 'suse-sles-12-sp2-v20161214-hvm-ssd-x86_64', - 'ami-165a0876': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2', - 'ami-2cade64c': 'RHEL-7.3_HVM_GA-20161026-x86_64-1-Hourly2-GP2'} - - -def getlocalaminame(ami): - """Return name for ami-number passed as arg.""" - amiName = amiNameList[ami] - return amiName - - -infoIdsOnly = { - 0: {'id': 'i-0c875fafa1e71327b'}, - 1: {'id': 'i-03120c2544fdf5b6f'}, - 2: {'id': 'i-0014d16e7f68ce746'}, - 3: {'id': 'i-0c459a77e113c6c9c'}, - 4: {'id': 'i-0341963e139617c75'}} - - -infoNoAmiName = { +infoAll = { 0: {'ami': 'ami-16efb076', + 'aminame': 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64', 'id': 'i-0c875fafa1e71327b', 'pub_dns_name': '', 'ssh_key': 'robert', 'state': 'stopped', 'tag': {'Name': 'Ubuntu', 'Role': 'Test'}}, 1: {'ami': 'ami-3e21725e', + 'aminame': 'ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64', 'id': 'i-0c459a77e113c6c9c', 'pub_dns_name': '', 'ssh_key': 'robert', 'state': 'stopped', 'tag': {'Name': 'Ubuntu'}}, 2: {'ami': 'ami-e09acc80', + 'aminame': 'suse-sles-12-sp2-v20161214-hvm-ssd-x86_64', 'id': 'i-0014d16e7f68ce746', 'pub_dns_name': '', 'ssh_key': 'robert', 'state': 'stopped', 'tag': {'Name': 'Suse', 'Role': 'Test'}}, 3: {'ami': 'ami-165a0876', + 'aminame': 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2', 'id': 'i-03120c2544fdf5b6f', 'pub_dns_name': '', 'ssh_key': 'robert', 'state': 'stopped', 'tag': {'Name': 'Amazon', 'Role': 'Test'}}, 4: {'ami': 'ami-2cade64c', + 'aminame': 'RHEL-7.3_HVM_GA-20161026-x86_64-1-Hourly2-GP2', 'id': 'i-0341963e139617c75', 'pub_dns_name': '', 'ssh_key': 'robert', @@ -62,11 +44,10 @@ def getlocalaminame(ami): 'tag': {'Name': 'RHEL', 'Role': 'Test'}}} -@mock.patch('awss.awsc.getaminame', getlocalaminame, create=True) def test_display_list(capsys): """Test list_instances function in awss.""" debg.init(False, False) outputTitle = "Test Report" - list_instances(outputTitle, infoNoAmiName) + list_instances(outputTitle, infoAll) out, err = capsys.readouterr() assert err == "" diff --git a/test/test_picklist.py b/test/test_picklist.py index 6e716b5..650f405 100644 --- a/test/test_picklist.py +++ b/test/test_picklist.py @@ -80,9 +80,10 @@ def test_determine_inst(ids, kys, anames, ide, tidx): global counter counter = 0 - def getlocalaminame(ami): - amiName = anames[ami] - return amiName + def getlclaminame(ami): + for i in ami: + ami[i]['aminame'] = anames[ami[i]['ami']] + return ami def RetKey(item1): global counter @@ -90,7 +91,7 @@ def RetKey(item1): counter += 1 return keye - with mock.patch('awss.awsc.getaminame', getlocalaminame, create=True): + with mock.patch('awss.awsc.get_all_aminames', getlclaminame, create=True): with mock.patch('awss.obtain_input', RetKey, create=True): if ide: debg.init(True, True) From dd510aac4d978643ebe12b7f4d470cd9ed95f61b Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 09:05:30 -0700 Subject: [PATCH 26/35] =?UTF-8?q?Possible=20work-around=20code=20analysis?= =?UTF-8?q?=20bug=20that=20doesn=E2=80=99t=20know=20ec2r.Image(instance=5F?= =?UTF-8?q?id).name=20returns=20a=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] --- awss/awsc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awss/awsc.py b/awss/awsc.py index 2924daa..1a86899 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -58,7 +58,8 @@ def get_all_aminames(i_info): """ for i in i_info: - i_info[i]['aminame'] = EC2R.Image(i_info[i]['ami']).name + # i_info[i]['aminame'] = EC2R.Image(i_info[i]['ami']).name + i_info[i]['aminame'] = getattr(EC2R.Image(i_info[i]['ami']), 'name') return i_info From c741322a78fe96f3259ee4db5e8dc258ffdac5f4 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 09:35:29 -0700 Subject: [PATCH 27/35] =?UTF-8?q?Possible=20work-around=20#2=20for=20code?= =?UTF-8?q?=20analysis=20bug=20that=20doesn=E2=80=99t=20know=20ec2r.Image(?= =?UTF-8?q?instance=5Fid).name=20returns=20a=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] --- awss/awsc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awss/awsc.py b/awss/awsc.py index 1a86899..59335f0 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -58,8 +58,8 @@ def get_all_aminames(i_info): """ for i in i_info: - # i_info[i]['aminame'] = EC2R.Image(i_info[i]['ami']).name - i_info[i]['aminame'] = getattr(EC2R.Image(i_info[i]['ami']), 'name') + # pylint: disable=maybe-no-member + i_info[i]['aminame'] = EC2R.Image(i_info[i]['ami']).name return i_info From d806f54450d6f7d657ccc863759d177a22c09ba8 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 10:00:11 -0700 Subject: [PATCH 28/35] Added list_tags function called by list_instances for each instance to print tags --- awss/__init__.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index b8db1f5..dc7cfab 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -21,9 +21,9 @@ import awss.awsc as awsc import awss.debg as debg -from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT +from awss.colors import C_NORM, C_HEAD, C_HEAD2, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.11' +__version__ = '0.9.6.12' def main(): # pragma: no cover @@ -386,23 +386,35 @@ def list_instances(title_out, i_info, numbered=False): if numbered: print("Instance {}#{}{}".format(C_WARN, i + 1, C_NORM)) - # print("\tName: {0}{3}{1}\t\tID: {0}{4}{1}\t\tStatus: {2}{5}{1}". - # format(C_TI, C_NORM, C_STAT[i_info[i]['state']], - # i_info[i]['tag']['Name'], i_info[i]['id'], - # i_info[i]['state'])) - # print("\tAMI: {0}{2}{1}\tAMI Name: {0}{3}{1}\n". - # format(C_TI, C_NORM, i_info[i]['ami'], i_info[i]['aminame'])) - print(" Name: {0}{3:<20}{1}ID: {0}{4:<20}{1:<18}Status: {2}{5}{1}". + print(" {6}Name: {1}{3:<22}{1}ID: {0}{4:<20}{1:<18}Status: {2}{5}{1}". format(C_TI, C_NORM, C_STAT[i_info[i]['state']], i_info[i]['tag']['Name'], i_info[i]['id'], - i_info[i]['state'])) - print(" AMI: {0}{2:<21}{1}AMI Name: {0}{3}{1}\n". + i_info[i]['state'], C_HEAD2)) + print(" AMI: {0}{2:<23}{1}AMI Name: {0}{3}{1}". format(C_TI, C_NORM, i_info[i]['ami'], i_info[i]['aminame'])) - + list_tags(i_info[i]['tag']) debg.dprintx("All Data") debg.dprintx(i_info, True) +def list_tags(tags): + """Print tags in dict passed so they allign with listing above.""" + c = 1 + padlu = {1: 38, 2: 49} + for k, v in tags.items(): + if k != "Name": + if c < 3: + pada = padlu[c] + sys.stdout.write(" {2}{0}:{3} {1}". + format(k, v, C_HEAD2, C_NORM).ljust(pada)) + c += 1 + else: + sys.stdout.write("{2}{0}:{3} {1}\n".format(k, v, C_HEAD2, + C_NORM)) + c = 1 + print("\n") + + def determine_inst(command, i_info, title_out): """Determine the instance-id of the target instance. From bed9d56cd6af54dc9ce6941b751a4020cbb3f4a2 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 12:44:51 -0700 Subject: [PATCH 29/35] Moved calculation of ssh login-user to new func cmd_ssh_user New algorithm can now determine the user-login name if the os-identifier is anywhere in the AMI-name All tags displayed under listing are now sorted AMI-Name displayed in listing is now truncated at 41 characters to keep display clean --- awss/__init__.py | 43 +++++++++++++++++++++++++++++++------------ setup.py | 2 +- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index dc7cfab..f611592 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -18,15 +18,16 @@ from builtins import range import argparse import sys +import operator import awss.awsc as awsc import awss.debg as debg from awss.colors import C_NORM, C_HEAD, C_HEAD2, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.6.12' +__version__ = '0.9.7rc1' -def main(): # pragma: no cover +def main(): # pragma: no cover """Collect user args and call command funct. Collect command line args and setup environment then call @@ -208,14 +209,9 @@ def cmd_ssh(options): home_dir = os.environ['HOME'] if options.user is None: tar_aminame = awsc.get_one_aminame(i_info[tar_idx]['ami']) - # only first 5 chars of AMI-name used to avoid version numbers - userlu = {"ubunt": "ubuntu", "debia": "admin", "fedor": "fedora", - "cento": "centos", "openB": "root"} - options.user = userlu.get(tar_aminame[:5], "ec2-user") - debg.dprint("loginuser Calculated: ", options.user) + options.user = cmd_ssh_user(tar_aminame) else: debg.dprint("LoginUser set by user: ", options.user) - if options.nopem: debg.dprint("Connect string: ", "ssh {}@{}". format(options.user, i_info[tar_idx]['pub_dns_name'])) @@ -234,6 +230,29 @@ def cmd_ssh(options): shell=True) +def cmd_ssh_user(tar_aminame): + """Calculate instance login-username based on image-name. + + Args: + tar_aminame (str): name of the image. + Returns: + username (str): name for ssh based on AMI-name. + + """ + # first 5 chars of AMI-name can be anywhere in AMI-Name + userlu = {"ubunt": "ubuntu", "debia": "admin", "fedor": "root", + "cento": "centos", "openB": "root"} + usertmp = [value for key, value in userlu.items() if key in + tar_aminame.lower()] + if usertmp: + username = usertmp[0] + else: + username = "ec2-user" + debg.dprint("loginuser Calculated: ", username) + + return username + + def gather_data(options): """Get Data specific for command selected. @@ -280,7 +299,6 @@ def process_results(qry_results): for k in range(len(inst_tags)): tag_dict[inst_tags[k]['Key']] = inst_tags[k]['Value'] i_info[i]['tag'] = tag_dict - # reference the name tag as: i_info[i]['tag']['Name'] debg.dprint("numInstances: ", len(i_info)) debg.dprintx("Details except AMI-name") debg.dprintx(i_info, True) @@ -390,7 +408,7 @@ def list_instances(title_out, i_info, numbered=False): format(C_TI, C_NORM, C_STAT[i_info[i]['state']], i_info[i]['tag']['Name'], i_info[i]['id'], i_info[i]['state'], C_HEAD2)) - print(" AMI: {0}{2:<23}{1}AMI Name: {0}{3}{1}". + print(" AMI: {0}{2:<23}{1}AMI Name: {0}{3:.41}{1}". format(C_TI, C_NORM, i_info[i]['ami'], i_info[i]['aminame'])) list_tags(i_info[i]['tag']) debg.dprintx("All Data") @@ -398,10 +416,11 @@ def list_instances(title_out, i_info, numbered=False): def list_tags(tags): - """Print tags in dict passed so they allign with listing above.""" + """Print tags in dict so they allign with listing above.""" + tags_sorted = sorted(tags.items(), key=operator.itemgetter(0)) c = 1 padlu = {1: 38, 2: 49} - for k, v in tags.items(): + for k, v in tags_sorted: if k != "Name": if c < 3: pada = padlu[c] diff --git a/setup.py b/setup.py index 33001a3..5a85af4 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ name='awss', packages=['awss'], entry_points={'console_scripts': ['awss=awss:main']}, - version='0.9.6', + version='0.9.7rc1', author="Robert Peteuil", author_email="robert.s.peteuil@gmail.com", url='https://github.com/robertpeteuil/aws-shortcuts', From be0dfd0aaf2242f1676b3badf0135fd7ce19dda6 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 13:00:24 -0700 Subject: [PATCH 30/35] Py3 Compatibility Fixes --- awss/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awss/__init__.py b/awss/__init__.py index f611592..27f9b31 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -24,7 +24,7 @@ import awss.debg as debg from awss.colors import C_NORM, C_HEAD, C_HEAD2, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.7rc1' +__version__ = '0.9.7rc2' def main(): # pragma: no cover @@ -242,7 +242,7 @@ def cmd_ssh_user(tar_aminame): # first 5 chars of AMI-name can be anywhere in AMI-Name userlu = {"ubunt": "ubuntu", "debia": "admin", "fedor": "root", "cento": "centos", "openB": "root"} - usertmp = [value for key, value in userlu.items() if key in + usertmp = [value for key, value in list(userlu.items()) if key in tar_aminame.lower()] if usertmp: username = usertmp[0] @@ -417,7 +417,7 @@ def list_instances(title_out, i_info, numbered=False): def list_tags(tags): """Print tags in dict so they allign with listing above.""" - tags_sorted = sorted(tags.items(), key=operator.itemgetter(0)) + tags_sorted = sorted(list(tags.items()), key=operator.itemgetter(0)) c = 1 padlu = {1: 38, 2: 49} for k, v in tags_sorted: From aa6c0b087b6cf86ef685475567cf919a5419b004 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 14:27:21 -0700 Subject: [PATCH 31/35] Prepare for 0.9.7 Release --- README.rst | 59 ++++++++++++++++++++++++++++-------------------- awss/__init__.py | 4 ++-- setup.py | 2 +- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/README.rst b/README.rst index 30d2f7f..9cfdb63 100644 --- a/README.rst +++ b/README.rst @@ -5,11 +5,11 @@ List, start, stop and ssh to AWS instances using Name or Instance-ID --------------------------------------------------------------------------------- -|TRAVIS| |AppVeyor| |Codacy Grade| |Codacy Cov| |PyPi release| |Py ver| |license s| +|TRAVIS| |AppVeyor| |Codacy Grade| |Codacy Cov| |PyPi release| |Py ver| |license sm| -------------- -AWS Shortcuts (awss) allows listing, starting, stopping and connecting to instances by name or instance-id. Future versions will also allow referencing instances with any ``Tag`` : ``Value`` combination. +AWS Shortcuts (awss) allows listing, starting, stopping and connecting to instances by name, instance-id, and supports wilcards. The ``awss list`` command displays every tag & value for each instances along with their status and core info. In the near future you will also be able to use any combination of ``Tag`` : ``Value`` combinations when specifying instances. Overview @@ -17,7 +17,7 @@ Overview ``awss`` has the following sub-commands: ``list``, ``start``, ``stop``, and ``ssh``. -- SSH to an Instance: ``awss ssh NAME`` or ``aws ssh -i ID`` +- SSH to an Instance: ``awss ssh NAME`` or ``awss ssh -i ID`` - Additional paramters described in `Details`_. @@ -33,14 +33,20 @@ Details - SSH to Instance: ``awss ssh NAME`` or ``awss ssh -i ID`` - - automatically calculates login-name based on the image-type of the instance + - typing ``awss ssh`` without a name or ID will display all running instances + + - this allows the user to select from the list if they can't remember the name. + - this can be combined with wilcards, for example ``awss ssh U*`` to display + a list of instances starting with "U" to select from. + + - the login-name is automatically calculated based on the image-type of the instance - override the calculated login-name ``-u USERNAME`` - connect without PEM keys (if properly configured) ``-p`` - command specific help ``awss ssh -h`` - List Instances: ``awss list`` - - list all instances (default). + - list all instances (default), or use wilcards ``awss list D*`` - list running instances ``-r`` or ``--running`` - list stopped instances ``-s`` or ``--stopped`` - list instances with specified name ``awss list NAME`` @@ -53,11 +59,23 @@ Details - Start Instance: ``awss start NAME`` or ``awss start -i ID`` + - typing ``awss start`` without a name or ID will display all stopped instances + + - this allows the user to select from the list if they can't remember the name. + - this can be combined with wilcards, for example ``awss start U*`` to display + a list of instances starting with "U" to select from. + - start instance by name or instance-id - command specific help ``awss start -h`` - Stop Instance: ``awss stop NAME`` or ``awss stop -i ID`` + - typing ``awss stop`` without a name or ID will display all running instances + + - this allows the user to select from the list if they can't remember the name. + - this can be combined with wilcards, for example ``awss stop U*`` to display + a list of instances starting with "U" to select from. + - start instance by name or instance-id - command specific help ``awss stop -h`` @@ -66,19 +84,7 @@ Target Instance Verification The ``start``, ``stop``, and ``ssh`` commands verify that their action will apply to only one instance. -- This check is performed by looking for other instances that match: - - - the instance-specification given (name or ID). - - the running-state appropriate for the command. - -- If multiple instances match these conditions, they are listed and the user selects the intended target. - -The **running-state** appropriate for each command is as follows: - -- The ``ssh`` command looks for **running** instances (it cannot connect to stopped instanced). -- The ``stop`` command looks for **running** instances (it cannot stop instances that are already stopped). -- The ``start`` command looks for **stopped** instances (it cannot start instances that are already started). -- The ``list`` command looks at all instances, unless optional parameters have been specified to narrow its search to **running**, **stopped** instances. +- When multiple instances match the specifications given, a selection list of matching instances is displayed, and the user enters the number displayed next to the intended target. Platforms & Python Versions Tested @@ -102,6 +108,8 @@ This utility can be installed with ``pip``: pip install awss + + .. |PyPi release| image:: https://img.shields.io/pypi/v/awss.svg :target: https://pypi.python.org/pypi/awss @@ -113,22 +121,25 @@ This utility can be installed with ``pip``: .. |Codacy Grade| image:: https://api.codacy.com/project/badge/Grade/477279a80d31407a99fb3c3551e066cb :target: https://www.codacy.com/app/robertpeteuil/aws-shortcuts?utm_source=github.com&utm_medium=referral&utm_content=robertpeteuil/aws-shortcuts&utm_campaign=Badge_Grade + .. |Codacy Cov| image:: https://api.codacy.com/project/badge/Coverage/477279a80d31407a99fb3c3551e066cb :target: https://www.codacy.com/app/robertpeteuil/aws-shortcuts?utm_source=github.com&utm_medium=referral&utm_content=robertpeteuil/aws-shortcuts&utm_campaign=Badge_Coverage -.. |license| image:: https://img.shields.io/github/license/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf - :target: https://github.com/robertpeteuil/aws-shortcuts -.. |license s| image:: https://img.shields.io/badge/license-MIT-1c64bf.svg?style=flat-square - :target: https://github.com/robertpeteuil/aws-shortcuts -.. |lang| image:: https://img.shields.io/badge/language-python-3572A5.svg?style=flat-square - :target: https://github.com/robertpeteuil/aws-shortcuts .. |Py ver| image:: https://img.shields.io/pypi/pyversions/awss.svg :target: https://pypi.python.org/pypi/bandit/ :alt: Python Versions +.. |license sm| image:: https://img.shields.io/badge/license-MIT-1c64bf.svg?style=flat-square + :target: https://github.com/robertpeteuil/aws-shortcuts + + .. |GitHub issues| image:: https://img.shields.io/github/issues/robertpeteuil/aws-shortcuts.svg :target: https://github.com/robertpeteuil/aws-shortcuts .. |GitHub release| image:: https://img.shields.io/github/release/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf :target: https://github.com/robertpeteuil/aws-shortcuts .. |Code Climate| image:: https://codeclimate.com/github/robertpeteuil/aws-shortcuts/badges/gpa.svg?style=flat-square :target: https://codeclimate.com/github/robertpeteuil/aws-shortcuts +.. |lang| image:: https://img.shields.io/badge/language-python-3572A5.svg?style=flat-square + :target: https://github.com/robertpeteuil/aws-shortcuts +.. |license| image:: https://img.shields.io/github/license/robertpeteuil/aws-shortcuts.svg?colorB=1c64bf + :target: https://github.com/robertpeteuil/aws-shortcuts diff --git a/awss/__init__.py b/awss/__init__.py index 27f9b31..bb5f4c1 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -24,7 +24,7 @@ import awss.debg as debg from awss.colors import C_NORM, C_HEAD, C_HEAD2, C_TI, C_WARN, C_ERR, C_STAT -__version__ = '0.9.7rc2' +__version__ = '0.9.7' def main(): # pragma: no cover @@ -184,7 +184,7 @@ def cmd_startstop(options): state_term = ('CurrentState', 'PreviousState') for i, j in enumerate(state_term): resp[i] = response["{0}".format(filt)][0]["{0}".format(j)]['Name'] - print("\tCurrent State: {}{}{} - Previous State: {}{}{}\n". + print("Current State: {}{}{} - Previous State: {}{}{}\n". format(C_STAT[resp[0]], resp[0], C_NORM, C_STAT[resp[1]], resp[1], C_NORM)) diff --git a/setup.py b/setup.py index 5a85af4..2aea194 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ name='awss', packages=['awss'], entry_points={'console_scripts': ['awss=awss:main']}, - version='0.9.7rc1', + version='0.9.7', author="Robert Peteuil", author_email="robert.s.peteuil@gmail.com", url='https://github.com/robertpeteuil/aws-shortcuts', From bc36f0bddead7fd130ae3eb93863dab7aaab561b Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 14:51:15 -0700 Subject: [PATCH 32/35] Update README.rst --- README.rst | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 9cfdb63..10ae17a 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,11 @@ Overview - Start Instance: ``awss start NAME`` or ``awss start -i ID`` - Stop Instance: ``awss stop NAME`` or ``awss stop -i ID`` +Example output of ``awss list`` +------------------------------- + +.. image:: https://cloud.githubusercontent.com/assets/1554603/25595372/6c3bd5e2-2e79-11e7-9ebc-4730f93c2cb6.png + Details ------- @@ -79,12 +84,15 @@ Details - start instance by name or instance-id - command specific help ``awss stop -h`` -Target Instance Verification ----------------------------- +Target Instance Determination +----------------------------- + +The ``start``, ``stop``, and ``ssh`` commands check if multiple instances match the parameters. +If so, the the matching instances are listed, and the user selects the intended target. -The ``start``, ``stop``, and ``ssh`` commands verify that their action will apply to only one instance. +Example screenshot of selecting instance from list: -- When multiple instances match the specifications given, a selection list of matching instances is displayed, and the user enters the number displayed next to the intended target. +.. image:: https://cloud.githubusercontent.com/assets/1554603/25595396/84b4ef64-2e79-11e7-922f-d645b007af57.png Platforms & Python Versions Tested From ffefb073a49d7dca292ce668b0b239d595406666 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 14:54:39 -0700 Subject: [PATCH 33/35] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 10ae17a..2e923dd 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ AWS Shortcuts for Command-Line Instance Control =============================================== -List, start, stop and ssh to AWS instances using Name or Instance-ID ---------------------------------------------------------------------------------- +List, start, stop and ssh to AWS instances using Name, ID or Wilcards +--------------------------------------------------------------------- |TRAVIS| |AppVeyor| |Codacy Grade| |Codacy Cov| |PyPi release| |Py ver| |license sm| From 0fdd181d3d5cb4fccec61bd6c3566eed220ab1b3 Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 14:55:03 -0700 Subject: [PATCH 34/35] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2e923dd..dfe6116 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ AWS Shortcuts for Command-Line Instance Control =============================================== -List, start, stop and ssh to AWS instances using Name, ID or Wilcards ---------------------------------------------------------------------- +List, start, stop and ssh to AWS instances using Name, ID and Wilcards +---------------------------------------------------------------------- |TRAVIS| |AppVeyor| |Codacy Grade| |Codacy Cov| |PyPi release| |Py ver| |license sm| From cc9596f4dc56b99d3226525840516486cd6a37ed Mon Sep 17 00:00:00 2001 From: Robert Peteuil Date: Mon, 1 May 2017 14:59:48 -0700 Subject: [PATCH 35/35] Prepare for 0.9.7 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index dfe6116..58fedd5 100644 --- a/README.rst +++ b/README.rst @@ -87,7 +87,7 @@ Details Target Instance Determination ----------------------------- -The ``start``, ``stop``, and ``ssh`` commands check if multiple instances match the parameters. +The ``start``, ``stop``, and ``ssh`` commands check if multiple instances match the parameters. If so, the the matching instances are listed, and the user selects the intended target. Example screenshot of selecting instance from list: