diff --git a/.appveyor.yml b/.appveyor.yml index f1943c3..82d46dc 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -18,13 +18,15 @@ environment: - TOXENV: py35 - TOXENV: py36 - TOXENV: flake8 + - TOXENV: bandit - TOXENV: readme cache: .tox build_script: - cmd: pip install -e . test_script: - cmd: tox -deploy_script: +deploy: off +on_success: - cmd: >- echo Test Success 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/.pylintrc b/.pylintrc index 3b0465d..64b65c5 100644 --- a/.pylintrc +++ b/.pylintrc @@ -59,11 +59,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= - -#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,W0603,W0123,W0612,E0401 [REPORTS] @@ -189,7 +185,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 +198,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 @@ -289,7 +285,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). @@ -348,7 +344,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 diff --git a/.travis.yml b/.travis.yml index 605e356..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 @@ -51,4 +53,3 @@ after_success: - coverage combine - coverage xml - python-codacy-coverage -r coverage.xml - - codeclimate-test-reporter diff --git a/README.rst b/README.rst index cafb847..58fedd5 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,15 @@ 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 and Wilcards +---------------------------------------------------------------------- -|TRAVIS| |Code Climate| |GitHub issues| |PyPi release| |lang| |license| - +|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 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, 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 @@ -20,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`_. @@ -31,24 +28,35 @@ 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 ------- - 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`` - 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`` @@ -56,36 +64,39 @@ 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`` - - start instance by name or instance-id - - command specific help ``awss stop -h`` - -Target Instance Verification ----------------------------- + - typing ``awss stop`` without a name or ID will display all running instances -The ``start``, ``stop``, and ``ssh`` commands verify that their action will apply to only one instance + - 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. -- This check is performed by looking for other instances that match: + - start instance by name or instance-id + - command specific help ``awss stop -h`` - - the instance-specification given (name or ID) - - the running-state appropriate for the command +Target Instance Determination +----------------------------- -- If multiple instances match these conditions, they are listed and the user selects the intended target. +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 **running-state** appropriate for each command is as follows: +Example screenshot of selecting instance from list: -- 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. +.. image:: https://cloud.githubusercontent.com/assets/1554603/25595396/84b4ef64-2e79-11e7-922f-d645b007af57.png -Supported Versions & Platforms ------------------------------- +Platforms & Python Versions Tested +---------------------------------- Python 2.7, 3.3, 3.4, 3.5, 3.6 @@ -105,17 +116,38 @@ 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 + + +.. |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 + +.. |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 -.. |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 diff --git a/awss/__init__.py b/awss/__init__.py index ec70d64..bb5f4c1 100755 --- a/awss/__init__.py +++ b/awss/__init__.py @@ -1,40 +1,38 @@ -"""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. URL: https://github.com/robertpeteuil/aws-shortcuts Author: Robert Peteuil @RobertPeteuil """ from __future__ import print_function +from builtins import input from builtins import range import argparse import sys -from awss.colors import C_NORM, C_HEAD, C_TI, C_WARN, C_ERR, C_STAT +import operator -from awss.getchar import _Getch 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' +__version__ = '0.9.7' -def main(): - """Prepare environment, collect args and call command funct. +def main(): # pragma: no cover + """Collect user args and call command funct. + + Collect command line args and setup environment then call + function for command specified in args. - This functions sets up the environment, collects relevant - agrs and cals the function specific to the command the user - has specified. """ parser = parser_setup() options = parser.parse_args() @@ -51,11 +49,14 @@ 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. + 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,40 +146,37 @@ 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) - if items: - i_info = awsc.getdetails(i_info) + """ + (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: - print("No instances found with parameters: %s" % (title_out)) + print("No instances found with parameters: {}".format(title_out)) 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. - """ + 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. + """ 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) + (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] @@ -186,98 +184,152 @@ 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("Current 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): - """ - '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. - """ + 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 + + """ 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) + (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(inst_img_id) - # 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) + tar_aminame = awsc.get_one_aminame(i_info[tar_idx]['ami']) + options.user = cmd_ssh_user(tar_aminame) else: 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_check(qry_string): - """ - Query String Validator: input: query string - in aws ec2 format +def cmd_ssh_user(tar_aminame): + """Calculate instance login-username based on image-name. - 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: + tar_aminame (str): name of the image. + Returns: + username (str): name for ssh based on AMI-name. - if qry_string == "ec2C.describe_instances()": - print("%sError%s - instance identifier not specified" % - (C_ERR, C_NORM)) - sys.exit(1) + """ + # 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 list(userlu.items()) if key in + tar_aminame.lower()] + if usertmp: + username = usertmp[0] else: - return + username = "ec2-user" + debg.dprint("loginuser Calculated: ", username) + return username + + +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, + 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. -def qry_create(options): """ - Query Creator: input: object - created by the parser - returns: Query_String, Report_Title + (qry_string, title_out) = qry_create(options) + qry_results = awsc.get_inst_info(qry_string) + i_info = process_results(qry_results) + return (i_info, title_out) + - 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. +def process_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. - 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. """ + 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'] + tag_dict = {} + for k in range(len(inst_tags)): + tag_dict[inst_tags[k]['Key']] = inst_tags[k]['Value'] + i_info[i]['tag'] = tag_dict + debg.dprint("numInstances: ", len(i_info)) + debg.dprintx("Details except AMI-name") + debg.dprintx(i_info, True) + return i_info + - qry_string = "EC2C.describe_instances(" +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, and creates the title for the 'list' function. + + 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 = filt_end = title_out = "" filt_st = "Filters=[" - filt_end = "" - title_out = "" 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 @@ -286,6 +338,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) @@ -294,7 +347,8 @@ def qry_create(options): title_out += "state: '%s'" % (options.inState) 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) @@ -303,147 +357,184 @@ 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): 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 - 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. + 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"): - """ - Displays Instance Information: - input: report_title, dict of inst_info, and - special_case_flag(optional) - - 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. - """ +def list_instances(title_out, i_info, numbered=False): + """Display a list of all instances and their details. + + 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): optional - indicates wheter the list should be + displayed with numbers before each instance. + This is used when called from user_picklist. - if numbered == "no": - print("\n%s\n" % (title_out)) - for i in range(len(i_info)): - if numbered == "yes": - 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, - 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)) + """ + if not numbered: + print("\n{}\n".format(title_out)) + + for i in i_info: + if numbered: + print("Instance {}#{}{}".format(C_WARN, i + 1, C_NORM)) + + 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'], C_HEAD2)) + 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") debg.dprintx(i_info, True) -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. - """ +def list_tags(tags): + """Print tags in dict so they allign with listing above.""" + tags_sorted = sorted(list(tags.items()), key=operator.itemgetter(0)) + c = 1 + padlu = {1: 38, 2: 49} + for k, v in tags_sorted: + 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. + + 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. + 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. + Raises: + SystemExit: if no instances are match parameters specified. + """ 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: {}".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): - """ - 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). - Entering a number outside of this range generates an invalid selection - error message, and the user is asked again. - """ + """Display list of instances matching args and ask user to select target. + + 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. + 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) - list_instances(title_out, i_info, "yes") - while entry_valid != "True": - 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(str(entry_raw), entry_raw) - sys.stdout.write(str(entry_display)) + """ + 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, + command, len(i_info))) + while not entry_valid: + entry_base = obtain_input(msg_txt) + try: + entry_raw = int(entry_base) + except ValueError: + entry_raw = 999 (tar_idx, entry_valid) = user_entry(entry_raw, command, len(i_info)) print() return tar_idx +def obtain_input(message_text): # pragma: no cover + """Perform input command as a function so it can be mocked.""" + return (input(message_text)) + + def user_entry(entry_raw, command, maxqty): - """ - User Entry Validation: input: user_entry, command, max_valid_entry - - 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. - """ + """Validate user entry and returns index and validity flag. + + 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): 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. + 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_int = int(entry_raw) - if entry_int == 0: - print("\n\n%saborting%s - %s instance\n" % - (C_ERR, C_NORM, command)) + """ + entry_valid = False + if not entry_raw: + print("{}aborting{} - {} instance\n". + format(C_ERR, C_NORM, command)) sys.exit() - elif entry_int >= 1 and entry_int <= maxqty: - entry_idx = entry_int - 1 - entry_valid = "True" + 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 + 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 f2cea1a..59335f0 100644 --- a/awss/awsc.py +++ b/awss/awsc.py @@ -1,144 +1,92 @@ -""" -This is part of the AWSS Utility located here: -https://github.com/robertpeteuil/aws-shortcuts +"""Communicate with AWS EC2 to get data and interact with instances. -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. +Functions for retrieving data for queried instances, retrieving +the name of the image of an instance (AMI Name), and for starting +or stopping instances. """ -from builtins import range import boto3 -import awss.debg as debg EC2C = "" EC2R = "" 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. - global EC2C - global EC2R - EC2C = boto3.client('ec2') - EC2R = boto3.resource('ec2') + 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. -def getids(qry_string=None): """ - Get All Instance-Ids that match the qry_string provided - input: qry_string (optional) - returns: dict containing Instance-Ids + global EC2C # pylint: disable=global-statement + global EC2R # pylint: disable=global-statement + EC2C = boto3.client('ec2') + EC2R = boto3.resource('ec2') - the dict will contain one indexed line, in the format: - "0: {'id': }" for each instance-id that matched - the query parameters. - 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. - """ +def get_inst_info(qry_string): + """Get details for instances that match the qry_string. - if qry_string is None: - qry_string = 'EC2C.describe_instances()' - summary_data = eval(qry_string) - i_info = {} - for i, j in enumerate(summary_data['Reservations']): - i_info[i] = {'id': j['Instances'][0]['InstanceId']} - debg.dprint("numInstances: ", len(i_info)) - debg.dprintx("InstanceIds Only") - debg.dprintx(i_info, True) - return i_info + Execute a query against the AWS EC2 client object, that is + based on the contents of qry_string. + Args: + qry_string (str): the query to be used against the aws ec2 client. + Returns: + qry_results (dict): raw information returned from AWS. -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. """ + qry_prefix = "EC2C.describe_instances(" + qry_real = qry_prefix + qry_string + ")" + qry_results = eval(qry_real) # pylint: disable=eval-used + return qry_results - if i_info is None: - i_info = getids() - for i in range(len(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 +def get_all_aminames(i_info): + """Get Image_Name for each instance in i_info. -def gettagvalue(inst_id, tag_title="Name"): - """ - Get Tag Value fpr Specified Instance-Id, and Tag - input: instance-id, and tag_title (optional) + Args: + i_info (dict): information on instances and details. + Returns: + i_info (dict): i_info is returned with the aminame + added for each instance. - If a tag_title is not provided, it defaults to retrieving - the value for the 'Name' tag. """ + for i in i_info: + # pylint: disable=maybe-no-member + i_info[i]['aminame'] = EC2R.Image(i_info[i]['ami']).name + return i_info - 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 +def get_one_aminame(inst_img_id): + """Get Image_Name for the image_id specified. -def getaminame(inst_img_id): - """ - Get Image_Name for the Image_Id specified - input: Instance_Image_Id - - 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. - """ + Args: + inst_img_id (str): image_id to get name value from. + Returns: + aminame (str): name of the image. + """ aminame = EC2R.Image(inst_img_id).name return aminame -def getsshinfo(inst_id): - """ - 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 - 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. + 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. -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 """ - tar_inst = EC2R.Instance(inst_id) thecmd = getattr(tar_inst, cmdtodo) response = thecmd() 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 56cde54..ed19fa9 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 +"""Debug print functions that execute if debug mode initialized. -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 @@ -17,50 +15,45 @@ 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. - global DEBUG - global DEBUGALL + Args: + deb1 (bool): value of DEBUG to set + deb2 (bool): optional - value of DEBUGALL to set, + defaults to False. + + """ + global DEBUG # pylint: disable=global-statement + global DEBUGALL # pylint: disable=global-statement DEBUG = deb1 DEBUGALL = deb2 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 + """ 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. + """ if DEBUGALL: if special: from pprint import pprint diff --git a/awss/getchar.py b/awss/getchar.py deleted file mode 100644 index 4caeb38..0000000 --- a/awss/getchar.py +++ /dev/null @@ -1,51 +0,0 @@ -'''Module holding cross-platform class for reading keys from the keyboard, -without requiring the enter key. -''' - -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 - try: - value = int(self.impl()) - except ValueError: - value = "999" - return value - - -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) - try: - tty.setraw(sys.stdin.fileno()) - ch = sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return ch - - -class _GetchWindows(object): - def __init__(self): - import msvcrt # noqa: F401 - - def __call__(self): # pragma: no cover - import msvcrt - return msvcrt.getch() 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/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 diff --git a/setup.py b/setup.py index 33001a3..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.6', + version='0.9.7', author="Robert Peteuil", author_email="robert.s.peteuil@gmail.com", url='https://github.com/robertpeteuil/aws-shortcuts', diff --git a/test/test_display.py b/test/test_display.py index b14b171..1695c1e 100644 --- a/test/test_display.py +++ b/test/test_display.py @@ -1,55 +1,53 @@ -#!/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 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): - amiName = amiNameList[ami] - return amiName - +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', + 'state': 'stopped', + 'tag': {'Name': 'RHEL', 'Role': 'Test'}}} -infoIdsOnly = { - 0: {'id': 'i-0c875fafa1e71327b'}, - 1: {'id': 'i-03120c2544fdf5b6f'}, - 2: {'id': 'i-0014d16e7f68ce746'}, - 3: {'id': 'i-0c459a77e113c6c9c'}, - 4: {'id': 'i-0341963e139617c75'}} - -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'}} - - -@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_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 f881a8d..650f405 100644 --- a/test/test_picklist.py +++ b/test/test_picklist.py @@ -1,86 +1,105 @@ -#!/usr/bin/env python +"""Test module for det_instance function in awss.""" from __future__ import print_function 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", "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', + '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', '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', + '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'}, - {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', + '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: {'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): + 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 - def getlocaldetails(info): - return ilist - - 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 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.getchar._Getch.int', RetKey, create=True): - if ide: + 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) + (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") - assert tar_inst == ide - else: - with pytest.raises(SystemExit): - debg.init(True, True) - tar_inst = det_instance("ssh", ids, "TEST") - pass + tar_inst = determine_inst("ssh", ids, "TEST") + pass diff --git a/test/test_querygen.py b/test/test_qrygen.py similarity index 58% rename from test/test_querygen.py rename to test/test_qrygen.py index af80a2a..afdaaed 100644 --- a/test/test_querygen.py +++ b/test/test_qrygen.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 @@ -36,46 +42,44 @@ 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): + """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)) diff --git a/tox.ini b/tox.ini index 38e51ec..0c501ab 100644 --- a/tox.ini +++ b/tox.ini @@ -1,68 +1,68 @@ # 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] deps = mock - pytest + pytest <= 3.0.7 coverage commands = coverage run -p --source awss -m py.test -# Linter Tests +# Linters [testenv:flake8] basepython = python3.5 skip_install = true deps = - radon - flake8 + radon <= 1.4.2 + flake8 <= 3.0.4 commands = flake8 awss/ test/ setup.py - flake8 . --radon-max-cc=5 - -# DOC TESTS -[testenv:readme] -basepython = python3.5 -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,.codeclimate.yml,requirements.txt,.coveragerc,.pylintrc,.appveyor.yml + flake8 awss --radon-max-cc=5 [testenv:pylint] basepython = python3.5 skip_install = true deps = - pyflakes - pylint + pyflakes <= 0.8.1 + pylint <= 1.71 commands = 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:linters] basepython = python3.5 skip_install = true deps = + {[testenv:flake8]deps} {[testenv:pylint]deps} - {[testenv:pydoc]deps} + {[testenv:bandit]deps} commands = + {[testenv:flake8]commands} {[testenv:pylint]commands} - {[testenv:pydoc]commands} + {[testenv:bandit]commands} + +# DOC TESTS +[testenv:readme] +basepython = python3.5 +skip_install = true +deps = + readme_renderer + doc8 <= 0.8.0 +commands = + python setup.py check -m -r -s + doc8 README.rst # RELEASE tooling [testenv:build] @@ -85,21 +85,9 @@ commands = twine upload --skip-existing dist/* [flake8] -ignore = D203 +ignore = D203,H301,H306 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 @@ -111,5 +99,10 @@ ignore=D001 [pydocstyle] ignore = D400 +# previously 'pep8' [pycodestyle] -ignore = F401 +ignore = + +[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