diff --git a/.pylintrc b/.pylintrc index 837b9dc..f044577 100644 --- a/.pylintrc +++ b/.pylintrc @@ -68,7 +68,7 @@ disable=import-star-module-level,old-octal-literal,oct-method,print-statement,un # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. -output-format=text +output-format=colorized # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be @@ -231,7 +231,7 @@ notes=FIXME,XXX,TODO [SIMILARITIES] # Minimum lines number of a similarity. -min-similarity-lines=4 +min-similarity-lines=10 # Ignore comments when computing similarities. ignore-comments=yes @@ -320,14 +320,14 @@ exclude-protected=_asdict,_fields,_replace,_source,_make [DESIGN] # Maximum number of arguments for function / method -max-args=5 +max-args=10 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body -max-locals=15 +max-locals=20 # Maximum number of return / yield for function / method body max-returns=6 diff --git a/.travis.yml b/.travis.yml index 0d9be00..0cd43df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,4 @@ language: python python: - "2.7" install: "pip install -r dev-requirements.pip" -script: py.test tests +script: pylint lecli && py.test tests diff --git a/README.md b/README.md index acb0bbf..16139d7 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ The user and account management functionality of the CLI can only be used with a It is worth noting that if your account does not have a specific owner set then some user management functions may fail with a 500 error; to check if an owner is set the 'getowner' command below can be used. If no owner is set then you must regenerate the owner API key, at which point you will be asked to set an account owner. ####List Users -The 'userlist' command will return a list of all users that have access to the account for which the CLI has been configured. The command will return the users first and last name, email address, user ID and the last time they logged in. The 'userlist' command does not accept any arguments. +The 'userlist' command will return a list of all users that have access to the account for which the CLI has been configured. The command will return the users first and last name, email address, user key and the last time they logged in. The 'userlist' command does not accept any arguments. Example usage: ``` @@ -148,14 +148,14 @@ lecli userlist The 'useradd' command allows you to add a user to your account. There are two ways to add users, depending on whether they are a new or existing user. A new user is a user that has no Logentries account. -To add a new user you must provide their first and last name, and email address. If successfully added the CLI will print the users account information, including their newly generated user Id. A user added via the CLI must then go to https://logentries.com/user/password-reset/ and enter their email address. They will then be sent a link that they can use to setup the password for their new account. +To add a new user you must provide their first and last name, and email address. If successfully added the CLI will print the users account information, including their newly generated user key. A user added via the CLI must then go to https://logentries.com/user/password-reset/ and enter their email address. They will then be sent a link that they can use to setup the password for their new account. A new user can be added using the following command ``` lecli useradd -f John -l Smith -e john.smith@email.com ``` -To add an existing user (i.e. a user that already has a Logentries account, even if not associated with your account), you must first obtain their user Id. The user can obtain their user Id from the account management page of the Logentries application at https://logentries.com +To add an existing user (i.e. a user that already has a Logentries account, even if not associated with your account), you must first obtain their user key. The user can obtain their user key from the account management page of the Logentries application at https://logentries.com An existing user can be added to your account use the following command ``` @@ -210,9 +210,9 @@ Rename a team with the given UUID to given name. ####Add User to a Team Add a new user to a team with the given UUID and user UUID respectively. - lecli addusertoteam + lecli addusertoteam ####Delete User from a Team Add a new user to a team with the given UUID and user UUID respectively. - lecli deleteuserfromteam + lecli deleteuserfromteam diff --git a/dev-requirements.pip b/dev-requirements.pip index db88596..892bcf1 100644 --- a/dev-requirements.pip +++ b/dev-requirements.pip @@ -7,4 +7,5 @@ requests==2.9.1 pytz==2016.4 termcolor==1.1.0 tabulate==0.7.5 -appdirs==1.4.0 \ No newline at end of file +appdirs==1.4.0 +pylint==1.6.4 diff --git a/lecli/__init__.py b/lecli/__init__.py index 2e2e03f..4c374db 100644 --- a/lecli/__init__.py +++ b/lecli/__init__.py @@ -1,2 +1,6 @@ +""" +lecli __init__.py +""" + from lecli._version import __version__ __author__ = 'Logentries by Rapid7' diff --git a/lecli/_version.py b/lecli/_version.py index 73e3bb4..9e07685 100644 --- a/lecli/_version.py +++ b/lecli/_version.py @@ -1 +1,4 @@ -__version__ = '0.3.2' +""" +Version module. We should change the version here when we need to. +""" +__version__ = '0.4.0' diff --git a/lecli/apiutils.py b/lecli/apiutils.py index 1655d74..bb2ee47 100644 --- a/lecli/apiutils.py +++ b/lecli/apiutils.py @@ -1,3 +1,6 @@ +""" +Configuration and api keys util module. +""" import ConfigParser import base64 import hashlib @@ -9,24 +12,47 @@ import lecli +AUTH_SECTION = 'Auth' CONFIG = ConfigParser.ConfigParser() +CONFIG_FILE_PATH = os.path.join(user_config_dir(lecli.__name__), 'config.ini') + + +def print_config_error_and_exit(section=None, config_key=None, value=None): + """ + Print appropriate apiutils error message and exit. + """ + if not section: + print "Error: Configuration file '%s' not found" % CONFIG_FILE_PATH + elif not config_key: + print "Error: Section '%s' was not found in configuration file(%s)" % ( + section, CONFIG_FILE_PATH) + elif not value: + print "Error: Configuration key for %s was not found in configuration file(%s) in '%s' " \ + "section" % (config_key, CONFIG_FILE_PATH, section) + else: + print "Error: %s = '%s' is not of correct length in section: '%s' of your configuration " \ + "file: '%s'" % (config_key, value, section, CONFIG_FILE_PATH) + + exit(1) def init_config(): + """ + Initialize config file in the OS specific config path if there is no config file exists. + """ config_dir = user_config_dir(lecli.__name__) - config_file_path = os.path.join(config_dir, 'config.ini') - if not os.path.exists(config_file_path): + if not os.path.exists(CONFIG_FILE_PATH): if not os.path.exists(config_dir): os.makedirs(config_dir) dummy_config = ConfigParser.ConfigParser() - config_file = open(config_file_path, 'w') - dummy_config.add_section('Auth') - dummy_config.set('Auth', 'account_resource_id', '') - dummy_config.set('Auth', 'owner_api_key_id', '') - dummy_config.set('Auth', 'owner_api_key', '') - dummy_config.set('Auth', 'rw_api_key', '') + config_file = open(CONFIG_FILE_PATH, 'w') + dummy_config.add_section(AUTH_SECTION) + dummy_config.set(AUTH_SECTION, 'account_resource_id', '') + dummy_config.set(AUTH_SECTION, 'owner_api_key_id', '') + dummy_config.set(AUTH_SECTION, 'owner_api_key', '') + dummy_config.set(AUTH_SECTION, 'rw_api_key', '') dummy_config.add_section('LogNicknames') dummy_config.add_section("LogGroups") @@ -35,23 +61,26 @@ def init_config(): print "An empty config file created in path %s, please check and configure it. To learn " \ "how to get necessary api keys, go to this Logentries documentation page: " \ "https://docs.logentries.com/docs/api-keys" % \ - config_file_path + CONFIG_FILE_PATH else: - print "Config file exists in the path: " + config_file_path + print "Config file exists in the path: " + CONFIG_FILE_PATH exit(1) def load_config(): - config_file = os.path.join(user_config_dir(lecli.__name__), 'config.ini') - files_read = CONFIG.read(config_file) + """ + Load config from OS specific config path into ConfigParser object. + :return: + """ + files_read = CONFIG.read(CONFIG_FILE_PATH) if len(files_read) != 1: - print "Error: Config file '%s' not found, generating one..." % config_file + print "Error: Config file '%s' not found, generating one..." % CONFIG_FILE_PATH init_config() exit(1) - if not CONFIG.has_section('Auth'): - print "Error: Config file '%s' is missing Auth section" % config_file - exit(1) + print_config_error_and_exit() + if not CONFIG.has_section(AUTH_SECTION): + print_config_error_and_exit(section=AUTH_SECTION) def get_ro_apikey(): @@ -59,15 +88,16 @@ def get_ro_apikey(): Get read-only api key from the config file. """ - ro_apikey = None + config_key = 'ro_api_key' try: - ro_apikey = CONFIG.get('Auth', 'ro_api_key') - if len(ro_apikey) != 36: - print 'Error: Read-only API Key not of correct length' + ro_api_key = CONFIG.get(AUTH_SECTION, config_key) + if len(ro_api_key) != 36: + print_config_error_and_exit(AUTH_SECTION, 'Read-only API key(%s)' % config_key, + ro_api_key) + else: + return ro_api_key except ConfigParser.NoOptionError: - print 'Error: Read-only API Key not configured in configuration file' - exit(1) - return ro_apikey + print_config_error_and_exit(AUTH_SECTION, 'Read-only API key(%s)' % config_key) def get_rw_apikey(): @@ -75,15 +105,16 @@ def get_rw_apikey(): Get read-write api key from the config file. """ - rw_apikey = None + config_key = 'rw_api_key' try: - rw_apikey = CONFIG.get('Auth', 'rw_api_key') - if len(rw_apikey) != 36: - print 'Error: Read/Write API Key not of correct length' + rw_api_key = CONFIG.get(AUTH_SECTION, config_key) + if len(rw_api_key) != 36: + print_config_error_and_exit(AUTH_SECTION, 'Read/Write API key(%s)' % config_key, + rw_api_key) + else: + return rw_api_key except ConfigParser.NoOptionError: - print 'Error: Read/Write API Key not configured in configuration file' - exit(1) - return rw_apikey + print_config_error_and_exit(AUTH_SECTION, 'Read/Write API key(%s)' % config_key) def get_owner_apikey(): @@ -91,15 +122,17 @@ def get_owner_apikey(): Get owner api key from the config file. """ - owner_apikey = None + config_key = 'owner_api_key' try: - owner_apikey = CONFIG.get('Auth', 'owner_api_key') - if len(owner_apikey) != 36: - print 'Error: Owner API Key not of correct length' + owner_api_key = CONFIG.get(AUTH_SECTION, config_key) + if len(owner_api_key) != 36: + print_config_error_and_exit(AUTH_SECTION, 'Owner API key(%s)' % config_key, + owner_api_key) + return + else: + return owner_api_key except ConfigParser.NoOptionError: - print 'Error: Owner API Key not configured in configuration file' - exit(1) - return owner_apikey + print_config_error_and_exit(AUTH_SECTION, 'Owner API key(%s)' % config_key) def get_owner_apikey_id(): @@ -107,15 +140,17 @@ def get_owner_apikey_id(): Get owner api key id from the config file. """ - owner_apikey_id = None + config_key = 'owner_api_key_id' try: - owner_apikey_id = CONFIG.get('Auth', 'owner_api_key_id') + owner_apikey_id = CONFIG.get(AUTH_SECTION, config_key) if len(owner_apikey_id) != 36: - print 'Error: Owner API Key ID not of correct length' + print_config_error_and_exit(AUTH_SECTION, 'Owner API key ID(%s)' % config_key, + owner_apikey_id) + return + else: + return owner_apikey_id except ConfigParser.NoOptionError: - print 'Error: Owner API Key ID not configured in configuration file' - exit(1) - return owner_apikey_id + print_config_error_and_exit(AUTH_SECTION, 'Owner API key ID(%s)' % config_key) def get_account_resource_id(): @@ -123,15 +158,17 @@ def get_account_resource_id(): Get account resource id from the config file. """ - account_resource_id = None + config_key = 'account_resource_id' try: - account_resource_id = CONFIG.get('Auth', 'account_resource_id') + account_resource_id = CONFIG.get(AUTH_SECTION, config_key) if len(account_resource_id) != 36: - print 'Error: Account Resource ID not of correct length' + print_config_error_and_exit(AUTH_SECTION, 'Account Resource ID(%s)' % config_key, + account_resource_id) + return + else: + return account_resource_id except ConfigParser.NoOptionError: - print 'Error: Account Resource ID not configured in configuration file' - exit(1) - return account_resource_id + print_config_error_and_exit(AUTH_SECTION, 'Account Resource ID(%s)' % config_key) def get_named_logkey_group(name): @@ -141,18 +178,20 @@ def get_named_logkey_group(name): :param name: name of the group """ - groups = dict(CONFIG.items('LogGroups')) - name = name.lower() - if name in groups: - logkeys = filter(None, str(groups[name]).splitlines()) - for logkey in logkeys: - if len(logkey) != 36: - print 'Error: Logkey is not of correct length.' - return - return logkeys - else: - print "Error: No group with name '%s'" % name - return None + section = 'LogGroups' + try: + groups = dict(CONFIG.items(section)) + name = name.lower() + if name in groups: + logkeys = [line for line in str(groups[name]).splitlines() if line is not None] + for logkey in logkeys: + if len(logkey) != 36: + print_config_error_and_exit(section, 'Named Logkey Group(%s)' % name, logkey) + return logkeys + else: + print_config_error_and_exit(section, 'Named Logkey Group(%s)' % name) + except ConfigParser.NoSectionError: + print_config_error_and_exit(section) def get_named_logkey(name): @@ -162,35 +201,42 @@ def get_named_logkey(name): :param name: name of the log key """ - nicknames = dict(CONFIG.items('LogNicknames')) - name = name.lower() - if name in nicknames: - logkey = (nicknames[name],) - if len(logkey[0]) != 36: - print 'Error: Logkey is not of correct length. ' + section = 'LogNicknames' + + try: + named_logkeys = dict(CONFIG.items(section)) + name = name.lower() + if name in named_logkeys: + logkey = (named_logkeys[name],) + if len(logkey[0]) != 36: + print_config_error_and_exit(section, 'Named Logkey(%s)' % name, logkey) + else: + return logkey else: - return logkey - else: - print "Error: No nickname with name '%s'" % name - return None + print_config_error_and_exit(section, 'Named Logkey(%s)' % name) + except ConfigParser.NoSectionError: + print_config_error_and_exit(section) -def get_query_from_nickname(qnick): +def get_named_query(name): """ Get named query from config file. - :param qnick: query nick + :param name: query nick """ - qnicknames = dict(CONFIG.items('QueryNicknames')) - qnick = qnick.lower() + section = 'QueryNicknames' - if qnick in qnicknames: - query = qnicknames[qnick] - return query - else: - print "Error: No query nickname with name '%s'" % qnick - return None + try: + named_queries = dict(CONFIG.items(section)) + name = name.lower() + if name in named_queries: + query = named_queries[name] + return query + else: + print_config_error_and_exit(section, 'Named Query(%s)' % name) + except ConfigParser.NoSectionError: + print_config_error_and_exit(section) def generate_headers(api_key_type, method=None, action=None, body=None): diff --git a/lecli/cli.py b/lecli/cli.py index bcba8fc..3476f8d 100644 --- a/lecli/cli.py +++ b/lecli/cli.py @@ -1,5 +1,9 @@ +""" +Main lecli module powered by click library. +""" import click +import lecli from lecli import apiutils from lecli import query_api from lecli import team_api @@ -7,7 +11,7 @@ @click.group() -@click.version_option(version=0.2) +@click.version_option(version=lecli.__version__) def cli(): """Logentries Command Line Interface""" # load configs from config.ini file in user_config_dir depending on running OS @@ -54,21 +58,21 @@ def renameteam(teamid, name): @cli.command() @click.argument('teamid', type=click.STRING, default=None) -@click.argument('userid', type=click.STRING, default=None) -def addusertoteam(teamid, userid): +@click.argument('userkey', type=click.STRING, default=None) +def addusertoteam(teamid, userkey): """Update the team with the provided id with name and user. This will add the user to this team if it exists""" - team_api.add_user_to_team(teamid, userid)\ + team_api.add_user_to_team(teamid, userkey)\ @cli.command() @click.argument('teamid', type=click.STRING, default=None) -@click.argument('userid', type=click.STRING, default=None) -def deleteuserfromteam(teamid, userid): +@click.argument('userkey', type=click.STRING, default=None) +def deleteuserfromteam(teamid, userkey): """Update the team with the provided id with name and user. This will add the user to this team if it exists""" - team_api.delete_user_from_team(teamid, userid) + team_api.delete_user_from_team(teamid, userkey) @cli.command() @@ -89,9 +93,10 @@ def deleteuserfromteam(teamid, userid): help='Date/Time to query from (ISO-8601 datetime)') @click.option('--dateto', help='Date/Time to query to (ISO-8601 datetime)') -def query(logkeys, lognick, loggroup, leql, querynick, timefrom, timeto, datefrom, dateto): +@click.option('-e', '--expand', is_flag=True, + help='Expand JSON') +def query(logkeys, lognick, loggroup, leql, querynick, timefrom, timeto, datefrom, dateto, expand): """Query logs using LEQL""" - if lognick is not None: logkeys = apiutils.get_named_logkey(lognick) @@ -101,12 +106,12 @@ def query(logkeys, lognick, loggroup, leql, querynick, timefrom, timeto, datefro if all([leql, querynick]): click.echo("Cannot define a LEQL query and query nickname in the same query request") elif querynick is not None: - leql = apiutils.get_query_from_nickname(querynick) + leql = apiutils.get_named_query(querynick) if all([logkeys, leql, timefrom, timeto]): - query_api.post_query(logkeys, leql, time_from=timefrom, time_to=timeto) + query_api.post_query(logkeys, leql, time_from=timefrom, time_to=timeto, expand=expand) elif all([logkeys, leql, datefrom, dateto]): - query_api.post_query(logkeys, leql, date_from=datefrom, date_to=dateto) + query_api.post_query(logkeys, leql, date_from=datefrom, date_to=dateto, expand=expand) else: click.echo("Example usage: lecli query 12345678-aaaa-bbbb-1234-1234cb123456 -q " "'where(method=GET) calculate(count)' -f 1465370400 -t 1465370500") @@ -135,7 +140,9 @@ def query(logkeys, lognick, loggroup, leql, querynick, timefrom, timeto, datefro help='Date/Time to get events from (ISO-8601 datetime)') @click.option('--dateto', help='Date/Time to get events to (ISO-8601 datetime)') -def events(logkeys, lognick, loggroup, timefrom, timeto, datefrom, dateto): +@click.option('-e', '--expand', is_flag=True, + help='Expand JSON') +def events(logkeys, lognick, loggroup, timefrom, timeto, datefrom, dateto, expand): """Get log events""" if lognick is not None: @@ -144,9 +151,9 @@ def events(logkeys, lognick, loggroup, timefrom, timeto, datefrom, dateto): logkeys = apiutils.get_named_logkey_group(loggroup) if all([logkeys, timefrom, timeto]): - query_api.get_events(logkeys, time_from=timefrom, time_to=timeto) + query_api.get_events(logkeys, time_from=timefrom, time_to=timeto, expand=expand) elif all([logkeys, datefrom, dateto]): - query_api.get_events(logkeys, date_from=datefrom, date_to=dateto) + query_api.get_events(logkeys, date_from=datefrom, date_to=dateto, expand=expand) else: click.echo("Example usage: lecli events 12345678-aaaa-bbbb-1234-1234cb123456 " "-f 1465370400 -t 1465370500") @@ -167,7 +174,9 @@ def events(logkeys, lognick, loggroup, timefrom, timeto, datefrom, dateto): @click.option('-l', '--last', default=1200, help='Time window from now to now-X in seconds over which events will be returned ' '(Defaults to 20 mins)') -def recentevents(logkeys, lognick, loggroup, last): +@click.option('-e', '--expand', is_flag=True, + help='Expand JSON') +def recentevents(logkeys, lognick, loggroup, last, expand): """Get recent log events""" if lognick is not None: @@ -176,7 +185,7 @@ def recentevents(logkeys, lognick, loggroup, last): logkeys = apiutils.get_named_logkey_group(loggroup) if all([logkeys, last]): - query_api.get_recent_events(logkeys, last) + query_api.get_recent_events(logkeys, last, expand=expand) else: click.echo( @@ -199,14 +208,14 @@ def listusers(): help='Last name of user to be added') @click.option('-e', '--email', type=click.STRING, help='Email address of user to be added') -@click.option('-u', '--userid', type=click.STRING, - help='User ID of user to be added') +@click.option('-u', '--userkey', type=click.STRING, + help='User Key of user to be added') @click.option('--force', is_flag=True, help='Force adding user with confirmation prompt') -def adduser(first, last, email, userid, force): +def adduser(first, last, email, userkey, force): """Add a user to account""" - if not any((first, last, email, userid)) or all((first, last, email, userid)): + if not any((first, last, email, userkey)) or all((first, last, email, userkey)): click.echo('Example usage\n' + 'Add a new user: lecli adduser -f John -l Smith -e john.smith@email.com\n' + 'Add an existing user: lecli adduser -u 1343423') @@ -218,24 +227,24 @@ def adduser(first, last, email, userid, force): if click.confirm('Please confirm you want to add user ' + first + ' ' + last): user_api.add_new_user(first, last, email) - elif userid is not None: + elif userkey is not None: if force: - user_api.add_existing_user(userid) + user_api.add_existing_user(userkey) else: - if click.confirm('Please confirm you want to add user with user ID ' + userid): - user_api.add_existing_user(userid) + if click.confirm('Please confirm you want to add user with User Key ' + userkey): + user_api.add_existing_user(userkey) @cli.command() -@click.option('-u', '--userid', type=click.STRING, - help='User ID of user to be deleted') -def deleteuser(userid): +@click.option('-u', '--userkey', type=click.STRING, + help='User Key of user to be deleted') +def deleteuser(userkey): """Delete a user from account""" - if userid is None: + if userkey is None: click.echo('Example usage: lecli deleteuser -u 12345678-aaaa-bbbb-1234-1234cb123456') else: - user_api.delete_user(userid) + user_api.delete_user(userkey) @cli.command() diff --git a/lecli/query_api.py b/lecli/query_api.py index e2d852c..c7400a7 100644 --- a/lecli/query_api.py +++ b/lecli/query_api.py @@ -1,4 +1,8 @@ +""" +Query API module. +""" from __future__ import division + import json import time @@ -8,12 +12,11 @@ from lecli import apiutils - def _url(path): """ Get rest query url of a specific path. """ - return 'https://rest.logentries.com/query/' + path + '/' + return 'https://rest.logentries.com/query/%s/' % path def response_error(response): @@ -43,7 +46,7 @@ def response_error(response): return False -def handle_response(response): +def handle_response(response, expand=False): """ Handle response. Exit if it has any errors, continue if status code is 202, print response if status code is 200. @@ -51,19 +54,19 @@ def handle_response(response): if response_error(response) is True: # Check response has no errors exit(1) elif response.status_code == 200: - print_response(response) + print_response(response, expand) if 'links' in response.json(): next_url = response.json()['links'][0]['href'] next_response = fetch_results(next_url) - handle_response(next_response) + handle_response(next_response, expand) return return elif response.status_code == 202: - continue_request(response) + continue_request(response, expand) return -def continue_request(response): +def continue_request(response, expand=False): """ Continue making request to the url in the response. """ @@ -71,7 +74,7 @@ def continue_request(response): if 'links' in response.json(): continue_url = response.json()['links'][0]['href'] new_response = fetch_results(continue_url) - handle_response(new_response) + handle_response(new_response, expand) def fetch_results(provided_url): @@ -86,7 +89,7 @@ def fetch_results(provided_url): exit(1) -def get_recent_events(log_keys, last_x_seconds=200): +def get_recent_events(log_keys, last_x_seconds=200, expand=False): """ Get recent events belonging to provided log_keys in the last_x_seconds. """ @@ -99,13 +102,13 @@ def get_recent_events(log_keys, last_x_seconds=200): try: response = requests.post(_url('logs'), headers=apiutils.generate_headers('rw'), json=payload) - handle_response(response) + handle_response(response, expand) except requests.exceptions.RequestException as error: print error exit(1) -def get_events(log_keys, time_from=None, time_to=None, date_from=None, date_to=None): +def get_events(log_keys, time_from=None, time_to=None, date_from=None, date_to=None, expand=False): """ Get events belonging to log_keys and within the time range provided. """ @@ -122,13 +125,14 @@ def get_events(log_keys, time_from=None, time_to=None, date_from=None, date_to=N try: response = requests.post(_url('logs'), headers=apiutils.generate_headers('rw'), json=payload) - handle_response(response) + handle_response(response, expand) except requests.exceptions.RequestException as error: print error exit(1) -def post_query(log_keys, query_string, time_from=None, time_to=None, date_from=None, date_to=None): +def post_query(log_keys, query_string, time_from=None, time_to=None, date_from=None, date_to=None, + expand=False): """ Post query to Logentries. """ @@ -145,23 +149,23 @@ def post_query(log_keys, query_string, time_from=None, time_to=None, date_from=N try: response = requests.post(_url('logs'), headers=apiutils.generate_headers('rw'), json=payload) - handle_response(response) + handle_response(response, expand) except requests.exceptions.RequestException as error: print error exit(1) -def print_response(response): +def print_response(response, expand=False): """ Print response in a human readable way. """ if 'events' in response.json(): - prettyprint_events(response) + prettyprint_events(response, expand) elif 'statistics' in response.json(): prettyprint_statistics(response) -def prettyprint_events(response): +def prettyprint_events(response, expand=False): """ Print events in a human readable way. """ @@ -169,11 +173,14 @@ def prettyprint_events(response): for event in data['events']: time_value = datetime.datetime.fromtimestamp(event['timestamp'] / 1000) human_ts = time_value.strftime('%Y-%m-%d %H:%M:%S') - try: - message = json.loads(event['message']) - print colored(str(human_ts), 'red') + '\t' + colored(json.dumps(message, indent=4, separators={':', ';'}) - , 'white') - except ValueError: + if expand: + try: + message = json.loads(event['message']) + print colored(str(human_ts), 'red') + '\t' + \ + colored(json.dumps(message, indent=4, separators={':', ';'}), 'white') + except ValueError: + print colored(str(human_ts), 'red') + '\t' + colored(event['message'], 'white') + else: print colored(str(human_ts), 'red') + '\t' + colored(event['message'], 'white') diff --git a/lecli/team_api.py b/lecli/team_api.py index 36b7088..25b4e50 100644 --- a/lecli/team_api.py +++ b/lecli/team_api.py @@ -1,3 +1,6 @@ +""" +Team API module. +""" import json import requests @@ -161,9 +164,9 @@ def rename_team(team_id, team_name): exit(1) -def add_user_to_team(team_id, user_id): +def add_user_to_team(team_id, user_key): """ - Add user with the provided user_id to team with provided team_id. + Add user with the provided user_key to team with provided team_id. """ headers = apiutils.generate_headers('rw') params = {'teamid': team_id} @@ -176,9 +179,9 @@ def add_user_to_team(team_id, user_id): 'team': { 'name': response.json()['team']['name'], 'users': [ - # we are doing a patch request here so it's safe to include the user_id + # we are doing a patch request here so it's safe to include the user_key # we want to add here - {'id': user_id} + {'id': user_key} ] } } @@ -186,11 +189,11 @@ def add_user_to_team(team_id, user_id): try: response = requests.patch(url, json=params, headers=headers) if response_error(response): # Check response has no errors - print 'Adding user to team with id: %s failed, status code: %d' \ + print 'Adding user to team with key: %s failed, status code: %d' \ % (team_id, response.status_code) exit(1) elif response.status_code == 200: - print "Added user with id: '%s' to team" % user_id + print "Added user with key: '%s' to team" % user_key except requests.exceptions.RequestException as error: print error exit(1) @@ -203,7 +206,7 @@ def add_user_to_team(team_id, user_id): exit(1) -def delete_user_from_team(team_id, user_id): +def delete_user_from_team(team_id, user_key): """ Delete a user from a team. """ @@ -218,18 +221,18 @@ def delete_user_from_team(team_id, user_id): 'team': { 'name': response.json()['team']['name'], 'users': [user for user in response.json()['team']['users'] if user['id'] != - user_id] + user_key] } } headers = apiutils.generate_headers('rw') try: response = requests.put(url, json=params, headers=headers) if response_error(response): # Check response has no errors - print 'Deleting user from team with id: %s failed, status code: %d' \ + print 'Deleting user from team with key: %s failed, status code: %d' \ % (team_id, response.status_code) exit(1) elif response.status_code == 200: - print "Deleted user with id: '%s' from team: %s" % (user_id, team_id) + print "Deleted user with key: '%s' from team: %s" % (user_key, team_id) except requests.exceptions.RequestException as error: print error exit(1) diff --git a/lecli/user_api.py b/lecli/user_api.py index 74d13d9..abe5ebd 100644 --- a/lecli/user_api.py +++ b/lecli/user_api.py @@ -1,3 +1,6 @@ +""" +User API module. +""" import json import requests @@ -11,11 +14,11 @@ def _url(endpoint): Get rest query url of account resource id. """ if endpoint == 'owner': - return 'https://rest.logentries.com/management/accounts/' + str( - apiutils.get_account_resource_id()) + '/owners' + return 'https://rest.logentries.com/management/accounts/%s/owners' % \ + apiutils.get_account_resource_id() elif endpoint == 'user': - return 'https://rest.logentries.com/management/accounts/' + str( - apiutils.get_account_resource_id()) + '/users' + return 'https://rest.logentries.com/management/accounts/%s/users' % \ + apiutils.get_account_resource_id() def response_error(response): @@ -65,18 +68,18 @@ def handle_create_user_response(response): print 'Failed to add user - User may have already been added this account or have a ' \ 'Logentries account' print 'To add a new user: lecli adduser -f John -l Smyth -e john@smyth.com' - print 'To add an existing user using their user ID: lecli adduser -u ' \ + print 'To add an existing user using their User Key: lecli adduser -u ' \ '12345678-aaaa-bbbb-1234-1234cb123456' exit(1) if response.status_code == 200: user = response.json()['user'] - print 'Added user to account:\nName: %s %s \nLogin: %s \nEmail: %s \nUser ID: %s' % \ + print 'Added user to account:\nName: %s %s \nLogin: %s \nEmail: %s \nUser Key: %s' % \ (user['first_name'], user['last_name'], user['login_name'], user['email'], user['id']) if response.status_code == 201: user = response.json()['user'] - print 'Added user to account:\nName: %s %s \nLogin: %s \nEmail: %s \nUser ID: %s' % \ + print 'Added user to account:\nName: %s %s \nLogin: %s \nEmail: %s \nUser Key: %s' % \ (user['first_name'], user['last_name'], user['login_name'], user['email'], user['id']) if response.status_code == 403: @@ -103,10 +106,12 @@ def add_new_user(first_name, last_name, email): """ action = 'management/accounts/' + str(apiutils.get_account_resource_id()) + '/users' json_content = { - "user": {"email": str(email), - "first_name": str(first_name), - "last_name": str(last_name) - } + "user": + { + "email": str(email), + "first_name": str(first_name), + "last_name": str(last_name) + } } body = json.dumps(json_content) headers = apiutils.generate_headers('owner', method='POST', action=action, body=body) @@ -119,11 +124,11 @@ def add_new_user(first_name, last_name, email): exit(1) -def add_existing_user(user_id): +def add_existing_user(user_key): """ Add a user that already exist to the current account. """ - url = _url('user') + '/' + str(user_id) + url = _url('user') + '/' + str(user_key) action = url.split("com/")[1] headers = apiutils.generate_headers('owner', method='POST', action=action, body='') @@ -135,11 +140,11 @@ def add_existing_user(user_id): exit(1) -def delete_user(user_id): +def delete_user(user_key): """ Delete a user from the current account. """ - url = _url('user') + '/' + str(user_id) + url = _url('user') + '/' + str(user_key) action = url.split("com/")[1] headers = apiutils.generate_headers('owner', method='DELETE', action=action, body='') diff --git a/tests/examples/misc_examples.py b/tests/examples/misc_examples.py index 8d9acce..32db2a3 100644 --- a/tests/examples/misc_examples.py +++ b/tests/examples/misc_examples.py @@ -2,7 +2,7 @@ TEST_OWNER_APIKEY_ID = 'test_owner_apikey_id' TEST_ACCOUNT_RESOURCE_ID = 'test_account_resource_id' -TEST_USER_ID = "test_user_id" +TEST_USER_KEY = "test_user_key" DUMMY_USER_CONTENT = {"user": {"first_name": "", "last_name": "", "login_name": "", diff --git a/tests/test_apiutils.py b/tests/test_apiutils.py index 5c332c4..d8db507 100644 --- a/tests/test_apiutils.py +++ b/tests/test_apiutils.py @@ -1,6 +1,7 @@ import ConfigParser import hmac +import pytest from mock import patch, Mock from lecli import apiutils @@ -72,11 +73,13 @@ def test_get_valid_ro_apikey(): def test_get_invalid_ro_apikey(capsys): with patch.object(ConfigParser.ConfigParser, 'get', return_value=misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH): - ro_api_key = apiutils.get_ro_apikey() - out, err = capsys.readouterr() + with pytest.raises(SystemExit): + ro_api_key = apiutils.get_ro_apikey() + out, err = capsys.readouterr() - assert ro_api_key == misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH - assert 'Error: Read-only API Key not of correct length\n' == out + assert ro_api_key is None + assert misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH in out + assert 'is not of correct length' in out def test_get_valid_rw_apikey(): @@ -90,12 +93,14 @@ def test_get_valid_rw_apikey(): def test_get_invalid_rw_apikey(capsys): with patch.object(ConfigParser.ConfigParser, 'get', return_value=misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH): - apiutils.load_config = Mock() - rw_api_key = apiutils.get_rw_apikey() - out, err = capsys.readouterr() + with pytest.raises(SystemExit): + apiutils.load_config = Mock() + result = apiutils.get_rw_apikey() + out, err = capsys.readouterr() - assert rw_api_key == misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH - assert 'Error: Read/Write API Key not of correct length\n' == out + assert result is None + assert misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH in out + assert 'is not of correct length' in out def test_get_valid_owner_apikey(): @@ -109,12 +114,14 @@ def test_get_valid_owner_apikey(): def test_get_invalid_owner_apikey(capsys): with patch.object(ConfigParser.ConfigParser, 'get', return_value=misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH): - apiutils.load_config = Mock() - owner_api_key = apiutils.get_owner_apikey() - out, err = capsys.readouterr() + with pytest.raises(SystemExit): + apiutils.load_config = Mock() + result = apiutils.get_owner_apikey() + out, err = capsys.readouterr() - assert owner_api_key == misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH - assert 'Error: Owner API Key not of correct length\n' == out + assert result is None + assert misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH in out + assert 'is not of correct length' in out def test_get_valid_owner_apikey_id(): @@ -128,12 +135,14 @@ def test_get_valid_owner_apikey_id(): def test_get_invalid_owner_apikey_id(capsys): with patch.object(ConfigParser.ConfigParser, 'get', return_value=misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH): - apiutils.load_config = Mock() - owner_api_key_id = apiutils.get_owner_apikey_id() - out, err = capsys.readouterr() + with pytest.raises(SystemExit): + apiutils.load_config = Mock() + result = apiutils.get_owner_apikey_id() + out, err = capsys.readouterr() - assert owner_api_key_id == misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH - assert 'Error: Owner API Key ID not of correct length\n' == out + assert result is None + assert misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH in out + assert 'is not of correct length' in out def test_get_valid_account_resource_id(): @@ -147,11 +156,13 @@ def test_get_valid_account_resource_id(): def test_get_invalid_account_resource_id(capsys): with patch.object(ConfigParser.ConfigParser, 'get', return_value=misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH): - account_resource_id = apiutils.get_account_resource_id() - out, err = capsys.readouterr() + with pytest.raises(SystemExit): + result = apiutils.get_account_resource_id() + out, err = capsys.readouterr() - assert account_resource_id == misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH - assert 'Error: Account Resource ID not of correct length\n' == out + assert result is None + assert misc_ex.TEST_APIKEY_WITH_INVALID_LENGTH in out + assert 'is not of correct length' in out def test_get_valid_named_logkey(): @@ -171,11 +182,14 @@ def test_case_insensitivity_of_named_logkey(): def test_get_invalid_named_logkey(capsys): with patch.object(ConfigParser.ConfigParser, 'items', return_value=[('test-logkey-nick', misc_ex.TEST_LOG_KEY)]): - nick_to_query = 'test-logkey-nick_invalid' - logkey = apiutils.get_named_logkey(nick_to_query) - out, err = capsys.readouterr() - assert logkey is None - assert 'Error: No nickname with name ' + '\'' + nick_to_query + '\'\n' == out + with pytest.raises(SystemExit): + nick_to_query = 'test-logkey-nick_invalid' + logkey = apiutils.get_named_logkey(nick_to_query) + out, err = capsys.readouterr() + + assert logkey is None + assert nick_to_query in out + assert 'was not found' in out def test_get_valid_named_group_key(): @@ -195,8 +209,11 @@ def test_case_insensitivity_of_named_groups_key(): def test_get_invalid_named_group_key(capsys): with patch.object(ConfigParser.ConfigParser, 'items', return_value=[('test-log-group-nick', ["test-log-key1", "test-log-key2"])]): - nick_to_query = 'test-log-group-nick-invalid' - logkeys = apiutils.get_named_logkey_group(nick_to_query) - out, err = capsys.readouterr() - assert logkeys is None - assert 'Error: No group with name ' + '\'' + nick_to_query + '\'\n' == out + with pytest.raises(SystemExit): + nick_to_query = 'test-log-group-nick-invalid' + result = apiutils.get_named_logkey_group(nick_to_query) + out, err = capsys.readouterr() + + assert result is None + assert nick_to_query in out + assert 'was not found' in out diff --git a/tests/test_lecli.py b/tests/test_lecli.py index 86cebf1..6333321 100644 --- a/tests/test_lecli.py +++ b/tests/test_lecli.py @@ -20,8 +20,8 @@ def test_userdel(mocked_delete_user): assert result.output == "Example usage: lecli deleteuser -u 12345678-aaaa-bbbb-1234-1234cb123456\n" - runner.invoke(cli.deleteuser, ['-u', misc_ex.TEST_USER_ID]) - mocked_delete_user.assert_called_once_with(misc_ex.TEST_USER_ID) + runner.invoke(cli.deleteuser, ['-u', misc_ex.TEST_USER_KEY]) + mocked_delete_user.assert_called_once_with(misc_ex.TEST_USER_KEY) @patch('lecli.cli.user_api.add_new_user') diff --git a/tests/test_team_api.py b/tests/test_team_api.py index 5fd39fc..cb2f753 100644 --- a/tests/test_team_api.py +++ b/tests/test_team_api.py @@ -125,13 +125,13 @@ def test_add_user_to_team(mocked_url, mocked_rw_apikey, mocked_account_resource_ team_api.add_user_to_team(test_team_id, user_id_to_add) out, err = capsys.readouterr() - assert "Added user with id: '%s' to team\n" % user_id_to_add == out + assert "Added user with key: '%s' to team\n" % user_id_to_add == out @httpretty.activate @patch('lecli.apiutils.get_account_resource_id') @patch('lecli.apiutils.get_rw_apikey') @patch('lecli.team_api._url') -def test_add_user_to_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): +def test_delete_user_from_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): test_team_id = misc_ex.TEST_TEAM_ID mocked_url.return_value = misc_ex.MOCK_TEAMSAPI_URL mocked_rw_apikey.return_value = misc_ex.TEST_APIKEY_WITH_VALID_LENGTH @@ -148,5 +148,5 @@ def test_add_user_to_team(mocked_url, mocked_rw_apikey, mocked_account_resource_ team_api.delete_user_from_team(test_team_id, user_id_to_add) out, err = capsys.readouterr() - assert "Deleted user with id: '%s' from team" % user_id_to_add in out + assert "Deleted user with key: '%s' from team" % user_id_to_add in out diff --git a/tests/test_userapi.py b/tests/test_userapi.py index ba0cc7d..22cdc44 100644 --- a/tests/test_userapi.py +++ b/tests/test_userapi.py @@ -5,8 +5,8 @@ from mock import patch from tabulate import tabulate -from lecli import user_api from examples import misc_examples as misc_ex +from lecli import user_api @httpretty.activate @@ -43,10 +43,10 @@ def test_delete_user(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, mocked_account_resource_id.return_value = misc_ex.TEST_ACCOUNT_RESOURCE_ID mocked_url.return_value = misc_ex.MOCK_USERAPI_URL - dest_url = misc_ex.MOCK_USERAPI_URL + '/' + str(misc_ex.TEST_USER_ID) + dest_url = misc_ex.MOCK_USERAPI_URL + '/' + str(misc_ex.TEST_USER_KEY) httpretty.register_uri(httpretty.DELETE, dest_url, status=204) - user_api.delete_user(misc_ex.TEST_USER_ID) + user_api.delete_user(misc_ex.TEST_USER_KEY) out, err = capsys.readouterr() assert 'Deleted user' in out @@ -64,13 +64,13 @@ def test_add_existing_user(mocked_url, mocked_owner_apikey, mocked_owner_apikey_ mocked_account_resource_id.return_value = misc_ex.TEST_ACCOUNT_RESOURCE_ID mocked_url.return_value = misc_ex.MOCK_USERAPI_URL - dest_url = misc_ex.MOCK_USERAPI_URL + '/' + str(misc_ex.TEST_USER_ID) + dest_url = misc_ex.MOCK_USERAPI_URL + '/' + str(misc_ex.TEST_USER_KEY) httpretty.register_uri(httpretty.POST, dest_url, body=json.dumps(misc_ex.DUMMY_USER_CONTENT), status=200, content_type='application/json') - user_api.add_existing_user(misc_ex.TEST_USER_ID) + user_api.add_existing_user(misc_ex.TEST_USER_KEY) out, err = capsys.readouterr() assert "Added user to account" in out @@ -138,7 +138,8 @@ def test_handle_create_user_response_status_200_with_success(mocked_account_reso @httpretty.activate @patch('lecli.apiutils.get_account_resource_id') -def test_handle_create_user_response_status_200_with_already_exists_error(mocked_account_resource_id, capsys): +def test_handle_create_user_response_status_200_with_already_exists_error( + mocked_account_resource_id, capsys): httpretty.register_uri(httpretty.GET, misc_ex.MOCK_USERAPI_URL, content_type="application/json", status=201, diff --git a/tox.ini b/tox.ini index 6dffadd..2a15523 100644 --- a/tox.ini +++ b/tox.ini @@ -19,4 +19,4 @@ commands=py.test -v -s --cov=lecli [testenv:pylint] basepython=python2.7 deps=pylint -commands=pylint -f colorized lecli +commands=pylint lecli