├── .github └── repository_metadata.yml ├── .gitignore ├── .pylintrc ├── .travis.yml ├── CHANGES.txt ├── LICENSE ├── MANIFEST.in ├── README.md ├── cortex.yaml ├── dev-requirements.pip ├── lecli ├── __init__.py ├── _version.py ├── api_key │ ├── __init__.py │ ├── api.py │ └── commands.py ├── api_utils.py ├── cli.py ├── log │ ├── __init__.py │ ├── api.py │ └── commands.py ├── logset │ ├── __init__.py │ ├── api.py │ └── commands.py ├── query │ ├── __init__.py │ ├── api.py │ └── commands.py ├── response_utils.py ├── saved_query │ ├── __init__.py │ ├── api.py │ └── commands.py ├── team │ ├── __init__.py │ ├── api.py │ └── commands.py ├── usage │ ├── __init__.py │ ├── api.py │ └── commands.py └── user │ ├── __init__.py │ ├── api.py │ └── commands.py ├── setup.py ├── tests ├── __init__.py ├── test_apikey_api.py ├── test_apiutils.py ├── test_lecli.py ├── test_log_api.py ├── test_logset_api.py ├── test_queryapi.py ├── test_saved_query_api.py ├── test_team_api.py ├── test_usage_api.py └── test_userapi.py └── tox.ini /.github/repository_metadata.yml: -------------------------------------------------------------------------------- 1 | 2 | repo_owners: 3 | - dublin-raphael 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | *.pyc 94 | bin/ 95 | .idea -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins= 20 | 21 | # Use multiple processes to speed up Pylint. 22 | jobs=1 23 | 24 | # Allow loading of arbitrary C extensions. Extensions are imported into the 25 | # active Python interpreter and may run arbitrary code. 26 | unsafe-load-any-extension=no 27 | 28 | # A comma-separated list of package or module names from where C extensions may 29 | # be loaded. Extensions are loading into the active Python interpreter and may 30 | # run arbitrary code 31 | extension-pkg-whitelist= 32 | 33 | # Allow optimization of some AST trees. This will activate a peephole AST 34 | # optimizer, which will apply various small optimizations. For instance, it can 35 | # be used to obtain the result of joining multiple strings with the addition 36 | # operator. Joining a lot of strings can lead to a maximum recursion error in 37 | # Pylint and this flag can prevent that. It has one side effect, the resulting 38 | # AST will be different than the one from reality. 39 | optimize-ast=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Enable the message, report, category or checker with the given id(s). You can 49 | # either give multiple identifier separated by comma (,) or put this option 50 | # multiple time (only on the command line, not in the configuration file where 51 | # it should appear only once). See also the "--disable" option for examples. 52 | #enable= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once).You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use"--disable=all --enable=classes 62 | # --disable=W" 63 | disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,R0903,R0201 64 | 65 | 66 | [REPORTS] 67 | 68 | # Set the output format. Available formats are text, parseable, colorized, msvs 69 | # (visual studio) and html. You can also give a reporter class, eg 70 | # mypackage.mymodule.MyReporterClass. 71 | output-format=colorized 72 | 73 | # Put messages in a separate file for each module / package specified on the 74 | # command line instead of printing them on stdout. Reports (if any) will be 75 | # written in a file name "pylint_global.[txt|html]". 76 | files-output=no 77 | 78 | # Tells whether to display a full report or only the messages 79 | reports=yes 80 | 81 | # Python expression which should return a note less than 10 (10 is the highest 82 | # note). You have access to the variables errors warning, statement which 83 | # respectively contain the number of errors / warnings messages and the total 84 | # number of statements analyzed. This is used by the global evaluation report 85 | # (RP0004). 86 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 87 | 88 | # Template used to display messages. This is a python new-style format string 89 | # used to format the message information. See doc for all details 90 | #msg-template= 91 | 92 | 93 | [BASIC] 94 | 95 | # List of builtins function names that should not be used, separated by a comma 96 | bad-functions=map,filter,input 97 | 98 | # Good variable names which should always be accepted, separated by a comma 99 | good-names=i,j,k,ex,Run,_ 100 | 101 | # Bad variable names which should always be refused, separated by a comma 102 | bad-names=foo,bar,baz,toto,tutu,tata 103 | 104 | # Colon-delimited sets of names that determine each other's naming style when 105 | # the name regexes allow several styles. 106 | name-group= 107 | 108 | # Include a hint for the correct naming format with invalid-name 109 | include-naming-hint=yes 110 | 111 | # Regular expression matching correct function names 112 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 113 | 114 | # Naming hint for function names 115 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 116 | 117 | # Regular expression matching correct variable names 118 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 119 | 120 | # Naming hint for variable names 121 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 122 | 123 | # Regular expression matching correct constant names 124 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 125 | 126 | # Naming hint for constant names 127 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 128 | 129 | # Regular expression matching correct attribute names 130 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 131 | 132 | # Naming hint for attribute names 133 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 134 | 135 | # Regular expression matching correct argument names 136 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 137 | 138 | # Naming hint for argument names 139 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 140 | 141 | # Regular expression matching correct class attribute names 142 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 143 | 144 | # Naming hint for class attribute names 145 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 146 | 147 | # Regular expression matching correct inline iteration names 148 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 149 | 150 | # Naming hint for inline iteration names 151 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 152 | 153 | # Regular expression matching correct class names 154 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 155 | 156 | # Naming hint for class names 157 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 158 | 159 | # Regular expression matching correct module names 160 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 161 | 162 | # Naming hint for module names 163 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 164 | 165 | # Regular expression matching correct method names 166 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 167 | 168 | # Naming hint for method names 169 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 170 | 171 | # Regular expression which should only match function or class names that do 172 | # not require a docstring. 173 | no-docstring-rgx=^_ 174 | 175 | # Minimum line length for functions/classes that require docstrings, shorter 176 | # ones are exempt. 177 | docstring-min-length=-1 178 | 179 | 180 | [ELIF] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | 186 | [FORMAT] 187 | 188 | # Maximum number of characters on a single line. 189 | max-line-length=100 190 | 191 | # Regexp for a line that is allowed to be longer than the limit. 192 | ignore-long-lines=^\s*(# )??$ 193 | 194 | # Allow the body of an if to be on the same line as the test if there is no 195 | # else. 196 | single-line-if-stmt=no 197 | 198 | # List of optional constructs for which whitespace checking is disabled. `dict- 199 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 200 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 201 | # `empty-line` allows space-only lines. 202 | no-space-check=trailing-comma,dict-separator 203 | 204 | # Maximum number of lines in a module 205 | max-module-lines=1000 206 | 207 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 208 | # tab). 209 | indent-string=' ' 210 | 211 | # Number of spaces of indent required inside a hanging or continued line. 212 | indent-after-paren=4 213 | 214 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 215 | expected-line-ending-format= 216 | 217 | 218 | [LOGGING] 219 | 220 | # Logging modules to check that the string format arguments are in logging 221 | # function parameter format 222 | logging-modules=logging 223 | 224 | 225 | [MISCELLANEOUS] 226 | 227 | # List of note tags to take in consideration, separated by a comma. 228 | notes=FIXME,XXX,TODO 229 | 230 | 231 | [SIMILARITIES] 232 | 233 | # Minimum lines number of a similarity. 234 | min-similarity-lines=10 235 | 236 | # Ignore comments when computing similarities. 237 | ignore-comments=yes 238 | 239 | # Ignore docstrings when computing similarities. 240 | ignore-docstrings=yes 241 | 242 | # Ignore imports when computing similarities. 243 | ignore-imports=no 244 | 245 | 246 | [SPELLING] 247 | 248 | # Spelling dictionary name. Available dictionaries: none. To make it working 249 | # install python-enchant package. 250 | spelling-dict= 251 | 252 | # List of comma separated words that should not be checked. 253 | spelling-ignore-words= 254 | 255 | # A path to a file that contains private dictionary; one word per line. 256 | spelling-private-dict-file= 257 | 258 | # Tells whether to store unknown words to indicated private dictionary in 259 | # --spelling-private-dict-file option instead of raising a message. 260 | spelling-store-unknown-words=no 261 | 262 | 263 | [TYPECHECK] 264 | 265 | # Tells whether missing members accessed in mixin class should be ignored. A 266 | # mixin class is detected if its name ends with "mixin" (case insensitive). 267 | ignore-mixin-members=yes 268 | 269 | # List of module names for which member attributes should not be checked 270 | # (useful for modules/projects where namespaces are manipulated during runtime 271 | # and thus existing member attributes cannot be deduced by static analysis. It 272 | # supports qualified module names, as well as Unix pattern matching. 273 | ignored-modules=twisted.internet.reactor 274 | 275 | # List of classes names for which member attributes should not be checked 276 | # (useful for classes with attributes dynamically set). This supports can work 277 | # with qualified names. 278 | ignored-classes= 279 | 280 | # List of members which are set dynamically and missed by pylint inference 281 | # system, and so shouldn't trigger E1101 when accessed. Python regular 282 | # expressions are accepted. 283 | generated-members= 284 | 285 | 286 | [VARIABLES] 287 | 288 | # Tells whether we should check for unused import in __init__ files. 289 | init-import=no 290 | 291 | # A regular expression matching the name of dummy variables (i.e. expectedly 292 | # not used). 293 | dummy-variables-rgx=_$|dummy 294 | 295 | # List of additional names supposed to be defined in builtins. Remember that 296 | # you should avoid to define new builtins when possible. 297 | additional-builtins= 298 | 299 | # List of strings which can identify a callback function by name. A callback 300 | # name must start or end with one of those strings. 301 | callbacks=cb_,_cb 302 | 303 | 304 | [CLASSES] 305 | 306 | # List of method names used to declare (i.e. assign) instance attributes. 307 | defining-attr-methods=__init__,__new__,setUp 308 | 309 | # List of valid names for the first argument in a class method. 310 | valid-classmethod-first-arg=cls 311 | 312 | # List of valid names for the first argument in a metaclass class method. 313 | valid-metaclass-classmethod-first-arg=mcs 314 | 315 | # List of member names, which should be excluded from the protected access 316 | # warning. 317 | exclude-protected=_asdict,_fields,_replace,_source,_make 318 | 319 | 320 | [DESIGN] 321 | 322 | # Maximum number of arguments for function / method 323 | max-args=10 324 | 325 | # Argument names that match this expression will be ignored. Default to name 326 | # with leading underscore 327 | ignored-argument-names=_.* 328 | 329 | # Maximum number of locals for function / method body 330 | max-locals=20 331 | 332 | # Maximum number of return / yield for function / method body 333 | max-returns=6 334 | 335 | # Maximum number of branch for function / method body 336 | max-branches=12 337 | 338 | # Maximum number of statements in function / method body 339 | max-statements=50 340 | 341 | # Maximum number of parents for a class (see R0901). 342 | max-parents=7 343 | 344 | # Maximum number of attributes for a class (see R0902). 345 | max-attributes=7 346 | 347 | # Minimum number of public methods for a class (see R0903). 348 | min-public-methods=2 349 | 350 | # Maximum number of public methods for a class (see R0904). 351 | max-public-methods=20 352 | 353 | # Maximum number of boolean expressions in a if statement 354 | max-bool-expr=5 355 | 356 | 357 | [IMPORTS] 358 | 359 | # Deprecated modules which should not be used, separated by a comma 360 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 361 | 362 | # Create a graph of every (i.e. internal and external) dependencies in the 363 | # given file (report RP0402 must not be disabled) 364 | import-graph= 365 | 366 | # Create a graph of external dependencies in the given file (report RP0402 must 367 | # not be disabled) 368 | ext-import-graph= 369 | 370 | # Create a graph of internal dependencies in the given file (report RP0402 must 371 | # not be disabled) 372 | int-import-graph= 373 | 374 | 375 | [EXCEPTIONS] 376 | 377 | # Exceptions that will emit a warning when being caught. Defaults to 378 | # "Exception" 379 | overgeneral-exceptions=Exception 380 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: "pip install -r dev-requirements.pip" 5 | script: pylint lecli && py.test tests 6 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.2, 20/06/2016 -- Unit testing for lecli. 2 | Directory structure refactoring for main python modules. 3 | CHANGES, COPYING, MANIFEST files added. 4 | v0.3, 14/07/2016 -- Teams API support with create/delete/rename teams and adding/deleting users 5 | from teams. 6 | Configuration file loading during installation. 7 | Travis-tox-pylint support. 8 | v0.4, 9/9/2016 -- Relative time range and progress bar support. 9 | v0.5, 10/11/2016 -- Account usage management support. 10 | v0.6, 13/12/2016 -- Saved query management support. 11 | v0.7 01/02/2017 -- Log management support. 12 | Commands now follow pattern. 13 | Deprecated old commands. 14 | v1.0 29/05/2017 -- Logset management support. 15 | Live tail support. 16 | Api key management support. 17 | Bug fixes/enhancements around configuration file and api url(s). 18 | Breaking change: Removed deprecated commands: addusertoteam, adduser, 19 | createsavedquery, createteam, deletesavedquery, deleteteam, deleteuser, 20 | deleteuserfromteam, events, getowner, getsavedqueries, getsavedquery, 21 | getteams, getteam, listusers, recentevents, renameteam, updatesavedquery, 22 | usage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Logentries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /cortex.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | info: 3 | title: Lecli 4 | description: Seamlessly view recent events, run queries and manage your account 5 | from the command line 6 | x-cortex-git: 7 | github: 8 | alias: r7org 9 | repository: rapid7/lecli 10 | x-cortex-tag: lecli 11 | x-cortex-type: service 12 | x-cortex-groups: 13 | - target:library 14 | - strategy:cloud 15 | - status:deprecated 16 | - exposure:internal-ship 17 | x-cortex-owners: 18 | - name: rapid7/dublin-raphael 19 | type: group 20 | provider: GITHUB 21 | x-cortex-domain-parents: 22 | - tag: logsearch-raphael 23 | openapi: 3.0.1 24 | servers: 25 | - url: "/" 26 | -------------------------------------------------------------------------------- /dev-requirements.pip: -------------------------------------------------------------------------------- 1 | mock==1.0.1 2 | httpretty==0.8.14 3 | pytest==2.9.1 4 | pytest-cov==2.2.1 5 | click==6.6 6 | requests==2.9.1 7 | pytz==2016.4 8 | termcolor==1.1.0 9 | tabulate==0.7.5 10 | appdirs==1.4.0 11 | pylint==1.6.4 12 | validators==0.11.2 13 | 14 | -------------------------------------------------------------------------------- /lecli/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | lecli __init__.py 3 | """ 4 | 5 | from lecli._version import __version__ 6 | __author__ = 'Logentries by Rapid7' 7 | -------------------------------------------------------------------------------- /lecli/_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Version module. We should change the version here when we need to. 3 | """ 4 | __version__ = '1.1.1' 5 | -------------------------------------------------------------------------------- /lecli/api_key/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/api_key/__init__.py -------------------------------------------------------------------------------- /lecli/api_key/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Api Keys API module. 3 | """ 4 | import json 5 | import sys 6 | 7 | import requests 8 | 9 | from lecli import api_utils 10 | from lecli import response_utils 11 | 12 | 13 | def _url(provided_parts=()): 14 | """ 15 | Get rest query "path" and "url" respectively 16 | """ 17 | ordered_path_parts = ['management', 'accounts', api_utils.get_account_resource_id(), 'apikeys'] 18 | ordered_path_parts.extend(provided_parts) 19 | return api_utils.build_url(ordered_path_parts) 20 | 21 | 22 | def handle_api_key_response(response): 23 | """ 24 | Handle get api key response 25 | """ 26 | if response_utils.response_error(response): 27 | sys.exit(1) 28 | elif response.status_code in [200, 201]: 29 | api_utils.pretty_print_string_as_json(response.text) 30 | 31 | 32 | def delete(api_key_id): 33 | """ 34 | Delete an api key with the provided ID 35 | """ 36 | action, url = _url((api_key_id,)) 37 | headers = api_utils.generate_headers('owner', method='DELETE', body='', action=action) 38 | 39 | try: 40 | response = requests.delete(url, headers=headers) 41 | if response_utils.response_error(response): 42 | sys.stderr.write('Deleting api key failed.') 43 | sys.exit(1) 44 | elif response.status_code == 204: 45 | sys.stdout.write('Deleted api key with id: %s \n' % api_key_id) 46 | except requests.exceptions.RequestException as error: 47 | sys.stderr.write(error) 48 | sys.exit(1) 49 | 50 | 51 | def get(api_key_id): 52 | """ 53 | Get a specific apikey 54 | """ 55 | action, url = _url((api_key_id,)) 56 | headers = api_utils.generate_headers('rw', method='GET', body='', 57 | action=action) 58 | try: 59 | response = requests.get(url, headers=headers) 60 | handle_api_key_response(response) 61 | except requests.exceptions.RequestException as error: 62 | sys.stderr.write(error) 63 | sys.exit(1) 64 | 65 | 66 | def get_all(owner=False): 67 | """ 68 | Get apikeys associated with the account - this uses rw apikey so does not return owner api keys 69 | """ 70 | action, url = _url() 71 | headers = api_utils.generate_headers('owner' if owner else 'rw', method='GET', body='', 72 | action=action) 73 | try: 74 | response = requests.get(url, headers=headers) 75 | handle_api_key_response(response) 76 | except requests.exceptions.RequestException as error: 77 | sys.stderr.write(error) 78 | sys.exit(1) 79 | 80 | 81 | def create(payload): 82 | """ 83 | Create an api key with the provided ID 84 | """ 85 | action, url = _url() 86 | 87 | headers = api_utils.generate_headers('owner', method='POST', body=json.dumps(payload), 88 | action=action) 89 | 90 | try: 91 | response = requests.post(url, headers=headers, json=payload) 92 | if response_utils.response_error(response): 93 | sys.stderr.write('Create api key failed.') 94 | sys.exit(1) 95 | elif response.status_code == 201: 96 | handle_api_key_response(response) 97 | except requests.exceptions.RequestException as error: 98 | sys.stderr.write(error) 99 | sys.exit(1) 100 | 101 | 102 | def update(api_key_id, active): 103 | """ 104 | Enable or disable an api key with given ID 105 | """ 106 | action, url = _url((api_key_id,)) 107 | payload = { 108 | "apikey": 109 | { 110 | "active": active 111 | } 112 | } 113 | 114 | headers = api_utils.generate_headers('owner', method='PATCH', body=json.dumps(payload), 115 | action=action) 116 | try: 117 | response = requests.patch(url, json=payload, headers=headers) 118 | if response_utils.response_error(response): 119 | sys.stderr.write('Failed to %s api key with id: %s \n' % 120 | ('enable' if active else 'disable', api_key_id)) 121 | sys.exit(1) 122 | elif response.status_code == 200: 123 | sys.stdout.write('%s api key with id: %s\n' % 124 | ('Enabled' if active else 'Disabled', api_key_id)) 125 | handle_api_key_response(response) 126 | except requests.exceptions.RequestException as error: 127 | sys.stderr.write(error) 128 | sys.exit(1) 129 | -------------------------------------------------------------------------------- /lecli/api_key/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Api Keys commands module. 3 | """ 4 | import json 5 | import sys 6 | 7 | import click 8 | 9 | from lecli.api_key import api 10 | 11 | 12 | @click.command() 13 | @click.argument('api_key', type=click.STRING) 14 | def get_api_key(api_key): 15 | """ 16 | Get a specific api key 17 | """ 18 | api.get(api_key) 19 | 20 | 21 | @click.command() 22 | @click.option('--owner/--no-owner', default=False) 23 | def get_api_keys(owner): 24 | """ 25 | Get all api keys 26 | """ 27 | api.get_all(owner) 28 | 29 | 30 | @click.command() 31 | @click.argument('filename', type=click.Path(exists=True, dir_okay=False)) 32 | def create_api_key(filename): 33 | """ 34 | Create an api key with the provided file. 35 | """ 36 | if filename is not None: 37 | with open(filename) as json_data: 38 | try: 39 | params = json.load(json_data) 40 | except ValueError as error: 41 | sys.stderr.write(error.message + '\n') 42 | sys.exit(1) 43 | 44 | api.create(params) 45 | else: 46 | click.echo('Example usage: lecli create apikey path_to_file.json') 47 | 48 | 49 | @click.command() 50 | @click.argument('api_key', type=click.STRING) 51 | def delete_api_key(api_key): 52 | """ 53 | Delete a specific api key 54 | """ 55 | api.delete(api_key) 56 | 57 | 58 | @click.command() 59 | @click.argument('api_key', type=click.STRING) 60 | @click.option('--enable/--disable', default=None) 61 | def update_api_key(api_key, enable): 62 | """ 63 | Enable or disable an api key 64 | """ 65 | if enable is not None: 66 | api.update(api_key, enable) 67 | else: 68 | click.echo("Example usage: lecli update apikey 12345678-aaaa-bbbb-1234-1234cb123456 " 69 | "--enable") 70 | click.echo("Example usage: lecli update apikey 12345678-aaaa-bbbb-1234-1234cb123456 " 71 | "--disable") 72 | -------------------------------------------------------------------------------- /lecli/api_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration and api keys util module. 3 | """ 4 | import sys 5 | import ConfigParser 6 | import base64 7 | import hashlib 8 | import hmac 9 | import os 10 | import json 11 | 12 | import datetime 13 | 14 | import click 15 | import validators 16 | from appdirs import user_config_dir 17 | 18 | import lecli 19 | 20 | AUTH_SECTION = 'Auth' 21 | URL_SECTION = 'Url' 22 | LOGGROUPS_SECTION = 'LogGroups' 23 | CLI_FAVORITES_SECTION = 'Cli_Favorites' 24 | CONFIG = ConfigParser.ConfigParser() 25 | CONFIG_FILE_PATH = os.path.join(user_config_dir(lecli.__name__), 'config.ini') 26 | DEFAULT_API_URL = 'https://rest.logentries.com' 27 | 28 | 29 | def print_config_error_and_exit(section=None, config_key=None, value=None): 30 | """ 31 | Print appropriate apiutils error message and exit. 32 | """ 33 | if not section: 34 | click.echo("Error: Configuration file '%s' not found" % CONFIG_FILE_PATH, err=True) 35 | elif not config_key: 36 | click.echo("Error: Section '%s' was not found in configuration file(%s)" % 37 | (section, CONFIG_FILE_PATH), err=True) 38 | elif not value: 39 | click.echo("Error: Configuration key for %s was not found in configuration file(%s) in " 40 | "'%s' section" % (config_key, CONFIG_FILE_PATH, section), err=True) 41 | else: 42 | click.echo("Error: %s = '%s' is not in section: '%s' of your configuration file: '%s'" % 43 | (config_key, value, section, CONFIG_FILE_PATH), err=True) 44 | 45 | sys.exit(1) 46 | 47 | 48 | def init_config(): 49 | """ 50 | Initialize config file in the OS specific config path if there is no config file exists. 51 | """ 52 | config_dir = user_config_dir(lecli.__name__) 53 | 54 | if not os.path.exists(CONFIG_FILE_PATH): 55 | if not os.path.exists(config_dir): 56 | os.makedirs(config_dir) 57 | 58 | dummy_config = ConfigParser.ConfigParser() 59 | config_file = open(CONFIG_FILE_PATH, 'w') 60 | dummy_config.add_section(AUTH_SECTION) 61 | dummy_config.set(AUTH_SECTION, 'account_resource_id', '') 62 | dummy_config.set(AUTH_SECTION, 'owner_api_key_id', '') 63 | dummy_config.set(AUTH_SECTION, 'owner_api_key', '') 64 | dummy_config.set(AUTH_SECTION, 'rw_api_key', '') 65 | dummy_config.set(AUTH_SECTION, 'ro_api_key', '') 66 | 67 | dummy_config.add_section(CLI_FAVORITES_SECTION) 68 | dummy_config.add_section(URL_SECTION) 69 | dummy_config.set(URL_SECTION, 'api_url', 'https://rest.logentries.com') 70 | 71 | dummy_config.write(config_file) 72 | config_file.close() 73 | click.echo("An empty config file created in path %s, please check and configure it. To " 74 | "learn how to get necessary api keys, go to this Logentries documentation " 75 | "page: https://docs.logentries.com/docs/api-keys" % CONFIG_FILE_PATH) 76 | else: 77 | click.echo("Config file exists in the path: " + CONFIG_FILE_PATH, err=True) 78 | 79 | sys.exit(1) 80 | 81 | 82 | def load_config(): 83 | """ 84 | Load config from OS specific config path into ConfigParser object. 85 | :return: 86 | """ 87 | files_read = CONFIG.read(CONFIG_FILE_PATH) 88 | if len(files_read) != 1: 89 | click.echo("Error: Config file '%s' not found, generating one..." % CONFIG_FILE_PATH, 90 | err=True) 91 | init_config() 92 | print_config_error_and_exit() 93 | if not CONFIG.has_section(AUTH_SECTION): 94 | print_config_error_and_exit(section=AUTH_SECTION) 95 | if CONFIG.has_section(LOGGROUPS_SECTION): 96 | replace_loggroup_section() 97 | 98 | 99 | def replace_loggroup_section(): 100 | """ 101 | If config has legacy LogGroup section, take all its items and add 102 | them to the CLI_Favorites section - then delete the legacy section. 103 | Update the config file with the changes. 104 | """ 105 | existing_groups = CONFIG.items(LOGGROUPS_SECTION) 106 | if not CONFIG.has_section(CLI_FAVORITES_SECTION): 107 | CONFIG.add_section(CLI_FAVORITES_SECTION) 108 | for group in existing_groups: 109 | CONFIG.set(CLI_FAVORITES_SECTION, group[0], group[1]) 110 | CONFIG.remove_section(LOGGROUPS_SECTION) 111 | config_file = open(CONFIG_FILE_PATH, 'w') 112 | CONFIG.write(config_file) 113 | config_file.close() 114 | 115 | 116 | def get_ro_apikey(): 117 | """ 118 | Get read-only api key from the config file. 119 | """ 120 | 121 | config_key = 'ro_api_key' 122 | try: 123 | ro_api_key = CONFIG.get(AUTH_SECTION, config_key) 124 | if not validators.uuid(ro_api_key): 125 | return get_rw_apikey() 126 | else: 127 | return ro_api_key 128 | except ConfigParser.NoOptionError: 129 | # because read-write api key is a superset of read-only api key 130 | return get_rw_apikey() 131 | 132 | 133 | def get_rw_apikey(): 134 | """ 135 | Get read-write api key from the config file. 136 | """ 137 | 138 | config_key = 'rw_api_key' 139 | try: 140 | rw_api_key = CONFIG.get(AUTH_SECTION, config_key) 141 | if not validators.uuid(rw_api_key): 142 | print_config_error_and_exit(AUTH_SECTION, 'Read/Write API key(%s)' % config_key, 143 | rw_api_key) 144 | else: 145 | return rw_api_key 146 | except ConfigParser.NoOptionError: 147 | print_config_error_and_exit(AUTH_SECTION, 'Read/Write API key(%s)' % config_key) 148 | 149 | 150 | def get_owner_apikey(): 151 | """ 152 | Get owner api key from the config file. 153 | """ 154 | 155 | config_key = 'owner_api_key' 156 | try: 157 | owner_api_key = CONFIG.get(AUTH_SECTION, config_key) 158 | if not validators.uuid(owner_api_key): 159 | print_config_error_and_exit(AUTH_SECTION, 'Owner API key(%s)' % config_key, 160 | owner_api_key) 161 | return 162 | else: 163 | return owner_api_key 164 | except ConfigParser.NoOptionError: 165 | print_config_error_and_exit(AUTH_SECTION, 'Owner API key(%s)' % config_key) 166 | 167 | 168 | def get_owner_apikey_id(): 169 | """ 170 | Get owner api key id from the config file. 171 | """ 172 | 173 | config_key = 'owner_api_key_id' 174 | try: 175 | owner_apikey_id = CONFIG.get(AUTH_SECTION, config_key) 176 | if not validators.uuid(owner_apikey_id): 177 | print_config_error_and_exit(AUTH_SECTION, 'Owner API key ID(%s)' % config_key, 178 | owner_apikey_id) 179 | return 180 | else: 181 | return owner_apikey_id 182 | except ConfigParser.NoOptionError: 183 | print_config_error_and_exit(AUTH_SECTION, 'Owner API key ID(%s)' % config_key) 184 | 185 | 186 | def get_account_resource_id(): 187 | """ 188 | Get account resource id from the config file. 189 | """ 190 | 191 | config_key = 'account_resource_id' 192 | try: 193 | account_resource_id = CONFIG.get(AUTH_SECTION, config_key) 194 | if not validators.uuid(account_resource_id): 195 | print_config_error_and_exit(AUTH_SECTION, 'Account Resource ID(%s)' % config_key, 196 | account_resource_id) 197 | return 198 | else: 199 | return account_resource_id 200 | except ConfigParser.NoOptionError: 201 | print_config_error_and_exit(AUTH_SECTION, 'Account Resource ID(%s)' % config_key) 202 | 203 | 204 | def get_named_logkey_group(name): 205 | """ 206 | Get named log-key list from the config file. 207 | 208 | :param name: name of the log key list 209 | """ 210 | 211 | section = CLI_FAVORITES_SECTION 212 | try: 213 | groups = dict(CONFIG.items(section)) 214 | name = name.lower() 215 | if name in groups: 216 | logkeys = [line for line in str(groups[name]).splitlines() if line is not None] 217 | for logkey in logkeys: 218 | if not validators.uuid(logkey): 219 | print_config_error_and_exit(section, 'Named Logkey Group(%s)' % name, logkey) 220 | return logkeys 221 | else: 222 | print_config_error_and_exit(section, 'Named Logkey Group(%s)' % name) 223 | except ConfigParser.NoSectionError: 224 | print_config_error_and_exit(section) 225 | 226 | 227 | def generate_headers(api_key_type, method=None, action=None, body=None): 228 | """ 229 | Generate request headers according to api_key_type that is being used. 230 | """ 231 | headers = None 232 | 233 | if api_key_type is 'ro': 234 | headers = { 235 | 'x-api-key': get_ro_apikey(), 236 | "Content-Type": "application/json" 237 | } 238 | elif api_key_type is 'rw': 239 | headers = { 240 | 'x-api-key': get_rw_apikey(), 241 | "Content-Type": "application/json" 242 | } 243 | elif api_key_type is 'owner': # Uses the owner-api-key 244 | date_h = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT") 245 | content_type_h = "application/json" 246 | signature = gensignature(get_owner_apikey(), date_h, content_type_h, method, action, body) 247 | headers = { 248 | "Date": date_h, 249 | "Content-Type": content_type_h, 250 | "authorization-api-key": "%s:%s" % ( 251 | get_owner_apikey_id().encode('utf8'), base64.b64encode(signature)) 252 | } 253 | 254 | headers['User-Agent'] = 'lecli' 255 | 256 | return headers 257 | 258 | 259 | def gensignature(api_key, date, content_type, request_method, query_path, request_body): 260 | """ 261 | Generate owner access signature. 262 | 263 | """ 264 | hashed_body = base64.b64encode(hashlib.sha256(request_body).digest()) 265 | canonical_string = request_method + content_type + date + query_path + hashed_body 266 | 267 | # Create a new hmac digester with the api key as the signing key and sha1 as the algorithm 268 | digest = hmac.new(api_key, digestmod=hashlib.sha1) 269 | digest.update(canonical_string) 270 | 271 | return digest.digest() 272 | 273 | 274 | def get_api_url(): 275 | """ 276 | Get management url from the config file 277 | """ 278 | config_key = 'api_url' 279 | try: 280 | url = CONFIG.get(URL_SECTION, config_key) 281 | if validators.url(str(url)): 282 | return url 283 | else: 284 | print_config_error_and_exit(URL_SECTION, 'REST API URL(%s)' % config_key) 285 | except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): 286 | return DEFAULT_API_URL 287 | 288 | 289 | def build_url(nodes): 290 | """ 291 | Build a url with the given array of nodes for the url and return path and url respectively 292 | Ordering is important 293 | """ 294 | path = str.join('/', nodes) 295 | url = str.join('/', [get_api_url(), path]) 296 | return path, url 297 | 298 | 299 | def pretty_print_string_as_json(text): 300 | """ 301 | Pretty prints a json string 302 | """ 303 | print json.dumps(json.loads(text), indent=4, sort_keys=True) 304 | 305 | 306 | def combine_objects(left, right): 307 | """ 308 | Merge two objects 309 | """ 310 | if isinstance(left, dict) and isinstance(right, dict): 311 | result = {} 312 | for key, value in left.iteritems(): 313 | if key not in right: 314 | result[key] = value 315 | else: 316 | result[key] = combine_objects(value, right[key]) 317 | for key, value in right.iteritems(): 318 | if key not in left: 319 | result[key] = value 320 | return result 321 | if isinstance(left, list) and isinstance(right, list): 322 | return left + right 323 | return right 324 | -------------------------------------------------------------------------------- /lecli/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main lecli module powered by click library. 3 | """ 4 | import click 5 | 6 | import lecli 7 | from lecli import api_utils 8 | from lecli.query import commands as query_commands 9 | from lecli.saved_query import commands as saved_query_commands 10 | from lecli.team import commands as team_commands 11 | from lecli.usage import commands as usage_commands 12 | from lecli.user import commands as user_commands 13 | from lecli.log import commands as log_commands 14 | from lecli.logset import commands as logset_commands 15 | from lecli.api_key import commands as api_key_commands 16 | 17 | 18 | @click.group() 19 | @click.version_option(version=lecli.__version__) 20 | def cli(): 21 | """Logentries Command Line Interface""" 22 | # load configs from config.ini file in user_config_dir depending on running OS 23 | api_utils.load_config() 24 | 25 | 26 | @cli.group() 27 | def get(): 28 | """Get a resource""" 29 | pass 30 | 31 | 32 | @cli.group() 33 | def create(): 34 | """Create a resource""" 35 | pass 36 | 37 | 38 | @cli.group() 39 | def update(): 40 | """Update a resource""" 41 | pass 42 | 43 | 44 | @cli.group() 45 | def rename(): 46 | """Rename a resource""" 47 | pass 48 | 49 | 50 | @cli.group() 51 | def replace(): 52 | """Replace a resource""" 53 | pass 54 | 55 | 56 | @cli.group() 57 | def delete(): 58 | """Delete a resource""" 59 | pass 60 | 61 | 62 | @cli.group() 63 | def tail(): 64 | """Tail logs""" 65 | pass 66 | 67 | if __name__ == '__main__': 68 | cli() 69 | 70 | cli.add_command(query_commands.query) 71 | 72 | get.add_command(query_commands.get_events, "events") 73 | get.add_command(query_commands.get_recent_events, "recentevents") 74 | get.add_command(saved_query_commands.get_saved_query, "savedquery") 75 | get.add_command(saved_query_commands.get_saved_queries, "savedqueries") 76 | get.add_command(team_commands.get_team, "team") 77 | get.add_command(team_commands.get_teams, "teams") 78 | get.add_command(usage_commands.get_usage, "usage") 79 | get.add_command(user_commands.get_owner, "owner") 80 | get.add_command(user_commands.get_users, "users") 81 | get.add_command(log_commands.getlog, "log") 82 | get.add_command(log_commands.getlogs, "logs") 83 | get.add_command(logset_commands.getlogset, "logset") 84 | get.add_command(logset_commands.getlogsets, "logsets") 85 | get.add_command(api_key_commands.get_api_key, "apikey") 86 | get.add_command(api_key_commands.get_api_keys, "apikeys") 87 | 88 | create.add_command(saved_query_commands.create_saved_query, "savedquery") 89 | create.add_command(team_commands.create_team, "team") 90 | create.add_command(user_commands.create_user, "user") 91 | create.add_command(log_commands.createlog, "log") 92 | create.add_command(logset_commands.createlogset, "logset") 93 | create.add_command(api_key_commands.create_api_key, "apikey") 94 | 95 | update.add_command(saved_query_commands.update_saved_query, "savedquery") 96 | update.add_command(team_commands.updateteam, "team") 97 | update.add_command(log_commands.updatelog, "log") 98 | update.add_command(logset_commands.updatelogset, "logset") 99 | update.add_command(api_key_commands.update_api_key, "apikey") 100 | 101 | rename.add_command(team_commands.rename_team, "team") 102 | rename.add_command(log_commands.renamelog, "log") 103 | rename.add_command(logset_commands.renamelogset, "logset") 104 | 105 | replace.add_command(log_commands.replacelog, "log") 106 | replace.add_command(logset_commands.replacelogset, "logset") 107 | 108 | delete.add_command(saved_query_commands.delete_saved_query, "savedquery") 109 | delete.add_command(team_commands.delete_team, "team") 110 | delete.add_command(user_commands.delete_user, "user") 111 | delete.add_command(log_commands.deletelog, "log") 112 | delete.add_command(logset_commands.deletelogset, "logset") 113 | delete.add_command(api_key_commands.delete_api_key, "apikey") 114 | 115 | tail.add_command(query_commands.tail_events, "events") 116 | -------------------------------------------------------------------------------- /lecli/log/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/log/__init__.py -------------------------------------------------------------------------------- /lecli/log/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Log API module. 3 | """ 4 | import sys 5 | import requests 6 | 7 | from lecli import api_utils 8 | from lecli import response_utils 9 | 10 | 11 | def _url(provided_parts=('logs',)): 12 | """ 13 | Get rest query url of log resource id 14 | """ 15 | ordered_path_parts = ['management'] 16 | ordered_path_parts.extend(provided_parts) 17 | return api_utils.build_url(ordered_path_parts) 18 | 19 | 20 | def handle_get_log_response(response): 21 | """ 22 | Handle get log response 23 | """ 24 | if response_utils.response_error(response): 25 | sys.exit(1) 26 | elif response.status_code == 200: 27 | api_utils.pretty_print_string_as_json(response.text) 28 | 29 | 30 | def get_logs(): 31 | """ 32 | Get logs associated with the user 33 | """ 34 | headers = api_utils.generate_headers('ro') 35 | try: 36 | response = requests.get(_url()[1], headers=headers) 37 | handle_get_log_response(response) 38 | except requests.exceptions.RequestException as error: 39 | sys.stderr.write(error) 40 | sys.exit(1) 41 | 42 | 43 | def get_log(log_id): 44 | """ 45 | Get a specific log 46 | """ 47 | headers = api_utils.generate_headers('ro') 48 | try: 49 | response = requests.get(_url(('logs', log_id))[1], headers=headers) 50 | handle_get_log_response(response) 51 | except requests.exceptions.RequestException as error: 52 | sys.stderr.write(error) 53 | sys.exit(1) 54 | 55 | 56 | def create_log(logname, params): 57 | """Add a new log to the current account. 58 | If a JSON object is given, use that as the request parameters. 59 | Otherwise, use the name provided 60 | """ 61 | if params is not None: 62 | request_params = params 63 | else: 64 | request_params = { 65 | 'log': { 66 | 'name': logname 67 | } 68 | } 69 | 70 | headers = api_utils.generate_headers('rw') 71 | 72 | try: 73 | response = requests.post(_url()[1], json=request_params, headers=headers) 74 | if response_utils.response_error(response): 75 | sys.stderr.write('Create log failed, status code: %d' % response.status_code) 76 | sys.exit(1) 77 | elif response.status_code == 201: 78 | api_utils.pretty_print_string_as_json(response.text) 79 | except requests.exceptions.RequestException as error: 80 | sys.stderr.write(error) 81 | sys.exit(1) 82 | 83 | 84 | def delete_log(log_id): 85 | """ 86 | Delete a log with the provided log ID 87 | """ 88 | headers = api_utils.generate_headers('rw') 89 | 90 | try: 91 | response = requests.delete(_url(('logs', log_id))[1], headers=headers) 92 | if response_utils.response_error(response): 93 | sys.stderr.write('Delete log failed.') 94 | sys.exit(1) 95 | elif response.status_code == 204: 96 | sys.stdout.write('Deleted log with id: %s \n' % log_id) 97 | except requests.exceptions.RequestException as error: 98 | sys.stderr.write(error) 99 | sys.exit(1) 100 | 101 | 102 | def replace_log(log_id, params): 103 | """ 104 | Replace the given log with the details provided 105 | """ 106 | headers = api_utils.generate_headers('rw') 107 | 108 | try: 109 | response = requests.put(_url(('logs', log_id))[1], json=params, headers=headers) 110 | if response_utils.response_error(response): 111 | sys.stderr.write('Update log failed.\n') 112 | sys.exit(1) 113 | elif response.status_code == 200: 114 | sys.stdout.write('Log: %s updated to:\n' % log_id) 115 | api_utils.pretty_print_string_as_json(response.text) 116 | except requests.exceptions.RequestException as error: 117 | sys.stderr.write(error) 118 | sys.exit(1) 119 | 120 | 121 | def rename_log(log_id, log_name): 122 | """ 123 | Rename the given log with the provided name 124 | """ 125 | headers = api_utils.generate_headers('ro') 126 | 127 | try: 128 | response = requests.get(_url(('logs', log_id))[1], headers=headers) 129 | if response_utils.response_error(response): 130 | sys.stderr.write('Rename log failed.\n') 131 | sys.exit(1) 132 | elif response.status_code == 200: 133 | params = response.json() 134 | params['log']['name'] = log_name 135 | replace_log(log_id, params) 136 | except requests.exceptions.RequestException as error: 137 | sys.stderr.write(error) 138 | sys.exit(1) 139 | 140 | 141 | def check_logset_exists(params): 142 | """ 143 | Check if a logset exists 144 | """ 145 | if 'logsets_info' in params: 146 | if 'log' in params: 147 | updates = params['log']['logsets_info'] 148 | else: 149 | updates = params['logsets_info'] 150 | 151 | for item in updates: 152 | if item['id']: 153 | url = _url(('logsets', item['id']))[1] 154 | headers = api_utils.generate_headers('ro') 155 | try: 156 | response = requests.get(url, headers=headers) 157 | if response.status_code is not 200: 158 | return False 159 | except requests.exceptions.RequestException as error: 160 | sys.stderr.write(error) 161 | sys.exit(1) 162 | return True 163 | 164 | 165 | def update_log(log_id, params): 166 | """ 167 | Update a log with the details provided 168 | """ 169 | url = _url(('logs', log_id))[1] 170 | headers = api_utils.generate_headers('ro') 171 | 172 | if check_logset_exists(params): 173 | try: 174 | response = requests.get(url, headers=headers) 175 | existing_log = response.json() 176 | replace_log(log_id, api_utils.combine_objects(existing_log, params)) 177 | except requests.exceptions.RequestException as error: 178 | sys.stderr.write(error) 179 | sys.exit(1) 180 | else: 181 | sys.stderr.write("One or more of the specified logsets does not exist.\n") 182 | -------------------------------------------------------------------------------- /lecli/log/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for log commands 3 | """ 4 | import sys 5 | import json 6 | import click 7 | 8 | from lecli.log import api 9 | 10 | 11 | @click.command() 12 | @click.option('-n', '--name', type=click.STRING, help="Name of new log") 13 | @click.option('-f', '--filename', type=click.Path(exists=True, dir_okay=False), 14 | help="Full or relative path to file containing JSON log object") 15 | def createlog(name, filename): 16 | """ 17 | Create a log with the provided name and details. 18 | This method will use the JSON file first if both name and file are provided 19 | """ 20 | if filename is not None: 21 | with open(filename) as json_data: 22 | try: 23 | params = json.load(json_data) 24 | api.create_log(None, params) 25 | except ValueError as error: 26 | sys.stderr.write(error.message + '\n') 27 | sys.exit(1) 28 | elif name is not None: 29 | api.create_log(name, None) 30 | else: 31 | click.echo('Example usage: lecli create log -n new_log_name') 32 | click.echo('Example usage: lecli create log -f path_to_file.json') 33 | 34 | 35 | @click.command() 36 | @click.argument('logid', type=click.STRING) 37 | def deletelog(logid): 38 | """ 39 | Delete a log with the provided id 40 | """ 41 | api.delete_log(logid) 42 | 43 | 44 | @click.command() 45 | def getlogs(): 46 | """ 47 | Get all logs for this account 48 | """ 49 | api.get_logs() 50 | 51 | 52 | @click.command() 53 | @click.argument('logid', type=click.STRING) 54 | def getlog(logid): 55 | """ 56 | Get a log with the given id 57 | """ 58 | api.get_log(logid) 59 | 60 | 61 | @click.command() 62 | @click.argument('logid', type=click.STRING) 63 | @click.argument('name', type=click.STRING) 64 | def renamelog(logid, name): 65 | """ 66 | Rename a log with the name provided 67 | """ 68 | api.rename_log(logid, name) 69 | 70 | 71 | @click.command() 72 | @click.argument('logid', type=click.STRING) 73 | @click.argument('filename', type=click.Path(exists=True, dir_okay=False)) 74 | def replacelog(logid, filename): 75 | """ 76 | Replace a log of a given id with new details 77 | """ 78 | with open(filename) as json_data: 79 | try: 80 | params = json.load(json_data) 81 | api.replace_log(logid, params) 82 | except ValueError as error: 83 | sys.stderr.write(error.message + '\n') 84 | sys.exit(1) 85 | 86 | 87 | @click.command() 88 | @click.argument('logid', type=click.STRING) 89 | @click.argument('filename', type=click.Path(exists=True, dir_okay=False)) 90 | def updatelog(logid, filename): 91 | """ 92 | Update a log of a given id with new details 93 | """ 94 | with open(filename) as json_data: 95 | try: 96 | params = json.load(json_data) 97 | api.update_log(logid, params) 98 | except ValueError as error: 99 | sys.stderr.write(error.message + '\n') 100 | sys.exit(1) 101 | -------------------------------------------------------------------------------- /lecli/logset/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/logset/__init__.py -------------------------------------------------------------------------------- /lecli/logset/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logset API module. 3 | """ 4 | import json 5 | import sys 6 | 7 | import requests 8 | 9 | from lecli import api_utils 10 | from lecli import response_utils 11 | 12 | 13 | def _url(provided_path_parts=()): 14 | """ 15 | Get rest query url of logset resource id. 16 | """ 17 | ordered_path_parts = ['management', 'logsets'] 18 | ordered_path_parts.extend(provided_path_parts) 19 | return api_utils.build_url(ordered_path_parts) 20 | 21 | 22 | def handle_response(response, error_message, success_code, success_message=None): 23 | """Handle logset responses""" 24 | if response_utils.response_error(response): 25 | sys.stderr.write(error_message) 26 | sys.exit(1) 27 | elif response.status_code == success_code: 28 | if success_message: 29 | sys.stdout.write(success_message) 30 | else: 31 | api_utils.pretty_print_string_as_json(response.text) 32 | 33 | 34 | def get_logsets(): 35 | """ 36 | Get all logsets 37 | """ 38 | headers = api_utils.generate_headers('ro') 39 | try: 40 | response = requests.request('GET', _url()[1], headers=headers) 41 | handle_response(response, 'Unable to fetch logsets\n', 200) 42 | except requests.exceptions.RequestException as error: 43 | sys.stderr.write(error) 44 | sys.exit(1) 45 | 46 | 47 | def get_logset(logset_id): 48 | """ 49 | Get the logset with the given id 50 | """ 51 | headers = api_utils.generate_headers('ro') 52 | try: 53 | response = requests.get(_url((logset_id,))[1], headers=headers) 54 | handle_response(response, 'Unable to fetch logset %s \n' % logset_id, 200) 55 | except requests.exceptions.RequestException as error: 56 | sys.stderr.write(error) 57 | sys.exit(1) 58 | 59 | 60 | def create_logset(logset_name=None, params=None): 61 | """ 62 | Add a new logset to the current account. 63 | If a filename is given, load the contents of the file 64 | as json parameters for the request. 65 | If a name is given, create a new logset with the given name 66 | """ 67 | if params is not None: 68 | request_params = params 69 | else: 70 | request_params = { 71 | 'logset': { 72 | 'name': logset_name 73 | } 74 | } 75 | 76 | headers = api_utils.generate_headers('rw') 77 | 78 | try: 79 | response = requests.post(_url()[1], json=request_params, headers=headers) 80 | handle_response(response, 'Creating logset failed.\n', 201) 81 | except requests.exceptions.RequestException as error: 82 | sys.stderr.write(error) 83 | sys.exit(1) 84 | 85 | 86 | def delete_logset(logset_id): 87 | """ 88 | Delete the logset with the given id 89 | """ 90 | headers = api_utils.generate_headers('rw') 91 | 92 | try: 93 | response = requests.delete(_url((logset_id,))[1], headers=headers) 94 | handle_response(response, 'Delete logset failed.\n', 204, 95 | 'Deleted logset with id: %s \n' % logset_id) 96 | except requests.exceptions.RequestException as error: 97 | sys.stderr.write(error) 98 | sys.exit(1) 99 | 100 | 101 | def rename_logset(logset_id, logset_name): 102 | """ 103 | Rename a given logset 104 | """ 105 | headers = api_utils.generate_headers('ro') 106 | 107 | try: 108 | response = requests.get(_url((logset_id,))[1], headers=headers) 109 | if response_utils.response_error(response): 110 | sys.stderr.write('Rename logset failed.\n') 111 | sys.exit(1) 112 | elif response.status_code == 200: 113 | params = response.json() 114 | params['logset']['name'] = logset_name 115 | replace_logset(logset_id, params) 116 | except requests.exceptions.RequestException as error: 117 | sys.stderr.write(error) 118 | sys.exit(1) 119 | 120 | 121 | def replace_logset(logset_id, params): 122 | """ 123 | Replace a given logset with the details provided 124 | """ 125 | headers = api_utils.generate_headers('rw') 126 | 127 | try: 128 | response = requests.put(_url((logset_id,))[1], json=params, headers=headers) 129 | handle_response(response, 'Update logset with details %s failed.\n' % params, 200) 130 | except requests.exceptions.RequestException as error: 131 | sys.stderr.write(error) 132 | sys.exit(1) 133 | 134 | 135 | def add_log(logset_id, log_id): 136 | """ 137 | Add a log to the logset 138 | """ 139 | params = { 140 | "logset":{ 141 | "logs_info": [{ 142 | "id": str(log_id) 143 | }] 144 | } 145 | } 146 | headers = api_utils.generate_headers('ro') 147 | 148 | try: 149 | response = requests.get(_url((logset_id,))[1], headers=headers) 150 | if response_utils.response_error(response): 151 | sys.stderr.write('Add log %s to logset %s failed\n' 152 | % (log_id, logset_id)) 153 | sys.exit(1) 154 | elif response.status_code == 200: 155 | existing_logset = response.json() 156 | replace_logset(logset_id, api_utils.combine_objects(existing_logset, params)) 157 | except requests.exceptions.RequestException as error: 158 | sys.stderr.write(error) 159 | sys.exit(1) 160 | 161 | 162 | def extract_log_from_logset(logset, log_id): 163 | """Helper method to remove a given log from a logset""" 164 | pruned_logset = [] 165 | if 'logs_info' in logset['logset']: 166 | for log in logset['logset']['logs_info']: 167 | if log['id'] != log_id: 168 | pruned_logset.append(log) 169 | 170 | if pruned_logset is not []: 171 | logset['logset']['logs_info'] = pruned_logset 172 | return logset 173 | else: 174 | sys.stderr.write("Log %s does not exist in logset ", (log_id)) 175 | sys.exit(1) 176 | 177 | 178 | def get_log_keys_from_logset(logset_id): 179 | """Helper method to get list of log IDs in a logset""" 180 | headers = api_utils.generate_headers('ro') 181 | log_ids = [] 182 | try: 183 | response = requests.get(_url((logset_id,))[1], headers=headers) 184 | if response_utils.response_error(response): 185 | sys.stderr.write('Attempt to access logset %s failed\n' % logset_id) 186 | sys.exit(1) 187 | elif response.status_code == 200: 188 | existing_logset = response.json() 189 | if 'logs_info' in existing_logset['logset']: 190 | for log in existing_logset['logset']['logs_info']: 191 | log_ids.append(log['id']) 192 | except requests.exceptions.RequestException as error: 193 | sys.stderr.write(error) 194 | sys.exit(1) 195 | 196 | if log_ids is not []: 197 | return log_ids 198 | else: 199 | sys.stderr.write("No logs found in logset %s " % logset_id) 200 | sys.exit(1) 201 | 202 | 203 | def delete_log(logset_id, log_id): 204 | """ 205 | Delete a log from the logset 206 | """ 207 | headers = api_utils.generate_headers('ro') 208 | try: 209 | response = requests.get(_url((logset_id,))[1], headers=headers) 210 | if response_utils.response_error(response): 211 | sys.stderr.write('Delete log %s from logset %s failed\n' 212 | % (log_id, logset_id)) 213 | sys.exit(1) 214 | elif response.status_code == 200: 215 | existing_logset = response.json() 216 | params = extract_log_from_logset(existing_logset, log_id) 217 | replace_logset(logset_id, params) 218 | except requests.exceptions.RequestException as error: 219 | sys.stderr.write(error) 220 | sys.exit(1) 221 | 222 | 223 | def replace_logset_from_file(logset_id, filename): 224 | """Helper method to load file contents as json 225 | in order to call replace""" 226 | with open(filename) as json_data: 227 | try: 228 | params = json.load(json_data) 229 | replace_logset(logset_id, params) 230 | except ValueError as error: 231 | sys.stderr.write(error.message + '\n') 232 | sys.exit(1) 233 | -------------------------------------------------------------------------------- /lecli/logset/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logset commands module 3 | """ 4 | import sys 5 | import json 6 | import click 7 | 8 | from lecli.logset import api 9 | 10 | 11 | @click.command() 12 | @click.option('-n', '--name', type=click.STRING, help="Name of new log") 13 | @click.option('-f', '--filename', type=click.Path(exists=True, dir_okay=False), 14 | help="Full or relative path to file containing JSON log object") 15 | def createlogset(name=None, filename=None): 16 | """ 17 | Create a logset with the provided name and details. 18 | This method will use the JSON file first if both name and file are provided 19 | """ 20 | if filename is not None: 21 | with open(filename) as json_data: 22 | try: 23 | params = json.load(json_data) 24 | api.create_logset(None, params) 25 | except ValueError as error: 26 | sys.stderr.write(error.message + '\n') 27 | sys.exit(1) 28 | elif name is not None: 29 | api.create_logset(name, None) 30 | else: 31 | click.echo('Example usage: lecli create logset -n new_log_name') 32 | click.echo('Example usage: lecli create logset -f path_to_file.json') 33 | 34 | 35 | @click.command() 36 | def getlogsets(): 37 | """ 38 | Get all logsets for this account 39 | """ 40 | api.get_logsets() 41 | 42 | 43 | @click.command() 44 | @click.argument('logset_id', type=click.STRING) 45 | def getlogset(logset_id): 46 | """ 47 | Get a logset with the provided ID 48 | """ 49 | api.get_logset(logset_id) 50 | 51 | 52 | @click.command() 53 | @click.argument('logset_id', type=click.STRING) 54 | @click.argument('new_name', type=click.STRING) 55 | def renamelogset(logset_id, new_name): 56 | """ 57 | Rename a given logset with the name provided 58 | """ 59 | api.rename_logset(logset_id, new_name) 60 | 61 | 62 | @click.command(help='Available commands are:\n\t"delete_log"\n\t"add_log"') 63 | @click.argument('command', type=click.STRING, default=None) 64 | @click.argument('logset_id', type=click.STRING, default=None) 65 | @click.argument('log_id', type=click.STRING, default=None) 66 | def updatelogset(command, logset_id, log_id): 67 | """Update a logset by adding or deleting a log.""" 68 | if command == 'add_log': 69 | api.add_log(logset_id, log_id) 70 | elif command == 'delete_log': 71 | api.delete_log(logset_id, log_id) 72 | 73 | 74 | @click.command() 75 | @click.argument('logset_id', type=click.STRING) 76 | def deletelogset(logset_id): 77 | """ 78 | Delete a logset 79 | """ 80 | api.delete_logset(logset_id) 81 | 82 | 83 | @click.command() 84 | @click.argument('logset_id', type=click.STRING) 85 | @click.argument('filename', type=click.Path(exists=True, dir_okay=False)) 86 | def replacelogset(logset_id, filename): 87 | """ 88 | Replace a logset of a given id with new details 89 | """ 90 | api.replace_logset_from_file(logset_id, filename) 91 | -------------------------------------------------------------------------------- /lecli/query/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/query/__init__.py -------------------------------------------------------------------------------- /lecli/query/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Query API module. 3 | """ 4 | from __future__ import division 5 | 6 | import json 7 | import sys 8 | import time 9 | import datetime 10 | 11 | import click 12 | import requests 13 | from termcolor import colored 14 | 15 | from lecli import api_utils 16 | from lecli import response_utils 17 | from lecli.logset import api 18 | 19 | ALL_EVENTS_QUERY = "where(/.*/)" 20 | 21 | 22 | def _url(provided_path_parts=()): 23 | """ 24 | Get rest query url of a specific path. 25 | """ 26 | ordered_path_parts = ['query'] 27 | ordered_path_parts.extend(provided_path_parts) 28 | return api_utils.build_url(ordered_path_parts) 29 | 30 | 31 | def handle_response(response, progress_bar): 32 | """ 33 | Handle response. Exit if it has any errors, continue if status code is 202, print response 34 | if status code is 200. 35 | """ 36 | 37 | if response_utils.response_error(response) is True: # Check response has no errors 38 | sys.exit(1) 39 | elif response.status_code == 200: 40 | progress = response.json().get('progress') 41 | if progress: 42 | progress_bar.update(progress) 43 | else: 44 | progress_bar.update(100) 45 | progress_bar.render_finish() 46 | print_response(response) 47 | if 'links' in response.json(): 48 | next_url = response.json()['links'][0]['href'] 49 | next_response = fetch_results(next_url) 50 | handle_response(next_response, progress_bar) 51 | elif response.status_code == 202: 52 | continue_request(response, progress_bar) 53 | 54 | 55 | def handle_tail(response, poll_interval, poll_iteration=1000): 56 | """ 57 | handle tailing loop 58 | """ 59 | for _ in range(poll_iteration): 60 | if response_utils.response_error(response): 61 | sys.exit(1) 62 | elif response.status_code == 200: 63 | print_response(response) 64 | 65 | # fetch results from the next link 66 | if 'links' in response.json(): 67 | next_url = response.json()['links'][0]['href'] 68 | time.sleep(poll_interval) 69 | response = fetch_results(next_url) 70 | else: 71 | click.echo('No continue link found in the received response.', err=True) 72 | 73 | 74 | def continue_request(response, progress_bar): 75 | """ 76 | Continue making request to the url in the response. 77 | """ 78 | progress_bar.update(0) 79 | time.sleep(1) # Wait for 1 second before hitting continue endpoint to prevent hitting API 80 | # limit 81 | if 'links' in response.json(): 82 | continue_url = response.json()['links'][0]['href'] 83 | new_response = fetch_results(continue_url) 84 | handle_response(new_response, progress_bar) 85 | 86 | 87 | def fetch_results(provided_url, params=None): 88 | """ 89 | Make the get request to the url and return the response. 90 | """ 91 | try: 92 | response = requests.get(provided_url, headers=api_utils.generate_headers('rw'), 93 | params=params) 94 | return response 95 | except requests.exceptions.RequestException as error: 96 | click.echo(error, err=True) 97 | sys.exit(1) 98 | 99 | 100 | def validate_query(**kwargs): 101 | """ 102 | Validate query options 103 | """ 104 | date_from = kwargs.get('date_from') 105 | time_from = kwargs.get('time_from') 106 | relative_time_range = kwargs.get('relative_time_range') 107 | saved_query_id = kwargs.get('saved_query_id') 108 | query_string = kwargs.get('query_string') 109 | log_keys = kwargs.get('log_keys') 110 | favorites = kwargs.get('favorites') 111 | logset = kwargs.get('logset') 112 | 113 | valid = True 114 | if all([any([favorites, logset]), log_keys]): 115 | valid = False 116 | click.echo('Cannot define favorites or logsets and logkeys together in the same query ' 117 | 'request.', err=True) 118 | if all([time_from, date_from]): 119 | valid = False 120 | click.echo('Cannot define start time(epoch) and start date(ISO-8601) in the same query ' 121 | 'request.', err=True) 122 | if all([saved_query_id, query_string, query_string != ALL_EVENTS_QUERY]): 123 | valid = False 124 | click.echo('Cannot define saved query and LEQL in the same query request.', 125 | err=True) 126 | if all([favorites, logset]): 127 | valid = False 128 | click.echo('Cannot define a log alias and a log set in the same query request.', 129 | err=True) 130 | if all([relative_time_range, any([time_from, date_from])]): 131 | valid = False 132 | click.echo('Cannot define relative time range and start time/date in the same query ' 133 | 'request.', err=True) 134 | if not any([log_keys, favorites, logset, saved_query_id]): 135 | valid = False 136 | click.echo('Either of log keys, log favorites, log set or saved query must be supplied.', 137 | err=True) 138 | if not any([time_from, date_from, relative_time_range, saved_query_id]): 139 | valid = False 140 | click.echo('Either of start time, start date or relative time range must be supplied.', 141 | err=True) 142 | return valid 143 | 144 | 145 | def query(**kwargs): 146 | """ 147 | Post query to Logentries. 148 | """ 149 | date_from = kwargs.get('date_from') 150 | date_to = kwargs.get('date_to') 151 | time_from = kwargs.get('time_from') 152 | time_to = kwargs.get('time_to') 153 | relative_time_range = kwargs.get('relative_time_range') 154 | saved_query_id = kwargs.get('saved_query_id') 155 | query_string = kwargs.get('query_string') 156 | log_keys = kwargs.get('log_keys') 157 | favorites = kwargs.get('favorites') 158 | logset = kwargs.get('logset') 159 | if not validate_query(date_from=date_from, time_from=time_from, query_string=query_string, 160 | relative_time_range=relative_time_range, saved_query_id=saved_query_id, 161 | log_keys=log_keys, favorites=favorites, logset=logset): 162 | return False 163 | 164 | time_range = prepare_time_range(time_from, time_to, relative_time_range, date_from, date_to) 165 | if favorites: 166 | log_keys = api_utils.get_named_logkey_group(favorites) 167 | if logset: 168 | log_keys = api.get_log_keys_from_logset(logset) 169 | try: 170 | if saved_query_id: 171 | response = run_saved_query(saved_query_id, time_range, log_keys) 172 | else: 173 | response = post_query(log_keys, query_string, time_range) 174 | with click.progressbar(length=100, label='Progress\t') as progress_bar: 175 | handle_response(response, progress_bar) 176 | return True 177 | except requests.exceptions.RequestException as error: 178 | click.echo(error) 179 | sys.exit(1) 180 | 181 | 182 | def post_query(log_keys, query_string, time_range): 183 | """ 184 | POST a request to Rest Query API 185 | 186 | :param log_keys: list of log keys 187 | :param query_string: leql query statement 188 | :param time_range: time range including either relative time range or start and end times 189 | :return: response 190 | """ 191 | payload = {"logs": log_keys, "leql": {"statement": query_string, "during": time_range}} 192 | response = requests.post(_url(('logs',))[1], headers=api_utils.generate_headers('rw'), 193 | json=payload) 194 | return response 195 | 196 | 197 | def prepare_time_range(time_from, time_to, relative_time_range, date_from=None, date_to=None): 198 | """ 199 | Prepare time range based on given options. Options are validated in advance. 200 | """ 201 | if relative_time_range: 202 | return {"time_range": relative_time_range} 203 | elif time_from and time_to: 204 | return {"from": int(time_from) * 1000, "to": int(time_to) * 1000} 205 | elif date_from and date_to: 206 | from_ts = int(time.mktime(time.strptime(date_from, "%Y-%m-%d %H:%M:%S"))) * 1000 207 | to_ts = int(time.mktime(time.strptime(date_to, "%Y-%m-%d %H:%M:%S"))) * 1000 208 | return {"from": from_ts, "to": to_ts} 209 | 210 | 211 | def tail_logs(logkeys, leql, poll_interval, favorites=None, logset=None, saved_query_id=None): 212 | """ 213 | Tail given logs 214 | """ 215 | if favorites: 216 | logkeys = api_utils.get_named_logkey_group(favorites) 217 | elif logset: 218 | logkeys = api.get_log_keys_from_logset(logset) 219 | if saved_query_id: 220 | if logkeys: 221 | url = _url(('live', 'logs', ':'.join(logkeys), str(saved_query_id)))[1] 222 | else: 223 | url = _url(('live', 'saved_query', str(saved_query_id)))[1] 224 | else: 225 | url = _url(('live', 'logs'))[1] 226 | try: 227 | if saved_query_id: 228 | response = requests.get(url, headers=api_utils.generate_headers('rw')) 229 | else: 230 | payload = {'logs': logkeys} 231 | if leql: 232 | payload.update({'leql': {'statement': leql}}) 233 | 234 | response = requests.post(url, headers=api_utils.generate_headers('rw'), json=payload) 235 | handle_tail(response, poll_interval) 236 | return True 237 | except requests.exceptions.RequestException as error: 238 | click.echo(error, err=True) 239 | sys.exit(1) 240 | 241 | 242 | def run_saved_query(saved_query_id, params, log_keys): 243 | """ 244 | Run the given saved query 245 | """ 246 | if log_keys: 247 | url = _url(('logs', ':'.join(log_keys), str(saved_query_id)))[1] 248 | else: 249 | url = _url(('saved_query', str(saved_query_id)))[1] 250 | 251 | return fetch_results(url, params) 252 | 253 | 254 | def print_response(response): 255 | """ 256 | Print response in a human readable way. 257 | """ 258 | if 'events' in response.json(): 259 | prettyprint_events(response) 260 | elif 'statistics' in response.json(): 261 | prettyprint_statistics(response) 262 | 263 | 264 | def prettyprint_events(response): 265 | """ 266 | Print events in a human readable way. 267 | """ 268 | data = response.json() 269 | for event in data['events']: 270 | time_value = datetime.datetime.fromtimestamp(event['timestamp'] / 1000) 271 | human_ts = time_value.strftime('%Y-%m-%d %H:%M:%S') 272 | try: 273 | message = json.loads(event['message']) 274 | click.echo( 275 | colored(str(human_ts), 'red') + '\t' + 276 | colored(json.dumps(message, indent=4, separators={':', ';'}), 'white')) 277 | except ValueError: 278 | click.echo(colored(str(human_ts), 'red') + '\t' + colored(event['message'], 'white')) 279 | 280 | 281 | def prettyprint_statistics(response): 282 | """ 283 | Print statistics in a human readable way. 284 | """ 285 | data = response.json() 286 | 287 | # Extract keys 288 | time_from = data['statistics']['from'] 289 | time_to = data['statistics']['to'] 290 | 291 | # Handle timeseries 292 | if len(data['statistics']['timeseries']) != 0: 293 | # Extract keys 294 | stats_key = data['statistics']['stats'].keys()[0] 295 | stats_calc_value = data['statistics']['stats'].get(stats_key).values() 296 | total = stats_calc_value[0] if len(stats_calc_value) != 0 else 0 297 | click.echo('Total: %s' % total) 298 | 299 | click.echo('Timeseries: ') 300 | timeseries_key = data['statistics']['timeseries'].keys()[0] 301 | time_range = time_to - time_from 302 | num_timeseries_values = len(data['statistics']['timeseries'].get(timeseries_key)) 303 | for index, value in enumerate(data['statistics']['timeseries'].get(timeseries_key)): 304 | timestamp = (time_from + (time_range / num_timeseries_values) * (index + 1)) / 1000 305 | time_value = datetime.datetime.fromtimestamp(timestamp) 306 | human_ts = time_value.strftime('%Y-%m-%d %H:%M:%S') 307 | click.echo(human_ts + ': ' + str(value.values()[0])) 308 | 309 | # Handle Groups 310 | elif len(data['statistics']['groups']) != 0: 311 | for group in data['statistics']['groups']: 312 | for key, value in group.iteritems(): 313 | click.echo(str(key) + ':') 314 | for innerkey, innervalue in value.iteritems(): 315 | click.echo('\t' + str(innerkey) + ': ' + str(innervalue)) 316 | 317 | else: 318 | click.echo(json.dumps(response.json(), indent=4, separators={':', ';'})) 319 | -------------------------------------------------------------------------------- /lecli/query/commands.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-arguments 2 | """ 3 | Module for query commands 4 | """ 5 | import time 6 | import click 7 | 8 | from lecli.query import api 9 | 10 | 11 | @click.command() 12 | # nargs (-1) makes sure multiple log keys is supported 13 | @click.argument('logkeys', type=click.STRING, nargs=-1) 14 | @click.option('-c', '--favorites', default=None, 15 | help='Alias of log in config file') 16 | @click.option('-g', '--logset', default=None, 17 | help='Name of logset to be got from server') 18 | @click.option('-l', '--leql', default=None, 19 | help='LEQL query') 20 | @click.option('-f', '--timefrom', 21 | help='Time to query from (unix epoch)', type=int) 22 | @click.option('-t', '--timeto', 23 | help='Time to query to (unix epoch)', type=int) 24 | @click.option('--datefrom', 25 | help='Date/Time to query from (ISO-8601 datetime)') 26 | @click.option('--dateto', 27 | help='Date/Time to query to (ISO-8601 datetime)') 28 | @click.option('-r', '--relative-range', 29 | help='Relative range to query until now (Examples: today, yesterday, last 10 min, ' 30 | 'last 6 weeks') 31 | @click.option('-s', '--saved-query', help='Saved query UUID to run.', type=click.UUID) 32 | def query(logkeys, favorites, logset, leql, timefrom, timeto, datefrom, dateto, 33 | relative_range, saved_query): 34 | """Query logs using LEQL""" 35 | success = api.query(log_keys=logkeys, query_string=leql, date_from=datefrom, date_to=dateto, 36 | time_from=timefrom, time_to=timeto, saved_query_id=saved_query, 37 | relative_time_range=relative_range, favorites=favorites, logset=logset) 38 | 39 | if not success: 40 | click.echo("Example usage: lecli query --logset mylogset --leql " 41 | "'where(method=GET) calculate(count)' " 42 | "--datefrom '2016-05-18 11:04:00' --dateto '2016-05-18 11:09:59' ") 43 | click.echo("Example usage: lecli query --favorites mylogalias --leql " 44 | "'where(method=GET) calculate(count)' " 45 | "--datefrom '2016-05-18 11:04:00' --dateto '2016-05-18 11:09:59' ") 46 | click.echo("Example usage: lecli query --favorites mylogalias --leql " 47 | "'where(method=GET) calculate(count)' " 48 | "-r 'last 3 days'") 49 | 50 | 51 | @click.command() 52 | # nargs (-1) makes sure multiple log keys is supported 53 | @click.argument('logkeys', type=click.STRING, nargs=-1) 54 | @click.option('-c', '--favorites', type=click.STRING, 55 | help='Alias of log in config file') 56 | @click.option('-g', '--logset', type=click.STRING, 57 | help='Name of logset to be got from server') 58 | @click.option('-f', '--timefrom', type=click.INT, 59 | help='Time to get events from (unix epoch)') 60 | @click.option('-t', '--timeto', type=click.INT, 61 | help='Time to get events to (unix epoch)') 62 | @click.option('--datefrom', type=click.STRING, 63 | help='Date/Time to get events from (ISO-8601 datetime)') 64 | @click.option('--dateto', type=click.STRING, 65 | help='Date/Time to get events to (ISO-8601 datetime)') 66 | @click.option('-r', '--relative-range', type=click.STRING, 67 | help='Relative range to query until now (Examples: today, yesterday, ' 68 | 'last x timeunit: last 2 hours, last 6 weeks etc.') 69 | @click.option('-s', '--saved-query', help='Saved query UUID to run.', type=click.UUID) 70 | def get_events(logkeys, favorites, logset, timefrom, timeto, datefrom, dateto, relative_range, 71 | saved_query): 72 | """Get log events""" 73 | success = api.query(log_keys=logkeys, time_from=timefrom, query_string=api.ALL_EVENTS_QUERY, 74 | time_to=timeto, date_from=datefrom, date_to=dateto, logset=logset, 75 | relative_time_range=relative_range, favorites=favorites, 76 | saved_query_id=saved_query) 77 | if not success: 78 | click.echo("Example usage: lecli get events 12345678-aaaa-bbbb-1234-1234cb123456 " 79 | "-f 1465370400 -t 1465370500") 80 | click.echo("Example usage: lecli get events 12345678-aaaa-bbbb-1234-1234cb123456 " 81 | "--datefrom '2016-05-18 11:04:00' --dateto '2016-05-18 11:09:59' ") 82 | click.echo("Example usage: lecli get events --logset mylogset " 83 | "--datefrom '2016-05-18 11:04:00' --dateto '2016-05-18 11:09:59' ") 84 | click.echo("Example usage: lecli get events --favorites mylogalias " 85 | "--datefrom '2016-05-18 11:04:00' --dateto '2016-05-18 11:09:59' ") 86 | click.echo("Example usage: lecli get events --favorites mylogalias " 87 | "-r 'last 3 hours'") 88 | 89 | 90 | @click.command() 91 | # nargs (-1) makes sure multiple log keys is supported 92 | @click.argument('logkeys', type=click.STRING, nargs=-1) 93 | @click.option('-c', '--favorites', type=click.STRING, default=None, 94 | help='Alias of log in config file') 95 | @click.option('-g', '--logset', type=click.STRING, default=None, 96 | help='Name of logset to be got from server') 97 | @click.option('-l', '--last', type=click.INT, default=1200, 98 | help='Time window from now to now-X in seconds over which events will be returned ' 99 | '(Defaults to 20 mins)') 100 | @click.option('-r', '--relative-range', type=click.STRING, 101 | help='Relative range to query until now (Examples: today, yesterday, ' 102 | 'last x timeunit: last 2 hours, last 6 weeks etc.') 103 | @click.option('-s', '--saved-query', help='Saved query to run', type=click.UUID) 104 | def get_recent_events(logkeys, favorites, logset, last, relative_range, saved_query): 105 | """Get recent log events""" 106 | start_time = now = None 107 | if not relative_range: 108 | now = time.time() 109 | start_time = now - last 110 | 111 | success = api.query(log_keys=logkeys, query_string=api.ALL_EVENTS_QUERY, 112 | time_from=start_time, time_to=now, relative_time_range=relative_range, 113 | favorites=favorites, logset=logset, saved_query_id=saved_query) 114 | if not success: 115 | click.echo( 116 | 'Example usage: lecli get recentevents 12345678-aaaa-bbbb-1234-1234cb123456 -l 200') 117 | click.echo('Example usage: lecli get recentevents -c mylogalias -l 200') 118 | click.echo('Example usage: lecli get recentevents -g mylogset -l 200') 119 | click.echo("Example usage: lecli get recentevents -g mylogset -r 'last 50 mins'") 120 | 121 | 122 | @click.command() 123 | # nargs (-1) makes sure multiple log keys is supported 124 | @click.argument('logkeys', type=click.STRING, nargs=-1) 125 | @click.option('-c', '--favorites', type=click.STRING, default=None, 126 | help='Alias of log in config file') 127 | @click.option('-g', '--logset', type=click.STRING, default=None, 128 | help='Name of logset to be got from server') 129 | @click.option('-l', '--leql', type=click.STRING, default=None, 130 | help='LEQL query to filter') 131 | @click.option('-i', '--poll-interval', type=click.FLOAT, default=1.0, 132 | help='Request interval of live tail in seconds, default is 1.0 second.') 133 | @click.option('-s', '--saved-query', type=click.UUID, help='Saved query id to tail.') 134 | def tail_events(logkeys, favorites, logset, leql, poll_interval, saved_query): 135 | """Tail events of given logkey(s) with provided options""" 136 | success = api.tail_logs(logkeys, leql, poll_interval, favorites, logset, saved_query) 137 | 138 | if not success: 139 | click.echo("Example usage: lecli tail events 12345678-aaaa-bbbb-1234-1234cb123456") 140 | -------------------------------------------------------------------------------- /lecli/response_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Response utils 3 | """ 4 | import sys 5 | import requests 6 | 7 | 8 | def response_error(response): 9 | """ 10 | Check response if it has any errors. 11 | """ 12 | if response.headers.get('X-RateLimit-Remaining') is not None: 13 | if int(response.headers['X-RateLimit-Remaining']) == 0: 14 | sys.stderr.write('Error: Rate Limit Reached, will reset in ' + response.headers.get( 15 | 'X-RateLimit-Reset') + ' seconds \n') 16 | return True 17 | try: 18 | response.raise_for_status() 19 | except requests.exceptions.HTTPError as error: 20 | sys.stderr.write("\nRequest Error:\t %s" % error.message) 21 | try: 22 | sys.stderr.write("\nError code:\t %s" % response.json()['errorCode']) 23 | sys.stderr.write("\nError message:\t %s " % response.json()['message']) 24 | except (ValueError, KeyError): 25 | pass 26 | 27 | if response.status_code == 500: 28 | sys.stderr.write('Your account may have no owner assigned. ' 29 | 'Please visit www.logentries.com for information on ' 30 | 'assigning an account owner. \n') 31 | return True 32 | 33 | if response.status_code == 200: 34 | if response.headers['Content-Type'] != 'application/json': 35 | sys.stderr.write('Unexpected Content Type Received in Response: ' + response.headers[ 36 | 'Content-Type']) 37 | return True 38 | else: 39 | return False 40 | return False 41 | -------------------------------------------------------------------------------- /lecli/saved_query/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/saved_query/__init__.py -------------------------------------------------------------------------------- /lecli/saved_query/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Saved Query API module. 3 | """ 4 | import sys 5 | import click 6 | import requests 7 | 8 | from lecli import api_utils 9 | from lecli import response_utils 10 | 11 | 12 | def _url(provided_path_parts=()): 13 | """ 14 | Get rest query url of account resource id. 15 | """ 16 | ordered_path_parts = ['query', 'saved_queries'] 17 | ordered_path_parts.extend(provided_path_parts) 18 | return api_utils.build_url(ordered_path_parts) 19 | 20 | 21 | def _pretty_print_saved_query(query): 22 | """ 23 | Pretty print saved query object 24 | :param query: 25 | """ 26 | click.echo("Name: \t%s" % query['name']) 27 | click.echo("Logs: \t%s" % ",".join(query['logs'])) 28 | click.echo("ID: \t%s" % query['id']) 29 | click.echo("LEQL \tStatement: \t%s" % query['leql']['statement']) 30 | click.echo("\tTime range: \t%s" % query['leql']['during'].get('time_range')) 31 | click.echo("\tFrom: \t\t%s" % query['leql']['during'].get('from')) 32 | click.echo("\tTo: \t\t%s" % query['leql']['during'].get('to')) 33 | click.echo("**********************************************") 34 | 35 | 36 | def _pretty_print_saved_query_error(response): 37 | """ 38 | Pretty print saved query error 39 | """ 40 | try: 41 | error_body = response.json() 42 | if 'fields' in error_body: 43 | click.echo('Invalid field: %s' % ",".join(response.json()['fields'])) 44 | if 'messages' in error_body: 45 | click.echo('Message: %s' % ",".join(response.json()['messages'])) 46 | except ValueError: 47 | sys.exit(1) 48 | 49 | 50 | def _handle_saved_query_response(response): 51 | """ 52 | Handle saved query response, check whether there are multiple entities in the response or not 53 | :param response: 54 | """ 55 | if response.json().get('saved_queries'): 56 | queries = response.json()['saved_queries'] 57 | for query in queries: 58 | _pretty_print_saved_query(query) 59 | elif response.json().get('saved_query'): 60 | query = response.json()['saved_query'] 61 | _pretty_print_saved_query(query) 62 | 63 | 64 | def get_saved_query(query_id=None): 65 | """ 66 | If query id is provided, get this specific saved query or get them all 67 | :param query_id: uuid of saved query to be retrieved(optional) 68 | """ 69 | endpoint_url = _url()[1] 70 | if query_id: 71 | endpoint_url = _url((query_id,))[1] 72 | headers = api_utils.generate_headers('rw') 73 | try: 74 | response = requests.get(endpoint_url, headers=headers) 75 | if response_utils.response_error(response): 76 | if query_id: 77 | sys.stderr.write("Unable to retrieve saved query with id %s" % query_id) 78 | else: 79 | sys.stderr.write("Unable to retrieve saved queries.") 80 | elif response.status_code == 200: 81 | _handle_saved_query_response(response) 82 | except requests.exceptions.RequestException as error: 83 | click.echo(error) 84 | sys.exit(1) 85 | 86 | 87 | def delete_saved_query(query_id): 88 | """ 89 | Delete a specific saved query 90 | :param query_id: uuid of saved query to be deleted 91 | """ 92 | headers = api_utils.generate_headers('rw') 93 | try: 94 | response = requests.delete(_url((query_id,))[1], headers=headers) 95 | if response_utils.response_error(response): 96 | sys.stderr.write('Delete saved query failed.\n') 97 | elif response.status_code == 204: 98 | click.echo('Deleted saved query with id: %s' % query_id) 99 | except requests.exceptions.RequestException as error: 100 | click.echo(error) 101 | sys.exit(1) 102 | 103 | 104 | def create_saved_query(name, statement, from_ts=None, to_ts=None, time_range=None, logs=None): 105 | """ 106 | Create a new saved query with the provided fields. 107 | :param name: name of the saved query (mandatory) 108 | :param statement: leql statement of the query (mandatory) 109 | :param from_ts: 'from' timestamp of query - unix epoch timestamp (optional) 110 | :param to_ts: 'to' timestamp of query - unix epoch timestamp (optional) 111 | :param time_range: time range of query - cannot be defimed with 'from' and/or 'to' fields( 112 | optional) 113 | :param logs: list of logs of the saved query, colon(:) separated uuids. 114 | """ 115 | headers = api_utils.generate_headers('rw') 116 | params = { 117 | 'saved_query': { 118 | 'name': name, 119 | 'leql': { 120 | 'statement': statement, 121 | 'during': { 122 | 'from': from_ts, 123 | 'to': to_ts, 124 | 'time_range': time_range 125 | } 126 | }, 127 | 'logs': logs.split(':') if logs else [] 128 | } 129 | } 130 | 131 | try: 132 | response = requests.post(_url()[1], json=params, headers=headers) 133 | if response_utils.response_error(response): 134 | sys.stderr.write('Creating saved query failed.\n') 135 | _pretty_print_saved_query_error(response) 136 | elif response.status_code == 201: 137 | click.echo('Saved query created with name: %s' % name) 138 | _pretty_print_saved_query(response.json()['saved_query']) 139 | except requests.exceptions.RequestException as error: 140 | click.echo(error) 141 | sys.exit(1) 142 | 143 | 144 | def update_saved_query(query_id, name=None, statement=None, from_ts=None, to_ts=None, 145 | time_range=None, logs=None): 146 | """ 147 | Update a saved query with the given parameters. 148 | :param query_id: id of the saved query to be updated 149 | :param name: new name of the saved query 150 | :param statement: new leql statement of the saved query 151 | :param from_ts: new 'from' timestamp of the saved query 152 | :param to_ts: new 'to' timestamp of the saved query 153 | :param time_range: new time range of the saved query 154 | :param logs: colon(:) separated list of logs of the saved query 155 | """ 156 | headers = api_utils.generate_headers('rw') 157 | params = { 158 | 'saved_query': { 159 | } 160 | } 161 | 162 | if name: 163 | params['saved_query']['name'] = name 164 | 165 | if logs: 166 | params['saved_query']['logs'] = logs.split(':') 167 | 168 | if any([statement, from_ts, to_ts, time_range]): 169 | leql = {} 170 | if statement: 171 | leql['statement'] = statement 172 | if any([from_ts, to_ts, time_range]): 173 | during = {} 174 | if from_ts: 175 | during.update({'from': from_ts, 'to': None, 'time_range': None}) 176 | if to_ts: 177 | during.update({'to': to_ts, 'time_range': None}) 178 | if time_range: 179 | during.update({'time_range': time_range, 'from': None, 'to': None}) 180 | leql['during'] = during 181 | params['saved_query']['leql'] = leql 182 | 183 | try: 184 | response = requests.patch(_url((query_id,))[1], json=params, headers=headers) 185 | if response_utils.response_error(response): 186 | sys.stderr.write('Updating saved query failed.\n') 187 | _pretty_print_saved_query_error(response) 188 | elif response.status_code == 200: 189 | click.echo('Saved query with id %s updated.' % query_id) 190 | _pretty_print_saved_query(response.json()['saved_query']) 191 | except requests.exceptions.RequestException as error: 192 | click.echo(error) 193 | sys.exit(1) 194 | -------------------------------------------------------------------------------- /lecli/saved_query/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for saved query commands 3 | """ 4 | import click 5 | 6 | from lecli.saved_query import api 7 | 8 | 9 | @click.command() 10 | def get_saved_queries(): 11 | """Get a list of saved queries""" 12 | api.get_saved_query() 13 | 14 | 15 | @click.command() 16 | @click.argument('query_id', type=click.STRING, default=None) 17 | def get_saved_query(query_id): 18 | """Get the saved query with the given ID""" 19 | api.get_saved_query(query_id) 20 | 21 | 22 | @click.command() 23 | @click.argument('name', type=click.STRING) 24 | @click.argument('statement', type=click.STRING) 25 | @click.option('-f', '--timefrom', help='Time to query from (unix epoch)', type=int) 26 | @click.option('-t', '--timeto', help='Time to query to (unix epoch)', type=int) 27 | @click.option('-r', '--relative-range', help='Relative time range (ex: last x :timeunit)', 28 | type=click.STRING) 29 | @click.option('-l', '--logs', help='Logs(colon delimited if multiple)', type=click.STRING) 30 | def create_saved_query(name, statement, timefrom, timeto, relative_range, logs): 31 | """Create a saved query with the given arguments""" 32 | api.create_saved_query(name, statement, timefrom, timeto, relative_range, logs) 33 | 34 | 35 | @click.command() 36 | @click.argument('query_id', type=click.STRING) 37 | @click.option('-n', '--name', help='Name of the saved query', type=click.STRING) 38 | @click.option('-s', '--statement', help='LEQL statement', type=click.STRING) 39 | @click.option('-f', '--timefrom', help='Time to query from (unix epoch)', type=int) 40 | @click.option('-t', '--timeto', help='Time to query to (unix epoch)', type=int) 41 | @click.option('-r', '--relative-range', help='Relative time range (ex: last x :timeunit)', 42 | type=click.STRING) 43 | @click.option('-l', '--logs', help='Logs(colon delimited if multiple)', type=click.STRING) 44 | def update_saved_query(query_id, name, statement, timefrom, timeto, relative_range, logs): 45 | """Update the saved query with the given arguments""" 46 | api.update_saved_query(query_id, name, statement, timefrom, timeto, relative_range, 47 | logs) 48 | 49 | 50 | @click.command() 51 | @click.argument('query_id', type=click.STRING) 52 | def delete_saved_query(query_id): 53 | """Delete the saved query with given ID""" 54 | api.delete_saved_query(query_id) 55 | -------------------------------------------------------------------------------- /lecli/team/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/team/__init__.py -------------------------------------------------------------------------------- /lecli/team/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Team API module. 3 | """ 4 | import sys 5 | 6 | import click 7 | import requests 8 | from tabulate import tabulate 9 | 10 | from lecli import api_utils 11 | from lecli import response_utils 12 | 13 | 14 | def _url(provided_path_parts=()): 15 | """ 16 | Get rest query url of account resource id. 17 | """ 18 | ordered_path_parts = ['management', 'accounts', api_utils.get_account_resource_id(), 'teams'] 19 | ordered_path_parts.extend(provided_path_parts) 20 | return api_utils.build_url(ordered_path_parts) 21 | 22 | 23 | def print_teams(response): 24 | """ 25 | Print teams. 26 | """ 27 | for item in response: 28 | click.echo("ID: %s" % item['id']) 29 | click.echo("Name: %s" % item['name']) 30 | click.echo("Users: %s" % tabulate(item['users'])) 31 | 32 | 33 | def print_team(response): 34 | """ 35 | Print team. 36 | """ 37 | click.echo("ID: %s" % response['id']) 38 | click.echo("Name: %s" % response['name']) 39 | click.echo("Users: %s" % tabulate(response['users'])) 40 | 41 | 42 | def handle_get_teams_response(response): 43 | """ 44 | Handle get teams response. 45 | """ 46 | if response_utils.response_error(response): 47 | sys.exit(1) 48 | elif response.status_code == 200: 49 | if response.json().get('teams'): 50 | print_teams(response.json()['teams']) 51 | elif response.json().get('team'): 52 | print_team(response.json()['team']) 53 | 54 | 55 | def get_teams(): 56 | """ 57 | Get teams associated with the user. 58 | """ 59 | headers = api_utils.generate_headers('rw') 60 | try: 61 | response = requests.get(_url()[1], data='', headers=headers) 62 | handle_get_teams_response(response) 63 | except requests.exceptions.RequestException as error: 64 | click.echo(error, err=True) 65 | sys.exit(1) 66 | 67 | 68 | def get_team(team_id): 69 | """ 70 | Get a specific team. 71 | """ 72 | headers = api_utils.generate_headers('rw') 73 | params = {'teamid': team_id} 74 | try: 75 | response = requests.get(_url((team_id,))[1], params=params, headers=headers) 76 | handle_get_teams_response(response) 77 | except requests.exceptions.RequestException as error: 78 | click.echo(error, err=True) 79 | sys.exit(1) 80 | 81 | 82 | def create_team(name): 83 | """ 84 | Add a new user to the current account. 85 | """ 86 | params = { 87 | 'team': { 88 | 'name': str(name), 89 | 'users': [] 90 | } 91 | } 92 | headers = api_utils.generate_headers('rw') 93 | 94 | try: 95 | response = requests.post(_url()[1], json=params, headers=headers) 96 | if response_utils.response_error(response): 97 | click.echo('Creating team failed.', err=True) 98 | sys.exit(1) 99 | elif response.status_code == 201: 100 | click.echo('Team created with name: %s' % name) 101 | 102 | except requests.exceptions.RequestException as error: 103 | click.echo(error, err=True) 104 | sys.exit(1) 105 | 106 | 107 | def delete_team(team_id): 108 | """ 109 | Delete a team with the provided team ID. 110 | """ 111 | headers = api_utils.generate_headers('rw') 112 | 113 | try: 114 | response = requests.delete(_url((team_id,))[1], headers=headers) 115 | if response_utils.response_error(response): # Check response has no errors 116 | click.echo('Delete team failed.', err=True) 117 | sys.exit(1) 118 | elif response.status_code == 204: 119 | click.echo('Deleted team with id: %s.' % team_id) 120 | except requests.exceptions.RequestException as error: 121 | click.echo(error, err=True) 122 | sys.exit(1) 123 | 124 | 125 | def rename_team(team_id, team_name): 126 | """ 127 | Rename team with the provided team_id. 128 | """ 129 | params = { 130 | 'team': { 131 | 'name': team_name, 132 | # as this is a patch request, it won't modify users in the team. 133 | # what we want is to update the name of the team only. 134 | 'users': [ 135 | {'id': ''} 136 | ] 137 | } 138 | } 139 | headers = api_utils.generate_headers('rw') 140 | 141 | try: 142 | response = requests.patch(_url((team_id,))[1], json=params, headers=headers) 143 | if response_utils.response_error(response): # Check response has no errors 144 | click.echo('Renaming team with id: %s failed.' % team_id, err=True) 145 | sys.exit(1) 146 | elif response.status_code == 200: 147 | click.echo("Team: '%s' renamed to: '%s'" % (team_id, team_name)) 148 | except requests.exceptions.RequestException as error: 149 | click.echo(error, err=True) 150 | sys.exit(1) 151 | 152 | 153 | def add_user_to_team(team_id, user_key): 154 | """ 155 | Add user with the provided user_key to team with provided team_id. 156 | """ 157 | headers = api_utils.generate_headers('rw') 158 | params = {'teamid': team_id} 159 | try: 160 | response = requests.get(_url((team_id,))[1], params=params, headers=headers) 161 | if response.status_code == 200: 162 | params = { 163 | 'team': { 164 | 'name': response.json()['team']['name'], 165 | 'users': [ 166 | # we are doing a patch request here so it's safe to include the user_key 167 | # we want to add here 168 | {'id': user_key} 169 | ] 170 | } 171 | } 172 | headers = api_utils.generate_headers('rw') 173 | try: 174 | response = requests.patch(_url((team_id,))[1], json=params, headers=headers) 175 | if response_utils.response_error(response): # Check response has no errors 176 | click.echo('Adding user to team with key: %s failed.' % team_id, err=True) 177 | sys.exit(1) 178 | elif response.status_code == 200: 179 | click.echo('Added user with key: %s to team.' % user_key) 180 | except requests.exceptions.RequestException as error: 181 | click.echo(error, err=True) 182 | sys.exit(1) 183 | elif response_utils.response_error(response): 184 | click.echo('Cannot find team. Adding user to team %s failed.' % team_id, err=True) 185 | sys.exit(1) 186 | except requests.exceptions.RequestException as error: 187 | click.echo(error, err=True) 188 | sys.exit(1) 189 | 190 | 191 | def delete_user_from_team(team_id, user_key): 192 | """ 193 | Delete a user from a team. 194 | """ 195 | headers = api_utils.generate_headers('rw') 196 | params = {'teamid': team_id} 197 | try: 198 | response = requests.request('GET', _url((team_id,))[1], params=params, 199 | headers=headers) 200 | if response.status_code == 200: 201 | params = { 202 | 'team': { 203 | 'name': response.json()['team']['name'], 204 | 'users': [user for user in response.json()['team']['users'] if user['id'] != 205 | user_key] 206 | } 207 | } 208 | headers = api_utils.generate_headers('rw') 209 | try: 210 | response = requests.put(_url((team_id,))[1], json=params, headers=headers) 211 | if response_utils.response_error(response): # Check response has no errors 212 | click.echo('Deleting user from team with key: %s failed.' % team_id, err=True) 213 | sys.exit(1) 214 | elif response.status_code == 200: 215 | click.echo("Deleted user with key: '%s' from team: %s" % (user_key, team_id)) 216 | except requests.exceptions.RequestException as error: 217 | click.echo(error, err=True) 218 | sys.exit(1) 219 | elif response_utils.response_error(response): 220 | click.echo('Cannot find team. Deleting user from team %s failed.' % team_id, err=True) 221 | sys.exit(1) 222 | except requests.exceptions.RequestException as error: 223 | click.echo(error, err=True) 224 | sys.exit(1) 225 | -------------------------------------------------------------------------------- /lecli/team/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for team commands 3 | """ 4 | import click 5 | 6 | from lecli.team import api 7 | 8 | 9 | @click.command() 10 | def get_teams(): 11 | """Get teams that are associated with this account""" 12 | api.get_teams() 13 | 14 | 15 | @click.command() 16 | @click.argument('teamid', type=click.STRING, default=None) 17 | def get_team(teamid): 18 | """Get team with the provided id""" 19 | if teamid is not None: 20 | api.get_team(teamid) 21 | 22 | 23 | @click.command() 24 | @click.argument('name', type=click.STRING, default=None) 25 | def create_team(name): 26 | """Create a team with the provided name""" 27 | if name is not None: 28 | api.create_team(name) 29 | 30 | 31 | @click.command() 32 | @click.argument('teamid', type=click.STRING, default=None) 33 | def delete_team(teamid): 34 | """Delete a team with the provided id""" 35 | api.delete_team(teamid) 36 | 37 | 38 | @click.command() 39 | @click.argument('teamid', type=click.STRING, default=None) 40 | @click.argument('name', type=click.STRING, default=None) 41 | def rename_team(teamid, name): 42 | """Rename a given team with the name provided""" 43 | api.rename_team(teamid, name) 44 | 45 | 46 | @click.command(help='Available commands are:\n\t"add_user"\n\t"delete_user"') 47 | @click.argument('command', type=click.STRING, default=None) 48 | @click.argument('teamid', type=click.STRING, default=None) 49 | @click.argument('userkey', type=click.STRING, default=None) 50 | def updateteam(command, teamid, userkey): 51 | """Update a team by adding or deleting a user.""" 52 | if command == 'add_user': 53 | api.add_user_to_team(teamid, userkey) 54 | elif command == 'delete_user': 55 | api.delete_user_from_team(teamid, userkey) 56 | else: 57 | click.echo('Missing argument "command".') 58 | 59 | 60 | @click.command() 61 | @click.argument('teamid', type=click.STRING, default=None) 62 | @click.argument('userkey', type=click.STRING, default=None) 63 | def addusertoteam(teamid, userkey): 64 | """ 65 | DEPRECATED 66 | Update the team with the provided id with name and user. 67 | This will add the user to this team if it exists""" 68 | api.add_user_to_team(teamid, userkey) 69 | 70 | 71 | @click.command() 72 | @click.argument('teamid', type=click.STRING, default=None) 73 | @click.argument('userkey', type=click.STRING, default=None) 74 | def deleteuserfromteam(teamid, userkey): 75 | """ 76 | DEPRECATED 77 | Delete the user with the given userkey from the team 78 | """ 79 | api.delete_user_from_team(teamid, userkey) 80 | -------------------------------------------------------------------------------- /lecli/usage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/usage/__init__.py -------------------------------------------------------------------------------- /lecli/usage/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Account usage API module. 3 | """ 4 | import sys 5 | import requests 6 | from tabulate import tabulate 7 | 8 | from lecli import api_utils 9 | from lecli import response_utils 10 | 11 | 12 | def _url(): 13 | """ 14 | Get rest query url of account resource id. 15 | """ 16 | ordered_path_parts = ['usage', 'accounts', api_utils.get_account_resource_id()] 17 | return api_utils.build_url(ordered_path_parts) 18 | 19 | 20 | def _handle_get_usage_response(response): 21 | """ 22 | Handle get usage response 23 | """ 24 | response_json = response.json() 25 | daily_usage = response_json['daily_usage'] 26 | total_usage = response_json['period_usage'] 27 | acc_name = response_json['name'] 28 | acc_id = response_json['id'] 29 | 30 | print tabulate(sorted(daily_usage, key=lambda x: x['day']), headers='keys', 31 | tablefmt='pipe') 32 | print "Total usage:\t%s" % total_usage 33 | print "Account name:\t%s" % acc_name 34 | print "Account ID:\t%s" % acc_id 35 | 36 | 37 | def get_usage(start, end): 38 | """ 39 | Get usage information for the account between start and end dates. 40 | """ 41 | headers = api_utils.generate_headers('rw') 42 | params = {'from': start, 43 | 'to': end} 44 | try: 45 | response = requests.get(_url()[1], params=params, headers=headers) 46 | if response_utils.response_error(response): 47 | sys.stderr.write("Getting account usage failed. Status code %s" 48 | % response.status_code) 49 | sys.exit(1) 50 | else: 51 | _handle_get_usage_response(response) 52 | except requests.exceptions.RequestException as error: 53 | sys.stderr.write(error) 54 | sys.exit(1) 55 | -------------------------------------------------------------------------------- /lecli/usage/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for usage commands 3 | """ 4 | import click 5 | 6 | from lecli.usage import api 7 | 8 | 9 | @click.command() 10 | @click.option('-s', '--start', type=click.STRING, default=None) 11 | @click.option('-e', '--end', type=click.STRING, default=None) 12 | def get_usage(start, end): 13 | """Get account's usage information""" 14 | if all([start, end]): 15 | api.get_usage(start, end) 16 | else: 17 | click.echo("Example usage: lecli get usage -s '2016-01-01' -e '2016-06-01'") 18 | click.echo("Note: Start and end dates should be in ISO-8601 format: YYYY-MM-DD") 19 | -------------------------------------------------------------------------------- /lecli/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/lecli/user/__init__.py -------------------------------------------------------------------------------- /lecli/user/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | User API module. 3 | """ 4 | import sys 5 | import json 6 | 7 | import requests 8 | from tabulate import tabulate 9 | 10 | from lecli import api_utils 11 | from lecli import response_utils 12 | 13 | 14 | def _url(provided_path_parts=()): 15 | """ 16 | Get rest query url of account resource id. 17 | """ 18 | ordered_path_parts = ['management', 'accounts', api_utils.get_account_resource_id()] 19 | ordered_path_parts.extend(provided_path_parts) 20 | return api_utils.build_url(ordered_path_parts) 21 | 22 | 23 | def handle_userlist_response(response): 24 | """ 25 | Handle userlist response. Exit if it has any errors, print if status code is 200. 26 | """ 27 | if response_utils.response_error(response) is True: # Check response has no errors 28 | sys.stderr.write(response.text) 29 | sys.exit(1) 30 | elif response.status_code == 200: 31 | print_users(response) 32 | 33 | 34 | def handle_create_user_response(response): 35 | """ 36 | Handle create user response. If it has any errors, print help. 37 | """ 38 | if response_utils.response_error(response) is True: # Check response has no errors 39 | if response.status_code >= 400: 40 | sys.stderr.write('Failed to add user - User may have already been ' 41 | 'added this account or have a Logentries account') 42 | sys.stderr.write('To add a new user: lecli adduser -f John -l Smyth -e john@smyth.com') 43 | sys.stderr.write('To add an existing user using their User Key: ' 44 | 'lecli adduser -u 12345678-aaaa-bbbb-1234-1234cb123456') 45 | sys.exit(1) 46 | 47 | if response.status_code == 200: 48 | user = response.json()['user'] 49 | print 'Added user to account:\nName: %s %s \nLogin: %s \nEmail: %s \nUser Key: %s' % \ 50 | (user['first_name'], user['last_name'], user['login_name'], user['email'], user['id']) 51 | 52 | if response.status_code == 201: 53 | user = response.json()['user'] 54 | print 'Added user to account:\nName: %s %s \nLogin: %s \nEmail: %s \nUser Key: %s' % \ 55 | (user['first_name'], user['last_name'], user['login_name'], user['email'], user['id']) 56 | 57 | if response.status_code == 403: 58 | sys.stderr.write("User you attempted to add is the account owner") 59 | 60 | 61 | def list_users(): 62 | """ 63 | List users that is in the current account. 64 | """ 65 | action, url = _url(('users',)) 66 | try: 67 | response = requests.request('GET', url, 68 | headers=api_utils.generate_headers('owner', 'GET', action, '')) 69 | handle_userlist_response(response) 70 | except requests.exceptions.RequestException as error: 71 | sys.stderr.write(error) 72 | sys.exit(1) 73 | 74 | 75 | def add_new_user(first_name, last_name, email): 76 | """ 77 | Add a new user to the current account. 78 | """ 79 | action, url = _url(('users',)) 80 | json_content = { 81 | "user": 82 | { 83 | "email": str(email), 84 | "first_name": str(first_name), 85 | "last_name": str(last_name) 86 | } 87 | } 88 | body = json.dumps(json_content) 89 | headers = api_utils.generate_headers('owner', method='POST', action=action, body=body) 90 | 91 | try: 92 | response = requests.request('POST', url, json=json_content, headers=headers) 93 | handle_create_user_response(response) 94 | except requests.exceptions.RequestException as error: 95 | sys.stderr.write(error) 96 | sys.exit(1) 97 | 98 | 99 | def add_existing_user(user_key): 100 | """ 101 | Add a user that already exist to the current account. 102 | """ 103 | action, url = _url(('users', user_key)) 104 | headers = api_utils.generate_headers('owner', method='POST', action=action, body='') 105 | 106 | try: 107 | response = requests.request('POST', url, data='', headers=headers) 108 | handle_create_user_response(response) 109 | except requests.exceptions.RequestException as error: 110 | sys.stderr.write(error) 111 | sys.exit(1) 112 | 113 | 114 | def delete_user(user_key): 115 | """ 116 | Delete a user from the current account. 117 | """ 118 | action, url = _url(('users', user_key)) 119 | headers = api_utils.generate_headers('owner', method='DELETE', action=action, body='') 120 | 121 | try: 122 | response = requests.request('DELETE', url, data='', headers=headers) 123 | if response_utils.response_error(response) is True: # Check response has no errors 124 | sys.stderr.write('Delete user failed, status code: %s' % response.status_code) 125 | sys.exit(1) 126 | elif response.status_code == 204: 127 | print 'Deleted user' 128 | except requests.exceptions.RequestException as error: 129 | sys.stderr.write(error) 130 | sys.exit(1) 131 | 132 | 133 | def get_owner(): 134 | """ 135 | Get owner information of the current account. 136 | """ 137 | action, url = _url(('owners',)) 138 | try: 139 | response = requests.request('GET', url, 140 | headers=api_utils.generate_headers('owner', 'GET', action, '')) 141 | handle_userlist_response(response) 142 | except requests.exceptions.RequestException as error: 143 | sys.stderr.write(error) 144 | sys.exit(1) 145 | 146 | 147 | def print_users(response): 148 | """ 149 | Print users in the current account. 150 | """ 151 | if 'users' in response.json(): 152 | print tabulate(response.json()['users'], headers={}) 153 | elif 'owners' in response.json(): 154 | print tabulate(response.json()['owners'], headers={}) 155 | -------------------------------------------------------------------------------- /lecli/user/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for user commands 3 | """ 4 | import click 5 | 6 | from lecli.user import api 7 | 8 | 9 | @click.command() 10 | def get_users(): 11 | """Get list of users in account""" 12 | api.list_users() 13 | 14 | 15 | @click.command() 16 | @click.option('-f', '--first', type=click.STRING, 17 | help='First name of user to be added') 18 | @click.option('-l', '--last', type=click.STRING, 19 | help='Last name of user to be added') 20 | @click.option('-e', '--email', type=click.STRING, 21 | help='Email address of user to be added') 22 | @click.option('-u', '--userkey', type=click.STRING, 23 | help='User Key of user to be added') 24 | @click.option('--force', is_flag=True, 25 | help='Force adding user with confirmation prompt') 26 | def create_user(first, last, email, userkey, force): 27 | """Create a user on this account""" 28 | 29 | if not any((first, last, email, userkey)) or all((first, last, email, userkey)): 30 | click.echo('Example usage\n' + 31 | 'Add a new user: lecli create user -f John -l Smith -e john.smith@email.com\n' + 32 | 'Add an existing user: lecli create user -u 1343423') 33 | 34 | elif first and last and email is not None: 35 | if force: 36 | api.add_new_user(first, last, email) 37 | else: 38 | if click.confirm('Please confirm you want to add user ' + first + ' ' + last): 39 | api.add_new_user(first, last, email) 40 | 41 | elif userkey is not None: 42 | if force: 43 | api.add_existing_user(userkey) 44 | else: 45 | if click.confirm('Please confirm you want to add user with User Key ' + userkey): 46 | api.add_existing_user(userkey) 47 | 48 | 49 | @click.command() 50 | @click.option('-u', '--userkey', type=click.STRING, 51 | help='User Key of user to be deleted') 52 | def delete_user(userkey): 53 | """Remove a user from this account and delete it. 54 | If the user is associated with other accounts, 55 | it will be removed from this account but not delete.""" 56 | if userkey is None: 57 | click.echo('Example usage: lecli delete user -u 12345678-aaaa-bbbb-1234-1234cb123456') 58 | 59 | else: 60 | api.delete_user(userkey) 61 | 62 | 63 | @click.command() 64 | def get_owner(): 65 | """Get account owner details""" 66 | api.get_owner() 67 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import lecli 3 | 4 | setup( 5 | name='logentries-lecli', 6 | version=lecli.__version__, 7 | author='John Fitzpatrick, Safa Topal', 8 | author_email='john.fitzpatrick@rapid7.com, safa.topal@rapid7.com', 9 | description='Logentries Command Line Interface', 10 | long_description=open('README.md').read(), 11 | packages=find_packages(exclude=['*tests*']), 12 | license='MIT', 13 | install_requires=['click==6.6', 'requests==2.9.1', 'pytz==2016.4', 'termcolor==1.1.0', 14 | 'tabulate==0.7.5', 'appdirs==1.4.0', 'validators==0.11.2'], 15 | entry_points={'console_scripts': ['lecli = lecli.cli:cli']}, 16 | url='https://github.com/logentries/lecli', 17 | zip_safe=False 18 | ) 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/lecli/ec361cf7a695827891f908b0012559a067b5a7db/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_apikey_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | from mock import patch 6 | 7 | from lecli.api_key import api 8 | 9 | MOCK_API_URL = 'http://mydummylink.com' 10 | 11 | 12 | @httpretty.activate 13 | @patch('lecli.api_utils.get_account_resource_id') 14 | @patch('lecli.api_utils.get_rw_apikey') 15 | @patch('lecli.api_key.api._url') 16 | def test_get_api_keys(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 17 | mocked_url.return_value = '', MOCK_API_URL 18 | mocked_rw_apikey.return_value = uuid.uuid4() 19 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 20 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 21 | status=200, 22 | content_type='application/json', 23 | body=json.dumps({})) 24 | 25 | api.get_all() 26 | 27 | out, err = capsys.readouterr() 28 | assert out 29 | assert not err 30 | 31 | 32 | @httpretty.activate 33 | @patch('lecli.api_utils.get_account_resource_id') 34 | @patch('lecli.api_utils.get_rw_apikey') 35 | @patch('lecli.api_key.api._url') 36 | def test_get_api_key(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 37 | api_key_id = str(uuid.uuid4()) 38 | mocked_url.return_value = '', MOCK_API_URL + '/' + api_key_id 39 | mocked_rw_apikey.return_value = uuid.uuid4() 40 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 41 | httpretty.register_uri(httpretty.GET, MOCK_API_URL + '/' + api_key_id, 42 | status=200, 43 | content_type='application/json', 44 | body=json.dumps({})) 45 | 46 | api.get(api_key_id) 47 | 48 | out, err = capsys.readouterr() 49 | assert not err 50 | 51 | 52 | @httpretty.activate 53 | @patch('lecli.api_utils.get_account_resource_id') 54 | @patch('lecli.api_utils.get_owner_apikey_id') 55 | @patch('lecli.api_utils.get_owner_apikey') 56 | @patch('lecli.api_key.api._url') 57 | def test_delete_api_key(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 58 | mocked_account_resource_id, capsys): 59 | api_key_id = str(uuid.uuid4()) 60 | mocked_url.return_value = '', MOCK_API_URL + '/' + api_key_id 61 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 62 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 63 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 64 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL + '/' + api_key_id, 65 | status=204, 66 | content_type='application/json') 67 | 68 | api.delete(api_key_id) 69 | 70 | out, err = capsys.readouterr() 71 | assert not err 72 | assert 'Deleted api key with id: %s' % api_key_id in out 73 | 74 | 75 | @httpretty.activate 76 | @patch('lecli.api_utils.get_account_resource_id') 77 | @patch('lecli.api_utils.get_owner_apikey_id') 78 | @patch('lecli.api_utils.get_owner_apikey') 79 | @patch('lecli.api_key.api._url') 80 | def test_create_api_key(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 81 | mocked_account_resource_id, capsys): 82 | mocked_url.return_value = '', MOCK_API_URL 83 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 84 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 85 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 86 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, 87 | status=201, 88 | content_type='application/json', 89 | body=json.dumps({})) 90 | 91 | api.create({}) 92 | 93 | out, err = capsys.readouterr() 94 | assert not err 95 | 96 | 97 | @httpretty.activate 98 | @patch('lecli.api_utils.get_account_resource_id') 99 | @patch('lecli.api_utils.get_owner_apikey_id') 100 | @patch('lecli.api_utils.get_owner_apikey') 101 | @patch('lecli.api_key.api._url') 102 | def test_disable_api_key(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 103 | mocked_account_resource_id, capsys): 104 | api_key_id = str(uuid.uuid4()) 105 | mocked_url.return_value = '', MOCK_API_URL 106 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 107 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 108 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 109 | httpretty.register_uri(httpretty.PATCH, MOCK_API_URL, 110 | status=200, 111 | content_type='application/json', 112 | body=json.dumps({})) 113 | 114 | api.update(api_key_id, False) 115 | 116 | out, err = capsys.readouterr() 117 | assert {'apikey': {'active': False}} == json.loads(httpretty.last_request().body) 118 | assert not err 119 | assert 'Disabled api key with id: %s' % api_key_id in out 120 | 121 | 122 | @httpretty.activate 123 | @patch('lecli.api_utils.get_account_resource_id') 124 | @patch('lecli.api_utils.get_owner_apikey_id') 125 | @patch('lecli.api_utils.get_owner_apikey') 126 | @patch('lecli.api_key.api._url') 127 | def test_enable_api_key(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 128 | mocked_account_resource_id, capsys): 129 | api_key_id = str(uuid.uuid4()) 130 | mocked_url.return_value = '', MOCK_API_URL 131 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 132 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 133 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 134 | httpretty.register_uri(httpretty.PATCH, MOCK_API_URL, 135 | status=200, 136 | content_type='application/json', 137 | body=json.dumps({})) 138 | 139 | api.update(api_key_id, True) 140 | 141 | out, err = capsys.readouterr() 142 | assert {'apikey': {'active': True}} == json.loads(httpretty.last_request().body) 143 | assert not err 144 | assert 'Enabled api key with id: %s' % api_key_id in out 145 | -------------------------------------------------------------------------------- /tests/test_apiutils.py: -------------------------------------------------------------------------------- 1 | import ConfigParser 2 | import hmac 3 | import uuid 4 | import os 5 | 6 | import pytest 7 | from mock import patch, Mock 8 | 9 | from lecli import api_utils, cli 10 | 11 | ID_WITH_VALID_LENGTH = str(uuid.uuid4()) 12 | ID_WITH_INVALID_LENGTH = str(uuid.uuid4()) + 'invalid' 13 | MOCK_API_URL = 'http://mydummylink.com' 14 | 15 | 16 | def test_gensignature(): 17 | with patch.object(hmac.HMAC, 'digest', return_value='digest_output'): 18 | api_key = ID_WITH_VALID_LENGTH 19 | date = 'date' 20 | content_type = 'content_type' 21 | request_method = 'method' 22 | request_body = 'body' 23 | query_path = 'path' 24 | 25 | signature = api_utils.gensignature(api_key, date, content_type, request_method, request_body, query_path) 26 | 27 | assert signature == 'digest_output' 28 | 29 | 30 | @patch('lecli.api_utils.get_ro_apikey') 31 | def test_generate_headers_ro(mocked_ro_apikey): 32 | mocked_ro_apikey.return_value = ID_WITH_VALID_LENGTH 33 | 34 | headers = api_utils.generate_headers(api_key_type='ro') 35 | 36 | assert "x-api-key" in headers 37 | assert headers["x-api-key"] == ID_WITH_VALID_LENGTH 38 | 39 | 40 | @patch('lecli.api_utils.get_rw_apikey') 41 | def test_generate_header_rw(mocked_rw_apikey): 42 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 43 | 44 | headers = api_utils.generate_headers(api_key_type='rw') 45 | 46 | assert 'x-api-key' in headers 47 | assert headers['x-api-key'] == ID_WITH_VALID_LENGTH 48 | 49 | 50 | @patch('lecli.api_utils.get_owner_apikey') 51 | @patch('lecli.api_utils.get_owner_apikey_id') 52 | def test_generate_header_owner(mocked_owner_apikey, mocked_owner_apikey_id): 53 | mocked_owner_apikey.return_value = ID_WITH_VALID_LENGTH 54 | mocked_owner_apikey_id.return_value = ID_WITH_VALID_LENGTH 55 | headers = api_utils.generate_headers(api_key_type='owner', body='', method="GET", action="action") 56 | 57 | assert 'Date' in headers 58 | assert 'authorization-api-key' in headers 59 | assert ID_WITH_VALID_LENGTH in headers['authorization-api-key'] 60 | 61 | 62 | @patch('lecli.api_utils.get_ro_apikey') 63 | def test_generate_headers_user_agent(mocked_ro_apikey): 64 | mocked_ro_apikey.return_value = ID_WITH_VALID_LENGTH 65 | headers = api_utils.generate_headers(api_key_type='ro') 66 | assert "User-Agent" in headers 67 | assert headers['User-Agent'] == 'lecli' 68 | 69 | 70 | def test_get_valid_ro_apikey(): 71 | with patch.object(ConfigParser.ConfigParser, 'get', return_value=ID_WITH_VALID_LENGTH): 72 | ro_api_key = api_utils.get_ro_apikey() 73 | 74 | assert ro_api_key == ID_WITH_VALID_LENGTH 75 | 76 | 77 | def raise_no_option_error_on_read_only_key(*args): 78 | if args[1] == 'ro_api_key': 79 | raise ConfigParser.NoOptionError('option', 'section') 80 | elif args[1] == 'rw_api_key': 81 | return ID_WITH_VALID_LENGTH 82 | 83 | 84 | @patch.object(ConfigParser.ConfigParser, 'get', side_effect=raise_no_option_error_on_read_only_key) 85 | def test_get_rw_apikey_if_ro_apikey_not_present(mock_get): 86 | read_write_api_key = ID_WITH_VALID_LENGTH 87 | api_key = api_utils.get_ro_apikey() 88 | assert api_key == read_write_api_key 89 | 90 | 91 | def test_get_invalid_ro_apikey(capsys): 92 | with patch.object(ConfigParser.ConfigParser, 'get', 93 | return_value=ID_WITH_INVALID_LENGTH): 94 | with pytest.raises(SystemExit): 95 | ro_api_key = api_utils.get_ro_apikey() 96 | out, err = capsys.readouterr() 97 | 98 | assert ro_api_key is None 99 | assert ID_WITH_INVALID_LENGTH in out 100 | assert 'is not of correct length' in out 101 | 102 | 103 | def test_get_valid_rw_apikey(): 104 | with patch.object(ConfigParser.ConfigParser, 'get', return_value=ID_WITH_VALID_LENGTH): 105 | rw_api_key = api_utils.get_rw_apikey() 106 | 107 | assert rw_api_key == ID_WITH_VALID_LENGTH 108 | 109 | 110 | def test_get_invalid_rw_apikey(capsys): 111 | with patch.object(ConfigParser.ConfigParser, 'get', return_value=ID_WITH_INVALID_LENGTH): 112 | with pytest.raises(SystemExit): 113 | api_utils.load_config = Mock() 114 | result = api_utils.get_rw_apikey() 115 | out, err = capsys.readouterr() 116 | 117 | assert result is None 118 | assert ID_WITH_INVALID_LENGTH in out 119 | assert 'is not of correct length' in out 120 | 121 | 122 | def test_get_valid_owner_apikey(): 123 | with patch.object(ConfigParser.ConfigParser, 'get', return_value=ID_WITH_VALID_LENGTH): 124 | owner_api_key = api_utils.get_owner_apikey() 125 | 126 | assert owner_api_key == ID_WITH_VALID_LENGTH 127 | 128 | 129 | def test_get_invalid_owner_apikey(capsys): 130 | with patch.object(ConfigParser.ConfigParser, 'get', 131 | return_value=ID_WITH_INVALID_LENGTH): 132 | with pytest.raises(SystemExit): 133 | api_utils.load_config = Mock() 134 | result = api_utils.get_owner_apikey() 135 | out, err = capsys.readouterr() 136 | 137 | assert result is None 138 | assert ID_WITH_INVALID_LENGTH in out 139 | assert 'is not of correct length' in out 140 | 141 | 142 | def test_get_valid_owner_apikey_id(): 143 | with patch.object(ConfigParser.ConfigParser, 'get', return_value=ID_WITH_VALID_LENGTH): 144 | owner_api_key_id = api_utils.get_owner_apikey_id() 145 | 146 | assert owner_api_key_id == ID_WITH_VALID_LENGTH 147 | 148 | 149 | def test_get_invalid_owner_apikey_id(capsys): 150 | with patch.object(ConfigParser.ConfigParser, 'get', return_value=ID_WITH_INVALID_LENGTH): 151 | with pytest.raises(SystemExit): 152 | api_utils.load_config = Mock() 153 | result = api_utils.get_owner_apikey_id() 154 | out, err = capsys.readouterr() 155 | 156 | assert result is None 157 | assert ID_WITH_INVALID_LENGTH in out 158 | assert 'is not of correct length' in out 159 | 160 | 161 | def test_get_valid_account_resource_id(): 162 | with patch.object(ConfigParser.ConfigParser, 'get', return_value=ID_WITH_VALID_LENGTH): 163 | account_resource_id = api_utils.get_account_resource_id() 164 | 165 | assert account_resource_id == ID_WITH_VALID_LENGTH 166 | 167 | 168 | def test_get_invalid_account_resource_id(capsys): 169 | with patch.object(ConfigParser.ConfigParser, 'get', 170 | return_value=ID_WITH_INVALID_LENGTH): 171 | with pytest.raises(SystemExit): 172 | result = api_utils.get_account_resource_id() 173 | out, err = capsys.readouterr() 174 | 175 | assert result is None 176 | assert ID_WITH_INVALID_LENGTH in out 177 | assert 'is not of correct length' in out 178 | 179 | 180 | def test_get_valid_named_group_key(): 181 | with patch.object(ConfigParser.ConfigParser, 'items', 182 | return_value=[('test-log-group-favs', ID_WITH_VALID_LENGTH)]): 183 | logkeys = api_utils.get_named_logkey_group('test-log-group-favs') 184 | assert logkeys == filter(None, ID_WITH_VALID_LENGTH.splitlines()) 185 | 186 | 187 | def test_case_insensitivity_of_named_groups_key(): 188 | with patch.object(ConfigParser.ConfigParser, 'items', 189 | return_value=[('test-log-group-favs', '')]): 190 | logkeys = api_utils.get_named_logkey_group('TEST-log-group-favs') 191 | assert logkeys == filter(None, ''.splitlines()) 192 | 193 | 194 | def test_get_invalid_named_group_key(capsys): 195 | with patch.object(ConfigParser.ConfigParser, 'items', 196 | return_value=[('test-log-group-favs', ["test-log-key1", "test-log-key2"])]): 197 | with pytest.raises(SystemExit): 198 | nick_to_query = 'test-log-group-favs-invalid' 199 | result = api_utils.get_named_logkey_group(nick_to_query) 200 | out, err = capsys.readouterr() 201 | 202 | assert result is None 203 | assert nick_to_query in out 204 | assert 'was not found' in out 205 | 206 | 207 | @patch('ConfigParser.ConfigParser') 208 | def test_replace_loggroup_section(mocked_configparser_class): 209 | 210 | config_dir = api_utils.user_config_dir(cli.lecli.__name__) 211 | if not os.path.exists(api_utils.CONFIG_FILE_PATH): 212 | if not os.path.exists(config_dir): 213 | os.makedirs(config_dir) 214 | 215 | loggroups_section = api_utils.LOGGROUPS_SECTION 216 | config_parser_mock = mocked_configparser_class.return_value 217 | config_parser_mock.add_section(loggroups_section) 218 | with patch.object(api_utils.CONFIG, 'items', 219 | return_value=[('test-log-group-favs', ["test-log-key1", "test-log-key2"])]): 220 | api_utils.replace_loggroup_section() 221 | assert not api_utils.CONFIG.has_section(loggroups_section) 222 | assert config_parser_mock.has_section(api_utils.CLI_FAVORITES_SECTION) 223 | 224 | try: 225 | os.remove(api_utils.CONFIG_FILE_PATH) 226 | os.rmdir(config_dir) 227 | except OSError: 228 | pass 229 | 230 | @patch('lecli.api_utils.get_api_url') 231 | def test_generate_api_url(mocked_api_url): 232 | mocked_api_url.return_value = MOCK_API_URL 233 | 234 | result = api_utils.get_api_url() 235 | 236 | assert MOCK_API_URL in result 237 | 238 | 239 | def test_default_api_url(): 240 | result = api_utils.get_api_url() 241 | 242 | assert "https://rest.logentries.com" in result 243 | 244 | 245 | def test_combine_objects(): 246 | left = { 247 | "log": { 248 | "id": "21dd21e7-708a-4bc4-bf45-ffbc78190ecd", 249 | "logsets_info": [], 250 | "name": "test_log_old", 251 | "structures": [], 252 | "tokens": [], 253 | "user_data": {} 254 | } 255 | } 256 | 257 | right = { 258 | "log": { 259 | "name": "test_log_new", 260 | "structures": [], 261 | "tokens": [], 262 | "user_data": {}, 263 | "logsets_info": [{ 264 | "id": "e227f890-7742-47b4-86b2-5ff1d345397e", 265 | "name": "test_logset" 266 | }] 267 | } 268 | } 269 | 270 | expected_result = { 271 | "log": { 272 | "id": "21dd21e7-708a-4bc4-bf45-ffbc78190ecd", 273 | "logsets_info": [{ 274 | "id": "e227f890-7742-47b4-86b2-5ff1d345397e", 275 | "name": "test_logset" 276 | }], 277 | "name": "test_log_new", 278 | "structures": [], 279 | "tokens": [], 280 | "user_data": {} 281 | } 282 | } 283 | 284 | result = api_utils.combine_objects(left, right) 285 | 286 | assert result == expected_result 287 | -------------------------------------------------------------------------------- /tests/test_lecli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | 4 | import time 5 | from click.testing import CliRunner 6 | from mock import patch 7 | from mock import MagicMock 8 | 9 | from lecli import cli 10 | 11 | 12 | @patch('lecli.user.api.get_owner') 13 | def test_get_owner(mocked_get_owner): 14 | runner = CliRunner() 15 | runner.invoke(cli.user_commands.get_owner) 16 | 17 | mocked_get_owner.assert_called_once_with() 18 | 19 | 20 | @patch('lecli.user.api.delete_user') 21 | def test_userdel(mocked_delete_user): 22 | runner = CliRunner() 23 | result = runner.invoke(cli.user_commands.delete_user, input=None) 24 | 25 | assert result.output == "Example usage: lecli delete user -u 12345678-aaaa-bbbb-1234-1234cb123456\n" 26 | 27 | user_key = str(uuid.uuid4()) 28 | runner.invoke(cli.user_commands.delete_user, ['-u', user_key]) 29 | mocked_delete_user.assert_called_once_with(user_key) 30 | 31 | 32 | @patch('lecli.user.api.add_new_user') 33 | def test_useradd(mocked_add_new_user): 34 | first = "first" 35 | last = "last" 36 | email = "email" 37 | 38 | runner = CliRunner() 39 | runner.invoke(cli.user_commands.create_user, ['-f', first, '-l', last, '-e', email], input='y') 40 | mocked_add_new_user.assert_called_once_with(first, last, email) 41 | 42 | 43 | @patch('lecli.user.api.list_users') 44 | def test_userlist(mocked_list_users): 45 | runner = CliRunner() 46 | runner.invoke(cli.user_commands.get_users) 47 | mocked_list_users.assert_called_once_with() 48 | 49 | 50 | @patch('lecli.query.api.query') 51 | def test_recentevents(mocked_query): 52 | runner = CliRunner() 53 | runner.invoke(cli.query_commands.get_recent_events, ['test']) 54 | 55 | assert mocked_query.called 56 | 57 | 58 | @patch('lecli.query.api.query') 59 | def test_recentevents_with_saved_query(mocked_query): 60 | runner = CliRunner() 61 | runner.invoke(cli.query_commands.get_recent_events, ['test', '-s', str(uuid.uuid4())]) 62 | 63 | assert mocked_query.called 64 | 65 | 66 | @patch('lecli.query.api.query') 67 | def test_recentevents_with_relative_range(mocked_query): 68 | runner = CliRunner() 69 | runner.invoke(cli.query_commands.get_recent_events, ['test', '-r', 'last 3 min']) 70 | 71 | assert mocked_query.called 72 | 73 | 74 | @patch('lecli.query.api.query') 75 | def test_events(mocked_query): 76 | runner = CliRunner() 77 | runner.invoke(cli.query_commands.get_events, ['', '-f', int(time.time()), '-t', int(time.time())]) 78 | 79 | assert mocked_query.called 80 | 81 | 82 | @patch('lecli.query.api.query') 83 | def test_events_with_relative_range(mocked_query): 84 | runner = CliRunner() 85 | runner.invoke(cli.query_commands.get_events, ['', '-r', 'last 3 min']) 86 | 87 | assert mocked_query.called 88 | 89 | 90 | @patch('lecli.query.api.query') 91 | def test_events_with_saved_query(mocked_query): 92 | runner = CliRunner() 93 | runner.invoke(cli.query_commands.get_events, ['123123123', '-s', str(uuid.uuid4())]) 94 | 95 | assert mocked_query.called 96 | 97 | 98 | @patch('lecli.query.api.tail_logs') 99 | def test_live_tail(mocked_tail_logs): 100 | runner = CliRunner() 101 | runner.invoke(cli.query_commands.tail_events, [str(uuid.uuid4())]) 102 | 103 | assert mocked_tail_logs.called 104 | 105 | 106 | @patch('lecli.query.api.tail_logs') 107 | def test_live_tail_with_saved_query(mocked_tail_logs): 108 | runner = CliRunner() 109 | runner.invoke(cli.query_commands.tail_events, ['', '-s', str(uuid.uuid4())]) 110 | 111 | assert mocked_tail_logs.called 112 | 113 | 114 | @patch('lecli.query.api.query') 115 | def test_query(mocked_post_query): 116 | runner = CliRunner() 117 | runner.invoke(cli.query_commands.query, [str(uuid.uuid4()), '-l', 'where(event)', '-f', 118 | int(time.time()), '-t', int(time.time())]) 119 | assert mocked_post_query.called 120 | 121 | 122 | @patch('lecli.query.api.query') 123 | def test_query_with_relative_range(mocked_post_query): 124 | runner = CliRunner() 125 | runner.invoke(cli.query_commands.query, ['', '-l', '', '-r', 'last 3 min']) 126 | 127 | assert mocked_post_query.called 128 | 129 | 130 | @patch('lecli.query.api.query') 131 | def test_query_with_saved_query(mocked_post_query): 132 | runner = CliRunner() 133 | runner.invoke(cli.query_commands.query, ['', '-s', str(uuid.uuid4())]) 134 | 135 | assert mocked_post_query.called 136 | 137 | 138 | @patch('lecli.team.api.get_teams') 139 | def test_get_teams(mocked_get_teams): 140 | runner = CliRunner() 141 | runner.invoke(cli.team_commands.get_teams) 142 | 143 | assert mocked_get_teams.called 144 | 145 | 146 | @patch('lecli.team.api.get_team') 147 | def test_get_team(mocked_get_team): 148 | runner = CliRunner() 149 | runner.invoke(cli.team_commands.get_team, [str(uuid.uuid4())]) 150 | 151 | assert mocked_get_team.called 152 | 153 | 154 | @patch('lecli.team.api.create_team') 155 | def test_create_team(mocked_create_team): 156 | runner = CliRunner() 157 | runner.invoke(cli.team_commands.create_team, ["test_team_name"]) 158 | 159 | assert mocked_create_team.called 160 | 161 | 162 | @patch('lecli.team.api.delete_team') 163 | def test_delete_team(mocked_delete_team): 164 | runner = CliRunner() 165 | runner.invoke(cli.team_commands.delete_team, [str(uuid.uuid4())]) 166 | 167 | assert mocked_delete_team.called 168 | 169 | 170 | @patch('lecli.team.api.rename_team') 171 | def test_rename_team(mocked_rename_team): 172 | runner = CliRunner() 173 | runner.invoke(cli.team_commands.rename_team, [str(uuid.uuid4()), "new_name"]) 174 | 175 | assert mocked_rename_team.called 176 | 177 | 178 | @patch('lecli.team.api.add_user_to_team') 179 | def test_add_user_to_team(mocked_add_user): 180 | runner = CliRunner() 181 | runner.invoke(cli.team_commands.addusertoteam, [str(uuid.uuid4()), "test_user_name"]) 182 | 183 | assert mocked_add_user.called 184 | 185 | 186 | @patch('lecli.usage.api.get_usage') 187 | def test_add_user_to_team(mocked_get_usage): 188 | runner = CliRunner() 189 | runner.invoke(cli.usage_commands.get_usage, ['-s', 'start', '-e', 'end']) 190 | 191 | assert mocked_get_usage.called 192 | 193 | 194 | @patch('lecli.saved_query.api.create_saved_query') 195 | def test_create_saved_query(mocked_create_saved_query): 196 | runner = CliRunner() 197 | runner.invoke(cli.saved_query_commands.create_saved_query, ['new_saved_query', 'where(/*/)', '-f', 10, '-t', 1000]) 198 | 199 | assert mocked_create_saved_query.called 200 | 201 | 202 | @patch('lecli.saved_query.api.create_saved_query') 203 | def test_create_query_with_missing_statement(mocked_create_saved_query): 204 | runner = CliRunner() 205 | runner.invoke(cli.saved_query_commands.create_saved_query, ['new_saved_query', '-f', 10, '-t', 1000]) 206 | 207 | assert not mocked_create_saved_query.called 208 | 209 | 210 | @patch('lecli.saved_query.api.update_saved_query') 211 | def test_update_saved_query(mocked_update_saved_query): 212 | runner = CliRunner() 213 | runner.invoke(cli.saved_query_commands.update_saved_query, ['123456789012345678901234567890123456', '-f', 10, '-t', 214 | 1000]) 215 | 216 | assert mocked_update_saved_query.called 217 | 218 | 219 | @patch('lecli.saved_query.api.update_saved_query') 220 | def test_failing_update_saved_query(mocked_create_saved_query): 221 | runner = CliRunner() 222 | runner.invoke(cli.saved_query_commands.create_saved_query, ['-f', 10, '-t', 1000]) 223 | 224 | assert not mocked_create_saved_query.called 225 | 226 | 227 | @patch('lecli.saved_query.api.get_saved_query') 228 | def test_get_saved_query(mocked_get_saved_query): 229 | runner = CliRunner() 230 | runner.invoke(cli.saved_query_commands.get_saved_query, ['123456789012345678901234567890123456']) 231 | 232 | assert mocked_get_saved_query.called 233 | 234 | 235 | @patch('lecli.saved_query.api.get_saved_query') 236 | def test_get_saved_queries(mocked_get_saved_queries): 237 | runner = CliRunner() 238 | runner.invoke(cli.saved_query_commands.get_saved_queries) 239 | 240 | assert mocked_get_saved_queries.called 241 | 242 | 243 | @patch('lecli.saved_query.api.get_saved_query') 244 | def test_get_saved_query(mocked_get_saved_query): 245 | runner = CliRunner() 246 | runner.invoke(cli.saved_query_commands.get_saved_query, ['12341234']) 247 | 248 | assert mocked_get_saved_query.called 249 | 250 | 251 | @patch('lecli.saved_query.api.delete_saved_query') 252 | def test_delete_saved_query(mocked_delete_saved_query): 253 | runner = CliRunner() 254 | runner.invoke(cli.saved_query_commands.delete_saved_query, ['123456789012345678901234567890123456']) 255 | 256 | assert mocked_delete_saved_query.called 257 | 258 | 259 | @patch('lecli.saved_query.api.delete_saved_query') 260 | def test_delete_saved_query_without_id(mocked_delete_saved_query): 261 | runner = CliRunner() 262 | runner.invoke(cli.saved_query_commands.delete_saved_query) 263 | 264 | assert not mocked_delete_saved_query.called 265 | 266 | 267 | @patch('lecli.log.api.get_logs') 268 | def test_get_logs(mocked_get_logs): 269 | runner = CliRunner() 270 | runner.invoke(cli.log_commands.getlogs) 271 | 272 | mocked_get_logs.assert_called_once() 273 | 274 | 275 | @patch('lecli.log.api.get_log') 276 | def test_get_log(mocked_get_log): 277 | runner = CliRunner() 278 | runner.invoke(cli.log_commands.getlog, ['123']) 279 | 280 | mocked_get_log.assert_called_once_with('123') 281 | 282 | 283 | @patch('lecli.log.api.create_log') 284 | def test_create_log(mocked_create_log): 285 | runner = CliRunner() 286 | runner.invoke(cli.log_commands.createlog, ['-n', 'new log']) 287 | 288 | mocked_create_log.assert_called_once_with('new log', None) 289 | 290 | 291 | @patch('lecli.log.api.delete_log') 292 | def test_delete_log(mocked_delete_log): 293 | runner = CliRunner() 294 | runner.invoke(cli.log_commands.deletelog, ['123']) 295 | 296 | mocked_delete_log.assert_called_once_with('123') 297 | 298 | 299 | @patch('lecli.log.api.rename_log') 300 | def test_rename_log(mocked_rename_log): 301 | runner = CliRunner() 302 | runner.invoke(cli.log_commands.renamelog, ['123', 'new name']) 303 | 304 | mocked_rename_log.assert_called_once_with('123', 'new name') 305 | 306 | 307 | @patch('lecli.log.api.replace_log') 308 | def test_replace_log(mocked_replace_log): 309 | runner = CliRunner() 310 | with open('file.json', 'w') as f: 311 | f.write('{"log": {"id": "ba2b371a-87fa-40ee-97fd-e9b0d2424b2f","name": "new_log"}}') 312 | 313 | runner.invoke(cli.log_commands.replacelog, ['1234', 'file.json']) 314 | 315 | mocked_replace_log.assert_called_once() 316 | try: 317 | os.remove('file.json') 318 | except OSError: 319 | pass 320 | 321 | 322 | @patch('lecli.log.api.create_log') 323 | @patch('os.path.exists', MagicMock(return_value=False)) 324 | @patch('os.path.isfile', MagicMock(return_value=False)) 325 | def test_non_existant_file(mocked_create_log): 326 | runner = CliRunner() 327 | runner.invoke(cli.log_commands.createlog, ['123', 'non_existant_file.json']) 328 | 329 | assert not mocked_create_log.called 330 | 331 | 332 | @patch('lecli.log.api.create_log') 333 | @patch('os.path.exists', MagicMock(return_value=True)) 334 | @patch('os.path.isfile', MagicMock(return_value=False)) 335 | def test_not_a_file(mocked_create_log): 336 | runner = CliRunner() 337 | runner.invoke(cli.log_commands.createlog, ['123', 'not_a_file']) 338 | 339 | assert not mocked_create_log.called 340 | 341 | 342 | @patch('lecli.logset.api.get_logsets') 343 | def test_get_logsets(mocked_get_logsets): 344 | runner = CliRunner() 345 | runner.invoke(cli.logset_commands.getlogsets) 346 | 347 | mocked_get_logsets.assert_called_once() 348 | 349 | 350 | @patch('lecli.logset.api.get_logset') 351 | def test_get_logset(mocked_get_logset): 352 | runner = CliRunner() 353 | runner.invoke(cli.logset_commands.getlogset, ['1234']) 354 | 355 | mocked_get_logset.assert_called_once_with('1234') 356 | 357 | 358 | @patch('lecli.logset.api.create_logset') 359 | def test_create_logset_with_name(mocked_create_logset): 360 | runner = CliRunner() 361 | runner.invoke(cli.logset_commands.createlogset, ['-n', 'Test Logset']) 362 | 363 | mocked_create_logset.assert_called_once_with('Test Logset', None) 364 | 365 | 366 | @patch('lecli.logset.api.rename_logset') 367 | def test_rename_logset(mocked_rename_logset): 368 | runner = CliRunner() 369 | runner.invoke(cli.logset_commands.renamelogset, ['123', 'new_name']) 370 | 371 | mocked_rename_logset.assert_called_once_with('123', 'new_name') 372 | 373 | 374 | @patch('lecli.logset.api.add_log') 375 | def test_add_log_to_logset(mocked_add_log_to_logset): 376 | runner = CliRunner() 377 | runner.invoke(cli.logset_commands.updatelogset, ['add_log','123', 'abc']) 378 | 379 | mocked_add_log_to_logset.assert_called_once_with('123', 'abc') 380 | 381 | 382 | @patch('lecli.logset.api.delete_log') 383 | def test_remove_log_from_logset(mocked_delete_log_from_logset): 384 | runner = CliRunner() 385 | runner.invoke(cli.logset_commands.updatelogset, ['delete_log','123', 'abc']) 386 | 387 | mocked_delete_log_from_logset.assert_called_once_with('123', 'abc') 388 | 389 | 390 | @patch('lecli.logset.api.delete_logset') 391 | def test_delete_logset(mocked_delete_logset): 392 | runner = CliRunner() 393 | runner.invoke(cli.logset_commands.deletelogset, ['123']) 394 | 395 | mocked_delete_logset.assert_called_once_with('123') 396 | 397 | 398 | @patch('lecli.logset.api.replace_logset') 399 | def test_replace_logset(mocked_replace_logset): 400 | runner = CliRunner() 401 | with open('logset.json', 'w') as f: 402 | f.write('{"logset": {"id": "ba2b371a-87fa-40ee-97fd-e9b0d2424b2f","name": "new logset"}}') 403 | 404 | runner.invoke(cli.logset_commands.replacelogset, ['1234', 'logset.json']) 405 | 406 | mocked_replace_logset.assert_called_once() 407 | try: 408 | os.remove('logset.json') 409 | except OSError: 410 | pass 411 | 412 | 413 | @patch('lecli.api_key.api.create') 414 | def test_create_apikey(mocked_create_apikey): 415 | runner = CliRunner() 416 | with open('file.json', 'w') as f: 417 | f.write('{"test": {"key": "value"}}') 418 | 419 | runner.invoke(cli.api_key_commands.create_api_key, ['file.json']) 420 | mocked_create_apikey.assert_called_once() 421 | try: 422 | os.remove('file.json') 423 | except OSError: 424 | pass 425 | 426 | 427 | @patch('lecli.api_key.api.delete') 428 | def test_delete_apikey(mocked_delete_apikey): 429 | runner = CliRunner() 430 | runner.invoke(cli.api_key_commands.delete_api_key, ['123']) 431 | 432 | mocked_delete_apikey.assert_called_once_with('123') 433 | 434 | 435 | @patch('lecli.api_key.api.update') 436 | def test_enable_apikey(mocked_update_apikey): 437 | runner = CliRunner() 438 | runner.invoke(cli.api_key_commands.update_api_key, ['123', '--enable']) 439 | 440 | mocked_update_apikey.assert_called_once_with('123', True) 441 | 442 | 443 | @patch('lecli.api_key.api.update') 444 | def test_disable_apikey(mocked_update_apikey): 445 | runner = CliRunner() 446 | runner.invoke(cli.api_key_commands.update_api_key, ['123', '--disable']) 447 | 448 | mocked_update_apikey.assert_called_once_with('123', False) 449 | -------------------------------------------------------------------------------- /tests/test_log_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | import pytest 6 | 7 | from mock import patch 8 | 9 | from lecli.log import api 10 | 11 | ID_WITH_VALID_LENGTH = str(uuid.uuid4()) 12 | MOCK_API_URL = 'http://mydummylink.com' 13 | LOG_RESPONSE = { 14 | "log" : { 15 | "id" : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 16 | "name" : "Test Log", 17 | "logsets_info" : [{ 18 | "id" : "XXXXXXXX-ABCD-ABCD-ABCD-XXXXXXXXXXXX", 19 | "name" : "Test Logset 1", 20 | "links" : { 21 | "rel" : "Self", 22 | "href" : "http://mydummyurl.com/management/logsets/XXXXXXXX-ABCD-ABCD-ABCD-XXXXXXXXXXXX" 23 | } 24 | }, 25 | { 26 | "id" : "XXXXXXXX-DCBA-DCBA-DCBA-XXXXXXXXXXXX", 27 | "name" : "Test Logset 2", 28 | "links" : { 29 | "rel" : "Self", 30 | "href" : "http://mydummyurl.com/management/logsets/XXXXXXXX-DCBA-DCBA-DCBA-XXXXXXXXXXXX" 31 | } 32 | }], 33 | "source_type" : "token", 34 | "token_seed" : "12345678-abcd-efgh-ijkl-12345678", 35 | "tokens": [{}], 36 | "structures" : [{}], 37 | "user_data": { 38 | "LocationDescription" : "All logs for DC1", 39 | "le_hostname" : "testhost" 40 | } 41 | } 42 | } 43 | 44 | 45 | @httpretty.activate 46 | @patch('lecli.api_utils.get_ro_apikey') 47 | @patch('lecli.log.api._url') 48 | def test_get_logs(mocked_url, mocked_ro_apikey, capsys): 49 | mocked_url.return_value = '', MOCK_API_URL 50 | mocked_ro_apikey.return_value = ID_WITH_VALID_LENGTH 51 | 52 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 53 | status=200, 54 | content_type='application/json', 55 | body=json.dumps({})) 56 | 57 | api.get_logs() 58 | out, err = capsys.readouterr() 59 | assert not err 60 | 61 | 62 | @httpretty.activate 63 | @patch('lecli.api_utils.get_ro_apikey') 64 | @patch('lecli.log.api._url') 65 | def test_get_log(mocked_url, mocked_ro_apikey, capsys): 66 | mocked_url.return_value = '', MOCK_API_URL 67 | mocked_ro_apikey.return_value = ID_WITH_VALID_LENGTH 68 | 69 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 70 | status=200, 71 | content_type='application/json', 72 | body=json.dumps(LOG_RESPONSE)) 73 | 74 | api.get_log(ID_WITH_VALID_LENGTH) 75 | out, err = capsys.readouterr() 76 | 77 | assert not err 78 | 79 | 80 | @httpretty.activate 81 | @patch('lecli.api_utils.get_rw_apikey') 82 | @patch('lecli.log.api._url') 83 | def test_create_log_with_default_source_type(mocked_url, mocked_rw_apikey, capsys): 84 | mocked_url.return_value = '', MOCK_API_URL 85 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 86 | 87 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, 88 | status=201, 89 | body=json.dumps(LOG_RESPONSE), 90 | content_type='application/json') 91 | 92 | api.create_log("Test Log", None) 93 | out, err = capsys.readouterr() 94 | 95 | assert 'Test Log' in out 96 | assert 'token' in out 97 | 98 | 99 | @httpretty.activate 100 | @patch('lecli.api_utils.get_rw_apikey') 101 | @patch('lecli.log.api._url') 102 | def test_create_log_with_source_type(mocked_url, mocked_rw_apikey, capsys): 103 | mocked_url.return_value = '', MOCK_API_URL 104 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 105 | 106 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, 107 | status=201, 108 | body='{"log": {"name": "test log","id": "2caec19c-d8a2-40ef-9c1e-91e89157fe28",' 109 | '"source_type": "syslog","logsets_info": [{"id": "20a9b70b-e70c-4cb5-a4f6-0e40b60b7118","name": "logset"}]}}', 110 | content_type = 'application/json') 111 | 112 | api.create_log("test log", None) 113 | out, err = capsys.readouterr() 114 | 115 | assert 'test log' in out 116 | assert 'syslog' in out 117 | 118 | 119 | @httpretty.activate 120 | @patch('lecli.api_utils.get_rw_apikey') 121 | @patch('lecli.log.api._url') 122 | def test_delete_log(mocked_url, mocked_rw_apikey, capsys): 123 | mocked_url.return_value = '', MOCK_API_URL 124 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 125 | 126 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL, status=204) 127 | log_id = str(uuid.uuid4()) 128 | api.delete_log(log_id) 129 | 130 | out, err = capsys.readouterr() 131 | assert "Deleted log with id: %s" % log_id in out 132 | 133 | 134 | @httpretty.activate 135 | @patch('lecli.api_utils.get_rw_apikey') 136 | @patch('lecli.api_utils.get_ro_apikey') 137 | @patch('lecli.log.api._url') 138 | def test_rename_log(mocked_url, mocked_rw_apikey, mocked_ro_apikey, capsys): 139 | test_log_id = str(uuid.uuid4()) 140 | mocked_url.return_value = '', MOCK_API_URL 141 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 142 | mocked_ro_apikey.return_value = ID_WITH_VALID_LENGTH 143 | 144 | request_body = '{"log": {"name": "test.log", "logsets_info": [], "source_type": "token"}}' 145 | expected_result = '{"log": {"name": "new_test_log_name", "logsets_info": [], "source_type": "token"}}' 146 | 147 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 148 | status=200, 149 | content_type='application/json', 150 | body=request_body) 151 | 152 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, status=200, 153 | body = expected_result, content_type='application/json') 154 | 155 | new_name_for_log = "new_test_log_name" 156 | api.rename_log(test_log_id, new_name_for_log) 157 | out, err = capsys.readouterr() 158 | 159 | assert new_name_for_log in out 160 | 161 | 162 | @httpretty.activate 163 | @patch('lecli.api_utils.get_rw_apikey') 164 | @patch('lecli.log.api._url') 165 | def test_replace_log(mocked_url, mocked_rw_apikey, capsys): 166 | log_id = str(uuid.uuid4()) 167 | 168 | request_body = '{"log": {"name": "test.log", "logsets_info": [], "source_type": "token"}}' 169 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 170 | mocked_url.return_value = '', MOCK_API_URL 171 | 172 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, status=200, body=request_body, 173 | content_type='application/json') 174 | 175 | api.replace_log(log_id, params=request_body) 176 | 177 | out, err = capsys.readouterr() 178 | assert "test.log" in out 179 | 180 | 181 | @httpretty.activate 182 | @patch('lecli.api_utils.get_rw_apikey') 183 | @patch('lecli.api_utils.get_ro_apikey') 184 | @patch('lecli.log.api._url') 185 | @patch('lecli.logset.api._url') 186 | def test_update_log(logset_url, log_url, mocked_ro_apikey, mocked_rw_apikey, capsys): 187 | test_log_id = str(uuid.uuid4()) 188 | log_url.return_value = '', MOCK_API_URL 189 | logset_url.return_value = '', MOCK_API_URL 190 | 191 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 192 | mocked_ro_apikey.return_value = ID_WITH_VALID_LENGTH 193 | 194 | request_body = '{"log": {"name": "test.log", "logsets_info": [], "source_type": "token"}}' 195 | expected_result = '{"log": {"name": "test.log", ' \ 196 | '"logsets_info": [{"id": "e227f890-7742-47b4-86b2-5ff1d345397e",' \ 197 | '"name": "test_logset"}], "source_type": "token"}}' 198 | 199 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 200 | content_type='application/json', body=request_body) 201 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 202 | content_type='application/json', body=request_body) 203 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, status=200, 204 | content_type='application/json', body=expected_result) 205 | 206 | logset_info = { 207 | "logsets_info": [ 208 | { 209 | "id": "e227f890-7742-47b4-86b2-5ff1d345397e", 210 | "name": "test_logset" 211 | } 212 | ] 213 | } 214 | 215 | api.update_log(test_log_id, logset_info) 216 | out, err = capsys.readouterr() 217 | 218 | assert "test_logset" in out 219 | 220 | 221 | @httpretty.activate 222 | @patch('lecli.api_utils.get_rw_apikey') 223 | @patch('lecli.log.api._url') 224 | def test_duplicate_log_id(mocked_url, mocked_rw_apikey): 225 | with pytest.raises(Exception) as exception: 226 | mocked_url.return_value = '', MOCK_API_URL 227 | mocked_rw_apikey.return_value = ID_WITH_VALID_LENGTH 228 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, status=409, content_type='application/json') 229 | 230 | api.create_log('existing log') 231 | 232 | assert exception.value.message=='409' -------------------------------------------------------------------------------- /tests/test_logset_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | import pytest 6 | 7 | from mock import patch 8 | 9 | from lecli.logset import api 10 | 11 | MOCK_API_URL = 'http://mydummylink.com' 12 | LOGSET_RESPONSE = { 13 | "logset": { 14 | "id": "XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXXXXXX", 15 | "name": "Test Logset", 16 | "description": "Example logset", 17 | "user_data": {}, 18 | "logs_info": [ 19 | { 20 | "id": "XXXXXXXX-ABCD-YYYY-DCBA-XXXXXXXXXXXX", 21 | "name": "SyslogD Log", 22 | "links": [ 23 | { 24 | "rel": "Self", 25 | "href": "https://rest.logentries.com/management/logs/XXXXXXXX-ABCD-YYYY-DCBA-XXXXXXXXXXXX" 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | } 32 | BASIC_LOGSET_RESPONSE = '{"logset": {"id": "XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXX", "logs_info": [],"name": "new logset name"}}' 33 | BASIC_LOGSET_RESPONSE_WITH_LOG = '{"logset": {"id": "XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXX", "logs_info": [{"id":"XXXXXXXX-ABCD-YYYY-DCBA-XXXXXXXXXXXX"}],"name": "new logset name"}}' 34 | 35 | 36 | @httpretty.activate 37 | @patch('lecli.api_utils.get_ro_apikey') 38 | @patch('lecli.logset.api._url') 39 | def test_get_logsets(mocked_url, mocked_ro_apikey, capsys): 40 | mocked_url.return_value = '', MOCK_API_URL 41 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 42 | 43 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 44 | status=200, 45 | content_type='application/json', 46 | body=json.dumps(LOGSET_RESPONSE)) 47 | 48 | api.get_logsets() 49 | out, err = capsys.readouterr() 50 | 51 | assert 'XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXXXXXX' in out 52 | 53 | 54 | @httpretty.activate 55 | @patch('lecli.api_utils.get_ro_apikey') 56 | @patch('lecli.logset.api._url') 57 | def test_get_logset(mocked_url, mocked_ro_apikey, capsys): 58 | mocked_url.return_value = '', MOCK_API_URL 59 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 60 | logset_id = 'XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXXXXXX' 61 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 62 | status=200, 63 | content_type='application/json', 64 | body=json.dumps(LOGSET_RESPONSE)) 65 | 66 | api.get_logset(logset_id) 67 | out, err = capsys.readouterr() 68 | 69 | assert logset_id in out 70 | 71 | 72 | @httpretty.activate 73 | @patch('lecli.api_utils.get_rw_apikey') 74 | @patch('lecli.logset.api._url') 75 | def test_create_logset_with_name(mocked_url, mocked_rw_apikey, capsys): 76 | mocked_url.return_value = '', MOCK_API_URL 77 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 78 | 79 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, status=201, 80 | content_type='application/json', 81 | body=json.dumps(LOGSET_RESPONSE)) 82 | 83 | api.create_logset('Test Logset') 84 | out, err = capsys.readouterr() 85 | 86 | assert 'Test Logset' in out 87 | 88 | 89 | @httpretty.activate 90 | @patch('lecli.api_utils.get_rw_apikey') 91 | @patch('lecli.logset.api._url') 92 | def test_create_logset_from_file(mocked_url, mocked_rw_apikey, capsys): 93 | mocked_url.return_value = '', MOCK_API_URL 94 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 95 | 96 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, status=201, 97 | content_type='application/json', 98 | body=json.dumps(LOGSET_RESPONSE)) 99 | 100 | params = { 101 | "logset": { 102 | "name": "Test Logset" 103 | } 104 | } 105 | 106 | api.create_logset(params=params) 107 | out, err = capsys.readouterr() 108 | 109 | assert 'Test Logset' in out 110 | 111 | 112 | @httpretty.activate 113 | @patch('lecli.api_utils.get_rw_apikey') 114 | @patch('lecli.logset.api._url') 115 | def test_create_logset_invalid_json(mocked_url, mocked_rw_apikey, capsys): 116 | with pytest.raises(SystemExit) as exit: 117 | mocked_url.return_value = '', MOCK_API_URL 118 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 119 | 120 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, status=400, 121 | content_type='application/json', 122 | body='Client Error: Bad Request for url: https://rest.logentries.com/management/logsets') 123 | 124 | invalid_params = { 125 | "logset": { 126 | "id": "12341234-XXXX-YYYY-XXXX-12341234", 127 | "unknown_field": "unknown value" 128 | } 129 | } 130 | 131 | api.create_logset(params=invalid_params) 132 | out, err = capsys.readouterr() 133 | 134 | assert exit.code is 1 135 | assert "Creating logset failed, status code: 400" in out 136 | 137 | 138 | @httpretty.activate 139 | @patch('lecli.api_utils.get_ro_apikey') 140 | @patch('lecli.api_utils.get_rw_apikey') 141 | @patch('lecli.logset.api._url') 142 | def test_rename_logset(mocked_url, mocked_rw_apikey, mocked_ro_apikey, capsys): 143 | mocked_url.return_value = '', MOCK_API_URL 144 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 145 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 146 | 147 | response_body = '{"logset": {"id": "XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXX","logs_info": [],"name": "old logset name"}}' 148 | expected_result = '{"logset": {"id": "XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXX","logs_info": [],"name": "new logset name"}}' 149 | 150 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 151 | content_type='application/json', 152 | body=response_body) 153 | 154 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, status=200, 155 | content_type='application/json', 156 | body=expected_result) 157 | 158 | api.rename_logset('XXXXXXXX-XXXX-YYYY-XXXX-XXXXXXXX', 'new logset name') 159 | out, err = capsys.readouterr() 160 | 161 | assert "new logset name" in out 162 | 163 | 164 | @httpretty.activate 165 | @patch('lecli.api_utils.get_ro_apikey') 166 | @patch('lecli.api_utils.get_rw_apikey') 167 | @patch('lecli.logset.api._url') 168 | def test_rename_unknown_logset(mocked_url, mocked_rw_apikey, mocked_ro_apikey, capsys): 169 | with pytest.raises(SystemExit) as exit: 170 | mocked_url.return_value = '', MOCK_API_URL 171 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 172 | 173 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=404, 174 | content_type='application/json') 175 | 176 | api.rename_logset('XXXXXXXX-XXXX-0000-XXXX-XXXXXXXX', 'new name') 177 | out, err = capsys.readouterr() 178 | 179 | assert "404" in out 180 | assert exit.code is 1 181 | 182 | 183 | @httpretty.activate 184 | @patch('lecli.api_utils.get_rw_apikey') 185 | @patch('lecli.api_utils.get_ro_apikey') 186 | @patch('lecli.logset.api._url') 187 | def test_add_log_to_logset(mocked_url, mocked_ro_apikey, mocked_rw_apikey, capsys): 188 | mocked_url.return_value = '', MOCK_API_URL 189 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 190 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 191 | 192 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, content_type='application/json', 193 | body=json.dumps(LOGSET_RESPONSE)) 194 | 195 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, status=200, content_type='application/json', 196 | body=json.dumps(LOGSET_RESPONSE)) 197 | 198 | api.add_log('XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX', 'XXXXXXXX-ABCD-YYYY-DCBA-XXXXXXXXXXXX') 199 | out, err = capsys.readouterr() 200 | 201 | assert "XXXXXXXX-ABCD-YYYY-DCBA-XXXXXXXXXXXX" in out 202 | 203 | 204 | @httpretty.activate 205 | @patch('lecli.api_utils.get_rw_apikey') 206 | @patch('lecli.api_utils.get_ro_apikey') 207 | @patch('lecli.logset.api._url') 208 | def test_add_unknown_log_to_logset(mocked_url, mocked_ro_apikey, mocked_rw_apikey, capsys): 209 | with pytest.raises(SystemExit) as exit: 210 | mocked_url.return_value = '', MOCK_API_URL 211 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 212 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 213 | 214 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 215 | content_type='application/json', body=json.dumps(LOGSET_RESPONSE)) 216 | 217 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, status=400, 218 | content_type='application/json') 219 | 220 | api.add_log('XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX', 'unknown_log') 221 | out, err = capsys.readouterr() 222 | 223 | assert "400" in out 224 | assert exit.code is 1 225 | 226 | 227 | @httpretty.activate 228 | @patch('lecli.api_utils.get_rw_apikey') 229 | @patch('lecli.api_utils.get_ro_apikey') 230 | @patch('lecli.logset.api._url') 231 | def test_add_log_to_unknown_logset(mocked_url, mocked_ro_apikey, mocked_rw_apikey, capsys): 232 | with pytest.raises(SystemExit) as exit: 233 | mocked_url.return_value = '', MOCK_API_URL 234 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 235 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 236 | 237 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 238 | status=404, 239 | content_type='application/json') 240 | 241 | api.add_log('123', '123') 242 | out, err = capsys.readouterr() 243 | 244 | assert "404" in out 245 | assert exit.code is 1 246 | 247 | 248 | @httpretty.activate 249 | @patch('lecli.api_utils.get_rw_apikey') 250 | @patch('lecli.api_utils.get_ro_apikey') 251 | @patch('lecli.logset.api._url') 252 | def test_remove_log_from_logset(mocked_url, mocked_ro_apikey, mocked_rw_apikey, capsys): 253 | mocked_url.return_value = '', MOCK_API_URL 254 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 255 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 256 | 257 | httpretty.register_uri(httpretty.GET, MOCK_API_URL , 258 | status=200, 259 | content_type='application/json', 260 | body=json.dumps(LOGSET_RESPONSE)) 261 | 262 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, 263 | status=200, 264 | content_Type='application/json', 265 | body=json.dumps(LOGSET_RESPONSE)) 266 | api.delete_log('123', str(uuid.uuid4())) 267 | out, err = capsys.readouterr() 268 | 269 | assert not err 270 | 271 | 272 | @httpretty.activate 273 | @patch('lecli.api_utils.get_rw_apikey') 274 | @patch('lecli.logset.api._url') 275 | def test_delete_logset(mocked_url, mocked_rw_apikey, capsys): 276 | mocked_url.return_value = '', MOCK_API_URL 277 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 278 | 279 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL, 280 | status=204, 281 | content_type='application/json') 282 | 283 | api.delete_logset('123') 284 | out, err = capsys.readouterr() 285 | 286 | assert not err 287 | assert "Deleted logset with id: 123" in out 288 | 289 | 290 | @httpretty.activate 291 | @patch('lecli.api_utils.get_rw_apikey') 292 | @patch('lecli.logset.api._url') 293 | def test_delete_unknown_logset(mocked_url, mocked_rw_apikey, capsys): 294 | with pytest.raises(SystemExit) as exit: 295 | mocked_url.return_value = '', MOCK_API_URL 296 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 297 | 298 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL, 299 | status=404, 300 | content_type='application/json') 301 | 302 | api.delete_logset('123') 303 | out, err = capsys.readouterr() 304 | 305 | assert err 306 | assert "404" in out 307 | assert exit.code is 1 308 | 309 | 310 | @httpretty.activate 311 | @patch('lecli.api_utils.get_ro_apikey') 312 | @patch('lecli.api_utils.get_rw_apikey') 313 | @patch('lecli.logset.api._url') 314 | def test_delete_logset_with_log_in_another_logset(mocked_url, mocked_rw_apikey, mocked_ro_apikey, capsys): 315 | mocked_url.return_value = '', MOCK_API_URL 316 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 317 | mocked_ro_apikey.return_value = str(uuid.uuid4()) 318 | 319 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL, 320 | status=204, content_type='application/json') 321 | 322 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 323 | status=200, content_type='application/json', 324 | body=json.dumps({})) 325 | 326 | api.delete_logset('123') 327 | api.get_logset('456') 328 | out, err = capsys.readouterr() 329 | 330 | assert not err 331 | 332 | 333 | @httpretty.activate 334 | @patch('lecli.api_utils.get_rw_apikey') 335 | @patch('lecli.logset.api._url') 336 | def test_replace_logset(mocked_url, mocked_rw_apikey, capsys): 337 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 338 | mocked_url.return_value = '', MOCK_API_URL 339 | 340 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, 341 | status=200, 342 | body=json.dumps({}), 343 | content_type='application/json') 344 | 345 | api.replace_logset('123', params=LOGSET_RESPONSE) 346 | 347 | out, err = capsys.readouterr() 348 | assert not err 349 | -------------------------------------------------------------------------------- /tests/test_queryapi.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | import requests 6 | from mock import patch, Mock 7 | 8 | from lecli.query import api 9 | 10 | DATE_FROM = '2016-05-18 11:04:00' 11 | DATE_TO = '2016-05-18 11:09:59' 12 | MOCK_API_URL = 'http://mydummylink.com' 13 | SAMPLE_EVENTS_RESPONSE = { 14 | 'events': [ 15 | {'timestamp': 1432080000011, 'message': 'Message contents1'}, 16 | {'timestamp': 1432080000021, 'message': 'Message contents2'}, 17 | {'timestamp': 1432080000033, 'message': 'Message contents3'} 18 | ] 19 | } 20 | 21 | 22 | def setup_httpretty(): 23 | httpretty.enable() 24 | 25 | 26 | def teardown_httpretty(): 27 | httpretty.disable() 28 | httpretty.reset() 29 | 30 | 31 | def test_prettyprint_statistics_groups(capsys): 32 | setup_httpretty() 33 | 34 | sample_group_response = { 35 | 'statistics': { 36 | 'from': 123123, 37 | 'to': 1231323, 38 | 'granularity': 0, 39 | 'count': 1234, 40 | 'timeseries': {}, 41 | 'groups': [{ 42 | '200': { 43 | 'count': 802.0, 44 | 'min': 802.0, 45 | 'max': 802.0, 46 | 'sum': 802.0, 47 | 'bytes': 802.0, 48 | 'percentile': 802.0, 49 | 'unique': 802.0, 50 | 'average': 802.0 51 | } 52 | }, { 53 | '400': { 54 | 'count': 839.0, 55 | 'min': 839.0, 56 | 'max': 839.0, 57 | 'sum': 839.0, 58 | 'bytes': 839.0, 59 | 'percentile': 839.0, 60 | 'unique': 839.0, 61 | 'average': 839.0 62 | } 63 | }, { 64 | '404': { 65 | 'count': 839.0, 66 | 'min': 839.0, 67 | 'max': 839.0, 68 | 'sum': 839.0, 69 | 'bytes': 839.0, 70 | 'percentile': 839.0, 71 | 'unique': 839.0, 72 | 'average': 839.0 73 | } 74 | }, { 75 | 'status': { 76 | 'count': 205.0, 77 | 'min': 205.0, 78 | 'max': 205.0, 79 | 'sum': 205.0, 80 | 'bytes': 205.0, 81 | 'percentile': 205.0, 82 | 'unique': 205.0, 83 | 'average': 205.0 84 | } 85 | }], 86 | 'stats': {} 87 | } 88 | } 89 | 90 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type='application/json', 91 | body=json.dumps(sample_group_response)) 92 | response = requests.get(MOCK_API_URL) 93 | api.prettyprint_statistics(response) 94 | 95 | out, err = capsys.readouterr() 96 | for group in response.json()['statistics']['groups']: 97 | for key, value in group.iteritems(): 98 | assert key in out 99 | 100 | teardown_httpretty() 101 | 102 | 103 | def test_prettyprint_statistics_timeseries(capsys): 104 | setup_httpretty() 105 | 106 | sample_ts_response = { 107 | 'statistics': { 108 | 'from': 123123, 109 | 'to': 123123, 110 | 'count': 1234, 111 | 'stats': { 112 | 'global_timeseries': 113 | {'count': 27733.0} 114 | }, 115 | 'granularity': 120000, 116 | 'timeseries': { 117 | 'global_timeseries': [ 118 | {'count': 2931.0}, 119 | {'count': 2869.0}, 120 | {'count': 2852.0}, 121 | {'count': 2946.0}, 122 | {'count': 2733.0}, 123 | {'count': 2564.0}, 124 | {'count': 2801.0}, 125 | {'count': 2773.0}, 126 | {'count': 2698.0}, 127 | {'count': 2566.0} 128 | ] 129 | } 130 | } 131 | } 132 | 133 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, 134 | content_type='application/json', 135 | body=json.dumps(sample_ts_response)) 136 | response = requests.get(MOCK_API_URL) 137 | api.prettyprint_statistics(response) 138 | 139 | out, err = capsys.readouterr() 140 | assert "Total" in out 141 | assert "Timeseries" in out 142 | 143 | teardown_httpretty() 144 | 145 | 146 | def test_prettyprint_statistics_timeseries_with_empty_result(capsys): 147 | setup_httpretty() 148 | 149 | sample_empty_ts_response = { 150 | 'statistics': { 151 | 'from': 123123, 152 | 'to': 123123, 153 | 'count': 0.0, 154 | 'stats': { 155 | 'global_timeseries': 156 | {} # empty global timeseries object 157 | }, 158 | 'granularity': 120000, 159 | 'timeseries': { 160 | 'global_timeseries': [ 161 | {'count': 0.0}, 162 | {'count': 0.0}, 163 | {'count': 0.0}, 164 | {'count': 0.0}, 165 | {'count': 0.0}, 166 | {'count': 0.0}, 167 | {'count': 0.0}, 168 | {'count': 0.0}, 169 | {'count': 0.0}, 170 | {'count': 0.0} 171 | ] 172 | } 173 | } 174 | } 175 | 176 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type='application/json', 177 | body=json.dumps(sample_empty_ts_response)) 178 | response = requests.get(MOCK_API_URL) 179 | api.prettyprint_statistics(response) 180 | 181 | out, err = capsys.readouterr() 182 | assert "Total" in out 183 | assert "Timeseries" in out 184 | 185 | teardown_httpretty() 186 | 187 | 188 | def test_prettyprint_events(capsys): 189 | setup_httpretty() 190 | 191 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type='application/json', 192 | body=json.dumps(SAMPLE_EVENTS_RESPONSE)) 193 | response = requests.get(MOCK_API_URL) 194 | api.prettyprint_events(response) 195 | 196 | out, err = capsys.readouterr() 197 | 198 | assert "Message contents1" in out 199 | assert "Message contents2" in out 200 | assert "Message contents3" in out 201 | 202 | teardown_httpretty() 203 | 204 | 205 | @patch('lecli.api_utils.generate_headers') 206 | @patch('lecli.query.api._url') 207 | def test_post_query_with_time(mocked_url, mocked_generate_headers, capsys): 208 | setup_httpretty() 209 | mocked_url.return_value = '', MOCK_API_URL 210 | 211 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, 212 | content_type='application/json', 213 | body=json.dumps(SAMPLE_EVENTS_RESPONSE)) 214 | api.query(query_string='foo', log_keys='foo', time_from='123456', time_to='123456') 215 | 216 | out, err = capsys.readouterr() 217 | 218 | assert mocked_generate_headers.called 219 | assert "Message contents1" in out 220 | assert "Message contents2" in out 221 | assert "Message contents3" in out 222 | 223 | teardown_httpretty() 224 | 225 | 226 | @patch('lecli.api_utils.generate_headers') 227 | @patch('lecli.query.api._url') 228 | def test_post_query_with_date(mocked_url, mocked_generate_headers, capsys): 229 | setup_httpretty() 230 | mocked_url.return_value = '', MOCK_API_URL 231 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, 232 | content_type='application/json', 233 | body=json.dumps(SAMPLE_EVENTS_RESPONSE)) 234 | api.query(query_string='foo', log_keys='foo', date_from=DATE_FROM, date_to=DATE_TO) 235 | 236 | out, err = capsys.readouterr() 237 | 238 | assert mocked_generate_headers.called 239 | assert "Message contents1" in out 240 | assert "Message contents2" in out 241 | assert "Message contents3" in out 242 | 243 | teardown_httpretty() 244 | 245 | 246 | @patch('lecli.api_utils.generate_headers') 247 | @patch('lecli.query.api._url') 248 | def test_post_query_with_relative_range(mocked_url, mocked_generate_headers, capsys): 249 | setup_httpretty() 250 | mocked_url.return_value = '', MOCK_API_URL 251 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, 252 | content_type='application/json', 253 | body=json.dumps(SAMPLE_EVENTS_RESPONSE)) 254 | api.query(query_string='foo', log_keys='foo', relative_time_range='last 3 min') 255 | 256 | out, err = capsys.readouterr() 257 | 258 | assert mocked_generate_headers.called 259 | assert "Message contents1" in out 260 | assert "Message contents2" in out 261 | assert "Message contents3" in out 262 | 263 | teardown_httpretty() 264 | 265 | 266 | @patch('lecli.api_utils.generate_headers') 267 | def test_fetch_results(mocked_generate_headers): 268 | setup_httpretty() 269 | dest_url = MOCK_API_URL + "_some_arbitrary_url_suffix" 270 | httpretty.register_uri(httpretty.GET, dest_url, content_type='application/json', 271 | body=json.dumps(SAMPLE_EVENTS_RESPONSE)) 272 | 273 | response = api.fetch_results(dest_url).json() 274 | 275 | assert mocked_generate_headers.called 276 | assert "Message contents1" in response['events'][0]['message'] 277 | assert "Message contents2" in response['events'][1]['message'] 278 | assert "Message contents3" in response['events'][2]['message'] 279 | 280 | teardown_httpretty() 281 | 282 | 283 | @patch('lecli.api_utils.generate_headers') 284 | @patch('lecli.query.api.handle_response') 285 | def test_continue_request(mocked_headers, mocked_response_handle): 286 | setup_httpretty() 287 | 288 | links_response = { 289 | 'links': [ 290 | { 291 | 'rel': 'Self', 292 | 'href': 'http://mydummylink.com/query/sample-continuity-suffix' 293 | } 294 | ], 295 | 'id': 'sample-continuity-id', 296 | } 297 | 298 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type='application/json', 299 | body=json.dumps(links_response)) 300 | 301 | dest_url = links_response['links'][0]['href'] 302 | httpretty.register_uri(httpretty.GET, dest_url, content_type='application/json') 303 | 304 | resp = requests.get(MOCK_API_URL) 305 | api.continue_request(resp, Mock()) 306 | 307 | assert mocked_response_handle.called 308 | assert mocked_headers.called 309 | 310 | teardown_httpretty() 311 | 312 | 313 | @patch('lecli.api_utils.generate_headers') 314 | @patch('lecli.query.api.handle_tail') 315 | @patch('lecli.query.api._url') 316 | def test_live_tail_api(mocked_url, mocked_handle_tail, mocked_generate_headers): 317 | setup_httpretty() 318 | 319 | mocked_url.return_value = '', MOCK_API_URL 320 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, content_type='application/json', 321 | body=json.dumps({})) 322 | 323 | api.tail_logs(logkeys=str(uuid.uuid4()), leql=None, poll_interval=0.5) 324 | 325 | assert mocked_generate_headers.called 326 | assert mocked_handle_tail.called 327 | 328 | teardown_httpretty() 329 | 330 | 331 | def test_handle_response(capsys): 332 | setup_httpretty() 333 | 334 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type='application/json', 335 | body=json.dumps(SAMPLE_EVENTS_RESPONSE)) 336 | resp = requests.get(MOCK_API_URL) 337 | 338 | api.handle_response(resp, Mock()) 339 | 340 | out, err = capsys.readouterr() 341 | 342 | assert "Message contents1" in out 343 | assert "Message contents2" in out 344 | assert "Message contents3" in out 345 | 346 | teardown_httpretty() 347 | 348 | 349 | def test_validate_query(): 350 | # general query 351 | assert api.validate_query(query_string='foo', log_keys='bar', time_from=123) is True 352 | assert api.validate_query(logset='foo', favorites='bar', time_from=123) is False 353 | assert api.validate_query(foo='bar') is False 354 | assert api.validate_query(query_string='foo') is False 355 | assert api.validate_query(log_keys='foo') is False 356 | 357 | # saved query 358 | assert api.validate_query(saved_query_id='foo') is True 359 | assert api.validate_query(saved_query_id='foo', time_from=123456) is True 360 | assert api.validate_query(saved_query_id='foo', date_from=123456) is True 361 | assert api.validate_query(saved_query_id='foo', relative_time_range='bar') is True 362 | assert api.validate_query(saved_query_id='foo', log_keys='bar') is True 363 | assert api.validate_query(saved_query_id='foo', query_string='bar') is False 364 | 365 | 366 | def test_prepare_time_range_from_to(): 367 | leql_time_range = api.prepare_time_range(time_from=1, time_to=2, relative_time_range=None) 368 | assert 'from' in leql_time_range 369 | assert 'to' in leql_time_range 370 | assert leql_time_range['from'] == 1 * 1000 371 | assert leql_time_range['to'] == 2 * 1000 372 | assert 'time_range' not in leql_time_range 373 | 374 | 375 | def test_prepare_time_range_relative_range(): 376 | leql_time_range = api.prepare_time_range(None, None, relative_time_range='last 10 min') 377 | assert 'from' not in leql_time_range 378 | assert 'to' not in leql_time_range 379 | assert 'time_range' in leql_time_range 380 | assert leql_time_range['time_range'] == 'last 10 min' 381 | 382 | 383 | def test_prepare_time_range_with_iso_dates(): 384 | leql_time_range = api.prepare_time_range(None, None, relative_time_range=None, 385 | date_from='1970-01-01 00:00:00', 386 | date_to='1970-01-01 00:00:00') 387 | assert 'from' in leql_time_range 388 | assert 'to' in leql_time_range 389 | assert 'time_range' not in leql_time_range 390 | -------------------------------------------------------------------------------- /tests/test_saved_query_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | from mock import patch 6 | 7 | from lecli.saved_query import api 8 | 9 | MOCK_API_URL = 'http://mydummylink.com' 10 | SAVED_QUERY_ERROR_RESPONSE = { 11 | "fields": ["time_range"], 12 | "messages": ["Invalid query: time_range cannot be specified with from and/or to fields"] 13 | } 14 | SAVED_QUERY_RESPONSE = { 15 | "id": "123456789012345678901234567890123456", 16 | "name": "test", 17 | "leql": { 18 | "statement": "where(/*/)", 19 | "during": { 20 | "time_range": None, 21 | "to": 123123, 22 | "from": 123 23 | } 24 | }, 25 | "logs": ['123456789012345678901234567890123456'] 26 | } 27 | 28 | 29 | @httpretty.activate 30 | @patch('lecli.api_utils.get_account_resource_id') 31 | @patch('lecli.api_utils.get_rw_apikey') 32 | @patch('lecli.saved_query.api._url') 33 | def test_get_saved_queries(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 34 | mocked_url.return_value = '', MOCK_API_URL 35 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 36 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 37 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, content_type='application/json', 38 | body=json.dumps({'saved_queries': [SAVED_QUERY_RESPONSE]})) 39 | 40 | api.get_saved_query() 41 | out, err = capsys.readouterr() 42 | 43 | assert "Name:" in out 44 | assert "Logs:" in out 45 | assert "ID:" in out 46 | assert "Statement:" in out 47 | assert "Time range:" in out 48 | assert "From:" in out 49 | assert "To:" in out 50 | 51 | 52 | @httpretty.activate 53 | @patch('lecli.api_utils.get_account_resource_id') 54 | @patch('lecli.api_utils.get_rw_apikey') 55 | @patch('lecli.saved_query.api._url') 56 | def test_get_saved_query(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 57 | saved_query_id = str(uuid.uuid4()) 58 | mocked_url.return_value = '', MOCK_API_URL 59 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 60 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 61 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 62 | content_type='application/json', 63 | body=json.dumps({'saved_query': SAVED_QUERY_RESPONSE})) 64 | 65 | api.get_saved_query(saved_query_id) 66 | out, err = capsys.readouterr() 67 | 68 | assert "Name:" in out 69 | assert "Logs:" in out 70 | assert "ID:" in out 71 | assert "Statement:" in out 72 | assert "Time range:" in out 73 | assert "From:" in out 74 | assert "To:" in out 75 | 76 | 77 | @httpretty.activate 78 | @patch('lecli.api_utils.get_account_resource_id') 79 | @patch('lecli.api_utils.get_rw_apikey') 80 | @patch('lecli.saved_query.api._url') 81 | def test_create_saved_query(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 82 | saved_query_name = "my_saved_query" 83 | mocked_url.return_value = '', MOCK_API_URL 84 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 85 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 86 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, status=201, 87 | content_type='application/json', 88 | body=json.dumps({"saved_query": SAVED_QUERY_RESPONSE})) 89 | 90 | api.create_saved_query(saved_query_name, "where(/*/)") 91 | out, err = capsys.readouterr() 92 | 93 | assert "Saved query created with name: %s" % saved_query_name in out 94 | 95 | 96 | @httpretty.activate 97 | @patch('lecli.api_utils.get_account_resource_id') 98 | @patch('lecli.api_utils.get_rw_apikey') 99 | @patch('lecli.saved_query.api._url') 100 | def test_delete_saved_query(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 101 | test_saved_query_id = str(uuid.uuid4()) 102 | mocked_url.return_value = '', MOCK_API_URL 103 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 104 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 105 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL, status=204) 106 | 107 | api.delete_saved_query(test_saved_query_id) 108 | out, err = capsys.readouterr() 109 | 110 | assert "Deleted saved query with id: %s" % test_saved_query_id in out 111 | 112 | 113 | @httpretty.activate 114 | @patch('lecli.api_utils.get_account_resource_id') 115 | @patch('lecli.api_utils.get_rw_apikey') 116 | @patch('lecli.saved_query.api._url') 117 | def test_patch_saved_query(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 118 | test_saved_query_id = str(uuid.uuid4()) 119 | mocked_url.return_value = '', MOCK_API_URL 120 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 121 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 122 | httpretty.register_uri(httpretty.PATCH, MOCK_API_URL, status=200, 123 | content_type='application/json', 124 | body=json.dumps({"saved_query": SAVED_QUERY_RESPONSE})) 125 | 126 | api.update_saved_query(test_saved_query_id, name="new_query_name", 127 | statement="new_statement", from_ts=123, to_ts=123456) 128 | out, err = capsys.readouterr() 129 | 130 | assert "Saved query with id %s updated" % test_saved_query_id in out 131 | 132 | 133 | @httpretty.activate 134 | @patch('lecli.api_utils.get_account_resource_id') 135 | @patch('lecli.api_utils.get_rw_apikey') 136 | @patch('lecli.saved_query.api._url') 137 | def test_patch_saved_query_none_fields(mocked_url, mocked_rw_apikey, mocked_account_resource_id, 138 | capsys): 139 | test_saved_query_id = str(uuid.uuid4()) 140 | mocked_url.return_value = '', MOCK_API_URL 141 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 142 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 143 | httpretty.register_uri(httpretty.PATCH, MOCK_API_URL, status=200, 144 | content_type='application/json', 145 | body=json.dumps({"saved_query": SAVED_QUERY_RESPONSE})) 146 | 147 | api.update_saved_query(test_saved_query_id, name=None, 148 | statement="new_statement") 149 | out, err = capsys.readouterr() 150 | 151 | assert "Saved query with id %s updated" % test_saved_query_id in out 152 | body = json.loads(httpretty.last_request().body)['saved_query'] 153 | assert "name" not in body 154 | assert "statement" in body['leql'] 155 | 156 | 157 | @httpretty.activate 158 | @patch('lecli.api_utils.get_account_resource_id') 159 | @patch('lecli.api_utils.get_rw_apikey') 160 | @patch('lecli.saved_query.api._url') 161 | def test_failing_patch_saved_query(mocked_url, mocked_rw_apikey, mocked_account_resource_id, 162 | capsys): 163 | test_saved_query_id = str(uuid.uuid4()) 164 | mocked_url.return_value = '', MOCK_API_URL 165 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 166 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 167 | httpretty.register_uri(httpretty.PATCH, MOCK_API_URL, status=400, 168 | content_type='application/json', 169 | body=json.dumps(SAVED_QUERY_ERROR_RESPONSE)) 170 | 171 | api.update_saved_query( 172 | test_saved_query_id, name="new_query_name", statement="new_statement", from_ts=123, 173 | to_ts=123456, time_range="last 10 days") 174 | out, err = capsys.readouterr() 175 | 176 | assert "Invalid field: time_range\n" in out 177 | assert "Message: Invalid query: time_range cannot be specified with from and/or to fields" in out 178 | 179 | 180 | @httpretty.activate 181 | @patch('lecli.api_utils.get_account_resource_id') 182 | @patch('lecli.api_utils.get_rw_apikey') 183 | @patch('lecli.saved_query.api._url') 184 | def test_failing_create_saved_query(mocked_url, mocked_rw_apikey, mocked_account_resource_id, 185 | capsys): 186 | mocked_url.return_value = '', MOCK_API_URL 187 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 188 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 189 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, status=400, 190 | content_type='application/json', 191 | body=json.dumps(SAVED_QUERY_ERROR_RESPONSE)) 192 | 193 | api.create_saved_query(name="new_query_name", statement="new_statement", 194 | from_ts=123, to_ts=123456, time_range="last 10 days") 195 | out, err = capsys.readouterr() 196 | 197 | assert "Invalid field: time_range\n" in out 198 | assert "Message: Invalid query: time_range cannot be specified with from and/or to fields" in out 199 | 200 | -------------------------------------------------------------------------------- /tests/test_team_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | from mock import patch 6 | 7 | from lecli.team import api 8 | 9 | MOCK_API_URL = 'http://mydummylink.com' 10 | TEAM_RESPONSE = { 11 | 'id': '123456789012345678901234567890123456', 12 | 'name': 'my_team', 13 | 'users': [ 14 | { 15 | 'id': '123456789012345678901234567890123456', 16 | 'links': { 17 | 'href': 'https://dummy.link', 18 | 'ref': 'Self' 19 | } 20 | } 21 | ] 22 | } 23 | 24 | 25 | @httpretty.activate 26 | @patch('lecli.api_utils.get_account_resource_id') 27 | @patch('lecli.api_utils.get_rw_apikey') 28 | @patch('lecli.team.api._url') 29 | def test_get_teams(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 30 | mocked_url.return_value = '', MOCK_API_URL 31 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 32 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 33 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, content_type='application/json', 34 | body=json.dumps({'teams': [TEAM_RESPONSE]})) 35 | 36 | api.get_teams() 37 | out, err = capsys.readouterr() 38 | 39 | assert "my_team" in out 40 | 41 | 42 | @httpretty.activate 43 | @patch('lecli.api_utils.get_account_resource_id') 44 | @patch('lecli.api_utils.get_rw_apikey') 45 | @patch('lecli.team.api._url') 46 | def test_get_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 47 | team_id = str(uuid.uuid4()) 48 | mocked_url.return_value = '', MOCK_API_URL 49 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 50 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 51 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, content_type='application/json', 52 | body=json.dumps({'team': TEAM_RESPONSE})) 53 | 54 | api.get_team(team_id) 55 | out, err = capsys.readouterr() 56 | 57 | assert "my_team" in out 58 | 59 | 60 | @httpretty.activate 61 | @patch('lecli.api_utils.get_account_resource_id') 62 | @patch('lecli.api_utils.get_rw_apikey') 63 | @patch('lecli.team.api._url') 64 | def test_create_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 65 | mocked_url.return_value = '', MOCK_API_URL 66 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 67 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 68 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, status=201, 69 | content_type='application/json') 70 | 71 | api.create_team("test team") 72 | out, err = capsys.readouterr() 73 | 74 | assert "Team created with name: test team\n" == out 75 | 76 | 77 | @httpretty.activate 78 | @patch('lecli.api_utils.get_account_resource_id') 79 | @patch('lecli.api_utils.get_rw_apikey') 80 | @patch('lecli.team.api._url') 81 | def test_delete_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 82 | test_team_id = str(uuid.uuid4()) 83 | mocked_url.return_value = '', MOCK_API_URL 84 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 85 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 86 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL, status=204) 87 | 88 | api.delete_team(test_team_id) 89 | out, err = capsys.readouterr() 90 | 91 | assert "Deleted team with id: %s.\n" % test_team_id == out 92 | 93 | 94 | @httpretty.activate 95 | @patch('lecli.api_utils.get_account_resource_id') 96 | @patch('lecli.api_utils.get_rw_apikey') 97 | @patch('lecli.team.api._url') 98 | def test_rename_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 99 | test_team_id = str(uuid.uuid4()) 100 | mocked_url.return_value = '', MOCK_API_URL 101 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 102 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 103 | httpretty.register_uri(httpretty.PATCH, MOCK_API_URL, status=200, 104 | content_type='application/json') 105 | 106 | new_name_for_team = "new_test_team_name" 107 | api.rename_team(test_team_id, new_name_for_team) 108 | out, err = capsys.readouterr() 109 | 110 | assert new_name_for_team in out 111 | 112 | 113 | @httpretty.activate 114 | @patch('lecli.api_utils.get_account_resource_id') 115 | @patch('lecli.api_utils.get_rw_apikey') 116 | @patch('lecli.team.api._url') 117 | def test_add_user_to_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 118 | test_team_id = str(uuid.uuid4()) 119 | mocked_url.return_value = '', MOCK_API_URL 120 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 121 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 122 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 123 | body=json.dumps({'team': TEAM_RESPONSE}), 124 | content_type='application/json') 125 | httpretty.register_uri(httpretty.PATCH, MOCK_API_URL, status=200, 126 | content_type='application/json') 127 | 128 | user_id_to_add = "user_id" 129 | api.add_user_to_team(test_team_id, user_id_to_add) 130 | out, err = capsys.readouterr() 131 | assert "Added user with key: %s to team.\n" % user_id_to_add == out 132 | 133 | 134 | @httpretty.activate 135 | @patch('lecli.api_utils.get_account_resource_id') 136 | @patch('lecli.api_utils.get_rw_apikey') 137 | @patch('lecli.team.api._url') 138 | def test_delete_user_from_team(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 139 | test_team_id = str(uuid.uuid4()) 140 | mocked_url.return_value = '', MOCK_API_URL 141 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 142 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 143 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 144 | body=json.dumps({'team': TEAM_RESPONSE}), 145 | content_type='application/json') 146 | httpretty.register_uri(httpretty.PUT, MOCK_API_URL, status=200, 147 | content_type='application/json') 148 | 149 | user_id_to_add = str(uuid.uuid4()) 150 | api.delete_user_from_team(test_team_id, user_id_to_add) 151 | out, err = capsys.readouterr() 152 | 153 | assert "Deleted user with key: '%s' from team: %s\n" % (user_id_to_add, test_team_id) == out 154 | 155 | -------------------------------------------------------------------------------- /tests/test_usage_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | from mock import patch 6 | 7 | from lecli.usage import api 8 | 9 | MOCK_API_URL = 'http://mydummylink.com' 10 | SAMPLE_USAGE_RESPONSE = { 11 | "id": "123456789012345678901234567890123456", 12 | "name": "Test", 13 | "period": { 14 | "to": "2016-06-01", 15 | "from": "2016-01-01" 16 | }, 17 | "period_usage": 170129010, 18 | "daily_usage": [ 19 | { 20 | "usage": 30618, 21 | "day": "2016-06-01" 22 | }, 23 | { 24 | "usage": 6397, 25 | "day": "2016-05-31" 26 | }, 27 | { 28 | "usage": 1606, 29 | "day": "2016-05-30" 30 | }, 31 | { 32 | "usage": 2406, 33 | "day": "2016-05-29" 34 | } 35 | ] 36 | } 37 | 38 | 39 | @httpretty.activate 40 | @patch('lecli.api_utils.get_account_resource_id') 41 | @patch('lecli.api_utils.get_rw_apikey') 42 | @patch('lecli.usage.api._url') 43 | def test_get_usage(mocked_url, mocked_rw_apikey, mocked_account_resource_id, capsys): 44 | mocked_url.return_value = '', MOCK_API_URL 45 | mocked_rw_apikey.return_value = str(uuid.uuid4()) 46 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 47 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, status=200, 48 | content_type='application/json', 49 | body=json.dumps(SAMPLE_USAGE_RESPONSE)) 50 | 51 | api.get_usage('start', 'end') 52 | 53 | out, err = capsys.readouterr() 54 | assert "Total usage:\t" in out 55 | assert "Account name:\t" in out 56 | assert "Account ID:\t" in out 57 | -------------------------------------------------------------------------------- /tests/test_userapi.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import httpretty 5 | import requests 6 | from mock import patch 7 | from tabulate import tabulate 8 | 9 | from lecli.user import api 10 | 11 | MOCK_API_URL = 'http://mydummylink.com' 12 | DUMMY_USER_RESPONSE = {"user": {"first_name": "", 13 | "last_name": "", 14 | "login_name": "", 15 | "email": "", 16 | "id": ""}} 17 | 18 | 19 | @httpretty.activate 20 | @patch('lecli.api_utils.get_account_resource_id') 21 | @patch('lecli.api_utils.get_owner_apikey_id') 22 | @patch('lecli.api_utils.get_owner_apikey') 23 | @patch('lecli.user.api._url') 24 | def test_get_owner(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 25 | mocked_account_resource_id, capsys): 26 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 27 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 28 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 29 | mocked_url.return_value = '', MOCK_API_URL 30 | 31 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, body='{"owners": "ownerinfo"}', 32 | content_type='application/json', ) 33 | 34 | api.get_owner() 35 | 36 | out, err = capsys.readouterr() 37 | assert tabulate("ownerinfo") in out 38 | 39 | 40 | @httpretty.activate 41 | @patch('lecli.api_utils.get_account_resource_id') 42 | @patch('lecli.api_utils.get_owner_apikey_id') 43 | @patch('lecli.api_utils.get_owner_apikey') 44 | @patch('lecli.user.api._url') 45 | def test_delete_user(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 46 | mocked_account_resource_id, capsys): 47 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 48 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 49 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 50 | mocked_url.return_value = '', MOCK_API_URL 51 | 52 | user_key = str(uuid.uuid4()) 53 | httpretty.register_uri(httpretty.DELETE, MOCK_API_URL, status=204) 54 | 55 | api.delete_user(user_key) 56 | 57 | out, err = capsys.readouterr() 58 | assert 'Deleted user' in out 59 | 60 | 61 | @httpretty.activate 62 | @patch('lecli.api_utils.get_account_resource_id') 63 | @patch('lecli.api_utils.get_owner_apikey_id') 64 | @patch('lecli.api_utils.get_owner_apikey') 65 | @patch('lecli.user.api._url') 66 | def test_add_existing_user(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 67 | mocked_account_resource_id, capsys): 68 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 69 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 70 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 71 | mocked_url.return_value = '', MOCK_API_URL 72 | 73 | user_key = str(uuid.uuid4()) 74 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, body=json.dumps(DUMMY_USER_RESPONSE), 75 | status=200, content_type='application/json') 76 | 77 | api.add_existing_user(user_key) 78 | 79 | out, err = capsys.readouterr() 80 | assert "Added user to account" in out 81 | 82 | 83 | @httpretty.activate 84 | @patch('lecli.api_utils.get_account_resource_id') 85 | @patch('lecli.api_utils.get_owner_apikey_id') 86 | @patch('lecli.api_utils.get_owner_apikey') 87 | @patch('lecli.user.api._url') 88 | def test_add_new_user(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 89 | mocked_account_resource_id, capsys): 90 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 91 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 92 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 93 | mocked_url.return_value = '', MOCK_API_URL 94 | 95 | httpretty.register_uri(httpretty.POST, MOCK_API_URL, body=json.dumps(DUMMY_USER_RESPONSE), 96 | status=200, content_type='application/json') 97 | 98 | api.add_new_user("first_name", "last_name", "email") 99 | 100 | out, err = capsys.readouterr() 101 | assert "Added user to account" in out 102 | 103 | 104 | @httpretty.activate 105 | @patch('lecli.api_utils.get_account_resource_id') 106 | @patch('lecli.api_utils.get_owner_apikey_id') 107 | @patch('lecli.api_utils.get_owner_apikey') 108 | @patch('lecli.user.api._url') 109 | def test_list_users(mocked_url, mocked_owner_apikey, mocked_owner_apikey_id, 110 | mocked_account_resource_id, capsys): 111 | mocked_owner_apikey.return_value = str(uuid.uuid4()) 112 | mocked_owner_apikey_id.return_value = str(uuid.uuid4()) 113 | mocked_account_resource_id.return_value = str(uuid.uuid4()) 114 | mocked_url.return_value = '', MOCK_API_URL 115 | 116 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, body='{"users":"userinfo"}', 117 | content_type="application/json") 118 | 119 | api.list_users() 120 | 121 | out, err = capsys.readouterr() 122 | assert tabulate("userinfo") in out 123 | 124 | 125 | @httpretty.activate 126 | @patch('lecli.api_utils.get_account_resource_id') 127 | def test_handle_create_user_response_status_200_with_success(mocked_account_resource_id, capsys): 128 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type="application/json", 129 | status=200, body=json.dumps(DUMMY_USER_RESPONSE)) 130 | response = requests.get(MOCK_API_URL) 131 | 132 | api.handle_create_user_response(response) 133 | 134 | out, err = capsys.readouterr() 135 | assert "Added user to account" in out 136 | 137 | 138 | @httpretty.activate 139 | @patch('lecli.api_utils.get_account_resource_id') 140 | def test_handle_create_user_response_status_200_with_already_exists_error( 141 | mocked_account_resource_id, capsys): 142 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type="application/json", 143 | status=201, body=json.dumps(DUMMY_USER_RESPONSE)) 144 | response = requests.get(MOCK_API_URL) 145 | 146 | api.handle_create_user_response(response) 147 | 148 | out, err = capsys.readouterr() 149 | assert "Added user to account" in out 150 | 151 | 152 | @httpretty.activate 153 | @patch('lecli.api_utils.get_account_resource_id') 154 | def test_handle_create_user_response_status_201(mocked_account_resource_id, capsys): 155 | httpretty.register_uri(httpretty.GET, MOCK_API_URL, content_type="application/json", 156 | status=201, body=json.dumps(DUMMY_USER_RESPONSE)) 157 | response = requests.get(MOCK_API_URL) 158 | 159 | api.handle_create_user_response(response) 160 | 161 | out, err = capsys.readouterr() 162 | assert "Added user to account" in out 163 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py30,py31,pylint 3 | 4 | [testenv:py27] 5 | changedir=tests 6 | deps=-rdev-requirements.pip 7 | commands=py.test -v -s --cov=lecli 8 | 9 | [testenv:py30] 10 | changedir=tests 11 | deps=-rdev-requirements.pip 12 | commands=py.test -v -s --cov=lecli 13 | 14 | [testenv:py31] 15 | changedir=tests 16 | deps=-rdev-requirements.pip 17 | commands=py.test -v -s --cov=lecli 18 | 19 | [testenv:pylint] 20 | basepython=python2.7 21 | deps=pylint 22 | commands=pylint lecli 23 | --------------------------------------------------------------------------------