Skip to content

Commit

Permalink
Merge pull request #4451 from ryanpetrello/awxkit
Browse files Browse the repository at this point in the history
  • Loading branch information
2 parents 9b836ab + adaa414 commit 76a1099
Show file tree
Hide file tree
Showing 104 changed files with 10,487 additions and 1 deletion.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ test:
. $(VENV_BASE)/awx/bin/activate; \
fi; \
PYTHONDONTWRITEBYTECODE=1 py.test -p no:cacheprovider -n auto $(TEST_DIRS)
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
awx-manage check_migrations --dry-run --check -n 'vNNN_missing_migration_file'

test_unit:
Expand Down
100 changes: 100 additions & 0 deletions awxkit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
report.xml
report.pylama
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# IPython Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# dotenv
.env

# virtualenv
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

# vim
*.swp

# mac OS
*.DS_Store

# pytest
*.pytest_cache
4 changes: 4 additions & 0 deletions awxkit/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include requirements.txt
include setup.py
recursive-include awxkit *.py *.yml *.md
recursive-include test *.py *.yml *.md
4 changes: 4 additions & 0 deletions awxkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
awxkit
======

Python library that backs the provided `awx` command line client.
4 changes: 4 additions & 0 deletions awxkit/awxkit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .api import pages, client, resources # NOQA
from .config import config # NOQA
from . import awx # NOQA
from .ws import WSClient # NOQA
2 changes: 2 additions & 0 deletions awxkit/awxkit/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .pages import * # NOQA
from .client import * # NOQA
121 changes: 121 additions & 0 deletions awxkit/awxkit/api/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import logging

import requests

from awxkit import exceptions as exc
from awxkit.config import config


log = logging.getLogger(__name__)


class ConnectionException(exc.Common):

pass


class Token_Auth(requests.auth.AuthBase):
def __init__(self, token, auth_type='Token'):
self.token = token
self.auth_type = auth_type

def __call__(self, request):
request.headers['Authorization'] = '{0.auth_type} {0.token}'.format(self)
return request


def log_elapsed(r, *args, **kwargs): # requests hook to display API elapsed time
log.debug('"{0.request.method} {0.url}" elapsed: {0.elapsed}'.format(r))


class Connection(object):
"""A requests.Session wrapper for establishing connection w/ AWX instance"""

def __init__(self, server, verify=False):
self.server = server
self.verify = verify

if not self.verify:
requests.packages.urllib3.disable_warnings()

self.session = requests.Session()
self.uses_session_cookie = False

def get_session_requirements(self, next='/api/'):
self.get('/api/') # this causes a cookie w/ the CSRF token to be set
return dict(next=next)

def login(self, username=None, password=None, token=None, **kwargs):
if username and password:
_next = kwargs.get('next')
if _next:
headers = self.session.headers.copy()
self.post('/api/login/', headers=headers,
data=dict(username=username, password=password, next=_next))
self.session_id = self.session.cookies.get('sessionid')
self.uses_session_cookie = True
else:
self.session.auth = (username, password)
elif token:
self.session.auth = Token_Auth(token, auth_type=kwargs.get('auth_type', 'Token'))
else:
self.session.auth = None

def logout(self):
if self.uses_session_cookie:
self.session.cookies.pop('sessionid', None)
else:
self.session.auth = None

def request(self, relative_endpoint, method='get', json=None, data=None, query_parameters=None, headers=None):
"""Core requests.Session wrapper that returns requests.Response objects"""
session_request_method = getattr(self.session, method, None)
if not session_request_method:
raise ConnectionException(message="Unknown request method: {0}".format(method))

use_endpoint = relative_endpoint
if self.server.endswith('/') and use_endpoint.startswith('/'):
raise RuntimeError('AWX URL given with trailing slash, remove slash.')
url = '{0.server}{1}'.format(self, use_endpoint)

kwargs = dict(verify=self.verify, params=query_parameters, json=json, data=data,
hooks=dict(response=log_elapsed))

if headers is not None:
kwargs['headers'] = headers

if method in ('post', 'put', 'patch', 'delete'):
kwargs.setdefault('headers', {})['X-CSRFToken'] = self.session.cookies.get('csrftoken')
kwargs['headers']['Referer'] = url

for attempt in range(1, config.client_connection_attempts + 1):
try:
response = session_request_method(url, **kwargs)
break
except requests.exceptions.ConnectionError as err:
if attempt == config.client_connection_attempts:
raise err
log.exception('Failed to reach url: {0}. Retrying.'.format(url))

return response

def delete(self, relative_endpoint):
return self.request(relative_endpoint, method='delete')

def get(self, relative_endpoint, query_parameters=None, headers=None):
return self.request(relative_endpoint, method='get', query_parameters=query_parameters, headers=headers)

def head(self, relative_endpoint):
return self.request(relative_endpoint, method='head')

def options(self, relative_endpoint):
return self.request(relative_endpoint, method='options')

def patch(self, relative_endpoint, json):
return self.request(relative_endpoint, method='patch', json=json)

def post(self, relative_endpoint, json=None, data=None, headers=None):
return self.request(relative_endpoint, method='post', json=json, data=data, headers=headers)

def put(self, relative_endpoint, json):
return self.request(relative_endpoint, method='put', json=json)
7 changes: 7 additions & 0 deletions awxkit/awxkit/api/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .has_create import * # NOQA
from .has_instance_groups import HasInstanceGroups # NOQA
from .has_notifications import HasNotifications # NOQA
from .has_status import HasStatus # NOQA
from .has_survey import HasSurvey # NOQA
from .has_variables import HasVariables # NOQA
from .has_copy import HasCopy # NOQA
15 changes: 15 additions & 0 deletions awxkit/awxkit/api/mixins/has_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from awxkit.api.pages import Page
from awxkit.utils import random_title


class HasCopy(object):

def can_copy(self):
return self.get_related('copy').can_copy

def copy(self, name=''):
"""Return a copy of current page"""
payload = {"name": name or "Copy - " + random_title()}
endpoint = self.json.related['copy']
page = Page(self.connection, endpoint=endpoint)
return page.post(payload)
Loading

0 comments on commit 76a1099

Please sign in to comment.