mirror of
https://github.com/ansible/awx.git
synced 2026-05-25 09:37:45 -02:30
Merge pull request #9634 from ryanpetrello/black
move code linting to a stricter pep8-esque auto-formatting tool, black black (https://pypi.org/project/black/#description) is a strict superset of PEP8 It's also a tool that auto-formats your code on the fly for you based on its ruleset. With this PR, you can run make check, and any style issues will automatically be applied. Additionally, with this PR, if you spin up the development environment using our make targets, you'll automatically get a pre-commit hook installed that automatically runs linting prior to commit. If you don't like this behavior, or don't want it locally, you can: ~ export AWX_IGNORE_BLACK=1 ...but it's important to note that we won't merge your PR if it doesn't adhere to our style guidelines (which will run automatically as part of pre-merge CI). Reviewed-by: Jeff Bradberry <None> Reviewed-by: Ryan Petrello <None> Reviewed-by: Christian Adams <rooftopcellist@gmail.com> Reviewed-by: Seth Foster <None> Reviewed-by: Shane McDonald <me@shanemcd.com> Reviewed-by: Rebeccah Hunter <rhunter@redhat.com>
This commit is contained in:
@@ -127,7 +127,7 @@ Fixes and Features for AWX will go through the Github pull request process. Subm
|
|||||||
Here are a few things you can do to help the visibility of your change, and increase the likelihood that it will be accepted:
|
Here are a few things you can do to help the visibility of your change, and increase the likelihood that it will be accepted:
|
||||||
|
|
||||||
* No issues when running linters/code checkers
|
* No issues when running linters/code checkers
|
||||||
* Python: flake8: `(container)/awx_devel$ make flake8`
|
* Python: black: `(container)/awx_devel$ make black`
|
||||||
* Javascript: JsHint: `(container)/awx_devel$ make jshint`
|
* Javascript: JsHint: `(container)/awx_devel$ make jshint`
|
||||||
* No issues from unit tests
|
* No issues from unit tests
|
||||||
* Python: py.test: `(container)/awx_devel$ make test`
|
* Python: py.test: `(container)/awx_devel$ make test`
|
||||||
|
|||||||
27
Makefile
27
Makefile
@@ -271,20 +271,12 @@ jupyter:
|
|||||||
reports:
|
reports:
|
||||||
mkdir -p $@
|
mkdir -p $@
|
||||||
|
|
||||||
pep8: reports
|
black: reports
|
||||||
@(set -o pipefail && $@ | tee reports/$@.report)
|
(set -o pipefail && $@ $(BLACK_ARGS) awx awxkit awx_collection | tee reports/$@.report)
|
||||||
|
|
||||||
flake8: reports
|
.git/hooks/pre-commit:
|
||||||
@if [ "$(VENV_BASE)" ]; then \
|
echo "black --check \`[ -z \$$AWX_IGNORE_BLACK ] && git diff --cached --name-only | grep -E '\.py$\'\` || (echo 'To fix this, run \`make black\` to auto-format your code prior to commit, or set AWX_IGNORE_BLACK=1' && exit 1)" > .git/hooks/pre-commit
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
chmod +x .git/hooks/pre-commit
|
||||||
fi; \
|
|
||||||
(set -o pipefail && $@ | tee reports/$@.report)
|
|
||||||
|
|
||||||
pyflakes: reports
|
|
||||||
@(set -o pipefail && $@ | tee reports/$@.report)
|
|
||||||
|
|
||||||
pylint: reports
|
|
||||||
@(set -o pipefail && $@ | reports/$@.report)
|
|
||||||
|
|
||||||
genschema: reports
|
genschema: reports
|
||||||
$(MAKE) swagger PYTEST_ARGS="--genschema --create-db "
|
$(MAKE) swagger PYTEST_ARGS="--genschema --create-db "
|
||||||
@@ -296,7 +288,7 @@ swagger: reports
|
|||||||
fi; \
|
fi; \
|
||||||
(set -o pipefail && py.test $(PYTEST_ARGS) awx/conf/tests/functional awx/main/tests/functional/api awx/main/tests/docs --release=$(VERSION_TARGET) | tee reports/$@.report)
|
(set -o pipefail && py.test $(PYTEST_ARGS) awx/conf/tests/functional awx/main/tests/functional/api awx/main/tests/docs --release=$(VERSION_TARGET) | tee reports/$@.report)
|
||||||
|
|
||||||
check: flake8 pep8 # pyflakes pylint
|
check: black
|
||||||
|
|
||||||
awx-link:
|
awx-link:
|
||||||
[ -d "/awx_devel/awx.egg-info" ] || python3 /awx_devel/setup.py egg_info_dev
|
[ -d "/awx_devel/awx.egg-info" ] || python3 /awx_devel/setup.py egg_info_dev
|
||||||
@@ -332,10 +324,7 @@ test_collection:
|
|||||||
# Second we will load any libraries out of the virtualenv (if it's unspecified that should be ok because python should not load out of an empty directory)
|
# Second we will load any libraries out of the virtualenv (if it's unspecified that should be ok because python should not load out of an empty directory)
|
||||||
# Finally we will add the system path so that the tests can find the ansible libraries
|
# Finally we will add the system path so that the tests can find the ansible libraries
|
||||||
|
|
||||||
flake8_collection:
|
test_collection_all: test_collection
|
||||||
flake8 awx_collection/ # Different settings, in main exclude list
|
|
||||||
|
|
||||||
test_collection_all: test_collection flake8_collection
|
|
||||||
|
|
||||||
# WARNING: symlinking a collection is fundamentally unstable
|
# WARNING: symlinking a collection is fundamentally unstable
|
||||||
# this is for rapid development iteration with playbooks, do not use with other test targets
|
# this is for rapid development iteration with playbooks, do not use with other test targets
|
||||||
@@ -476,7 +465,7 @@ awx/projects:
|
|||||||
COMPOSE_UP_OPTS ?=
|
COMPOSE_UP_OPTS ?=
|
||||||
CLUSTER_NODE_COUNT ?= 1
|
CLUSTER_NODE_COUNT ?= 1
|
||||||
|
|
||||||
docker-compose-sources:
|
docker-compose-sources: .git/hooks/pre-commit
|
||||||
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
||||||
-e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \
|
-e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \
|
||||||
-e awx_image_tag=$(COMPOSE_TAG) \
|
-e awx_image_tag=$(COMPOSE_TAG) \
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ __all__ = ['__version__']
|
|||||||
# Check for the presence/absence of "devonly" module to determine if running
|
# Check for the presence/absence of "devonly" module to determine if running
|
||||||
# from a source code checkout or release packaage.
|
# from a source code checkout or release packaage.
|
||||||
try:
|
try:
|
||||||
import awx.devonly # noqa
|
import awx.devonly # noqa
|
||||||
|
|
||||||
MODE = 'development'
|
MODE = 'development'
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
MODE = 'production'
|
MODE = 'production'
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ import hashlib
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import django # noqa: F401
|
import django # noqa: F401
|
||||||
|
|
||||||
HAS_DJANGO = True
|
HAS_DJANGO = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_DJANGO = False
|
HAS_DJANGO = False
|
||||||
@@ -40,6 +42,7 @@ if HAS_DJANGO is True:
|
|||||||
try:
|
try:
|
||||||
names_digest('foo', 'bar', 'baz', length=8)
|
names_digest('foo', 'bar', 'baz', length=8)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
||||||
def names_digest(*args, length):
|
def names_digest(*args, length):
|
||||||
"""
|
"""
|
||||||
Generate a 32-bit digest of a set of arguments that can be used to shorten
|
Generate a 32-bit digest of a set of arguments that can be used to shorten
|
||||||
@@ -64,7 +67,7 @@ def find_commands(management_dir):
|
|||||||
continue
|
continue
|
||||||
elif f.endswith('.py') and f[:-3] not in commands:
|
elif f.endswith('.py') and f[:-3] not in commands:
|
||||||
commands.append(f[:-3])
|
commands.append(f[:-3])
|
||||||
elif f.endswith('.pyc') and f[:-4] not in commands: # pragma: no cover
|
elif f.endswith('.pyc') and f[:-4] not in commands: # pragma: no cover
|
||||||
commands.append(f[:-4])
|
commands.append(f[:-4])
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
@@ -75,6 +78,7 @@ def oauth2_getattribute(self, attr):
|
|||||||
# Custom method to override
|
# Custom method to override
|
||||||
# oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__
|
# oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
val = None
|
val = None
|
||||||
if 'migrate' not in sys.argv:
|
if 'migrate' not in sys.argv:
|
||||||
# certain Django OAuth Toolkit migrations actually reference
|
# certain Django OAuth Toolkit migrations actually reference
|
||||||
@@ -94,33 +98,38 @@ def prepare_env():
|
|||||||
# Hide DeprecationWarnings when running in production. Need to first load
|
# Hide DeprecationWarnings when running in production. Need to first load
|
||||||
# settings to apply our filter after Django's own warnings filter.
|
# settings to apply our filter after Django's own warnings filter.
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
if not settings.DEBUG: # pragma: no cover
|
|
||||||
|
if not settings.DEBUG: # pragma: no cover
|
||||||
warnings.simplefilter('ignore', DeprecationWarning)
|
warnings.simplefilter('ignore', DeprecationWarning)
|
||||||
# Monkeypatch Django find_commands to also work with .pyc files.
|
# Monkeypatch Django find_commands to also work with .pyc files.
|
||||||
import django.core.management
|
import django.core.management
|
||||||
|
|
||||||
django.core.management.find_commands = find_commands
|
django.core.management.find_commands = find_commands
|
||||||
|
|
||||||
# Monkeypatch Oauth2 toolkit settings class to check for settings
|
# Monkeypatch Oauth2 toolkit settings class to check for settings
|
||||||
# in django.conf settings each time, not just once during import
|
# in django.conf settings each time, not just once during import
|
||||||
import oauth2_provider.settings
|
import oauth2_provider.settings
|
||||||
|
|
||||||
oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__ = oauth2_getattribute
|
oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__ = oauth2_getattribute
|
||||||
|
|
||||||
# Use the AWX_TEST_DATABASE_* environment variables to specify the test
|
# Use the AWX_TEST_DATABASE_* environment variables to specify the test
|
||||||
# database settings to use when management command is run as an external
|
# database settings to use when management command is run as an external
|
||||||
# program via unit tests.
|
# program via unit tests.
|
||||||
for opt in ('ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'): # pragma: no cover
|
for opt in ('ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'): # pragma: no cover
|
||||||
if os.environ.get('AWX_TEST_DATABASE_%s' % opt, None):
|
if os.environ.get('AWX_TEST_DATABASE_%s' % opt, None):
|
||||||
settings.DATABASES['default'][opt] = os.environ['AWX_TEST_DATABASE_%s' % opt]
|
settings.DATABASES['default'][opt] = os.environ['AWX_TEST_DATABASE_%s' % opt]
|
||||||
# Disable capturing all SQL queries in memory when in DEBUG mode.
|
# Disable capturing all SQL queries in memory when in DEBUG mode.
|
||||||
if settings.DEBUG and not getattr(settings, 'SQL_DEBUG', True):
|
if settings.DEBUG and not getattr(settings, 'SQL_DEBUG', True):
|
||||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||||
from django.db.backends.utils import CursorWrapper
|
from django.db.backends.utils import CursorWrapper
|
||||||
|
|
||||||
BaseDatabaseWrapper.make_debug_cursor = lambda self, cursor: CursorWrapper(cursor, self)
|
BaseDatabaseWrapper.make_debug_cursor = lambda self, cursor: CursorWrapper(cursor, self)
|
||||||
|
|
||||||
# Use the default devserver addr/port defined in settings for runserver.
|
# Use the default devserver addr/port defined in settings for runserver.
|
||||||
default_addr = getattr(settings, 'DEVSERVER_DEFAULT_ADDR', '127.0.0.1')
|
default_addr = getattr(settings, 'DEVSERVER_DEFAULT_ADDR', '127.0.0.1')
|
||||||
default_port = getattr(settings, 'DEVSERVER_DEFAULT_PORT', 8000)
|
default_port = getattr(settings, 'DEVSERVER_DEFAULT_PORT', 8000)
|
||||||
from django.core.management.commands import runserver as core_runserver
|
from django.core.management.commands import runserver as core_runserver
|
||||||
|
|
||||||
original_handle = core_runserver.Command.handle
|
original_handle = core_runserver.Command.handle
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
@@ -139,7 +148,8 @@ def manage():
|
|||||||
# Now run the command (or display the version).
|
# Now run the command (or display the version).
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
if len(sys.argv) >= 2 and sys.argv[1] in ('version', '--version'): # pragma: no cover
|
|
||||||
|
if len(sys.argv) >= 2 and sys.argv[1] in ('version', '--version'): # pragma: no cover
|
||||||
sys.stdout.write('%s\n' % __version__)
|
sys.stdout.write('%s\n' % __version__)
|
||||||
# If running as a user without permission to read settings, display an
|
# If running as a user without permission to read settings, display an
|
||||||
# error message. Allow --help to still work.
|
# error message. Allow --help to still work.
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ logger = logging.getLogger('awx.api.authentication')
|
|||||||
|
|
||||||
|
|
||||||
class LoggedBasicAuthentication(authentication.BasicAuthentication):
|
class LoggedBasicAuthentication(authentication.BasicAuthentication):
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
if not settings.AUTH_BASIC_ENABLED:
|
if not settings.AUTH_BASIC_ENABLED:
|
||||||
return
|
return
|
||||||
@@ -35,22 +34,18 @@ class LoggedBasicAuthentication(authentication.BasicAuthentication):
|
|||||||
|
|
||||||
|
|
||||||
class SessionAuthentication(authentication.SessionAuthentication):
|
class SessionAuthentication(authentication.SessionAuthentication):
|
||||||
|
|
||||||
def authenticate_header(self, request):
|
def authenticate_header(self, request):
|
||||||
return 'Session'
|
return 'Session'
|
||||||
|
|
||||||
|
|
||||||
class LoggedOAuth2Authentication(OAuth2Authentication):
|
class LoggedOAuth2Authentication(OAuth2Authentication):
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
ret = super(LoggedOAuth2Authentication, self).authenticate(request)
|
ret = super(LoggedOAuth2Authentication, self).authenticate(request)
|
||||||
if ret:
|
if ret:
|
||||||
user, token = ret
|
user, token = ret
|
||||||
username = user.username if user else '<none>'
|
username = user.username if user else '<none>'
|
||||||
logger.info(smart_text(
|
logger.info(
|
||||||
u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format(
|
smart_text(u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format(username, request.method, request.path, token.pk))
|
||||||
username, request.method, request.path, token.pk
|
)
|
||||||
)
|
|
||||||
))
|
|
||||||
setattr(user, 'oauth_scopes', [x for x in token.scope.split() if x])
|
setattr(user, 'oauth_scopes', [x for x in token.scope.split() if x])
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -38,16 +38,20 @@ register(
|
|||||||
register(
|
register(
|
||||||
'OAUTH2_PROVIDER',
|
'OAUTH2_PROVIDER',
|
||||||
field_class=OAuth2ProviderField,
|
field_class=OAuth2ProviderField,
|
||||||
default={'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
default={
|
||||||
'AUTHORIZATION_CODE_EXPIRE_SECONDS': oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
'REFRESH_TOKEN_EXPIRE_SECONDS': oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS},
|
'AUTHORIZATION_CODE_EXPIRE_SECONDS': oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
|
'REFRESH_TOKEN_EXPIRE_SECONDS': oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
},
|
||||||
label=_('OAuth 2 Timeout Settings'),
|
label=_('OAuth 2 Timeout Settings'),
|
||||||
help_text=_('Dictionary for customizing OAuth 2 timeouts, available items are '
|
help_text=_(
|
||||||
'`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number '
|
'Dictionary for customizing OAuth 2 timeouts, available items are '
|
||||||
'of seconds, `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of '
|
'`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number '
|
||||||
'authorization codes in the number of seconds, and `REFRESH_TOKEN_EXPIRE_SECONDS`, '
|
'of seconds, `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of '
|
||||||
'the duration of refresh tokens, after expired access tokens, '
|
'authorization codes in the number of seconds, and `REFRESH_TOKEN_EXPIRE_SECONDS`, '
|
||||||
'in the number of seconds.'),
|
'the duration of refresh tokens, after expired access tokens, '
|
||||||
|
'in the number of seconds.'
|
||||||
|
),
|
||||||
category=_('Authentication'),
|
category=_('Authentication'),
|
||||||
category_slug='authentication',
|
category_slug='authentication',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -57,10 +61,12 @@ register(
|
|||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
default=False,
|
default=False,
|
||||||
label=_('Allow External Users to Create OAuth2 Tokens'),
|
label=_('Allow External Users to Create OAuth2 Tokens'),
|
||||||
help_text=_('For security reasons, users from external auth providers (LDAP, SAML, '
|
help_text=_(
|
||||||
'SSO, Radius, and others) are not allowed to create OAuth2 tokens. '
|
'For security reasons, users from external auth providers (LDAP, SAML, '
|
||||||
'To change this behavior, enable this setting. Existing tokens will '
|
'SSO, Radius, and others) are not allowed to create OAuth2 tokens. '
|
||||||
'not be deleted when this setting is toggled off.'),
|
'To change this behavior, enable this setting. Existing tokens will '
|
||||||
|
'not be deleted when this setting is toggled off.'
|
||||||
|
),
|
||||||
category=_('Authentication'),
|
category=_('Authentication'),
|
||||||
category_slug='authentication',
|
category_slug='authentication',
|
||||||
)
|
)
|
||||||
@@ -71,8 +77,7 @@ register(
|
|||||||
required=False,
|
required=False,
|
||||||
default='',
|
default='',
|
||||||
label=_('Login redirect override URL'),
|
label=_('Login redirect override URL'),
|
||||||
help_text=_('URL to which unauthorized users will be redirected to log in. '
|
help_text=_('URL to which unauthorized users will be redirected to log in. If blank, users will be sent to the Tower login page.'),
|
||||||
'If blank, users will be sent to the Tower login page.'),
|
|
||||||
category=_('Authentication'),
|
category=_('Authentication'),
|
||||||
category_slug='authentication',
|
category_slug='authentication',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,4 @@ class ActiveJobConflict(ValidationError):
|
|||||||
# turn everything in self.detail into string by using force_text.
|
# turn everything in self.detail into string by using force_text.
|
||||||
# Declare detail afterwards circumvent this behavior.
|
# Declare detail afterwards circumvent this behavior.
|
||||||
super(ActiveJobConflict, self).__init__()
|
super(ActiveJobConflict, self).__init__()
|
||||||
self.detail = {
|
self.detail = {"error": _("Resource is being used by running jobs."), "active_jobs": active_jobs}
|
||||||
"error": _("Resource is being used by running jobs."),
|
|
||||||
"active_jobs": active_jobs
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ __all__ = ['BooleanNullField', 'CharNullField', 'ChoiceNullField', 'VerbatimFiel
|
|||||||
|
|
||||||
|
|
||||||
class NullFieldMixin(object):
|
class NullFieldMixin(object):
|
||||||
'''
|
"""
|
||||||
Mixin to prevent shortcutting validation when we want to allow null input,
|
Mixin to prevent shortcutting validation when we want to allow null input,
|
||||||
but coerce the resulting value to another type.
|
but coerce the resulting value to another type.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def validate_empty_values(self, data):
|
def validate_empty_values(self, data):
|
||||||
(is_empty_value, data) = super(NullFieldMixin, self).validate_empty_values(data)
|
(is_empty_value, data) = super(NullFieldMixin, self).validate_empty_values(data)
|
||||||
@@ -29,18 +29,18 @@ class NullFieldMixin(object):
|
|||||||
|
|
||||||
|
|
||||||
class BooleanNullField(NullFieldMixin, serializers.NullBooleanField):
|
class BooleanNullField(NullFieldMixin, serializers.NullBooleanField):
|
||||||
'''
|
"""
|
||||||
Custom boolean field that allows null and empty string as False values.
|
Custom boolean field that allows null and empty string as False values.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
return bool(super(BooleanNullField, self).to_internal_value(data))
|
return bool(super(BooleanNullField, self).to_internal_value(data))
|
||||||
|
|
||||||
|
|
||||||
class CharNullField(NullFieldMixin, serializers.CharField):
|
class CharNullField(NullFieldMixin, serializers.CharField):
|
||||||
'''
|
"""
|
||||||
Custom char field that allows null as input and coerces to an empty string.
|
Custom char field that allows null as input and coerces to an empty string.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs['allow_null'] = True
|
kwargs['allow_null'] = True
|
||||||
@@ -51,9 +51,9 @@ class CharNullField(NullFieldMixin, serializers.CharField):
|
|||||||
|
|
||||||
|
|
||||||
class ChoiceNullField(NullFieldMixin, serializers.ChoiceField):
|
class ChoiceNullField(NullFieldMixin, serializers.ChoiceField):
|
||||||
'''
|
"""
|
||||||
Custom choice field that allows null as input and coerces to an empty string.
|
Custom choice field that allows null as input and coerces to an empty string.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs['allow_null'] = True
|
kwargs['allow_null'] = True
|
||||||
@@ -64,9 +64,9 @@ class ChoiceNullField(NullFieldMixin, serializers.ChoiceField):
|
|||||||
|
|
||||||
|
|
||||||
class VerbatimField(serializers.Field):
|
class VerbatimField(serializers.Field):
|
||||||
'''
|
"""
|
||||||
Custom field that passes the value through without changes.
|
Custom field that passes the value through without changes.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
return data
|
return data
|
||||||
@@ -77,22 +77,19 @@ class VerbatimField(serializers.Field):
|
|||||||
|
|
||||||
class OAuth2ProviderField(fields.DictField):
|
class OAuth2ProviderField(fields.DictField):
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {'invalid_key_names': _('Invalid key names: {invalid_key_names}')}
|
||||||
'invalid_key_names': _('Invalid key names: {invalid_key_names}'),
|
|
||||||
}
|
|
||||||
valid_key_names = {'ACCESS_TOKEN_EXPIRE_SECONDS', 'AUTHORIZATION_CODE_EXPIRE_SECONDS', 'REFRESH_TOKEN_EXPIRE_SECONDS'}
|
valid_key_names = {'ACCESS_TOKEN_EXPIRE_SECONDS', 'AUTHORIZATION_CODE_EXPIRE_SECONDS', 'REFRESH_TOKEN_EXPIRE_SECONDS'}
|
||||||
child = fields.IntegerField(min_value=1)
|
child = fields.IntegerField(min_value=1)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
data = super(OAuth2ProviderField, self).to_internal_value(data)
|
data = super(OAuth2ProviderField, self).to_internal_value(data)
|
||||||
invalid_flags = (set(data.keys()) - self.valid_key_names)
|
invalid_flags = set(data.keys()) - self.valid_key_names
|
||||||
if invalid_flags:
|
if invalid_flags:
|
||||||
self.fail('invalid_key_names', invalid_key_names=', '.join(list(invalid_flags)))
|
self.fail('invalid_key_names', invalid_key_names=', '.join(list(invalid_flags)))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedCredentialField(serializers.IntegerField):
|
class DeprecatedCredentialField(serializers.IntegerField):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs['allow_null'] = True
|
kwargs['allow_null'] = True
|
||||||
kwargs['default'] = None
|
kwargs['default'] = None
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ from awx.main.utils.db import get_all_field_names
|
|||||||
|
|
||||||
|
|
||||||
class TypeFilterBackend(BaseFilterBackend):
|
class TypeFilterBackend(BaseFilterBackend):
|
||||||
'''
|
"""
|
||||||
Filter on type field now returned with all objects.
|
Filter on type field now returned with all objects.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def filter_queryset(self, request, queryset, view):
|
def filter_queryset(self, request, queryset, view):
|
||||||
try:
|
try:
|
||||||
@@ -64,7 +64,7 @@ class TypeFilterBackend(BaseFilterBackend):
|
|||||||
|
|
||||||
|
|
||||||
def get_fields_from_path(model, path):
|
def get_fields_from_path(model, path):
|
||||||
'''
|
"""
|
||||||
Given a Django ORM lookup path (possibly over multiple models)
|
Given a Django ORM lookup path (possibly over multiple models)
|
||||||
Returns the fields in the line, and also the revised lookup path
|
Returns the fields in the line, and also the revised lookup path
|
||||||
ex., given
|
ex., given
|
||||||
@@ -73,7 +73,7 @@ def get_fields_from_path(model, path):
|
|||||||
returns tuple of fields traversed as well and a corrected path,
|
returns tuple of fields traversed as well and a corrected path,
|
||||||
for special cases we do substitutions
|
for special cases we do substitutions
|
||||||
([<IntegerField for timeout>], 'project__timeout')
|
([<IntegerField for timeout>], 'project__timeout')
|
||||||
'''
|
"""
|
||||||
# Store of all the fields used to detect repeats
|
# Store of all the fields used to detect repeats
|
||||||
field_list = []
|
field_list = []
|
||||||
new_parts = []
|
new_parts = []
|
||||||
@@ -82,12 +82,9 @@ def get_fields_from_path(model, path):
|
|||||||
raise ParseError(_('No related model for field {}.').format(name))
|
raise ParseError(_('No related model for field {}.').format(name))
|
||||||
# HACK: Make project and inventory source filtering by old field names work for backwards compatibility.
|
# HACK: Make project and inventory source filtering by old field names work for backwards compatibility.
|
||||||
if model._meta.object_name in ('Project', 'InventorySource'):
|
if model._meta.object_name in ('Project', 'InventorySource'):
|
||||||
name = {
|
name = {'current_update': 'current_job', 'last_update': 'last_job', 'last_update_failed': 'last_job_failed', 'last_updated': 'last_job_run'}.get(
|
||||||
'current_update': 'current_job',
|
name, name
|
||||||
'last_update': 'last_job',
|
)
|
||||||
'last_update_failed': 'last_job_failed',
|
|
||||||
'last_updated': 'last_job_run',
|
|
||||||
}.get(name, name)
|
|
||||||
|
|
||||||
if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model):
|
if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model):
|
||||||
name = 'polymorphic_ctype'
|
name = 'polymorphic_ctype'
|
||||||
@@ -121,28 +118,42 @@ def get_fields_from_path(model, path):
|
|||||||
|
|
||||||
|
|
||||||
def get_field_from_path(model, path):
|
def get_field_from_path(model, path):
|
||||||
'''
|
"""
|
||||||
Given a Django ORM lookup path (possibly over multiple models)
|
Given a Django ORM lookup path (possibly over multiple models)
|
||||||
Returns the last field in the line, and the revised lookup path
|
Returns the last field in the line, and the revised lookup path
|
||||||
ex.
|
ex.
|
||||||
(<IntegerField for timeout>, 'project__timeout')
|
(<IntegerField for timeout>, 'project__timeout')
|
||||||
'''
|
"""
|
||||||
field_list, new_path = get_fields_from_path(model, path)
|
field_list, new_path = get_fields_from_path(model, path)
|
||||||
return (field_list[-1], new_path)
|
return (field_list[-1], new_path)
|
||||||
|
|
||||||
|
|
||||||
class FieldLookupBackend(BaseFilterBackend):
|
class FieldLookupBackend(BaseFilterBackend):
|
||||||
'''
|
"""
|
||||||
Filter using field lookups provided via query string parameters.
|
Filter using field lookups provided via query string parameters.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by',
|
RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by', 'search', 'type', 'host_filter', 'count_disabled', 'no_truncate')
|
||||||
'search', 'type', 'host_filter', 'count_disabled', 'no_truncate')
|
|
||||||
|
|
||||||
SUPPORTED_LOOKUPS = ('exact', 'iexact', 'contains', 'icontains',
|
SUPPORTED_LOOKUPS = (
|
||||||
'startswith', 'istartswith', 'endswith', 'iendswith',
|
'exact',
|
||||||
'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'in',
|
'iexact',
|
||||||
'isnull', 'search')
|
'contains',
|
||||||
|
'icontains',
|
||||||
|
'startswith',
|
||||||
|
'istartswith',
|
||||||
|
'endswith',
|
||||||
|
'iendswith',
|
||||||
|
'regex',
|
||||||
|
'iregex',
|
||||||
|
'gt',
|
||||||
|
'gte',
|
||||||
|
'lt',
|
||||||
|
'lte',
|
||||||
|
'in',
|
||||||
|
'isnull',
|
||||||
|
'search',
|
||||||
|
)
|
||||||
|
|
||||||
# A list of fields that we know can be filtered on without the possiblity
|
# A list of fields that we know can be filtered on without the possiblity
|
||||||
# of introducing duplicates
|
# of introducing duplicates
|
||||||
@@ -189,10 +200,7 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
try:
|
try:
|
||||||
return self.to_python_related(value)
|
return self.to_python_related(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ParseError(_('Invalid {field_name} id: {field_id}').format(
|
raise ParseError(_('Invalid {field_name} id: {field_id}').format(field_name=getattr(field, 'name', 'related field'), field_id=value))
|
||||||
field_name=getattr(field, 'name', 'related field'),
|
|
||||||
field_id=value)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return field.to_python(value)
|
return field.to_python(value)
|
||||||
|
|
||||||
@@ -205,13 +213,13 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
field_list, new_lookup = self.get_fields_from_lookup(model, lookup)
|
field_list, new_lookup = self.get_fields_from_lookup(model, lookup)
|
||||||
field = field_list[-1]
|
field = field_list[-1]
|
||||||
|
|
||||||
needs_distinct = (not all(isinstance(f, self.NO_DUPLICATES_ALLOW_LIST) for f in field_list))
|
needs_distinct = not all(isinstance(f, self.NO_DUPLICATES_ALLOW_LIST) for f in field_list)
|
||||||
|
|
||||||
# Type names are stored without underscores internally, but are presented and
|
# Type names are stored without underscores internally, but are presented and
|
||||||
# and serialized over the API containing underscores so we remove `_`
|
# and serialized over the API containing underscores so we remove `_`
|
||||||
# for polymorphic_ctype__model lookups.
|
# for polymorphic_ctype__model lookups.
|
||||||
if new_lookup.startswith('polymorphic_ctype__model'):
|
if new_lookup.startswith('polymorphic_ctype__model'):
|
||||||
value = value.replace('_','')
|
value = value.replace('_', '')
|
||||||
elif new_lookup.endswith('__isnull'):
|
elif new_lookup.endswith('__isnull'):
|
||||||
value = to_python_boolean(value)
|
value = to_python_boolean(value)
|
||||||
elif new_lookup.endswith('__in'):
|
elif new_lookup.endswith('__in'):
|
||||||
@@ -329,24 +337,20 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
args = []
|
args = []
|
||||||
for n, k, v in and_filters:
|
for n, k, v in and_filters:
|
||||||
if n:
|
if n:
|
||||||
args.append(~Q(**{k:v}))
|
args.append(~Q(**{k: v}))
|
||||||
else:
|
else:
|
||||||
args.append(Q(**{k:v}))
|
args.append(Q(**{k: v}))
|
||||||
for role_name in role_filters:
|
for role_name in role_filters:
|
||||||
if not hasattr(queryset.model, 'accessible_pk_qs'):
|
if not hasattr(queryset.model, 'accessible_pk_qs'):
|
||||||
raise ParseError(_(
|
raise ParseError(_('Cannot apply role_level filter to this list because its model ' 'does not use roles for access control.'))
|
||||||
'Cannot apply role_level filter to this list because its model '
|
args.append(Q(pk__in=queryset.model.accessible_pk_qs(request.user, role_name)))
|
||||||
'does not use roles for access control.'))
|
|
||||||
args.append(
|
|
||||||
Q(pk__in=queryset.model.accessible_pk_qs(request.user, role_name))
|
|
||||||
)
|
|
||||||
if or_filters:
|
if or_filters:
|
||||||
q = Q()
|
q = Q()
|
||||||
for n,k,v in or_filters:
|
for n, k, v in or_filters:
|
||||||
if n:
|
if n:
|
||||||
q |= ~Q(**{k:v})
|
q |= ~Q(**{k: v})
|
||||||
else:
|
else:
|
||||||
q |= Q(**{k:v})
|
q |= Q(**{k: v})
|
||||||
args.append(q)
|
args.append(q)
|
||||||
if search_filters and search_filter_relation == 'OR':
|
if search_filters and search_filter_relation == 'OR':
|
||||||
q = Q()
|
q = Q()
|
||||||
@@ -360,11 +364,11 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
for constrain in constrains:
|
for constrain in constrains:
|
||||||
q_chain |= Q(**{constrain: term})
|
q_chain |= Q(**{constrain: term})
|
||||||
queryset = queryset.filter(q_chain)
|
queryset = queryset.filter(q_chain)
|
||||||
for n,k,v in chain_filters:
|
for n, k, v in chain_filters:
|
||||||
if n:
|
if n:
|
||||||
q = ~Q(**{k:v})
|
q = ~Q(**{k: v})
|
||||||
else:
|
else:
|
||||||
q = Q(**{k:v})
|
q = Q(**{k: v})
|
||||||
queryset = queryset.filter(q)
|
queryset = queryset.filter(q)
|
||||||
queryset = queryset.filter(*args)
|
queryset = queryset.filter(*args)
|
||||||
if needs_distinct:
|
if needs_distinct:
|
||||||
@@ -377,9 +381,9 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
|
|
||||||
|
|
||||||
class OrderByBackend(BaseFilterBackend):
|
class OrderByBackend(BaseFilterBackend):
|
||||||
'''
|
"""
|
||||||
Filter to apply ordering based on query string parameters.
|
Filter to apply ordering based on query string parameters.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def filter_queryset(self, request, queryset, view):
|
def filter_queryset(self, request, queryset, view):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -35,55 +35,50 @@ from rest_framework.negotiation import DefaultContentNegotiation
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.api.filters import FieldLookupBackend
|
from awx.api.filters import FieldLookupBackend
|
||||||
from awx.main.models import (
|
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
||||||
UnifiedJob, UnifiedJobTemplate, User, Role, Credential,
|
|
||||||
WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
|
||||||
)
|
|
||||||
from awx.main.access import access_registry
|
from awx.main.access import access_registry
|
||||||
from awx.main.utils import (
|
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
|
||||||
camelcase_to_underscore,
|
|
||||||
get_search_fields,
|
|
||||||
getattrd,
|
|
||||||
get_object_or_400,
|
|
||||||
decrypt_field,
|
|
||||||
get_awx_version,
|
|
||||||
)
|
|
||||||
from awx.main.utils.db import get_all_field_names
|
from awx.main.utils.db import get_all_field_names
|
||||||
from awx.main.views import ApiErrorView
|
from awx.main.views import ApiErrorView
|
||||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
||||||
from awx.api.versioning import URLPathVersioning
|
from awx.api.versioning import URLPathVersioning
|
||||||
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
||||||
|
|
||||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
__all__ = [
|
||||||
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
'APIView',
|
||||||
'SubListDestroyAPIView',
|
'GenericAPIView',
|
||||||
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
'ListAPIView',
|
||||||
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
'SimpleListAPIView',
|
||||||
'RetrieveUpdateDestroyAPIView',
|
'ListCreateAPIView',
|
||||||
'SubDetailAPIView',
|
'SubListAPIView',
|
||||||
'ResourceAccessList',
|
'SubListCreateAPIView',
|
||||||
'ParentMixin',
|
'SubListDestroyAPIView',
|
||||||
'DeleteLastUnattachLabelMixin',
|
'SubListCreateAttachDetachAPIView',
|
||||||
'SubListAttachDetachAPIView',
|
'RetrieveAPIView',
|
||||||
'CopyAPIView', 'BaseUsersList',]
|
'RetrieveUpdateAPIView',
|
||||||
|
'RetrieveDestroyAPIView',
|
||||||
|
'RetrieveUpdateDestroyAPIView',
|
||||||
|
'SubDetailAPIView',
|
||||||
|
'ResourceAccessList',
|
||||||
|
'ParentMixin',
|
||||||
|
'DeleteLastUnattachLabelMixin',
|
||||||
|
'SubListAttachDetachAPIView',
|
||||||
|
'CopyAPIView',
|
||||||
|
'BaseUsersList',
|
||||||
|
]
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.generics')
|
logger = logging.getLogger('awx.api.generics')
|
||||||
analytics_logger = logging.getLogger('awx.analytics.performance')
|
analytics_logger = logging.getLogger('awx.analytics.performance')
|
||||||
|
|
||||||
|
|
||||||
class LoggedLoginView(auth_views.LoginView):
|
class LoggedLoginView(auth_views.LoginView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# The django.auth.contrib login form doesn't perform the content
|
# The django.auth.contrib login form doesn't perform the content
|
||||||
# negotiation we've come to expect from DRF; add in code to catch
|
# negotiation we've come to expect from DRF; add in code to catch
|
||||||
# situations where Accept != text/html (or */*) and reply with
|
# situations where Accept != text/html (or */*) and reply with
|
||||||
# an HTTP 406
|
# an HTTP 406
|
||||||
try:
|
try:
|
||||||
DefaultContentNegotiation().select_renderer(
|
DefaultContentNegotiation().select_renderer(request, [StaticHTMLRenderer], 'html')
|
||||||
request,
|
|
||||||
[StaticHTMLRenderer],
|
|
||||||
'html'
|
|
||||||
)
|
|
||||||
except NotAcceptable:
|
except NotAcceptable:
|
||||||
resp = Response(status=status.HTTP_406_NOT_ACCEPTABLE)
|
resp = Response(status=status.HTTP_406_NOT_ACCEPTABLE)
|
||||||
resp.accepted_renderer = StaticHTMLRenderer()
|
resp.accepted_renderer = StaticHTMLRenderer()
|
||||||
@@ -96,7 +91,7 @@ class LoggedLoginView(auth_views.LoginView):
|
|||||||
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
|
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
|
||||||
current_user = getattr(request, 'user', None)
|
current_user = getattr(request, 'user', None)
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
logger.info(smart_text(u"User {} logged in from {}".format(self.request.user.username,request.META.get('REMOTE_ADDR', None))))
|
logger.info(smart_text(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None))))
|
||||||
ret.set_cookie('userLoggedIn', 'true')
|
ret.set_cookie('userLoggedIn', 'true')
|
||||||
current_user = UserSerializer(self.request.user)
|
current_user = UserSerializer(self.request.user)
|
||||||
current_user = smart_text(JSONRenderer().render(current_user.data))
|
current_user = smart_text(JSONRenderer().render(current_user.data))
|
||||||
@@ -106,29 +101,27 @@ class LoggedLoginView(auth_views.LoginView):
|
|||||||
return ret
|
return ret
|
||||||
else:
|
else:
|
||||||
if 'username' in self.request.POST:
|
if 'username' in self.request.POST:
|
||||||
logger.warn(smart_text(u"Login failed for user {} from {}".format(self.request.POST.get('username'),request.META.get('REMOTE_ADDR', None))))
|
logger.warn(smart_text(u"Login failed for user {} from {}".format(self.request.POST.get('username'), request.META.get('REMOTE_ADDR', None))))
|
||||||
ret.status_code = 401
|
ret.status_code = 401
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class LoggedLogoutView(auth_views.LogoutView):
|
class LoggedLogoutView(auth_views.LogoutView):
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
original_user = getattr(request, 'user', None)
|
original_user = getattr(request, 'user', None)
|
||||||
ret = super(LoggedLogoutView, self).dispatch(request, *args, **kwargs)
|
ret = super(LoggedLogoutView, self).dispatch(request, *args, **kwargs)
|
||||||
current_user = getattr(request, 'user', None)
|
current_user = getattr(request, 'user', None)
|
||||||
ret.set_cookie('userLoggedIn', 'false')
|
ret.set_cookie('userLoggedIn', 'false')
|
||||||
if (not current_user or not getattr(current_user, 'pk', True)) \
|
if (not current_user or not getattr(current_user, 'pk', True)) and current_user != original_user:
|
||||||
and current_user != original_user:
|
|
||||||
logger.info("User {} logged out.".format(original_user.username))
|
logger.info("User {} logged out.".format(original_user.username))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def get_view_description(view, html=False):
|
def get_view_description(view, html=False):
|
||||||
'''Wrapper around REST framework get_view_description() to continue
|
"""Wrapper around REST framework get_view_description() to continue
|
||||||
to support our historical div.
|
to support our historical div.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
desc = views.get_view_description(view, html=html)
|
desc = views.get_view_description(view, html=html)
|
||||||
if html:
|
if html:
|
||||||
desc = '<div class="description">%s</div>' % desc
|
desc = '<div class="description">%s</div>' % desc
|
||||||
@@ -138,6 +131,7 @@ def get_view_description(view, html=False):
|
|||||||
def get_default_schema():
|
def get_default_schema():
|
||||||
if settings.SETTINGS_MODULE == 'awx.settings.development':
|
if settings.SETTINGS_MODULE == 'awx.settings.development':
|
||||||
from awx.api.swagger import AutoSchema
|
from awx.api.swagger import AutoSchema
|
||||||
|
|
||||||
return AutoSchema()
|
return AutoSchema()
|
||||||
else:
|
else:
|
||||||
return views.APIView.schema
|
return views.APIView.schema
|
||||||
@@ -149,21 +143,23 @@ class APIView(views.APIView):
|
|||||||
versioning_class = URLPathVersioning
|
versioning_class = URLPathVersioning
|
||||||
|
|
||||||
def initialize_request(self, request, *args, **kwargs):
|
def initialize_request(self, request, *args, **kwargs):
|
||||||
'''
|
"""
|
||||||
Store the Django REST Framework Request object as an attribute on the
|
Store the Django REST Framework Request object as an attribute on the
|
||||||
normal Django request, store time the request started.
|
normal Django request, store time the request started.
|
||||||
'''
|
"""
|
||||||
self.time_started = time.time()
|
self.time_started = time.time()
|
||||||
if getattr(settings, 'SQL_DEBUG', False):
|
if getattr(settings, 'SQL_DEBUG', False):
|
||||||
self.queries_before = len(connection.queries)
|
self.queries_before = len(connection.queries)
|
||||||
|
|
||||||
# If there are any custom headers in REMOTE_HOST_HEADERS, make sure
|
# If there are any custom headers in REMOTE_HOST_HEADERS, make sure
|
||||||
# they respect the allowed proxy list
|
# they respect the allowed proxy list
|
||||||
if all([
|
if all(
|
||||||
settings.PROXY_IP_ALLOWED_LIST,
|
[
|
||||||
request.environ.get('REMOTE_ADDR') not in settings.PROXY_IP_ALLOWED_LIST,
|
settings.PROXY_IP_ALLOWED_LIST,
|
||||||
request.environ.get('REMOTE_HOST') not in settings.PROXY_IP_ALLOWED_LIST
|
request.environ.get('REMOTE_ADDR') not in settings.PROXY_IP_ALLOWED_LIST,
|
||||||
]):
|
request.environ.get('REMOTE_HOST') not in settings.PROXY_IP_ALLOWED_LIST,
|
||||||
|
]
|
||||||
|
):
|
||||||
for custom_header in settings.REMOTE_HOST_HEADERS:
|
for custom_header in settings.REMOTE_HOST_HEADERS:
|
||||||
if custom_header.startswith('HTTP_'):
|
if custom_header.startswith('HTTP_'):
|
||||||
request.environ.pop(custom_header, None)
|
request.environ.pop(custom_header, None)
|
||||||
@@ -178,17 +174,19 @@ class APIView(views.APIView):
|
|||||||
request.drf_request_user = None
|
request.drf_request_user = None
|
||||||
self.__init_request_error__ = exc
|
self.__init_request_error__ = exc
|
||||||
except UnsupportedMediaType as exc:
|
except UnsupportedMediaType as exc:
|
||||||
exc.detail = _('You did not use correct Content-Type in your HTTP request. '
|
exc.detail = _(
|
||||||
'If you are using our REST API, the Content-Type must be application/json')
|
'You did not use correct Content-Type in your HTTP request. ' 'If you are using our REST API, the Content-Type must be application/json'
|
||||||
|
)
|
||||||
self.__init_request_error__ = exc
|
self.__init_request_error__ = exc
|
||||||
return drf_request
|
return drf_request
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
'''
|
"""
|
||||||
Log warning for 400 requests. Add header with elapsed time.
|
Log warning for 400 requests. Add header with elapsed time.
|
||||||
'''
|
"""
|
||||||
from awx.main.utils import get_licenser
|
from awx.main.utils import get_licenser
|
||||||
from awx.main.utils.licensing import OpenLicense
|
from awx.main.utils.licensing import OpenLicense
|
||||||
|
|
||||||
#
|
#
|
||||||
# If the URL was rewritten, and we get a 404, we should entirely
|
# If the URL was rewritten, and we get a 404, we should entirely
|
||||||
# replace the view in the request context with an ApiErrorView()
|
# replace the view in the request context with an ApiErrorView()
|
||||||
@@ -212,8 +210,12 @@ class APIView(views.APIView):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
if response.status_code >= 400:
|
if response.status_code >= 400:
|
||||||
status_msg = "status %s received by user %s attempting to access %s from %s" % \
|
status_msg = "status %s received by user %s attempting to access %s from %s" % (
|
||||||
(response.status_code, request.user, request.path, request.META.get('REMOTE_ADDR', None))
|
response.status_code,
|
||||||
|
request.user,
|
||||||
|
request.path,
|
||||||
|
request.META.get('REMOTE_ADDR', None),
|
||||||
|
)
|
||||||
if hasattr(self, '__init_request_error__'):
|
if hasattr(self, '__init_request_error__'):
|
||||||
response = self.handle_exception(self.__init_request_error__)
|
response = self.handle_exception(self.__init_request_error__)
|
||||||
if response.status_code == 401:
|
if response.status_code == 401:
|
||||||
@@ -311,18 +313,12 @@ class APIView(views.APIView):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def determine_version(self, request, *args, **kwargs):
|
def determine_version(self, request, *args, **kwargs):
|
||||||
return (
|
return (getattr(request, 'version', None), getattr(request, 'versioning_scheme', None))
|
||||||
getattr(request, 'version', None),
|
|
||||||
getattr(request, 'versioning_scheme', None),
|
|
||||||
)
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if self.versioning_class is not None:
|
if self.versioning_class is not None:
|
||||||
scheme = self.versioning_class()
|
scheme = self.versioning_class()
|
||||||
request.version, request.versioning_scheme = (
|
request.version, request.versioning_scheme = (scheme.determine_version(request, *args, **kwargs), scheme)
|
||||||
scheme.determine_version(request, *args, **kwargs),
|
|
||||||
scheme
|
|
||||||
)
|
|
||||||
if 'version' in kwargs:
|
if 'version' in kwargs:
|
||||||
kwargs.pop('version')
|
kwargs.pop('version')
|
||||||
return super(APIView, self).dispatch(request, *args, **kwargs)
|
return super(APIView, self).dispatch(request, *args, **kwargs)
|
||||||
@@ -378,25 +374,22 @@ class GenericAPIView(generics.GenericAPIView, APIView):
|
|||||||
d = super(GenericAPIView, self).get_description_context()
|
d = super(GenericAPIView, self).get_description_context()
|
||||||
if hasattr(self.model, "_meta"):
|
if hasattr(self.model, "_meta"):
|
||||||
if hasattr(self.model._meta, "verbose_name"):
|
if hasattr(self.model._meta, "verbose_name"):
|
||||||
d.update({
|
d.update(
|
||||||
'model_verbose_name': smart_text(self.model._meta.verbose_name),
|
{
|
||||||
'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
|
'model_verbose_name': smart_text(self.model._meta.verbose_name),
|
||||||
})
|
'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
|
||||||
|
}
|
||||||
|
)
|
||||||
serializer = self.get_serializer()
|
serializer = self.get_serializer()
|
||||||
metadata = self.metadata_class()
|
metadata = self.metadata_class()
|
||||||
metadata.request = self.request
|
metadata.request = self.request
|
||||||
for method, key in [
|
for method, key in [('GET', 'serializer_fields'), ('POST', 'serializer_create_fields'), ('PUT', 'serializer_update_fields')]:
|
||||||
('GET', 'serializer_fields'),
|
|
||||||
('POST', 'serializer_create_fields'),
|
|
||||||
('PUT', 'serializer_update_fields')
|
|
||||||
]:
|
|
||||||
d[key] = metadata.get_serializer_info(serializer, method=method)
|
d[key] = metadata.get_serializer_info(serializer, method=method)
|
||||||
d['settings'] = settings
|
d['settings'] = settings
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class SimpleListAPIView(generics.ListAPIView, GenericAPIView):
|
class SimpleListAPIView(generics.ListAPIView, GenericAPIView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.user.get_queryset(self.model)
|
return self.request.user.get_queryset(self.model)
|
||||||
|
|
||||||
@@ -413,9 +406,7 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
|
|||||||
else:
|
else:
|
||||||
order_field = 'name'
|
order_field = 'name'
|
||||||
d = super(ListAPIView, self).get_description_context()
|
d = super(ListAPIView, self).get_description_context()
|
||||||
d.update({
|
d.update({'order_field': order_field})
|
||||||
'order_field': order_field,
|
|
||||||
})
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -426,9 +417,13 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
|
|||||||
def related_search_fields(self):
|
def related_search_fields(self):
|
||||||
def skip_related_name(name):
|
def skip_related_name(name):
|
||||||
return (
|
return (
|
||||||
name is None or name.endswith('_role') or name.startswith('_') or
|
name is None
|
||||||
name.startswith('deprecated_') or name.endswith('_set') or
|
or name.endswith('_role')
|
||||||
name == 'polymorphic_ctype')
|
or name.startswith('_')
|
||||||
|
or name.startswith('deprecated_')
|
||||||
|
or name.endswith('_set')
|
||||||
|
or name == 'polymorphic_ctype'
|
||||||
|
)
|
||||||
|
|
||||||
fields = set([])
|
fields = set([])
|
||||||
for field in self.model._meta.fields:
|
for field in self.model._meta.fields:
|
||||||
@@ -482,9 +477,7 @@ class ParentMixin(object):
|
|||||||
def get_parent_object(self):
|
def get_parent_object(self):
|
||||||
if self.parent_object is not None:
|
if self.parent_object is not None:
|
||||||
return self.parent_object
|
return self.parent_object
|
||||||
parent_filter = {
|
parent_filter = {self.lookup_field: self.kwargs.get(self.lookup_field, None)}
|
||||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
|
||||||
}
|
|
||||||
self.parent_object = get_object_or_404(self.parent_model, **parent_filter)
|
self.parent_object = get_object_or_404(self.parent_model, **parent_filter)
|
||||||
return self.parent_object
|
return self.parent_object
|
||||||
|
|
||||||
@@ -513,10 +506,12 @@ class SubListAPIView(ParentMixin, ListAPIView):
|
|||||||
|
|
||||||
def get_description_context(self):
|
def get_description_context(self):
|
||||||
d = super(SubListAPIView, self).get_description_context()
|
d = super(SubListAPIView, self).get_description_context()
|
||||||
d.update({
|
d.update(
|
||||||
'parent_model_verbose_name': smart_text(self.parent_model._meta.verbose_name),
|
{
|
||||||
'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
|
'parent_model_verbose_name': smart_text(self.parent_model._meta.verbose_name),
|
||||||
})
|
'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
|
||||||
|
}
|
||||||
|
)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -531,7 +526,6 @@ class SubListAPIView(ParentMixin, ListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class DestroyAPIView(generics.DestroyAPIView):
|
class DestroyAPIView(generics.DestroyAPIView):
|
||||||
|
|
||||||
def has_delete_permission(self, obj):
|
def has_delete_permission(self, obj):
|
||||||
return self.request.user.can_access(self.model, 'delete', obj)
|
return self.request.user.can_access(self.model, 'delete', obj)
|
||||||
|
|
||||||
@@ -545,12 +539,12 @@ class SubListDestroyAPIView(DestroyAPIView, SubListAPIView):
|
|||||||
"""
|
"""
|
||||||
Concrete view for deleting everything related by `relationship`.
|
Concrete view for deleting everything related by `relationship`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
check_sub_obj_permission = True
|
check_sub_obj_permission = True
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
instance_list = self.get_queryset()
|
instance_list = self.get_queryset()
|
||||||
if (not self.check_sub_obj_permission and
|
if not self.check_sub_obj_permission and not request.user.can_access(self.parent_model, 'delete', self.get_parent_object()):
|
||||||
not request.user.can_access(self.parent_model, 'delete', self.get_parent_object())):
|
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
self.perform_list_destroy(instance_list)
|
self.perform_list_destroy(instance_list)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@@ -574,9 +568,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
|||||||
|
|
||||||
def get_description_context(self):
|
def get_description_context(self):
|
||||||
d = super(SubListCreateAPIView, self).get_description_context()
|
d = super(SubListCreateAPIView, self).get_description_context()
|
||||||
d.update({
|
d.update({'parent_key': getattr(self, 'parent_key', None)})
|
||||||
'parent_key': getattr(self, 'parent_key', None),
|
|
||||||
})
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -610,8 +602,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
|||||||
# attempt to deserialize the object
|
# attempt to deserialize the object
|
||||||
serializer = self.get_serializer(data=data)
|
serializer = self.get_serializer(data=data)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(serializer.errors,
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# Verify we have permission to add the object as given.
|
# Verify we have permission to add the object as given.
|
||||||
if not request.user.can_access(self.model, 'add', serializer.validated_data):
|
if not request.user.can_access(self.model, 'add', serializer.validated_data):
|
||||||
@@ -635,9 +626,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
|||||||
|
|
||||||
def get_description_context(self):
|
def get_description_context(self):
|
||||||
d = super(SubListCreateAttachDetachAPIView, self).get_description_context()
|
d = super(SubListCreateAttachDetachAPIView, self).get_description_context()
|
||||||
d.update({
|
d.update({"has_attach": True})
|
||||||
"has_attach": True,
|
|
||||||
})
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def attach_validate(self, request):
|
def attach_validate(self, request):
|
||||||
@@ -675,9 +664,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
|||||||
sub = get_object_or_400(self.model, pk=sub_id)
|
sub = get_object_or_400(self.model, pk=sub_id)
|
||||||
|
|
||||||
# Verify we have permission to attach.
|
# Verify we have permission to attach.
|
||||||
if not request.user.can_access(self.parent_model, 'attach', parent, sub,
|
if not request.user.can_access(self.parent_model, 'attach', parent, sub, self.relationship, data, skip_sub_obj_read_check=created):
|
||||||
self.relationship, data,
|
|
||||||
skip_sub_obj_read_check=created):
|
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
# Verify that the relationship to be added is valid.
|
# Verify that the relationship to be added is valid.
|
||||||
@@ -716,8 +703,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
|||||||
relationship = getattrd(parent, self.relationship)
|
relationship = getattrd(parent, self.relationship)
|
||||||
sub = get_object_or_400(self.model, pk=sub_id)
|
sub = get_object_or_400(self.model, pk=sub_id)
|
||||||
|
|
||||||
if not request.user.can_access(self.parent_model, 'unattach', parent,
|
if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship, request.data):
|
||||||
sub, self.relationship, request.data):
|
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
if parent_key:
|
if parent_key:
|
||||||
@@ -735,28 +721,24 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
return Response('invalid type for post data',
|
return Response('invalid type for post data', status=status.HTTP_400_BAD_REQUEST)
|
||||||
status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
if 'disassociate' in request.data:
|
if 'disassociate' in request.data:
|
||||||
return self.unattach(request, *args, **kwargs)
|
return self.unattach(request, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return self.attach(request, *args, **kwargs)
|
return self.attach(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
|
class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
|
||||||
'''
|
"""
|
||||||
Derived version of SubListCreateAttachDetachAPIView that prohibits creation
|
Derived version of SubListCreateAttachDetachAPIView that prohibits creation
|
||||||
'''
|
"""
|
||||||
|
|
||||||
metadata_class = SublistAttachDetatchMetadata
|
metadata_class = SublistAttachDetatchMetadata
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
sub_id = request.data.get('id', None)
|
sub_id = request.data.get('id', None)
|
||||||
if not sub_id:
|
if not sub_id:
|
||||||
return Response(
|
return Response(dict(msg=_("{} 'id' field is missing.".format(self.model._meta.verbose_name.title()))), status=status.HTTP_400_BAD_REQUEST)
|
||||||
dict(msg=_("{} 'id' field is missing.".format(
|
|
||||||
self.model._meta.verbose_name.title()))),
|
|
||||||
status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
return super(SubListAttachDetachAPIView, self).post(request, *args, **kwargs)
|
return super(SubListAttachDetachAPIView, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
def update_raw_data(self, data):
|
def update_raw_data(self, data):
|
||||||
@@ -768,11 +750,11 @@ class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class DeleteLastUnattachLabelMixin(object):
|
class DeleteLastUnattachLabelMixin(object):
|
||||||
'''
|
"""
|
||||||
Models for which you want the last instance to be deleted from the database
|
Models for which you want the last instance to be deleted from the database
|
||||||
when the last disassociate is called should inherit from this class. Further,
|
when the last disassociate is called should inherit from this class. Further,
|
||||||
the model should implement is_detached()
|
the model should implement is_detached()
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def unattach(self, request, *args, **kwargs):
|
def unattach(self, request, *args, **kwargs):
|
||||||
(sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request)
|
(sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request)
|
||||||
@@ -798,7 +780,6 @@ class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
|
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
self.update_filter(request, *args, **kwargs)
|
self.update_filter(request, *args, **kwargs)
|
||||||
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)
|
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)
|
||||||
@@ -839,6 +820,7 @@ class ResourceAccessList(ParentMixin, ListAPIView):
|
|||||||
|
|
||||||
def trigger_delayed_deep_copy(*args, **kwargs):
|
def trigger_delayed_deep_copy(*args, **kwargs):
|
||||||
from awx.main.tasks import deep_copy_model_obj
|
from awx.main.tasks import deep_copy_model_obj
|
||||||
|
|
||||||
connection.on_commit(lambda: deep_copy_model_obj.delay(*args, **kwargs))
|
connection.on_commit(lambda: deep_copy_model_obj.delay(*args, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
@@ -869,8 +851,7 @@ class CopyAPIView(GenericAPIView):
|
|||||||
field_val[secret] = decrypt_field(obj, secret)
|
field_val[secret] = decrypt_field(obj, secret)
|
||||||
elif isinstance(field_val, dict):
|
elif isinstance(field_val, dict):
|
||||||
for sub_field in field_val:
|
for sub_field in field_val:
|
||||||
if isinstance(sub_field, str) \
|
if isinstance(sub_field, str) and isinstance(field_val[sub_field], str):
|
||||||
and isinstance(field_val[sub_field], str):
|
|
||||||
field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
|
field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
|
||||||
elif isinstance(field_val, str):
|
elif isinstance(field_val, str):
|
||||||
try:
|
try:
|
||||||
@@ -882,15 +863,11 @@ class CopyAPIView(GenericAPIView):
|
|||||||
def _build_create_dict(self, obj):
|
def _build_create_dict(self, obj):
|
||||||
ret = {}
|
ret = {}
|
||||||
if self.copy_return_serializer_class:
|
if self.copy_return_serializer_class:
|
||||||
all_fields = Metadata().get_serializer_info(
|
all_fields = Metadata().get_serializer_info(self._get_copy_return_serializer(), method='POST')
|
||||||
self._get_copy_return_serializer(), method='POST'
|
|
||||||
)
|
|
||||||
for field_name, field_info in all_fields.items():
|
for field_name, field_info in all_fields.items():
|
||||||
if not hasattr(obj, field_name) or field_info.get('read_only', True):
|
if not hasattr(obj, field_name) or field_info.get('read_only', True):
|
||||||
continue
|
continue
|
||||||
ret[field_name] = CopyAPIView._decrypt_model_field_if_needed(
|
ret[field_name] = CopyAPIView._decrypt_model_field_if_needed(obj, field_name, getattr(obj, field_name))
|
||||||
obj, field_name, getattr(obj, field_name)
|
|
||||||
)
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -908,9 +885,11 @@ class CopyAPIView(GenericAPIView):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
continue
|
continue
|
||||||
# Adjust copy blocked fields here.
|
# Adjust copy blocked fields here.
|
||||||
if field.name in fields_to_discard or field.name in [
|
if (
|
||||||
'id', 'pk', 'polymorphic_ctype', 'unifiedjobtemplate_ptr', 'created_by', 'modified_by'
|
field.name in fields_to_discard
|
||||||
] or field.name.endswith('_role'):
|
or field.name in ['id', 'pk', 'polymorphic_ctype', 'unifiedjobtemplate_ptr', 'created_by', 'modified_by']
|
||||||
|
or field.name.endswith('_role')
|
||||||
|
):
|
||||||
create_kwargs.pop(field.name, None)
|
create_kwargs.pop(field.name, None)
|
||||||
continue
|
continue
|
||||||
if field.one_to_many:
|
if field.one_to_many:
|
||||||
@@ -926,33 +905,24 @@ class CopyAPIView(GenericAPIView):
|
|||||||
elif field.name == 'name' and not old_parent:
|
elif field.name == 'name' and not old_parent:
|
||||||
create_kwargs[field.name] = copy_name or field_val + ' copy'
|
create_kwargs[field.name] = copy_name or field_val + ' copy'
|
||||||
elif field.name in fields_to_preserve:
|
elif field.name in fields_to_preserve:
|
||||||
create_kwargs[field.name] = CopyAPIView._decrypt_model_field_if_needed(
|
create_kwargs[field.name] = CopyAPIView._decrypt_model_field_if_needed(obj, field.name, field_val)
|
||||||
obj, field.name, field_val
|
|
||||||
)
|
|
||||||
|
|
||||||
# WorkflowJobTemplateNodes that represent an approval are *special*;
|
# WorkflowJobTemplateNodes that represent an approval are *special*;
|
||||||
# when we copy them, we actually want to *copy* the UJT they point at
|
# when we copy them, we actually want to *copy* the UJT they point at
|
||||||
# rather than share the template reference between nodes in disparate
|
# rather than share the template reference between nodes in disparate
|
||||||
# workflows
|
# workflows
|
||||||
if (
|
if isinstance(obj, WorkflowJobTemplateNode) and isinstance(getattr(obj, 'unified_job_template'), WorkflowApprovalTemplate):
|
||||||
isinstance(obj, WorkflowJobTemplateNode) and
|
new_approval_template, sub_objs = CopyAPIView.copy_model_obj(None, None, WorkflowApprovalTemplate, obj.unified_job_template, creater)
|
||||||
isinstance(getattr(obj, 'unified_job_template'), WorkflowApprovalTemplate)
|
|
||||||
):
|
|
||||||
new_approval_template, sub_objs = CopyAPIView.copy_model_obj(
|
|
||||||
None, None, WorkflowApprovalTemplate,
|
|
||||||
obj.unified_job_template, creater
|
|
||||||
)
|
|
||||||
create_kwargs['unified_job_template'] = new_approval_template
|
create_kwargs['unified_job_template'] = new_approval_template
|
||||||
|
|
||||||
new_obj = model.objects.create(**create_kwargs)
|
new_obj = model.objects.create(**create_kwargs)
|
||||||
logger.debug('Deep copy: Created new object {}({})'.format(
|
logger.debug('Deep copy: Created new object {}({})'.format(new_obj, model))
|
||||||
new_obj, model
|
|
||||||
))
|
|
||||||
# Need to save separatedly because Djang-crum get_current_user would
|
# Need to save separatedly because Djang-crum get_current_user would
|
||||||
# not work properly in non-request-response-cycle context.
|
# not work properly in non-request-response-cycle context.
|
||||||
new_obj.created_by = creater
|
new_obj.created_by = creater
|
||||||
new_obj.save()
|
new_obj.save()
|
||||||
from awx.main.signals import disable_activity_stream
|
from awx.main.signals import disable_activity_stream
|
||||||
|
|
||||||
with disable_activity_stream():
|
with disable_activity_stream():
|
||||||
for m2m in m2m_to_preserve:
|
for m2m in m2m_to_preserve:
|
||||||
for related_obj in m2m_to_preserve[m2m].all():
|
for related_obj in m2m_to_preserve[m2m].all():
|
||||||
@@ -978,8 +948,7 @@ class CopyAPIView(GenericAPIView):
|
|||||||
for key in create_kwargs:
|
for key in create_kwargs:
|
||||||
create_kwargs[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key]
|
create_kwargs[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key]
|
||||||
try:
|
try:
|
||||||
can_copy = request.user.can_access(self.model, 'add', create_kwargs) and \
|
can_copy = request.user.can_access(self.model, 'add', create_kwargs) and request.user.can_access(self.model, 'copy_related', obj)
|
||||||
request.user.can_access(self.model, 'copy_related', obj)
|
|
||||||
except PermissionDenied:
|
except PermissionDenied:
|
||||||
return Response({'can_copy': False})
|
return Response({'can_copy': False})
|
||||||
return Response({'can_copy': can_copy})
|
return Response({'can_copy': can_copy})
|
||||||
@@ -998,8 +967,7 @@ class CopyAPIView(GenericAPIView):
|
|||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
new_obj, sub_objs = CopyAPIView.copy_model_obj(
|
new_obj, sub_objs = CopyAPIView.copy_model_obj(
|
||||||
None, None, self.model, obj, request.user, create_kwargs=create_kwargs,
|
None, None, self.model, obj, request.user, create_kwargs=create_kwargs, copy_name=serializer.validated_data.get('name', '')
|
||||||
copy_name=serializer.validated_data.get('name', '')
|
|
||||||
)
|
)
|
||||||
if hasattr(new_obj, 'admin_role') and request.user not in new_obj.admin_role.members.all():
|
if hasattr(new_obj, 'admin_role') and request.user not in new_obj.admin_role.members.all():
|
||||||
new_obj.admin_role.members.add(request.user)
|
new_obj.admin_role.members.add(request.user)
|
||||||
@@ -1011,13 +979,9 @@ class CopyAPIView(GenericAPIView):
|
|||||||
cache.set(key, sub_objs, timeout=3600)
|
cache.set(key, sub_objs, timeout=3600)
|
||||||
permission_check_func = None
|
permission_check_func = None
|
||||||
if hasattr(type(self), 'deep_copy_permission_check_func'):
|
if hasattr(type(self), 'deep_copy_permission_check_func'):
|
||||||
permission_check_func = (
|
permission_check_func = (type(self).__module__, type(self).__name__, 'deep_copy_permission_check_func')
|
||||||
type(self).__module__, type(self).__name__, 'deep_copy_permission_check_func'
|
|
||||||
)
|
|
||||||
trigger_delayed_deep_copy(
|
trigger_delayed_deep_copy(
|
||||||
self.model.__module__, self.model.__name__,
|
self.model.__module__, self.model.__name__, obj.pk, new_obj.pk, request.user.pk, key, permission_check_func=permission_check_func
|
||||||
obj.pk, new_obj.pk, request.user.pk, key,
|
|
||||||
permission_check_func=permission_check_func
|
|
||||||
)
|
)
|
||||||
serializer = self._get_copy_return_serializer(new_obj)
|
serializer = self._get_copy_return_serializer(new_obj)
|
||||||
headers = {'Location': new_obj.get_absolute_url(request=request)}
|
headers = {'Location': new_obj.get_absolute_url(request=request)}
|
||||||
@@ -1026,7 +990,7 @@ class CopyAPIView(GenericAPIView):
|
|||||||
|
|
||||||
class BaseUsersList(SubListCreateAttachDetachAPIView):
|
class BaseUsersList(SubListCreateAttachDetachAPIView):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
ret = super(BaseUsersList, self).post( request, *args, **kwargs)
|
ret = super(BaseUsersList, self).post(request, *args, **kwargs)
|
||||||
if ret.status_code != 201:
|
if ret.status_code != 201:
|
||||||
return ret
|
return ret
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -28,18 +28,23 @@ from awx.main.tasks import AWXReceptorJob
|
|||||||
|
|
||||||
|
|
||||||
class Metadata(metadata.SimpleMetadata):
|
class Metadata(metadata.SimpleMetadata):
|
||||||
|
|
||||||
def get_field_info(self, field):
|
def get_field_info(self, field):
|
||||||
field_info = OrderedDict()
|
field_info = OrderedDict()
|
||||||
field_info['type'] = self.label_lookup[field]
|
field_info['type'] = self.label_lookup[field]
|
||||||
field_info['required'] = getattr(field, 'required', False)
|
field_info['required'] = getattr(field, 'required', False)
|
||||||
|
|
||||||
text_attrs = [
|
text_attrs = [
|
||||||
'read_only', 'label', 'help_text',
|
'read_only',
|
||||||
'min_length', 'max_length',
|
'label',
|
||||||
'min_value', 'max_value',
|
'help_text',
|
||||||
'category', 'category_slug',
|
'min_length',
|
||||||
'defined_in_file', 'unit',
|
'max_length',
|
||||||
|
'min_value',
|
||||||
|
'max_value',
|
||||||
|
'category',
|
||||||
|
'category_slug',
|
||||||
|
'defined_in_file',
|
||||||
|
'unit',
|
||||||
]
|
]
|
||||||
|
|
||||||
for attr in text_attrs:
|
for attr in text_attrs:
|
||||||
@@ -61,8 +66,9 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
'type': _('Data type for this {}.'),
|
'type': _('Data type for this {}.'),
|
||||||
'url': _('URL for this {}.'),
|
'url': _('URL for this {}.'),
|
||||||
'related': _('Data structure with URLs of related resources.'),
|
'related': _('Data structure with URLs of related resources.'),
|
||||||
'summary_fields': _('Data structure with name/description for related resources. '
|
'summary_fields': _(
|
||||||
'The output for some objects may be limited for performance reasons.'),
|
'Data structure with name/description for related resources. ' 'The output for some objects may be limited for performance reasons.'
|
||||||
|
),
|
||||||
'created': _('Timestamp when this {} was created.'),
|
'created': _('Timestamp when this {} was created.'),
|
||||||
'modified': _('Timestamp when this {} was last modified.'),
|
'modified': _('Timestamp when this {} was last modified.'),
|
||||||
}
|
}
|
||||||
@@ -101,9 +107,7 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
field_info['children'] = self.get_serializer_info(field)
|
field_info['children'] = self.get_serializer_info(field)
|
||||||
|
|
||||||
if not isinstance(field, (RelatedField, ManyRelatedField)) and hasattr(field, 'choices'):
|
if not isinstance(field, (RelatedField, ManyRelatedField)) and hasattr(field, 'choices'):
|
||||||
choices = [
|
choices = [(choice_value, choice_name) for choice_value, choice_name in field.choices.items()]
|
||||||
(choice_value, choice_name) for choice_value, choice_name in field.choices.items()
|
|
||||||
]
|
|
||||||
if not any(choice in ('', None) for choice, _ in choices):
|
if not any(choice in ('', None) for choice, _ in choices):
|
||||||
if field.allow_blank:
|
if field.allow_blank:
|
||||||
choices = [("", "---------")] + choices
|
choices = [("", "---------")] + choices
|
||||||
@@ -131,7 +135,6 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
for (notification_type_name, notification_tr_name, notification_type_class) in NotificationTemplate.NOTIFICATION_TYPES:
|
for (notification_type_name, notification_tr_name, notification_type_class) in NotificationTemplate.NOTIFICATION_TYPES:
|
||||||
field_info[notification_type_name] = notification_type_class.default_messages
|
field_info[notification_type_name] = notification_type_class.default_messages
|
||||||
|
|
||||||
|
|
||||||
# Update type of fields returned...
|
# Update type of fields returned...
|
||||||
model_field = None
|
model_field = None
|
||||||
if serializer and hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'):
|
if serializer and hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'):
|
||||||
@@ -149,22 +152,19 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
field_info['type'] = 'integer'
|
field_info['type'] = 'integer'
|
||||||
elif field.field_name in ('created', 'modified'):
|
elif field.field_name in ('created', 'modified'):
|
||||||
field_info['type'] = 'datetime'
|
field_info['type'] = 'datetime'
|
||||||
elif (
|
elif RelatedField in field.__class__.__bases__ or isinstance(model_field, ForeignKey):
|
||||||
RelatedField in field.__class__.__bases__ or
|
|
||||||
isinstance(model_field, ForeignKey)
|
|
||||||
):
|
|
||||||
field_info['type'] = 'id'
|
field_info['type'] = 'id'
|
||||||
elif (
|
elif (
|
||||||
isinstance(field, JSONField) or
|
isinstance(field, JSONField)
|
||||||
isinstance(model_field, JSONField) or
|
or isinstance(model_field, JSONField)
|
||||||
isinstance(field, DRFJSONField) or
|
or isinstance(field, DRFJSONField)
|
||||||
isinstance(getattr(field, 'model_field', None), JSONField) or
|
or isinstance(getattr(field, 'model_field', None), JSONField)
|
||||||
field.field_name == 'credential_passwords'
|
or field.field_name == 'credential_passwords'
|
||||||
):
|
):
|
||||||
field_info['type'] = 'json'
|
field_info['type'] = 'json'
|
||||||
elif (
|
elif (
|
||||||
isinstance(field, ManyRelatedField) and
|
isinstance(field, ManyRelatedField)
|
||||||
field.field_name == 'credentials'
|
and field.field_name == 'credentials'
|
||||||
# launch-time credentials
|
# launch-time credentials
|
||||||
):
|
):
|
||||||
field_info['type'] = 'list_of_ids'
|
field_info['type'] = 'list_of_ids'
|
||||||
@@ -175,10 +175,7 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
|
|
||||||
def get_serializer_info(self, serializer, method=None):
|
def get_serializer_info(self, serializer, method=None):
|
||||||
filterer = getattr(serializer, 'filter_field_metadata', lambda fields, method: fields)
|
filterer = getattr(serializer, 'filter_field_metadata', lambda fields, method: fields)
|
||||||
return filterer(
|
return filterer(super(Metadata, self).get_serializer_info(serializer), method)
|
||||||
super(Metadata, self).get_serializer_info(serializer),
|
|
||||||
method
|
|
||||||
)
|
|
||||||
|
|
||||||
def determine_actions(self, request, view):
|
def determine_actions(self, request, view):
|
||||||
# Add field information for GET requests (so field names/labels are
|
# Add field information for GET requests (so field names/labels are
|
||||||
@@ -274,6 +271,7 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
metadata['object_roles'] = roles
|
metadata['object_roles'] = roles
|
||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
if isinstance(view, generics.ListAPIView) and hasattr(view, 'paginator'):
|
if isinstance(view, generics.ListAPIView) and hasattr(view, 'paginator'):
|
||||||
metadata['max_page_size'] = view.paginator.max_page_size
|
metadata['max_page_size'] = view.paginator.max_page_size
|
||||||
|
|
||||||
@@ -293,7 +291,6 @@ class RoleMetadata(Metadata):
|
|||||||
|
|
||||||
|
|
||||||
class SublistAttachDetatchMetadata(Metadata):
|
class SublistAttachDetatchMetadata(Metadata):
|
||||||
|
|
||||||
def determine_actions(self, request, view):
|
def determine_actions(self, request, view):
|
||||||
actions = super(SublistAttachDetatchMetadata, self).determine_actions(request, view)
|
actions = super(SublistAttachDetatchMetadata, self).determine_actions(request, view)
|
||||||
method = 'POST'
|
method = 'POST'
|
||||||
|
|||||||
@@ -3,13 +3,9 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import MetricsView
|
||||||
MetricsView
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [url(r'^$', MetricsView.as_view(), name='metrics_view')]
|
||||||
url(r'^$', MetricsView.as_view(), name='metrics_view'),
|
|
||||||
]
|
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from rest_framework.utils.urls import replace_query_param
|
|||||||
|
|
||||||
|
|
||||||
class DisabledPaginator(DjangoPaginator):
|
class DisabledPaginator(DjangoPaginator):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def num_pages(self):
|
def num_pages(self):
|
||||||
return 1
|
return 1
|
||||||
@@ -49,8 +48,7 @@ class Pagination(pagination.PageNumberPagination):
|
|||||||
|
|
||||||
def get_html_context(self):
|
def get_html_context(self):
|
||||||
context = super().get_html_context()
|
context = super().get_html_context()
|
||||||
context['page_links'] = [pl._replace(url=self.cap_page_size(pl.url))
|
context['page_links'] = [pl._replace(url=self.cap_page_size(pl.url)) for pl in context['page_links']]
|
||||||
for pl in context['page_links']]
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,25 @@ from awx.main.utils import get_object_or_400
|
|||||||
|
|
||||||
logger = logging.getLogger('awx.api.permissions')
|
logger = logging.getLogger('awx.api.permissions')
|
||||||
|
|
||||||
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', 'VariableDataPermission',
|
__all__ = [
|
||||||
'TaskPermission', 'ProjectUpdatePermission', 'InventoryInventorySourcesUpdatePermission',
|
'ModelAccessPermission',
|
||||||
'UserPermission', 'IsSuperUser', 'InstanceGroupTowerPermission', 'WorkflowApprovalPermission']
|
'JobTemplateCallbackPermission',
|
||||||
|
'VariableDataPermission',
|
||||||
|
'TaskPermission',
|
||||||
|
'ProjectUpdatePermission',
|
||||||
|
'InventoryInventorySourcesUpdatePermission',
|
||||||
|
'UserPermission',
|
||||||
|
'IsSuperUser',
|
||||||
|
'InstanceGroupTowerPermission',
|
||||||
|
'WorkflowApprovalPermission',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ModelAccessPermission(permissions.BasePermission):
|
class ModelAccessPermission(permissions.BasePermission):
|
||||||
'''
|
"""
|
||||||
Default permissions class to check user access based on the model and
|
Default permissions class to check user access based on the model and
|
||||||
request method, optionally verifying the request data.
|
request method, optionally verifying the request data.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def check_options_permissions(self, request, view, obj=None):
|
def check_options_permissions(self, request, view, obj=None):
|
||||||
return self.check_get_permissions(request, view, obj)
|
return self.check_get_permissions(request, view, obj)
|
||||||
@@ -35,8 +44,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
|||||||
def check_get_permissions(self, request, view, obj=None):
|
def check_get_permissions(self, request, view, obj=None):
|
||||||
if hasattr(view, 'parent_model'):
|
if hasattr(view, 'parent_model'):
|
||||||
parent_obj = view.get_parent_object()
|
parent_obj = view.get_parent_object()
|
||||||
if not check_user_access(request.user, view.parent_model, 'read',
|
if not check_user_access(request.user, view.parent_model, 'read', parent_obj):
|
||||||
parent_obj):
|
|
||||||
return False
|
return False
|
||||||
if not obj:
|
if not obj:
|
||||||
return True
|
return True
|
||||||
@@ -45,8 +53,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
|||||||
def check_post_permissions(self, request, view, obj=None):
|
def check_post_permissions(self, request, view, obj=None):
|
||||||
if hasattr(view, 'parent_model'):
|
if hasattr(view, 'parent_model'):
|
||||||
parent_obj = view.get_parent_object()
|
parent_obj = view.get_parent_object()
|
||||||
if not check_user_access(request.user, view.parent_model, 'read',
|
if not check_user_access(request.user, view.parent_model, 'read', parent_obj):
|
||||||
parent_obj):
|
|
||||||
return False
|
return False
|
||||||
if hasattr(view, 'parent_key'):
|
if hasattr(view, 'parent_key'):
|
||||||
if not check_user_access(request.user, view.model, 'add', {view.parent_key: parent_obj}):
|
if not check_user_access(request.user, view.model, 'add', {view.parent_key: parent_obj}):
|
||||||
@@ -60,10 +67,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
|||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
if view.obj_permission_type == 'admin':
|
if view.obj_permission_type == 'admin':
|
||||||
extra_kwargs['data'] = {}
|
extra_kwargs['data'] = {}
|
||||||
return check_user_access(
|
return check_user_access(request.user, view.model, view.obj_permission_type, obj, **extra_kwargs)
|
||||||
request.user, view.model, view.obj_permission_type, obj,
|
|
||||||
**extra_kwargs
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if obj:
|
if obj:
|
||||||
return True
|
return True
|
||||||
@@ -74,8 +78,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
|||||||
# FIXME: For some reason this needs to return True
|
# FIXME: For some reason this needs to return True
|
||||||
# because it is first called with obj=None?
|
# because it is first called with obj=None?
|
||||||
return True
|
return True
|
||||||
return check_user_access(request.user, view.model, 'change', obj,
|
return check_user_access(request.user, view.model, 'change', obj, request.data)
|
||||||
request.data)
|
|
||||||
|
|
||||||
def check_patch_permissions(self, request, view, obj=None):
|
def check_patch_permissions(self, request, view, obj=None):
|
||||||
return self.check_put_permissions(request, view, obj)
|
return self.check_put_permissions(request, view, obj)
|
||||||
@@ -89,10 +92,10 @@ class ModelAccessPermission(permissions.BasePermission):
|
|||||||
return check_user_access(request.user, view.model, 'delete', obj)
|
return check_user_access(request.user, view.model, 'delete', obj)
|
||||||
|
|
||||||
def check_permissions(self, request, view, obj=None):
|
def check_permissions(self, request, view, obj=None):
|
||||||
'''
|
"""
|
||||||
Perform basic permissions checking before delegating to the appropriate
|
Perform basic permissions checking before delegating to the appropriate
|
||||||
method based on the request method.
|
method based on the request method.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# Don't allow anonymous users. 401, not 403, hence no raised exception.
|
# Don't allow anonymous users. 401, not 403, hence no raised exception.
|
||||||
if not request.user or request.user.is_anonymous:
|
if not request.user or request.user.is_anonymous:
|
||||||
@@ -117,9 +120,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def has_permission(self, request, view, obj=None):
|
def has_permission(self, request, view, obj=None):
|
||||||
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)',
|
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)', request.user, request.method, request.data, view.__class__.__name__, obj)
|
||||||
request.user, request.method, request.data,
|
|
||||||
view.__class__.__name__, obj)
|
|
||||||
try:
|
try:
|
||||||
response = self.check_permissions(request, view, obj)
|
response = self.check_permissions(request, view, obj)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -134,10 +135,10 @@ class ModelAccessPermission(permissions.BasePermission):
|
|||||||
|
|
||||||
|
|
||||||
class JobTemplateCallbackPermission(ModelAccessPermission):
|
class JobTemplateCallbackPermission(ModelAccessPermission):
|
||||||
'''
|
"""
|
||||||
Permission check used by job template callback view for requests from
|
Permission check used by job template callback view for requests from
|
||||||
empheral hosts.
|
empheral hosts.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def has_permission(self, request, view, obj=None):
|
def has_permission(self, request, view, obj=None):
|
||||||
# If another authentication method was used and it's not a POST, return
|
# If another authentication method was used and it's not a POST, return
|
||||||
@@ -160,18 +161,16 @@ class JobTemplateCallbackPermission(ModelAccessPermission):
|
|||||||
|
|
||||||
|
|
||||||
class VariableDataPermission(ModelAccessPermission):
|
class VariableDataPermission(ModelAccessPermission):
|
||||||
|
|
||||||
def check_put_permissions(self, request, view, obj=None):
|
def check_put_permissions(self, request, view, obj=None):
|
||||||
if not obj:
|
if not obj:
|
||||||
return True
|
return True
|
||||||
return check_user_access(request.user, view.model, 'change', obj,
|
return check_user_access(request.user, view.model, 'change', obj, dict(variables=request.data))
|
||||||
dict(variables=request.data))
|
|
||||||
|
|
||||||
|
|
||||||
class TaskPermission(ModelAccessPermission):
|
class TaskPermission(ModelAccessPermission):
|
||||||
'''
|
"""
|
||||||
Permission checks used for API callbacks from running a task.
|
Permission checks used for API callbacks from running a task.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def has_permission(self, request, view, obj=None):
|
def has_permission(self, request, view, obj=None):
|
||||||
# If another authentication method was used other than the one for
|
# If another authentication method was used other than the one for
|
||||||
@@ -182,8 +181,7 @@ class TaskPermission(ModelAccessPermission):
|
|||||||
# Verify that the ID present in the auth token is for a valid, active
|
# Verify that the ID present in the auth token is for a valid, active
|
||||||
# unified job.
|
# unified job.
|
||||||
try:
|
try:
|
||||||
unified_job = UnifiedJob.objects.get(status='running',
|
unified_job = UnifiedJob.objects.get(status='running', pk=int(request.auth.split('-')[0]))
|
||||||
pk=int(request.auth.split('-')[0]))
|
|
||||||
except (UnifiedJob.DoesNotExist, TypeError):
|
except (UnifiedJob.DoesNotExist, TypeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -197,10 +195,10 @@ class TaskPermission(ModelAccessPermission):
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowApprovalPermission(ModelAccessPermission):
|
class WorkflowApprovalPermission(ModelAccessPermission):
|
||||||
'''
|
"""
|
||||||
Permission check used by workflow `approval` and `deny` views to determine
|
Permission check used by workflow `approval` and `deny` views to determine
|
||||||
who has access to approve and deny paused workflow nodes
|
who has access to approve and deny paused workflow nodes
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def check_post_permissions(self, request, view, obj=None):
|
def check_post_permissions(self, request, view, obj=None):
|
||||||
approval = get_object_or_400(view.model, pk=view.kwargs['pk'])
|
approval = get_object_or_400(view.model, pk=view.kwargs['pk'])
|
||||||
@@ -208,9 +206,10 @@ class WorkflowApprovalPermission(ModelAccessPermission):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectUpdatePermission(ModelAccessPermission):
|
class ProjectUpdatePermission(ModelAccessPermission):
|
||||||
'''
|
"""
|
||||||
Permission check used by ProjectUpdateView to determine who can update projects
|
Permission check used by ProjectUpdateView to determine who can update projects
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def check_get_permissions(self, request, view, obj=None):
|
def check_get_permissions(self, request, view, obj=None):
|
||||||
project = get_object_or_400(view.model, pk=view.kwargs['pk'])
|
project = get_object_or_400(view.model, pk=view.kwargs['pk'])
|
||||||
return check_user_access(request.user, view.model, 'read', project)
|
return check_user_access(request.user, view.model, 'read', project)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from rest_framework.utils import encoders
|
|||||||
|
|
||||||
|
|
||||||
class SurrogateEncoder(encoders.JSONEncoder):
|
class SurrogateEncoder(encoders.JSONEncoder):
|
||||||
|
|
||||||
def encode(self, obj):
|
def encode(self, obj):
|
||||||
ret = super(SurrogateEncoder, self).encode(obj)
|
ret = super(SurrogateEncoder, self).encode(obj)
|
||||||
try:
|
try:
|
||||||
@@ -28,9 +27,9 @@ class DefaultJSONRenderer(renderers.JSONRenderer):
|
|||||||
|
|
||||||
|
|
||||||
class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
|
class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
|
||||||
'''
|
"""
|
||||||
Customizations to the default browsable API renderer.
|
Customizations to the default browsable API renderer.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def get_default_renderer(self, view):
|
def get_default_renderer(self, view):
|
||||||
renderer = super(BrowsableAPIRenderer, self).get_default_renderer(view)
|
renderer = super(BrowsableAPIRenderer, self).get_default_renderer(view)
|
||||||
@@ -48,9 +47,7 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
|
|||||||
# see: https://github.com/ansible/awx/issues/3108
|
# see: https://github.com/ansible/awx/issues/3108
|
||||||
# https://code.djangoproject.com/ticket/28121
|
# https://code.djangoproject.com/ticket/28121
|
||||||
return data
|
return data
|
||||||
return super(BrowsableAPIRenderer, self).get_content(renderer, data,
|
return super(BrowsableAPIRenderer, self).get_content(renderer, data, accepted_media_type, renderer_context)
|
||||||
accepted_media_type,
|
|
||||||
renderer_context)
|
|
||||||
|
|
||||||
def get_context(self, data, accepted_media_type, renderer_context):
|
def get_context(self, data, accepted_media_type, renderer_context):
|
||||||
# Store the associated response status to know how to populate the raw
|
# Store the associated response status to know how to populate the raw
|
||||||
@@ -125,18 +122,13 @@ class AnsiDownloadRenderer(PlainTextRenderer):
|
|||||||
|
|
||||||
|
|
||||||
class PrometheusJSONRenderer(renderers.JSONRenderer):
|
class PrometheusJSONRenderer(renderers.JSONRenderer):
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
# HTTP errors are {'detail': ErrorDetail(string='...', code=...)}
|
# HTTP errors are {'detail': ErrorDetail(string='...', code=...)}
|
||||||
return super(PrometheusJSONRenderer, self).render(
|
return super(PrometheusJSONRenderer, self).render(data, accepted_media_type, renderer_context)
|
||||||
data, accepted_media_type, renderer_context
|
|
||||||
)
|
|
||||||
parsed_metrics = text_string_to_metric_families(data)
|
parsed_metrics = text_string_to_metric_families(data)
|
||||||
data = {}
|
data = {}
|
||||||
for family in parsed_metrics:
|
for family in parsed_metrics:
|
||||||
for sample in family.samples:
|
for sample in family.samples:
|
||||||
data[sample[0]] = {"labels": sample[1], "value": sample[2]}
|
data[sample[0]] = {"labels": sample[1], "value": sample[2]}
|
||||||
return super(PrometheusJSONRenderer, self).render(
|
return super(PrometheusJSONRenderer, self).render(data, accepted_media_type, renderer_context)
|
||||||
data, accepted_media_type, renderer_context
|
|
||||||
)
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@ from rest_framework_swagger import renderers
|
|||||||
|
|
||||||
|
|
||||||
class SuperUserSchemaGenerator(SchemaGenerator):
|
class SuperUserSchemaGenerator(SchemaGenerator):
|
||||||
|
|
||||||
def has_view_permissions(self, path, method, view):
|
def has_view_permissions(self, path, method, view):
|
||||||
#
|
#
|
||||||
# Generate the Swagger schema as if you were a superuser and
|
# Generate the Swagger schema as if you were a superuser and
|
||||||
@@ -25,17 +24,17 @@ class SuperUserSchemaGenerator(SchemaGenerator):
|
|||||||
|
|
||||||
|
|
||||||
class AutoSchema(DRFAuthSchema):
|
class AutoSchema(DRFAuthSchema):
|
||||||
|
|
||||||
def get_link(self, path, method, base_url):
|
def get_link(self, path, method, base_url):
|
||||||
link = super(AutoSchema, self).get_link(path, method, base_url)
|
link = super(AutoSchema, self).get_link(path, method, base_url)
|
||||||
try:
|
try:
|
||||||
serializer = self.view.get_serializer()
|
serializer = self.view.get_serializer()
|
||||||
except Exception:
|
except Exception:
|
||||||
serializer = None
|
serializer = None
|
||||||
warnings.warn('{}.get_serializer() raised an exception during '
|
warnings.warn(
|
||||||
'schema generation. Serializer fields will not be '
|
'{}.get_serializer() raised an exception during '
|
||||||
'generated for {} {}.'
|
'schema generation. Serializer fields will not be '
|
||||||
.format(self.view.__class__.__name__, method, path))
|
'generated for {} {}.'.format(self.view.__class__.__name__, method, path)
|
||||||
|
)
|
||||||
|
|
||||||
link.__dict__['deprecated'] = getattr(self.view, 'deprecated', False)
|
link.__dict__['deprecated'] = getattr(self.view, 'deprecated', False)
|
||||||
|
|
||||||
@@ -43,9 +42,7 @@ class AutoSchema(DRFAuthSchema):
|
|||||||
if hasattr(self.view, 'swagger_topic'):
|
if hasattr(self.view, 'swagger_topic'):
|
||||||
link.__dict__['topic'] = str(self.view.swagger_topic).title()
|
link.__dict__['topic'] = str(self.view.swagger_topic).title()
|
||||||
elif serializer and hasattr(serializer, 'Meta'):
|
elif serializer and hasattr(serializer, 'Meta'):
|
||||||
link.__dict__['topic'] = str(
|
link.__dict__['topic'] = str(serializer.Meta.model._meta.verbose_name_plural).title()
|
||||||
serializer.Meta.model._meta.verbose_name_plural
|
|
||||||
).title()
|
|
||||||
elif hasattr(self.view, 'model'):
|
elif hasattr(self.view, 'model'):
|
||||||
link.__dict__['topic'] = str(self.view.model._meta.verbose_name_plural).title()
|
link.__dict__['topic'] = str(self.view.model._meta.verbose_name_plural).title()
|
||||||
else:
|
else:
|
||||||
@@ -62,18 +59,10 @@ class SwaggerSchemaView(APIView):
|
|||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
exclude_from_schema = True
|
exclude_from_schema = True
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
renderer_classes = [
|
renderer_classes = [CoreJSONRenderer, renderers.OpenAPIRenderer, renderers.SwaggerUIRenderer]
|
||||||
CoreJSONRenderer,
|
|
||||||
renderers.OpenAPIRenderer,
|
|
||||||
renderers.SwaggerUIRenderer
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
generator = SuperUserSchemaGenerator(
|
generator = SuperUserSchemaGenerator(title='Ansible Tower API', patterns=None, urlconf=None)
|
||||||
title='Ansible Tower API',
|
|
||||||
patterns=None,
|
|
||||||
urlconf=None
|
|
||||||
)
|
|
||||||
schema = generator.get_schema(request=request)
|
schema = generator.get_schema(request=request)
|
||||||
# python core-api doesn't support the deprecation yet, so track it
|
# python core-api doesn't support the deprecation yet, so track it
|
||||||
# ourselves and return it in a response header
|
# ourselves and return it in a response header
|
||||||
@@ -103,11 +92,6 @@ class SwaggerSchemaView(APIView):
|
|||||||
schema._data[topic]._data[path] = node
|
schema._data[topic]._data[path] = node
|
||||||
|
|
||||||
if not schema:
|
if not schema:
|
||||||
raise exceptions.ValidationError(
|
raise exceptions.ValidationError('The schema generator did not return a schema Document')
|
||||||
'The schema generator did not return a schema Document'
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
return Response(schema, headers={'X-Deprecated-Paths': json.dumps(_deprecated)})
|
||||||
schema,
|
|
||||||
headers={'X-Deprecated-Paths': json.dumps(_deprecated)}
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import ActivityStreamList, ActivityStreamDetail
|
||||||
ActivityStreamList,
|
|
||||||
ActivityStreamDetail,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import AdHocCommandEventList, AdHocCommandEventDetail
|
||||||
AdHocCommandEventList,
|
|
||||||
AdHocCommandEventDetail,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import CredentialInputSourceDetail, CredentialInputSourceList
|
||||||
CredentialInputSourceDetail,
|
|
||||||
CredentialInputSourceList,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,13 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import CredentialTypeList, CredentialTypeDetail, CredentialTypeCredentialList, CredentialTypeActivityStreamList, CredentialTypeExternalTest
|
||||||
CredentialTypeList,
|
|
||||||
CredentialTypeDetail,
|
|
||||||
CredentialTypeCredentialList,
|
|
||||||
CredentialTypeActivityStreamList,
|
|
||||||
CredentialTypeExternalTest,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,20 +3,14 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import InstanceList, InstanceDetail, InstanceUnifiedJobsList, InstanceInstanceGroupsList
|
||||||
InstanceList,
|
|
||||||
InstanceDetail,
|
|
||||||
InstanceUnifiedJobsList,
|
|
||||||
InstanceInstanceGroupsList,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
url(r'^$', InstanceList.as_view(), name='instance_list'),
|
url(r'^$', InstanceList.as_view(), name='instance_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', InstanceDetail.as_view(), name='instance_detail'),
|
url(r'^(?P<pk>[0-9]+)/$', InstanceDetail.as_view(), name='instance_detail'),
|
||||||
url(r'^(?P<pk>[0-9]+)/jobs/$', InstanceUnifiedJobsList.as_view(), name='instance_unified_jobs_list'),
|
url(r'^(?P<pk>[0-9]+)/jobs/$', InstanceUnifiedJobsList.as_view(), name='instance_unified_jobs_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/instance_groups/$', InstanceInstanceGroupsList.as_view(),
|
url(r'^(?P<pk>[0-9]+)/instance_groups/$', InstanceInstanceGroupsList.as_view(), name='instance_instance_groups_list'),
|
||||||
name='instance_instance_groups_list'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import InstanceGroupList, InstanceGroupDetail, InstanceGroupUnifiedJobsList, InstanceGroupInstanceList
|
||||||
InstanceGroupList,
|
|
||||||
InstanceGroupDetail,
|
|
||||||
InstanceGroupUnifiedJobsList,
|
|
||||||
InstanceGroupInstanceList,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import InventoryScriptList, InventoryScriptDetail, InventoryScriptObjectRolesList, InventoryScriptCopy
|
||||||
InventoryScriptList,
|
|
||||||
InventoryScriptDetail,
|
|
||||||
InventoryScriptObjectRolesList,
|
|
||||||
InventoryScriptCopy,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -29,12 +29,21 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/credentials/$', InventorySourceCredentialsList.as_view(), name='inventory_source_credentials_list'),
|
url(r'^(?P<pk>[0-9]+)/credentials/$', InventorySourceCredentialsList.as_view(), name='inventory_source_credentials_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/groups/$', InventorySourceGroupsList.as_view(), name='inventory_source_groups_list'),
|
url(r'^(?P<pk>[0-9]+)/groups/$', InventorySourceGroupsList.as_view(), name='inventory_source_groups_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/hosts/$', InventorySourceHostsList.as_view(), name='inventory_source_hosts_list'),
|
url(r'^(?P<pk>[0-9]+)/hosts/$', InventorySourceHostsList.as_view(), name='inventory_source_hosts_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', InventorySourceNotificationTemplatesStartedList.as_view(),
|
url(
|
||||||
name='inventory_source_notification_templates_started_list'),
|
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', InventorySourceNotificationTemplatesErrorList.as_view(),
|
InventorySourceNotificationTemplatesStartedList.as_view(),
|
||||||
name='inventory_source_notification_templates_error_list'),
|
name='inventory_source_notification_templates_started_list',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', InventorySourceNotificationTemplatesSuccessList.as_view(),
|
),
|
||||||
name='inventory_source_notification_templates_success_list'),
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
|
||||||
|
InventorySourceNotificationTemplatesErrorList.as_view(),
|
||||||
|
name='inventory_source_notification_templates_error_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
|
||||||
|
InventorySourceNotificationTemplatesSuccessList.as_view(),
|
||||||
|
name='inventory_source_notification_templates_success_list',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import JobEventList, JobEventDetail, JobEventChildrenList, JobEventHostsList
|
||||||
JobEventList,
|
|
||||||
JobEventDetail,
|
|
||||||
JobEventChildrenList,
|
|
||||||
JobEventHostsList,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,13 +3,9 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import JobHostSummaryDetail
|
||||||
JobHostSummaryDetail,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [url(r'^(?P<pk>[0-9]+)/$', JobHostSummaryDetail.as_view(), name='job_host_summary_detail')]
|
||||||
url(r'^(?P<pk>[0-9]+)/$', JobHostSummaryDetail.as_view(), name='job_host_summary_detail'),
|
|
||||||
]
|
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -34,12 +34,21 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/schedules/$', JobTemplateSchedulesList.as_view(), name='job_template_schedules_list'),
|
url(r'^(?P<pk>[0-9]+)/schedules/$', JobTemplateSchedulesList.as_view(), name='job_template_schedules_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/survey_spec/$', JobTemplateSurveySpec.as_view(), name='job_template_survey_spec'),
|
url(r'^(?P<pk>[0-9]+)/survey_spec/$', JobTemplateSurveySpec.as_view(), name='job_template_survey_spec'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', JobTemplateActivityStreamList.as_view(), name='job_template_activity_stream_list'),
|
url(r'^(?P<pk>[0-9]+)/activity_stream/$', JobTemplateActivityStreamList.as_view(), name='job_template_activity_stream_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', JobTemplateNotificationTemplatesStartedList.as_view(),
|
url(
|
||||||
name='job_template_notification_templates_started_list'),
|
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', JobTemplateNotificationTemplatesErrorList.as_view(),
|
JobTemplateNotificationTemplatesStartedList.as_view(),
|
||||||
name='job_template_notification_templates_error_list'),
|
name='job_template_notification_templates_started_list',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', JobTemplateNotificationTemplatesSuccessList.as_view(),
|
),
|
||||||
name='job_template_notification_templates_success_list'),
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
|
||||||
|
JobTemplateNotificationTemplatesErrorList.as_view(),
|
||||||
|
name='job_template_notification_templates_error_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
|
||||||
|
JobTemplateNotificationTemplatesSuccessList.as_view(),
|
||||||
|
name='job_template_notification_templates_success_list',
|
||||||
|
),
|
||||||
url(r'^(?P<pk>[0-9]+)/instance_groups/$', JobTemplateInstanceGroupsList.as_view(), name='job_template_instance_groups_list'),
|
url(r'^(?P<pk>[0-9]+)/instance_groups/$', JobTemplateInstanceGroupsList.as_view(), name='job_template_instance_groups_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/access_list/$', JobTemplateAccessList.as_view(), name='job_template_access_list'),
|
url(r'^(?P<pk>[0-9]+)/access_list/$', JobTemplateAccessList.as_view(), name='job_template_access_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/object_roles/$', JobTemplateObjectRolesList.as_view(), name='job_template_object_roles_list'),
|
url(r'^(?P<pk>[0-9]+)/object_roles/$', JobTemplateObjectRolesList.as_view(), name='job_template_object_roles_list'),
|
||||||
|
|||||||
@@ -3,15 +3,9 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import LabelList, LabelDetail
|
||||||
LabelList,
|
|
||||||
LabelDetail,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [url(r'^$', LabelList.as_view(), name='label_list'), url(r'^(?P<pk>[0-9]+)/$', LabelDetail.as_view(), name='label_detail')]
|
||||||
url(r'^$', LabelList.as_view(), name='label_list'),
|
|
||||||
url(r'^(?P<pk>[0-9]+)/$', LabelDetail.as_view(), name='label_detail'),
|
|
||||||
]
|
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -3,15 +3,9 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import NotificationList, NotificationDetail
|
||||||
NotificationList,
|
|
||||||
NotificationDetail,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [url(r'^$', NotificationList.as_view(), name='notification_list'), url(r'^(?P<pk>[0-9]+)/$', NotificationDetail.as_view(), name='notification_detail')]
|
||||||
url(r'^$', NotificationList.as_view(), name='notification_list'),
|
|
||||||
url(r'^(?P<pk>[0-9]+)/$', NotificationDetail.as_view(), name='notification_detail'),
|
|
||||||
]
|
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -16,32 +16,12 @@ from awx.api.views import (
|
|||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
||||||
url(
|
url(r'^applications/(?P<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
|
||||||
r'^applications/(?P<pk>[0-9]+)/$',
|
url(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='o_auth2_application_token_list'),
|
||||||
OAuth2ApplicationDetail.as_view(),
|
url(r'^applications/(?P<pk>[0-9]+)/activity_stream/$', OAuth2ApplicationActivityStreamList.as_view(), name='o_auth2_application_activity_stream_list'),
|
||||||
name='o_auth2_application_detail'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^applications/(?P<pk>[0-9]+)/tokens/$',
|
|
||||||
ApplicationOAuth2TokenList.as_view(),
|
|
||||||
name='o_auth2_application_token_list'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^applications/(?P<pk>[0-9]+)/activity_stream/$',
|
|
||||||
OAuth2ApplicationActivityStreamList.as_view(),
|
|
||||||
name='o_auth2_application_activity_stream_list'
|
|
||||||
),
|
|
||||||
url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
||||||
url(
|
url(r'^tokens/(?P<pk>[0-9]+)/$', OAuth2TokenDetail.as_view(), name='o_auth2_token_detail'),
|
||||||
r'^tokens/(?P<pk>[0-9]+)/$',
|
url(r'^tokens/(?P<pk>[0-9]+)/activity_stream/$', OAuth2TokenActivityStreamList.as_view(), name='o_auth2_token_activity_stream_list'),
|
||||||
OAuth2TokenDetail.as_view(),
|
|
||||||
name='o_auth2_token_detail'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^tokens/(?P<pk>[0-9]+)/activity_stream/$',
|
|
||||||
OAuth2TokenActivityStreamList.as_view(),
|
|
||||||
name='o_auth2_token_activity_stream_list'
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -10,13 +10,10 @@ from oauthlib import oauth2
|
|||||||
from oauth2_provider import views
|
from oauth2_provider import views
|
||||||
|
|
||||||
from awx.main.models import RefreshToken
|
from awx.main.models import RefreshToken
|
||||||
from awx.api.views import (
|
from awx.api.views import ApiOAuthAuthorizationRootView
|
||||||
ApiOAuthAuthorizationRootView,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TokenView(views.TokenView):
|
class TokenView(views.TokenView):
|
||||||
|
|
||||||
def create_token_response(self, request):
|
def create_token_response(self, request):
|
||||||
# Django OAuth2 Toolkit has a bug whereby refresh tokens are *never*
|
# Django OAuth2 Toolkit has a bug whereby refresh tokens are *never*
|
||||||
# properly expired (ugh):
|
# properly expired (ugh):
|
||||||
@@ -26,9 +23,7 @@ class TokenView(views.TokenView):
|
|||||||
# This code detects and auto-expires them on refresh grant
|
# This code detects and auto-expires them on refresh grant
|
||||||
# requests.
|
# requests.
|
||||||
if request.POST.get('grant_type') == 'refresh_token' and 'refresh_token' in request.POST:
|
if request.POST.get('grant_type') == 'refresh_token' and 'refresh_token' in request.POST:
|
||||||
refresh_token = RefreshToken.objects.filter(
|
refresh_token = RefreshToken.objects.filter(token=request.POST['refresh_token']).first()
|
||||||
token=request.POST['refresh_token']
|
|
||||||
).first()
|
|
||||||
if refresh_token:
|
if refresh_token:
|
||||||
expire_seconds = settings.OAUTH2_PROVIDER.get('REFRESH_TOKEN_EXPIRE_SECONDS', 0)
|
expire_seconds = settings.OAUTH2_PROVIDER.get('REFRESH_TOKEN_EXPIRE_SECONDS', 0)
|
||||||
if refresh_token.created + timedelta(seconds=expire_seconds) < now():
|
if refresh_token.created + timedelta(seconds=expire_seconds) < now():
|
||||||
|
|||||||
@@ -43,14 +43,26 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/credentials/$', OrganizationCredentialList.as_view(), name='organization_credential_list'),
|
url(r'^(?P<pk>[0-9]+)/credentials/$', OrganizationCredentialList.as_view(), name='organization_credential_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', OrganizationActivityStreamList.as_view(), name='organization_activity_stream_list'),
|
url(r'^(?P<pk>[0-9]+)/activity_stream/$', OrganizationActivityStreamList.as_view(), name='organization_activity_stream_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates/$', OrganizationNotificationTemplatesList.as_view(), name='organization_notification_templates_list'),
|
url(r'^(?P<pk>[0-9]+)/notification_templates/$', OrganizationNotificationTemplatesList.as_view(), name='organization_notification_templates_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', OrganizationNotificationTemplatesStartedList.as_view(),
|
url(
|
||||||
name='organization_notification_templates_started_list'),
|
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', OrganizationNotificationTemplatesErrorList.as_view(),
|
OrganizationNotificationTemplatesStartedList.as_view(),
|
||||||
name='organization_notification_templates_error_list'),
|
name='organization_notification_templates_started_list',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', OrganizationNotificationTemplatesSuccessList.as_view(),
|
),
|
||||||
name='organization_notification_templates_success_list'),
|
url(
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_approvals/$', OrganizationNotificationTemplatesApprovalList.as_view(),
|
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
|
||||||
name='organization_notification_templates_approvals_list'),
|
OrganizationNotificationTemplatesErrorList.as_view(),
|
||||||
|
name='organization_notification_templates_error_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
|
||||||
|
OrganizationNotificationTemplatesSuccessList.as_view(),
|
||||||
|
name='organization_notification_templates_success_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_approvals/$',
|
||||||
|
OrganizationNotificationTemplatesApprovalList.as_view(),
|
||||||
|
name='organization_notification_templates_approvals_list',
|
||||||
|
),
|
||||||
url(r'^(?P<pk>[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'),
|
url(r'^(?P<pk>[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/galaxy_credentials/$', OrganizationGalaxyCredentialsList.as_view(), name='organization_galaxy_credentials_list'),
|
url(r'^(?P<pk>[0-9]+)/galaxy_credentials/$', OrganizationGalaxyCredentialsList.as_view(), name='organization_galaxy_credentials_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'),
|
url(r'^(?P<pk>[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'),
|
||||||
|
|||||||
@@ -35,10 +35,16 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', ProjectActivityStreamList.as_view(), name='project_activity_stream_list'),
|
url(r'^(?P<pk>[0-9]+)/activity_stream/$', ProjectActivityStreamList.as_view(), name='project_activity_stream_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/schedules/$', ProjectSchedulesList.as_view(), name='project_schedules_list'),
|
url(r'^(?P<pk>[0-9]+)/schedules/$', ProjectSchedulesList.as_view(), name='project_schedules_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', ProjectNotificationTemplatesErrorList.as_view(), name='project_notification_templates_error_list'),
|
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', ProjectNotificationTemplatesErrorList.as_view(), name='project_notification_templates_error_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', ProjectNotificationTemplatesSuccessList.as_view(),
|
url(
|
||||||
name='project_notification_templates_success_list'),
|
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', ProjectNotificationTemplatesStartedList.as_view(),
|
ProjectNotificationTemplatesSuccessList.as_view(),
|
||||||
name='project_notification_templates_started_list'),
|
name='project_notification_templates_success_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
|
||||||
|
ProjectNotificationTemplatesStartedList.as_view(),
|
||||||
|
name='project_notification_templates_started_list',
|
||||||
|
),
|
||||||
url(r'^(?P<pk>[0-9]+)/object_roles/$', ProjectObjectRolesList.as_view(), name='project_object_roles_list'),
|
url(r'^(?P<pk>[0-9]+)/object_roles/$', ProjectObjectRolesList.as_view(), name='project_object_roles_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/access_list/$', ProjectAccessList.as_view(), name='project_access_list'),
|
url(r'^(?P<pk>[0-9]+)/access_list/$', ProjectAccessList.as_view(), name='project_access_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/copy/$', ProjectCopy.as_view(), name='project_copy'),
|
url(r'^(?P<pk>[0-9]+)/copy/$', ProjectCopy.as_view(), name='project_copy'),
|
||||||
|
|||||||
@@ -3,14 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import RoleList, RoleDetail, RoleUsersList, RoleTeamsList, RoleParentsList, RoleChildrenList
|
||||||
RoleList,
|
|
||||||
RoleDetail,
|
|
||||||
RoleUsersList,
|
|
||||||
RoleTeamsList,
|
|
||||||
RoleParentsList,
|
|
||||||
RoleChildrenList,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import ScheduleList, ScheduleDetail, ScheduleUnifiedJobsList, ScheduleCredentialsList
|
||||||
ScheduleList,
|
|
||||||
ScheduleDetail,
|
|
||||||
ScheduleUnifiedJobsList,
|
|
||||||
ScheduleCredentialsList,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,13 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import SystemJobList, SystemJobDetail, SystemJobCancel, SystemJobNotificationsList, SystemJobEventsList
|
||||||
SystemJobList,
|
|
||||||
SystemJobDetail,
|
|
||||||
SystemJobCancel,
|
|
||||||
SystemJobNotificationsList,
|
|
||||||
SystemJobEventsList
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -21,12 +21,21 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/launch/$', SystemJobTemplateLaunch.as_view(), name='system_job_template_launch'),
|
url(r'^(?P<pk>[0-9]+)/launch/$', SystemJobTemplateLaunch.as_view(), name='system_job_template_launch'),
|
||||||
url(r'^(?P<pk>[0-9]+)/jobs/$', SystemJobTemplateJobsList.as_view(), name='system_job_template_jobs_list'),
|
url(r'^(?P<pk>[0-9]+)/jobs/$', SystemJobTemplateJobsList.as_view(), name='system_job_template_jobs_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/schedules/$', SystemJobTemplateSchedulesList.as_view(), name='system_job_template_schedules_list'),
|
url(r'^(?P<pk>[0-9]+)/schedules/$', SystemJobTemplateSchedulesList.as_view(), name='system_job_template_schedules_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', SystemJobTemplateNotificationTemplatesStartedList.as_view(),
|
url(
|
||||||
name='system_job_template_notification_templates_started_list'),
|
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', SystemJobTemplateNotificationTemplatesErrorList.as_view(),
|
SystemJobTemplateNotificationTemplatesStartedList.as_view(),
|
||||||
name='system_job_template_notification_templates_error_list'),
|
name='system_job_template_notification_templates_started_list',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', SystemJobTemplateNotificationTemplatesSuccessList.as_view(),
|
),
|
||||||
name='system_job_template_notification_templates_success_list'),
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
|
||||||
|
SystemJobTemplateNotificationTemplatesErrorList.as_view(),
|
||||||
|
name='system_job_template_notification_templates_error_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
|
||||||
|
SystemJobTemplateNotificationTemplatesSuccessList.as_view(),
|
||||||
|
name='system_job_template_notification_templates_success_list',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
|
||||||
from awx.api.generics import (
|
from awx.api.generics import LoggedLoginView, LoggedLogoutView
|
||||||
LoggedLoginView,
|
|
||||||
LoggedLogoutView,
|
|
||||||
)
|
|
||||||
from awx.api.views import (
|
from awx.api.views import (
|
||||||
ApiRootView,
|
ApiRootView,
|
||||||
ApiV2RootView,
|
ApiV2RootView,
|
||||||
@@ -33,9 +30,7 @@ from awx.api.views import (
|
|||||||
OAuth2ApplicationDetail,
|
OAuth2ApplicationDetail,
|
||||||
)
|
)
|
||||||
|
|
||||||
from awx.api.views.metrics import (
|
from awx.api.views.metrics import MetricsView
|
||||||
MetricsView,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .organization import urls as organization_urls
|
from .organization import urls as organization_urls
|
||||||
from .user import urls as user_urls
|
from .user import urls as user_urls
|
||||||
@@ -146,17 +141,11 @@ app_name = 'api'
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
|
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
|
||||||
url(r'^(?P<version>(v2))/', include(v2_urls)),
|
url(r'^(?P<version>(v2))/', include(v2_urls)),
|
||||||
url(r'^login/$', LoggedLoginView.as_view(
|
url(r'^login/$', LoggedLoginView.as_view(template_name='rest_framework/login.html', extra_context={'inside_login_context': True}), name='login'),
|
||||||
template_name='rest_framework/login.html',
|
url(r'^logout/$', LoggedLogoutView.as_view(next_page='/api/', redirect_field_name='next'), name='logout'),
|
||||||
extra_context={'inside_login_context': True}
|
|
||||||
), name='login'),
|
|
||||||
url(r'^logout/$', LoggedLogoutView.as_view(
|
|
||||||
next_page='/api/', redirect_field_name='next'
|
|
||||||
), name='logout'),
|
|
||||||
url(r'^o/', include(oauth2_root_urls)),
|
url(r'^o/', include(oauth2_root_urls)),
|
||||||
]
|
]
|
||||||
if settings.SETTINGS_MODULE == 'awx.settings.development':
|
if settings.SETTINGS_MODULE == 'awx.settings.development':
|
||||||
from awx.api.swagger import SwaggerSchemaView
|
from awx.api.swagger import SwaggerSchemaView
|
||||||
urlpatterns += [
|
|
||||||
url(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view'),
|
urlpatterns += [url(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view')]
|
||||||
]
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/tokens/$', OAuth2UserTokenList.as_view(), name='o_auth2_token_list'),
|
url(r'^(?P<pk>[0-9]+)/tokens/$', OAuth2UserTokenList.as_view(), name='o_auth2_token_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/authorized_tokens/$', UserAuthorizedTokenList.as_view(), name='user_authorized_token_list'),
|
url(r'^(?P<pk>[0-9]+)/authorized_tokens/$', UserAuthorizedTokenList.as_view(), name='user_authorized_token_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/personal_tokens/$', UserPersonalTokenList.as_view(), name='user_personal_token_list'),
|
url(r'^(?P<pk>[0-9]+)/personal_tokens/$', UserPersonalTokenList.as_view(), name='user_personal_token_list'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver
|
||||||
WebhookKeyView,
|
|
||||||
GithubWebhookReceiver,
|
|
||||||
GitlabWebhookReceiver,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import WorkflowApprovalList, WorkflowApprovalDetail, WorkflowApprovalApprove, WorkflowApprovalDeny
|
||||||
WorkflowApprovalList,
|
|
||||||
WorkflowApprovalDetail,
|
|
||||||
WorkflowApprovalApprove,
|
|
||||||
WorkflowApprovalDeny,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import WorkflowApprovalTemplateDetail, WorkflowApprovalTemplateJobsList
|
||||||
WorkflowApprovalTemplateDetail,
|
|
||||||
WorkflowApprovalTemplateJobsList,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -33,14 +33,26 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/survey_spec/$', WorkflowJobTemplateSurveySpec.as_view(), name='workflow_job_template_survey_spec'),
|
url(r'^(?P<pk>[0-9]+)/survey_spec/$', WorkflowJobTemplateSurveySpec.as_view(), name='workflow_job_template_survey_spec'),
|
||||||
url(r'^(?P<pk>[0-9]+)/workflow_nodes/$', WorkflowJobTemplateWorkflowNodesList.as_view(), name='workflow_job_template_workflow_nodes_list'),
|
url(r'^(?P<pk>[0-9]+)/workflow_nodes/$', WorkflowJobTemplateWorkflowNodesList.as_view(), name='workflow_job_template_workflow_nodes_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', WorkflowJobTemplateActivityStreamList.as_view(), name='workflow_job_template_activity_stream_list'),
|
url(r'^(?P<pk>[0-9]+)/activity_stream/$', WorkflowJobTemplateActivityStreamList.as_view(), name='workflow_job_template_activity_stream_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', WorkflowJobTemplateNotificationTemplatesStartedList.as_view(),
|
url(
|
||||||
name='workflow_job_template_notification_templates_started_list'),
|
r'^(?P<pk>[0-9]+)/notification_templates_started/$',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', WorkflowJobTemplateNotificationTemplatesErrorList.as_view(),
|
WorkflowJobTemplateNotificationTemplatesStartedList.as_view(),
|
||||||
name='workflow_job_template_notification_templates_error_list'),
|
name='workflow_job_template_notification_templates_started_list',
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', WorkflowJobTemplateNotificationTemplatesSuccessList.as_view(),
|
),
|
||||||
name='workflow_job_template_notification_templates_success_list'),
|
url(
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_approvals/$', WorkflowJobTemplateNotificationTemplatesApprovalList.as_view(),
|
r'^(?P<pk>[0-9]+)/notification_templates_error/$',
|
||||||
name='workflow_job_template_notification_templates_approvals_list'),
|
WorkflowJobTemplateNotificationTemplatesErrorList.as_view(),
|
||||||
|
name='workflow_job_template_notification_templates_error_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_success/$',
|
||||||
|
WorkflowJobTemplateNotificationTemplatesSuccessList.as_view(),
|
||||||
|
name='workflow_job_template_notification_templates_success_list',
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^(?P<pk>[0-9]+)/notification_templates_approvals/$',
|
||||||
|
WorkflowJobTemplateNotificationTemplatesApprovalList.as_view(),
|
||||||
|
name='workflow_job_template_notification_templates_approvals_list',
|
||||||
|
),
|
||||||
url(r'^(?P<pk>[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'),
|
url(r'^(?P<pk>[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'),
|
url(r'^(?P<pk>[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'),
|
url(r'^(?P<pk>[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'),
|
||||||
|
|||||||
@@ -40,13 +40,10 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
|
|||||||
|
|
||||||
|
|
||||||
class URLPathVersioning(BaseVersioning):
|
class URLPathVersioning(BaseVersioning):
|
||||||
|
|
||||||
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||||
if request.version is not None:
|
if request.version is not None:
|
||||||
kwargs = {} if (kwargs is None) else kwargs
|
kwargs = {} if (kwargs is None) else kwargs
|
||||||
kwargs[self.version_param] = request.version
|
kwargs[self.version_param] = request.version
|
||||||
request = None
|
request = None
|
||||||
|
|
||||||
return super(BaseVersioning, self).reverse(
|
return super(BaseVersioning, self).reverse(viewname, args, kwargs, request, format, **extra)
|
||||||
viewname, args, kwargs, request, format, **extra
|
|
||||||
)
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,14 +28,7 @@ from awx.main.models import (
|
|||||||
InventorySource,
|
InventorySource,
|
||||||
CustomInventoryScript,
|
CustomInventoryScript,
|
||||||
)
|
)
|
||||||
from awx.api.generics import (
|
from awx.api.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView, SubListAPIView, SubListAttachDetachAPIView, ResourceAccessList, CopyAPIView
|
||||||
ListCreateAPIView,
|
|
||||||
RetrieveUpdateDestroyAPIView,
|
|
||||||
SubListAPIView,
|
|
||||||
SubListAttachDetachAPIView,
|
|
||||||
ResourceAccessList,
|
|
||||||
CopyAPIView,
|
|
||||||
)
|
|
||||||
|
|
||||||
from awx.api.serializers import (
|
from awx.api.serializers import (
|
||||||
InventorySerializer,
|
InventorySerializer,
|
||||||
@@ -46,10 +39,7 @@ from awx.api.serializers import (
|
|||||||
CustomInventoryScriptSerializer,
|
CustomInventoryScriptSerializer,
|
||||||
JobTemplateSerializer,
|
JobTemplateSerializer,
|
||||||
)
|
)
|
||||||
from awx.api.views.mixin import (
|
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, ControlledByScmMixin
|
||||||
RelatedJobsPreventDeleteMixin,
|
|
||||||
ControlledByScmMixin,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.views.organization')
|
logger = logging.getLogger('awx.api.views.organization')
|
||||||
|
|
||||||
@@ -101,7 +91,7 @@ class InventoryScriptObjectRolesList(SubListAPIView):
|
|||||||
model = Role
|
model = Role
|
||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
parent_model = CustomInventoryScript
|
parent_model = CustomInventoryScript
|
||||||
search_fields = ('role_field', 'content_type__model',)
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
@@ -134,8 +124,7 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, Retri
|
|||||||
|
|
||||||
# Do not allow changes to an Inventory kind.
|
# Do not allow changes to an Inventory kind.
|
||||||
if kind is not None and obj.kind != kind:
|
if kind is not None and obj.kind != kind:
|
||||||
return Response(dict(error=_('You cannot turn a regular inventory into a "smart" inventory.')),
|
return Response(dict(error=_('You cannot turn a regular inventory into a "smart" inventory.')), status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
||||||
return super(InventoryDetail, self).update(request, *args, **kwargs)
|
return super(InventoryDetail, self).update(request, *args, **kwargs)
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
@@ -175,7 +164,7 @@ class InventoryInstanceGroupsList(SubListAttachDetachAPIView):
|
|||||||
|
|
||||||
class InventoryAccessList(ResourceAccessList):
|
class InventoryAccessList(ResourceAccessList):
|
||||||
|
|
||||||
model = User # needs to be User for AccessLists's
|
model = User # needs to be User for AccessLists's
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
|
|
||||||
|
|
||||||
@@ -184,7 +173,7 @@ class InventoryObjectRolesList(SubListAPIView):
|
|||||||
model = Role
|
model = Role
|
||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
search_fields = ('role_field', 'content_type__model',)
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ from rest_framework.exceptions import PermissionDenied
|
|||||||
from awx.main.analytics.metrics import metrics
|
from awx.main.analytics.metrics import metrics
|
||||||
from awx.api import renderers
|
from awx.api import renderers
|
||||||
|
|
||||||
from awx.api.generics import (
|
from awx.api.generics import APIView
|
||||||
APIView,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.analytics')
|
logger = logging.getLogger('awx.analytics')
|
||||||
@@ -30,13 +28,10 @@ class MetricsView(APIView):
|
|||||||
name = _('Metrics')
|
name = _('Metrics')
|
||||||
swagger_topic = 'Metrics'
|
swagger_topic = 'Metrics'
|
||||||
|
|
||||||
renderer_classes = [renderers.PlainTextRenderer,
|
renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer]
|
||||||
renderers.PrometheusJSONRenderer,
|
|
||||||
renderers.BrowsableAPIRenderer,]
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
''' Show Metrics Details '''
|
''' Show Metrics Details '''
|
||||||
if (request.user.is_superuser or request.user.is_system_auditor):
|
if request.user.is_superuser or request.user.is_system_auditor:
|
||||||
return Response(metrics().decode('UTF-8'))
|
return Response(metrics().decode('UTF-8'))
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,8 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from awx.main.constants import ACTIVE_STATES
|
from awx.main.constants import ACTIVE_STATES
|
||||||
from awx.main.utils import (
|
from awx.main.utils import get_object_or_400, parse_yaml_or_json
|
||||||
get_object_or_400,
|
from awx.main.models.ha import Instance, InstanceGroup
|
||||||
parse_yaml_or_json,
|
|
||||||
)
|
|
||||||
from awx.main.models.ha import (
|
|
||||||
Instance,
|
|
||||||
InstanceGroup,
|
|
||||||
)
|
|
||||||
from awx.main.models.organization import Team
|
from awx.main.models.organization import Team
|
||||||
from awx.main.models.projects import Project
|
from awx.main.models.projects import Project
|
||||||
from awx.main.models.inventory import Inventory
|
from awx.main.models.inventory import Inventory
|
||||||
@@ -34,9 +28,10 @@ logger = logging.getLogger('awx.api.views.mixin')
|
|||||||
|
|
||||||
|
|
||||||
class UnifiedJobDeletionMixin(object):
|
class UnifiedJobDeletionMixin(object):
|
||||||
'''
|
"""
|
||||||
Special handling when deleting a running unified job object.
|
Special handling when deleting a running unified job object.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if not request.user.can_access(self.model, 'delete', obj):
|
if not request.user.can_access(self.model, 'delete', obj):
|
||||||
@@ -53,22 +48,21 @@ class UnifiedJobDeletionMixin(object):
|
|||||||
# Prohibit deletion if job events are still coming in
|
# Prohibit deletion if job events are still coming in
|
||||||
if obj.finished and now() < obj.finished + dateutil.relativedelta.relativedelta(minutes=1):
|
if obj.finished and now() < obj.finished + dateutil.relativedelta.relativedelta(minutes=1):
|
||||||
# less than 1 minute has passed since job finished and events are not in
|
# less than 1 minute has passed since job finished and events are not in
|
||||||
return Response({"error": _("Job has not finished processing events.")},
|
return Response({"error": _("Job has not finished processing events.")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
else:
|
||||||
# if it has been > 1 minute, events are probably lost
|
# if it has been > 1 minute, events are probably lost
|
||||||
logger.warning('Allowing deletion of {} through the API without all events '
|
logger.warning('Allowing deletion of {} through the API without all events ' 'processed.'.format(obj.log_format))
|
||||||
'processed.'.format(obj.log_format))
|
|
||||||
obj.delete()
|
obj.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class InstanceGroupMembershipMixin(object):
|
class InstanceGroupMembershipMixin(object):
|
||||||
'''
|
"""
|
||||||
This mixin overloads attach/detach so that it calls InstanceGroup.save(),
|
This mixin overloads attach/detach so that it calls InstanceGroup.save(),
|
||||||
triggering a background recalculation of policy-based instance group
|
triggering a background recalculation of policy-based instance group
|
||||||
membership.
|
membership.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def attach(self, request, *args, **kwargs):
|
def attach(self, request, *args, **kwargs):
|
||||||
response = super(InstanceGroupMembershipMixin, self).attach(request, *args, **kwargs)
|
response = super(InstanceGroupMembershipMixin, self).attach(request, *args, **kwargs)
|
||||||
sub_id, res = self.attach_validate(request)
|
sub_id, res = self.attach_validate(request)
|
||||||
@@ -84,9 +78,7 @@ class InstanceGroupMembershipMixin(object):
|
|||||||
ig_obj = get_object_or_400(ig_qs, pk=sub_id)
|
ig_obj = get_object_or_400(ig_qs, pk=sub_id)
|
||||||
else:
|
else:
|
||||||
# similar to get_parent_object, but selected for update
|
# similar to get_parent_object, but selected for update
|
||||||
parent_filter = {
|
parent_filter = {self.lookup_field: self.kwargs.get(self.lookup_field, None)}
|
||||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
|
||||||
}
|
|
||||||
ig_obj = get_object_or_404(ig_qs, **parent_filter)
|
ig_obj = get_object_or_404(ig_qs, **parent_filter)
|
||||||
if inst_name not in ig_obj.policy_instance_list:
|
if inst_name not in ig_obj.policy_instance_list:
|
||||||
ig_obj.policy_instance_list.append(inst_name)
|
ig_obj.policy_instance_list.append(inst_name)
|
||||||
@@ -126,9 +118,7 @@ class InstanceGroupMembershipMixin(object):
|
|||||||
ig_obj = get_object_or_400(ig_qs, pk=sub_id)
|
ig_obj = get_object_or_400(ig_qs, pk=sub_id)
|
||||||
else:
|
else:
|
||||||
# similar to get_parent_object, but selected for update
|
# similar to get_parent_object, but selected for update
|
||||||
parent_filter = {
|
parent_filter = {self.lookup_field: self.kwargs.get(self.lookup_field, None)}
|
||||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
|
||||||
}
|
|
||||||
ig_obj = get_object_or_404(ig_qs, **parent_filter)
|
ig_obj = get_object_or_404(ig_qs, **parent_filter)
|
||||||
if inst_name in ig_obj.policy_instance_list:
|
if inst_name in ig_obj.policy_instance_list:
|
||||||
ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name))
|
ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name))
|
||||||
@@ -146,16 +136,13 @@ class RelatedJobsPreventDeleteMixin(object):
|
|||||||
if len(active_jobs) > 0:
|
if len(active_jobs) > 0:
|
||||||
raise ActiveJobConflict(active_jobs)
|
raise ActiveJobConflict(active_jobs)
|
||||||
time_cutoff = now() - dateutil.relativedelta.relativedelta(minutes=1)
|
time_cutoff = now() - dateutil.relativedelta.relativedelta(minutes=1)
|
||||||
recent_jobs = obj._get_related_jobs().filter(finished__gte = time_cutoff)
|
recent_jobs = obj._get_related_jobs().filter(finished__gte=time_cutoff)
|
||||||
for unified_job in recent_jobs.get_real_instances():
|
for unified_job in recent_jobs.get_real_instances():
|
||||||
if not unified_job.event_processing_finished:
|
if not unified_job.event_processing_finished:
|
||||||
raise PermissionDenied(_(
|
raise PermissionDenied(_('Related job {} is still processing events.').format(unified_job.log_format))
|
||||||
'Related job {} is still processing events.'
|
|
||||||
).format(unified_job.log_format))
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationCountsMixin(object):
|
class OrganizationCountsMixin(object):
|
||||||
|
|
||||||
def get_serializer_context(self, *args, **kwargs):
|
def get_serializer_context(self, *args, **kwargs):
|
||||||
full_context = super(OrganizationCountsMixin, self).get_serializer_context(*args, **kwargs)
|
full_context = super(OrganizationCountsMixin, self).get_serializer_context(*args, **kwargs)
|
||||||
|
|
||||||
@@ -177,26 +164,23 @@ class OrganizationCountsMixin(object):
|
|||||||
# Produce counts of Foreign Key relationships
|
# Produce counts of Foreign Key relationships
|
||||||
db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
||||||
|
|
||||||
db_results['teams'] = Team.accessible_objects(
|
db_results['teams'] = (
|
||||||
self.request.user, 'read_role').values('organization').annotate(
|
Team.accessible_objects(self.request.user, 'read_role').values('organization').annotate(Count('organization')).order_by('organization')
|
||||||
Count('organization')).order_by('organization')
|
)
|
||||||
|
|
||||||
db_results['job_templates'] = jt_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
db_results['job_templates'] = jt_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
||||||
|
|
||||||
db_results['projects'] = project_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
db_results['projects'] = project_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
||||||
|
|
||||||
# Other members and admins of organization are always viewable
|
# Other members and admins of organization are always viewable
|
||||||
db_results['users'] = org_qs.annotate(
|
db_results['users'] = org_qs.annotate(users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True)).values(
|
||||||
users=Count('member_role__members', distinct=True),
|
'id', 'users', 'admins'
|
||||||
admins=Count('admin_role__members', distinct=True)
|
)
|
||||||
).values('id', 'users', 'admins')
|
|
||||||
|
|
||||||
count_context = {}
|
count_context = {}
|
||||||
for org in org_id_list:
|
for org in org_id_list:
|
||||||
org_id = org['id']
|
org_id = org['id']
|
||||||
count_context[org_id] = {
|
count_context[org_id] = {'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0, 'admins': 0, 'projects': 0}
|
||||||
'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0,
|
|
||||||
'admins': 0, 'projects': 0}
|
|
||||||
|
|
||||||
for res, count_qs in db_results.items():
|
for res, count_qs in db_results.items():
|
||||||
if res == 'users':
|
if res == 'users':
|
||||||
@@ -218,21 +202,20 @@ class OrganizationCountsMixin(object):
|
|||||||
|
|
||||||
|
|
||||||
class ControlledByScmMixin(object):
|
class ControlledByScmMixin(object):
|
||||||
'''
|
"""
|
||||||
Special method to reset SCM inventory commit hash
|
Special method to reset SCM inventory commit hash
|
||||||
if anything that it manages changes.
|
if anything that it manages changes.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def _reset_inv_src_rev(self, obj):
|
def _reset_inv_src_rev(self, obj):
|
||||||
if self.request.method in SAFE_METHODS or not obj:
|
if self.request.method in SAFE_METHODS or not obj:
|
||||||
return
|
return
|
||||||
project_following_sources = obj.inventory_sources.filter(
|
project_following_sources = obj.inventory_sources.filter(update_on_project_update=True, source='scm')
|
||||||
update_on_project_update=True, source='scm')
|
|
||||||
if project_following_sources:
|
if project_following_sources:
|
||||||
# Allow inventory changes unrelated to variables
|
# Allow inventory changes unrelated to variables
|
||||||
if self.model == Inventory and (
|
if self.model == Inventory and (
|
||||||
not self.request or not self.request.data or
|
not self.request or not self.request.data or parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)
|
||||||
parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)):
|
):
|
||||||
return
|
return
|
||||||
project_following_sources.update(scm_last_revision='')
|
project_following_sources.update(scm_last_revision='')
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from awx.main.models import (
|
|||||||
User,
|
User,
|
||||||
Team,
|
Team,
|
||||||
InstanceGroup,
|
InstanceGroup,
|
||||||
Credential
|
Credential,
|
||||||
)
|
)
|
||||||
from awx.api.generics import (
|
from awx.api.generics import (
|
||||||
ListCreateAPIView,
|
ListCreateAPIView,
|
||||||
@@ -47,13 +47,12 @@ from awx.api.serializers import (
|
|||||||
NotificationTemplateSerializer,
|
NotificationTemplateSerializer,
|
||||||
InstanceGroupSerializer,
|
InstanceGroupSerializer,
|
||||||
ExecutionEnvironmentSerializer,
|
ExecutionEnvironmentSerializer,
|
||||||
ProjectSerializer, JobTemplateSerializer, WorkflowJobTemplateSerializer,
|
ProjectSerializer,
|
||||||
CredentialSerializer
|
JobTemplateSerializer,
|
||||||
)
|
WorkflowJobTemplateSerializer,
|
||||||
from awx.api.views.mixin import (
|
CredentialSerializer,
|
||||||
RelatedJobsPreventDeleteMixin,
|
|
||||||
OrganizationCountsMixin,
|
|
||||||
)
|
)
|
||||||
|
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, OrganizationCountsMixin
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.views.organization')
|
logger = logging.getLogger('awx.api.views.organization')
|
||||||
|
|
||||||
@@ -84,23 +83,20 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
|
|||||||
|
|
||||||
org_counts = {}
|
org_counts = {}
|
||||||
access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'}
|
access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'}
|
||||||
direct_counts = Organization.objects.filter(id=org_id).annotate(
|
direct_counts = (
|
||||||
users=Count('member_role__members', distinct=True),
|
Organization.objects.filter(id=org_id)
|
||||||
admins=Count('admin_role__members', distinct=True)
|
.annotate(users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True))
|
||||||
).values('users', 'admins')
|
.values('users', 'admins')
|
||||||
|
)
|
||||||
|
|
||||||
if not direct_counts:
|
if not direct_counts:
|
||||||
return full_context
|
return full_context
|
||||||
|
|
||||||
org_counts = direct_counts[0]
|
org_counts = direct_counts[0]
|
||||||
org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(
|
org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||||
organization__id=org_id).count()
|
org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||||
org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(
|
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||||
organization__id=org_id).count()
|
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||||
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(
|
|
||||||
organization__id=org_id).count()
|
|
||||||
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(
|
|
||||||
organization__id=org_id).count()
|
|
||||||
org_counts['hosts'] = Host.objects.org_active_count(org_id)
|
org_counts['hosts'] = Host.objects.org_active_count(org_id)
|
||||||
|
|
||||||
full_context['related_field_counts'] = {}
|
full_context['related_field_counts'] = {}
|
||||||
@@ -240,14 +236,12 @@ class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
|
|||||||
|
|
||||||
def is_valid_relation(self, parent, sub, created=False):
|
def is_valid_relation(self, parent, sub, created=False):
|
||||||
if sub.kind != 'galaxy_api_token':
|
if sub.kind != 'galaxy_api_token':
|
||||||
return {'msg': _(
|
return {'msg': _(f"Credential must be a Galaxy credential, not {sub.credential_type.name}.")}
|
||||||
f"Credential must be a Galaxy credential, not {sub.credential_type.name}."
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationAccessList(ResourceAccessList):
|
class OrganizationAccessList(ResourceAccessList):
|
||||||
|
|
||||||
model = User # needs to be User for AccessLists's
|
model = User # needs to be User for AccessLists's
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
|
|
||||||
|
|
||||||
@@ -256,7 +250,7 @@ class OrganizationObjectRolesList(SubListAPIView):
|
|||||||
model = Role
|
model = Role
|
||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
search_fields = ('role_field', 'content_type__model',)
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
|
|||||||
@@ -24,22 +24,11 @@ from awx.api.generics import APIView
|
|||||||
from awx.conf.registry import settings_registry
|
from awx.conf.registry import settings_registry
|
||||||
from awx.main.analytics import all_collectors
|
from awx.main.analytics import all_collectors
|
||||||
from awx.main.ha import is_ha_environment
|
from awx.main.ha import is_ha_environment
|
||||||
from awx.main.utils import (
|
from awx.main.utils import get_awx_version, get_ansible_version, get_custom_venv_choices, to_python_boolean
|
||||||
get_awx_version,
|
|
||||||
get_ansible_version,
|
|
||||||
get_custom_venv_choices,
|
|
||||||
to_python_boolean,
|
|
||||||
)
|
|
||||||
from awx.main.utils.licensing import validate_entitlement_manifest
|
from awx.main.utils.licensing import validate_entitlement_manifest
|
||||||
from awx.api.versioning import reverse, drf_reverse
|
from awx.api.versioning import reverse, drf_reverse
|
||||||
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
||||||
from awx.main.models import (
|
from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate
|
||||||
Project,
|
|
||||||
Organization,
|
|
||||||
Instance,
|
|
||||||
InstanceGroup,
|
|
||||||
JobTemplate,
|
|
||||||
)
|
|
||||||
from awx.main.utils import set_environ
|
from awx.main.utils import set_environ
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.views.root')
|
logger = logging.getLogger('awx.api.views.root')
|
||||||
@@ -60,7 +49,7 @@ class ApiRootView(APIView):
|
|||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
data['description'] = _('AWX REST API')
|
data['description'] = _('AWX REST API')
|
||||||
data['current_version'] = v2
|
data['current_version'] = v2
|
||||||
data['available_versions'] = dict(v2 = v2)
|
data['available_versions'] = dict(v2=v2)
|
||||||
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
||||||
data['custom_logo'] = settings.CUSTOM_LOGO
|
data['custom_logo'] = settings.CUSTOM_LOGO
|
||||||
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
||||||
@@ -146,6 +135,7 @@ class ApiV2PingView(APIView):
|
|||||||
"""A simple view that reports very basic information about this
|
"""A simple view that reports very basic information about this
|
||||||
instance, which is acceptable to be public information.
|
instance, which is acceptable to be public information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
authentication_classes = ()
|
authentication_classes = ()
|
||||||
name = _('Ping')
|
name = _('Ping')
|
||||||
@@ -157,23 +147,19 @@ class ApiV2PingView(APIView):
|
|||||||
Everything returned here should be considered public / insecure, as
|
Everything returned here should be considered public / insecure, as
|
||||||
this requires no auth and is intended for use by the installer process.
|
this requires no auth and is intended for use by the installer process.
|
||||||
"""
|
"""
|
||||||
response = {
|
response = {'ha': is_ha_environment(), 'version': get_awx_version(), 'active_node': settings.CLUSTER_HOST_ID, 'install_uuid': settings.INSTALL_UUID}
|
||||||
'ha': is_ha_environment(),
|
|
||||||
'version': get_awx_version(),
|
|
||||||
'active_node': settings.CLUSTER_HOST_ID,
|
|
||||||
'install_uuid': settings.INSTALL_UUID,
|
|
||||||
}
|
|
||||||
|
|
||||||
response['instances'] = []
|
response['instances'] = []
|
||||||
for instance in Instance.objects.all():
|
for instance in Instance.objects.all():
|
||||||
response['instances'].append(dict(node=instance.hostname, uuid=instance.uuid, heartbeat=instance.modified,
|
response['instances'].append(
|
||||||
capacity=instance.capacity, version=instance.version))
|
dict(node=instance.hostname, uuid=instance.uuid, heartbeat=instance.modified, capacity=instance.capacity, version=instance.version)
|
||||||
|
)
|
||||||
sorted(response['instances'], key=operator.itemgetter('node'))
|
sorted(response['instances'], key=operator.itemgetter('node'))
|
||||||
response['instance_groups'] = []
|
response['instance_groups'] = []
|
||||||
for instance_group in InstanceGroup.objects.prefetch_related('instances'):
|
for instance_group in InstanceGroup.objects.prefetch_related('instances'):
|
||||||
response['instance_groups'].append(dict(name=instance_group.name,
|
response['instance_groups'].append(
|
||||||
capacity=instance_group.capacity,
|
dict(name=instance_group.name, capacity=instance_group.capacity, instances=[x.hostname for x in instance_group.instances.all()])
|
||||||
instances=[x.hostname for x in instance_group.instances.all()]))
|
)
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
@@ -190,6 +176,7 @@ class ApiV2SubscriptionView(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
from awx.main.utils.common import get_licenser
|
from awx.main.utils.common import get_licenser
|
||||||
|
|
||||||
data = request.data.copy()
|
data = request.data.copy()
|
||||||
if data.get('subscriptions_password') == '$encrypted$':
|
if data.get('subscriptions_password') == '$encrypted$':
|
||||||
data['subscriptions_password'] = settings.SUBSCRIPTIONS_PASSWORD
|
data['subscriptions_password'] = settings.SUBSCRIPTIONS_PASSWORD
|
||||||
@@ -203,10 +190,7 @@ class ApiV2SubscriptionView(APIView):
|
|||||||
settings.SUBSCRIPTIONS_PASSWORD = data['subscriptions_password']
|
settings.SUBSCRIPTIONS_PASSWORD = data['subscriptions_password']
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
msg = _("Invalid Subscription")
|
msg = _("Invalid Subscription")
|
||||||
if (
|
if isinstance(exc, requests.exceptions.HTTPError) and getattr(getattr(exc, 'response', None), 'status_code', None) == 401:
|
||||||
isinstance(exc, requests.exceptions.HTTPError) and
|
|
||||||
getattr(getattr(exc, 'response', None), 'status_code', None) == 401
|
|
||||||
):
|
|
||||||
msg = _("The provided credentials are invalid (HTTP 401).")
|
msg = _("The provided credentials are invalid (HTTP 401).")
|
||||||
elif isinstance(exc, requests.exceptions.ProxyError):
|
elif isinstance(exc, requests.exceptions.ProxyError):
|
||||||
msg = _("Unable to connect to proxy server.")
|
msg = _("Unable to connect to proxy server.")
|
||||||
@@ -215,8 +199,7 @@ class ApiV2SubscriptionView(APIView):
|
|||||||
elif isinstance(exc, (ValueError, OSError)) and exc.args:
|
elif isinstance(exc, (ValueError, OSError)) and exc.args:
|
||||||
msg = exc.args[0]
|
msg = exc.args[0]
|
||||||
else:
|
else:
|
||||||
logger.exception(smart_text(u"Invalid subscription submitted."),
|
logger.exception(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
|
||||||
extra=dict(actor=request.user.username))
|
|
||||||
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
return Response(validated)
|
return Response(validated)
|
||||||
@@ -242,16 +225,14 @@ class ApiV2AttachView(APIView):
|
|||||||
pw = getattr(settings, 'SUBSCRIPTIONS_PASSWORD', None)
|
pw = getattr(settings, 'SUBSCRIPTIONS_PASSWORD', None)
|
||||||
if pool_id and user and pw:
|
if pool_id and user and pw:
|
||||||
from awx.main.utils.common import get_licenser
|
from awx.main.utils.common import get_licenser
|
||||||
|
|
||||||
data = request.data.copy()
|
data = request.data.copy()
|
||||||
try:
|
try:
|
||||||
with set_environ(**settings.AWX_TASK_ENV):
|
with set_environ(**settings.AWX_TASK_ENV):
|
||||||
validated = get_licenser().validate_rh(user, pw)
|
validated = get_licenser().validate_rh(user, pw)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
msg = _("Invalid Subscription")
|
msg = _("Invalid Subscription")
|
||||||
if (
|
if isinstance(exc, requests.exceptions.HTTPError) and getattr(getattr(exc, 'response', None), 'status_code', None) == 401:
|
||||||
isinstance(exc, requests.exceptions.HTTPError) and
|
|
||||||
getattr(getattr(exc, 'response', None), 'status_code', None) == 401
|
|
||||||
):
|
|
||||||
msg = _("The provided credentials are invalid (HTTP 401).")
|
msg = _("The provided credentials are invalid (HTTP 401).")
|
||||||
elif isinstance(exc, requests.exceptions.ProxyError):
|
elif isinstance(exc, requests.exceptions.ProxyError):
|
||||||
msg = _("Unable to connect to proxy server.")
|
msg = _("Unable to connect to proxy server.")
|
||||||
@@ -260,8 +241,7 @@ class ApiV2AttachView(APIView):
|
|||||||
elif isinstance(exc, (ValueError, OSError)) and exc.args:
|
elif isinstance(exc, (ValueError, OSError)) and exc.args:
|
||||||
msg = exc.args[0]
|
msg = exc.args[0]
|
||||||
else:
|
else:
|
||||||
logger.exception(smart_text(u"Invalid subscription submitted."),
|
logger.exception(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
|
||||||
extra=dict(actor=request.user.username))
|
|
||||||
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
for sub in validated:
|
for sub in validated:
|
||||||
if sub['pool_id'] == pool_id:
|
if sub['pool_id'] == pool_id:
|
||||||
@@ -287,6 +267,7 @@ class ApiV2ConfigView(APIView):
|
|||||||
'''Return various sitewide configuration settings'''
|
'''Return various sitewide configuration settings'''
|
||||||
|
|
||||||
from awx.main.utils.common import get_licenser
|
from awx.main.utils.common import get_licenser
|
||||||
|
|
||||||
license_data = get_licenser().validate()
|
license_data = get_licenser().validate()
|
||||||
|
|
||||||
if not license_data.get('valid_key', False):
|
if not license_data.get('valid_key', False):
|
||||||
@@ -314,22 +295,23 @@ class ApiV2ConfigView(APIView):
|
|||||||
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
|
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
|
||||||
data['user_ldap_fields'] = user_ldap_fields
|
data['user_ldap_fields'] = user_ldap_fields
|
||||||
|
|
||||||
if request.user.is_superuser \
|
if (
|
||||||
or request.user.is_system_auditor \
|
request.user.is_superuser
|
||||||
or Organization.accessible_objects(request.user, 'admin_role').exists() \
|
or request.user.is_system_auditor
|
||||||
or Organization.accessible_objects(request.user, 'auditor_role').exists() \
|
or Organization.accessible_objects(request.user, 'admin_role').exists()
|
||||||
or Organization.accessible_objects(request.user, 'project_admin_role').exists():
|
or Organization.accessible_objects(request.user, 'auditor_role').exists()
|
||||||
data.update(dict(
|
or Organization.accessible_objects(request.user, 'project_admin_role').exists()
|
||||||
project_base_dir = settings.PROJECTS_ROOT,
|
):
|
||||||
project_local_paths = Project.get_local_path_choices(),
|
data.update(
|
||||||
custom_virtualenvs = get_custom_venv_choices()
|
dict(
|
||||||
))
|
project_base_dir=settings.PROJECTS_ROOT, project_local_paths=Project.get_local_path_choices(), custom_virtualenvs=get_custom_venv_choices()
|
||||||
|
)
|
||||||
|
)
|
||||||
elif JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
elif JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
||||||
data['custom_virtualenvs'] = get_custom_venv_choices()
|
data['custom_virtualenvs'] = get_custom_venv_choices()
|
||||||
|
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
return Response({"error": _("Invalid subscription data")}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": _("Invalid subscription data")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -346,11 +328,11 @@ class ApiV2ConfigView(APIView):
|
|||||||
try:
|
try:
|
||||||
data_actual = json.dumps(request.data)
|
data_actual = json.dumps(request.data)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.info(smart_text(u"Invalid JSON submitted for license."),
|
logger.info(smart_text(u"Invalid JSON submitted for license."), extra=dict(actor=request.user.username))
|
||||||
extra=dict(actor=request.user.username))
|
|
||||||
return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
from awx.main.utils.common import get_licenser
|
from awx.main.utils.common import get_licenser
|
||||||
|
|
||||||
license_data = json.loads(data_actual)
|
license_data = json.loads(data_actual)
|
||||||
if 'license_key' in license_data:
|
if 'license_key' in license_data:
|
||||||
return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -358,10 +340,7 @@ class ApiV2ConfigView(APIView):
|
|||||||
try:
|
try:
|
||||||
json_actual = json.loads(base64.b64decode(license_data['manifest']))
|
json_actual = json.loads(base64.b64decode(license_data['manifest']))
|
||||||
if 'license_key' in json_actual:
|
if 'license_key' in json_actual:
|
||||||
return Response(
|
return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{"error": _('Legacy license submitted. A subscription manifest is now required.')},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
@@ -375,8 +354,7 @@ class ApiV2ConfigView(APIView):
|
|||||||
try:
|
try:
|
||||||
license_data_validated = get_licenser().license_from_manifest(license_data)
|
license_data_validated = get_licenser().license_from_manifest(license_data)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(smart_text(u"Invalid subscription submitted."),
|
logger.warning(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
|
||||||
extra=dict(actor=request.user.username))
|
|
||||||
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
else:
|
else:
|
||||||
license_data_validated = get_licenser().validate()
|
license_data_validated = get_licenser().validate()
|
||||||
@@ -387,8 +365,7 @@ class ApiV2ConfigView(APIView):
|
|||||||
settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host())
|
settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host())
|
||||||
return Response(license_data_validated)
|
return Response(license_data_validated)
|
||||||
|
|
||||||
logger.warning(smart_text(u"Invalid subscription submitted."),
|
logger.warning(smart_text(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
|
||||||
extra=dict(actor=request.user.username))
|
|
||||||
return Response({"error": _("Invalid subscription")}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": _("Invalid subscription")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ class WebhookKeyView(GenericAPIView):
|
|||||||
permission_classes = (WebhookKeyPermission,)
|
permission_classes = (WebhookKeyPermission,)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs_models = {
|
qs_models = {'job_templates': JobTemplate, 'workflow_job_templates': WorkflowJobTemplate}
|
||||||
'job_templates': JobTemplate,
|
|
||||||
'workflow_job_templates': WorkflowJobTemplate,
|
|
||||||
}
|
|
||||||
self.model = qs_models.get(self.kwargs['model_kwarg'])
|
self.model = qs_models.get(self.kwargs['model_kwarg'])
|
||||||
|
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
@@ -57,10 +54,7 @@ class WebhookReceiverBase(APIView):
|
|||||||
ref_keys = {}
|
ref_keys = {}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs_models = {
|
qs_models = {'job_templates': JobTemplate, 'workflow_job_templates': WorkflowJobTemplate}
|
||||||
'job_templates': JobTemplate,
|
|
||||||
'workflow_job_templates': WorkflowJobTemplate,
|
|
||||||
}
|
|
||||||
model = qs_models.get(self.kwargs['model_kwarg'])
|
model = qs_models.get(self.kwargs['model_kwarg'])
|
||||||
if model is None:
|
if model is None:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
@@ -120,10 +114,7 @@ class WebhookReceiverBase(APIView):
|
|||||||
# Ensure that the full contents of the request are captured for multiple uses.
|
# Ensure that the full contents of the request are captured for multiple uses.
|
||||||
request.body
|
request.body
|
||||||
|
|
||||||
logger.debug(
|
logger.debug("headers: {}\n" "data: {}\n".format(request.headers, request.data))
|
||||||
"headers: {}\n"
|
|
||||||
"data: {}\n".format(request.headers, request.data)
|
|
||||||
)
|
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
self.check_signature(obj)
|
self.check_signature(obj)
|
||||||
|
|
||||||
@@ -132,16 +123,11 @@ class WebhookReceiverBase(APIView):
|
|||||||
event_ref = self.get_event_ref()
|
event_ref = self.get_event_ref()
|
||||||
status_api = self.get_event_status_api()
|
status_api = self.get_event_status_api()
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {'unified_job_template_id': obj.id, 'webhook_service': obj.webhook_service, 'webhook_guid': event_guid}
|
||||||
'unified_job_template_id': obj.id,
|
|
||||||
'webhook_service': obj.webhook_service,
|
|
||||||
'webhook_guid': event_guid,
|
|
||||||
}
|
|
||||||
if WorkflowJob.objects.filter(**kwargs).exists() or Job.objects.filter(**kwargs).exists():
|
if WorkflowJob.objects.filter(**kwargs).exists() or Job.objects.filter(**kwargs).exists():
|
||||||
# Short circuit if this webhook has already been received and acted upon.
|
# Short circuit if this webhook has already been received and acted upon.
|
||||||
logger.debug("Webhook previously received, returning without action.")
|
logger.debug("Webhook previously received, returning without action.")
|
||||||
return Response({'message': _("Webhook previously received, aborting.")},
|
return Response({'message': _("Webhook previously received, aborting.")}, status=status.HTTP_202_ACCEPTED)
|
||||||
status=status.HTTP_202_ACCEPTED)
|
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'_eager_fields': {
|
'_eager_fields': {
|
||||||
@@ -156,7 +142,7 @@ class WebhookReceiverBase(APIView):
|
|||||||
'tower_webhook_event_ref': event_ref,
|
'tower_webhook_event_ref': event_ref,
|
||||||
'tower_webhook_status_api': status_api,
|
'tower_webhook_status_api': status_api,
|
||||||
'tower_webhook_payload': request.data,
|
'tower_webhook_payload': request.data,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
new_job = obj.create_unified_job(**kwargs)
|
new_job = obj.create_unified_job(**kwargs)
|
||||||
@@ -205,11 +191,7 @@ class GithubWebhookReceiver(WebhookReceiverBase):
|
|||||||
class GitlabWebhookReceiver(WebhookReceiverBase):
|
class GitlabWebhookReceiver(WebhookReceiverBase):
|
||||||
service = 'gitlab'
|
service = 'gitlab'
|
||||||
|
|
||||||
ref_keys = {
|
ref_keys = {'Push Hook': 'checkout_sha', 'Tag Push Hook': 'checkout_sha', 'Merge Request Hook': 'object_attributes.last_commit.id'}
|
||||||
'Push Hook': 'checkout_sha',
|
|
||||||
'Tag Push Hook': 'checkout_sha',
|
|
||||||
'Merge Request Hook': 'object_attributes.last_commit.id',
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_event_type(self):
|
def get_event_type(self):
|
||||||
return self.request.META.get('HTTP_X_GITLAB_EVENT')
|
return self.request.META.get('HTTP_X_GITLAB_EVENT')
|
||||||
@@ -229,8 +211,7 @@ class GitlabWebhookReceiver(WebhookReceiverBase):
|
|||||||
return
|
return
|
||||||
parsed = urllib.parse.urlparse(repo_url)
|
parsed = urllib.parse.urlparse(repo_url)
|
||||||
|
|
||||||
return "{}://{}/api/v4/projects/{}/statuses/{}".format(
|
return "{}://{}/api/v4/projects/{}/statuses/{}".format(parsed.scheme, parsed.netloc, project['id'], self.get_event_ref())
|
||||||
parsed.scheme, parsed.netloc, project['id'], self.get_event_ref())
|
|
||||||
|
|
||||||
def get_signature(self):
|
def get_signature(self):
|
||||||
return force_bytes(self.request.META.get('HTTP_X_GITLAB_TOKEN') or '')
|
return force_bytes(self.request.META.get('HTTP_X_GITLAB_TOKEN') or '')
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import django
|
import django
|
||||||
from awx import __version__ as tower_version
|
from awx import __version__ as tower_version
|
||||||
|
|
||||||
# Prepare the AWX environment.
|
# Prepare the AWX environment.
|
||||||
from awx import prepare_env, MODE
|
from awx import prepare_env, MODE
|
||||||
from channels.routing import get_default_application # noqa
|
from channels.routing import get_default_application # noqa
|
||||||
prepare_env() # NOQA
|
|
||||||
|
|
||||||
|
prepare_env() # NOQA
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ from awx.conf.models import Setting
|
|||||||
|
|
||||||
|
|
||||||
class SettingAccess(BaseAccess):
|
class SettingAccess(BaseAccess):
|
||||||
'''
|
"""
|
||||||
- I can see settings when I am a super user or system auditor.
|
- I can see settings when I am a super user or system auditor.
|
||||||
- I can edit settings when I am a super user.
|
- I can edit settings when I am a super user.
|
||||||
- I can clear settings when I am a super user.
|
- I can clear settings when I am a super user.
|
||||||
- I can always see/edit/clear my own user settings.
|
- I can always see/edit/clear my own user settings.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
model = Setting
|
model = Setting
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Django
|
# Django
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
# from django.core import checks
|
# from django.core import checks
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
@@ -12,4 +13,5 @@ class ConfConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
self.module.autodiscover()
|
self.module.autodiscover()
|
||||||
from .settings import SettingsWrapper
|
from .settings import SettingsWrapper
|
||||||
|
|
||||||
SettingsWrapper.initialize()
|
SettingsWrapper.initialize()
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ from django.core.validators import URLValidator, _lazy_re_compile
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework.fields import ( # noqa
|
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, DateTimeField, EmailField, IntegerField, ListField, NullBooleanField # noqa
|
||||||
BooleanField, CharField, ChoiceField, DictField, DateTimeField, EmailField,
|
|
||||||
IntegerField, ListField, NullBooleanField
|
|
||||||
)
|
|
||||||
from rest_framework.serializers import PrimaryKeyRelatedField # noqa
|
from rest_framework.serializers import PrimaryKeyRelatedField # noqa
|
||||||
|
|
||||||
logger = logging.getLogger('awx.conf.fields')
|
logger = logging.getLogger('awx.conf.fields')
|
||||||
@@ -27,7 +24,6 @@ logger = logging.getLogger('awx.conf.fields')
|
|||||||
|
|
||||||
|
|
||||||
class CharField(CharField):
|
class CharField(CharField):
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
# django_rest_frameworks' default CharField implementation casts `None`
|
# django_rest_frameworks' default CharField implementation casts `None`
|
||||||
# to a string `"None"`:
|
# to a string `"None"`:
|
||||||
@@ -39,7 +35,6 @@ class CharField(CharField):
|
|||||||
|
|
||||||
|
|
||||||
class IntegerField(IntegerField):
|
class IntegerField(IntegerField):
|
||||||
|
|
||||||
def get_value(self, dictionary):
|
def get_value(self, dictionary):
|
||||||
ret = super(IntegerField, self).get_value(dictionary)
|
ret = super(IntegerField, self).get_value(dictionary)
|
||||||
# Handle UI corner case
|
# Handle UI corner case
|
||||||
@@ -60,9 +55,7 @@ class StringListField(ListField):
|
|||||||
|
|
||||||
class StringListBooleanField(ListField):
|
class StringListBooleanField(ListField):
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.')}
|
||||||
'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.'),
|
|
||||||
}
|
|
||||||
child = CharField()
|
child = CharField()
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
@@ -101,10 +94,7 @@ class StringListBooleanField(ListField):
|
|||||||
|
|
||||||
class StringListPathField(StringListField):
|
class StringListPathField(StringListField):
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {'type_error': _('Expected list of strings but got {input_type} instead.'), 'path_error': _('{path} is not a valid path choice.')}
|
||||||
'type_error': _('Expected list of strings but got {input_type} instead.'),
|
|
||||||
'path_error': _('{path} is not a valid path choice.'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_internal_value(self, paths):
|
def to_internal_value(self, paths):
|
||||||
if isinstance(paths, (list, tuple)):
|
if isinstance(paths, (list, tuple)):
|
||||||
@@ -123,12 +113,12 @@ class URLField(CharField):
|
|||||||
# these lines set up a custom regex that allow numbers in the
|
# these lines set up a custom regex that allow numbers in the
|
||||||
# top-level domain
|
# top-level domain
|
||||||
tld_re = (
|
tld_re = (
|
||||||
r'\.' # dot
|
r'\.' # dot
|
||||||
r'(?!-)' # can't start with a dash
|
r'(?!-)' # can't start with a dash
|
||||||
r'(?:[a-z' + URLValidator.ul + r'0-9' + '-]{2,63}' # domain label, this line was changed from the original URLValidator
|
r'(?:[a-z' + URLValidator.ul + r'0-9' + '-]{2,63}' # domain label, this line was changed from the original URLValidator
|
||||||
r'|xn--[a-z0-9]{1,59})' # or punycode label
|
r'|xn--[a-z0-9]{1,59})' # or punycode label
|
||||||
r'(?<!-)' # can't end with a dash
|
r'(?<!-)' # can't end with a dash
|
||||||
r'\.?' # may have a trailing dot
|
r'\.?' # may have a trailing dot
|
||||||
)
|
)
|
||||||
|
|
||||||
host_re = '(' + URLValidator.hostname_re + URLValidator.domain_re + tld_re + '|localhost)'
|
host_re = '(' + URLValidator.hostname_re + URLValidator.domain_re + tld_re + '|localhost)'
|
||||||
@@ -139,7 +129,9 @@ class URLField(CharField):
|
|||||||
r'(?:' + URLValidator.ipv4_re + '|' + URLValidator.ipv6_re + '|' + host_re + ')'
|
r'(?:' + URLValidator.ipv4_re + '|' + URLValidator.ipv6_re + '|' + host_re + ')'
|
||||||
r'(?::\d{2,5})?' # port
|
r'(?::\d{2,5})?' # port
|
||||||
r'(?:[/?#][^\s]*)?' # resource path
|
r'(?:[/?#][^\s]*)?' # resource path
|
||||||
r'\Z', re.IGNORECASE)
|
r'\Z',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
schemes = kwargs.pop('schemes', None)
|
schemes = kwargs.pop('schemes', None)
|
||||||
@@ -184,9 +176,7 @@ class URLField(CharField):
|
|||||||
|
|
||||||
class KeyValueField(DictField):
|
class KeyValueField(DictField):
|
||||||
child = CharField()
|
child = CharField()
|
||||||
default_error_messages = {
|
default_error_messages = {'invalid_child': _('"{input}" is not a valid string.')}
|
||||||
'invalid_child': _('"{input}" is not a valid string.')
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
ret = super(KeyValueField, self).to_internal_value(data)
|
ret = super(KeyValueField, self).to_internal_value(data)
|
||||||
@@ -199,9 +189,7 @@ class KeyValueField(DictField):
|
|||||||
|
|
||||||
|
|
||||||
class ListTuplesField(ListField):
|
class ListTuplesField(ListField):
|
||||||
default_error_messages = {
|
default_error_messages = {'type_error': _('Expected a list of tuples of max length 2 but got {input_type} instead.')}
|
||||||
'type_error': _('Expected a list of tuples of max length 2 but got {input_type} instead.'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if isinstance(value, (list, tuple)):
|
if isinstance(value, (list, tuple)):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ __all__ = ['get_license']
|
|||||||
|
|
||||||
def _get_validated_license_data():
|
def _get_validated_license_data():
|
||||||
from awx.main.utils import get_licenser
|
from awx.main.utils import get_licenser
|
||||||
|
|
||||||
return get_licenser().validate()
|
return get_licenser().validate()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@@ -21,11 +19,11 @@ class Migration(migrations.Migration):
|
|||||||
('modified', models.DateTimeField(default=None, editable=False)),
|
('modified', models.DateTimeField(default=None, editable=False)),
|
||||||
('key', models.CharField(max_length=255)),
|
('key', models.CharField(max_length=255)),
|
||||||
('value', jsonfield.fields.JSONField(null=True)),
|
('value', jsonfield.fields.JSONField(null=True)),
|
||||||
('user', models.ForeignKey(related_name='settings', default=None, editable=False,
|
(
|
||||||
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)),
|
'user',
|
||||||
|
models.ForeignKey(related_name='settings', default=None, editable=False, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={'abstract': False},
|
||||||
'abstract': False,
|
)
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,11 +15,7 @@ def copy_tower_settings(apps, schema_editor):
|
|||||||
if tower_setting.key == 'LICENSE':
|
if tower_setting.key == 'LICENSE':
|
||||||
value = json.loads(value)
|
value = json.loads(value)
|
||||||
setting, created = Setting.objects.get_or_create(
|
setting, created = Setting.objects.get_or_create(
|
||||||
key=tower_setting.key,
|
key=tower_setting.key, user=tower_setting.user, created=tower_setting.created, modified=tower_setting.modified, defaults=dict(value=value)
|
||||||
user=tower_setting.user,
|
|
||||||
created=tower_setting.created,
|
|
||||||
modified=tower_setting.modified,
|
|
||||||
defaults=dict(value=value),
|
|
||||||
)
|
)
|
||||||
if not created and setting.value != value:
|
if not created and setting.value != value:
|
||||||
setting.value = value
|
setting.value = value
|
||||||
@@ -36,18 +32,9 @@ def revert_tower_settings(apps, schema_editor):
|
|||||||
# LICENSE is stored as a JSON object; convert it back to a string.
|
# LICENSE is stored as a JSON object; convert it back to a string.
|
||||||
if setting.key == 'LICENSE':
|
if setting.key == 'LICENSE':
|
||||||
value = json.dumps(value)
|
value = json.dumps(value)
|
||||||
defaults = dict(
|
defaults = dict(value=value, value_type='string', description='', category='')
|
||||||
value=value,
|
|
||||||
value_type='string',
|
|
||||||
description='',
|
|
||||||
category='',
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
tower_setting, created = TowerSettings.objects.get_or_create(
|
tower_setting, created = TowerSettings.objects.get_or_create(key=setting.key, user=setting.user, defaults=defaults)
|
||||||
key=setting.key,
|
|
||||||
user=setting.user,
|
|
||||||
defaults=defaults,
|
|
||||||
)
|
|
||||||
if not created:
|
if not created:
|
||||||
update_fields = []
|
update_fields = []
|
||||||
for k, v in defaults.items():
|
for k, v in defaults.items():
|
||||||
@@ -62,15 +49,8 @@ def revert_tower_settings(apps, schema_editor):
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [('conf', '0001_initial'), ('main', '0004_squashed_v310_release')]
|
||||||
('conf', '0001_initial'),
|
|
||||||
('main', '0004_squashed_v310_release'),
|
|
||||||
]
|
|
||||||
|
|
||||||
run_before = [
|
run_before = [('main', '0005_squashed_v310_v313_updates')]
|
||||||
('main', '0005_squashed_v310_v313_updates'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [migrations.RunPython(copy_tower_settings, revert_tower_settings)]
|
||||||
migrations.RunPython(copy_tower_settings, revert_tower_settings),
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import awx.main.fields
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [('conf', '0002_v310_copy_tower_settings')]
|
||||||
('conf', '0002_v310_copy_tower_settings'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [migrations.AlterField(model_name='setting', name='value', field=awx.main.fields.JSONField(null=True))]
|
||||||
migrations.AlterField(
|
|
||||||
model_name='setting',
|
|
||||||
name='value',
|
|
||||||
field=awx.main.fields.JSONField(null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [('conf', '0003_v310_JSONField_changes')]
|
||||||
('conf', '0003_v310_JSONField_changes'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
# This list is intentionally empty.
|
# This list is intentionally empty.
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ def reverse_copy_session_settings(apps, schema_editor):
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [('conf', '0004_v320_reencrypt')]
|
||||||
('conf', '0004_v320_reencrypt'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(copy_session_settings, reverse_copy_session_settings),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(copy_session_settings, reverse_copy_session_settings)]
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ from django.db import migrations
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [('conf', '0005_v330_rename_two_session_settings')]
|
||||||
('conf', '0005_v330_rename_two_session_settings'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [migrations.RunPython(fill_ldap_group_type_params)]
|
||||||
migrations.RunPython(fill_ldap_group_type_params),
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ def copy_allowed_ips(apps, schema_editor):
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [('conf', '0006_v331_ldap_group_type')]
|
||||||
('conf', '0006_v331_ldap_group_type'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [migrations.RunPython(copy_allowed_ips)]
|
||||||
migrations.RunPython(copy_allowed_ips),
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ def _noop(apps, schema_editor):
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [('conf', '0007_v380_rename_more_settings')]
|
||||||
('conf', '0007_v380_rename_more_settings'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(clear_old_license, _noop), migrations.RunPython(prefill_rh_credentials, _noop)]
|
||||||
operations = [
|
|
||||||
migrations.RunPython(clear_old_license, _noop),
|
|
||||||
migrations.RunPython(prefill_rh_credentials, _noop)
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -16,10 +15,7 @@ def fill_ldap_group_type_params(apps, schema_editor):
|
|||||||
entry = qs[0]
|
entry = qs[0]
|
||||||
group_type_params = entry.value
|
group_type_params = entry.value
|
||||||
else:
|
else:
|
||||||
entry = Setting(key='AUTH_LDAP_GROUP_TYPE_PARAMS',
|
entry = Setting(key='AUTH_LDAP_GROUP_TYPE_PARAMS', value=group_type_params, created=now(), modified=now())
|
||||||
value=group_type_params,
|
|
||||||
created=now(),
|
|
||||||
modified=now())
|
|
||||||
|
|
||||||
init_attrs = set(inspect.getargspec(group_type.__init__).args[1:])
|
init_attrs = set(inspect.getargspec(group_type.__init__).args[1:])
|
||||||
for k in list(group_type_params.keys()):
|
for k in list(group_type_params.keys()):
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ __all__ = ['get_encryption_key', 'decrypt_field']
|
|||||||
|
|
||||||
|
|
||||||
def get_encryption_key(field_name, pk=None):
|
def get_encryption_key(field_name, pk=None):
|
||||||
'''
|
"""
|
||||||
Generate key for encrypted password based on field name,
|
Generate key for encrypted password based on field name,
|
||||||
``settings.SECRET_KEY``, and instance pk (if available).
|
``settings.SECRET_KEY``, and instance pk (if available).
|
||||||
|
|
||||||
:param pk: (optional) the primary key of the ``awx.conf.model.Setting``;
|
:param pk: (optional) the primary key of the ``awx.conf.model.Setting``;
|
||||||
can be omitted in situations where you're encrypting a setting
|
can be omitted in situations where you're encrypting a setting
|
||||||
that is not database-persistent (like a read-only setting)
|
that is not database-persistent (like a read-only setting)
|
||||||
'''
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
h = hashlib.sha1()
|
h = hashlib.sha1()
|
||||||
h.update(settings.SECRET_KEY)
|
h.update(settings.SECRET_KEY)
|
||||||
if pk is not None:
|
if pk is not None:
|
||||||
@@ -29,11 +30,11 @@ def get_encryption_key(field_name, pk=None):
|
|||||||
|
|
||||||
|
|
||||||
def decrypt_value(encryption_key, value):
|
def decrypt_value(encryption_key, value):
|
||||||
raw_data = value[len('$encrypted$'):]
|
raw_data = value[len('$encrypted$') :]
|
||||||
# If the encrypted string contains a UTF8 marker, discard it
|
# If the encrypted string contains a UTF8 marker, discard it
|
||||||
utf8 = raw_data.startswith('UTF8$')
|
utf8 = raw_data.startswith('UTF8$')
|
||||||
if utf8:
|
if utf8:
|
||||||
raw_data = raw_data[len('UTF8$'):]
|
raw_data = raw_data[len('UTF8$') :]
|
||||||
algo, b64data = raw_data.split('$', 1)
|
algo, b64data = raw_data.split('$', 1)
|
||||||
if algo != 'AES':
|
if algo != 'AES':
|
||||||
raise ValueError('unsupported algorithm: %s' % algo)
|
raise ValueError('unsupported algorithm: %s' % algo)
|
||||||
@@ -48,9 +49,9 @@ def decrypt_value(encryption_key, value):
|
|||||||
|
|
||||||
|
|
||||||
def decrypt_field(instance, field_name, subfield=None):
|
def decrypt_field(instance, field_name, subfield=None):
|
||||||
'''
|
"""
|
||||||
Return content of the given instance and field name decrypted.
|
Return content of the given instance and field name decrypted.
|
||||||
'''
|
"""
|
||||||
value = getattr(instance, field_name)
|
value = getattr(instance, field_name)
|
||||||
if isinstance(value, dict) and subfield is not None:
|
if isinstance(value, dict) and subfield is not None:
|
||||||
value = value[subfield]
|
value = value[subfield]
|
||||||
|
|||||||
@@ -24,9 +24,4 @@ def rename_setting(apps, schema_editor, old_key, new_key):
|
|||||||
if hasattr(settings, old_key):
|
if hasattr(settings, old_key):
|
||||||
old_setting = getattr(settings, old_key)
|
old_setting = getattr(settings, old_key)
|
||||||
if old_setting is not None:
|
if old_setting is not None:
|
||||||
Setting.objects.create(key=new_key,
|
Setting.objects.create(key=new_key, value=old_setting, created=now(), modified=now())
|
||||||
value=old_setting,
|
|
||||||
created=now(),
|
|
||||||
modified=now()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ def _migrate_setting(apps, old_key, new_key, encrypted=False):
|
|||||||
Setting = apps.get_model('conf', 'Setting')
|
Setting = apps.get_model('conf', 'Setting')
|
||||||
if not Setting.objects.filter(key=old_key).exists():
|
if not Setting.objects.filter(key=old_key).exists():
|
||||||
return
|
return
|
||||||
new_setting = Setting.objects.create(key=new_key,
|
new_setting = Setting.objects.create(key=new_key, created=now(), modified=now())
|
||||||
created=now(),
|
|
||||||
modified=now()
|
|
||||||
)
|
|
||||||
if encrypted:
|
if encrypted:
|
||||||
new_setting.value = decrypt_field(Setting.objects.filter(key=old_key).first(), 'value')
|
new_setting.value = decrypt_field(Setting.objects.filter(key=old_key).first(), 'value')
|
||||||
new_setting.value = encrypt_field(new_setting, 'value')
|
new_setting.value = encrypt_field(new_setting, 'value')
|
||||||
|
|||||||
@@ -18,20 +18,9 @@ __all__ = ['Setting']
|
|||||||
|
|
||||||
class Setting(CreatedModifiedModel):
|
class Setting(CreatedModifiedModel):
|
||||||
|
|
||||||
key = models.CharField(
|
key = models.CharField(max_length=255)
|
||||||
max_length=255,
|
value = JSONField(null=True)
|
||||||
)
|
user = prevent_search(models.ForeignKey('auth.User', related_name='settings', default=None, null=True, editable=False, on_delete=models.CASCADE))
|
||||||
value = JSONField(
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
user = prevent_search(models.ForeignKey(
|
|
||||||
'auth.User',
|
|
||||||
related_name='settings',
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
editable=False,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
))
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
try:
|
try:
|
||||||
@@ -66,6 +55,7 @@ class Setting(CreatedModifiedModel):
|
|||||||
# field and save again.
|
# field and save again.
|
||||||
if encrypted and new_instance:
|
if encrypted and new_instance:
|
||||||
from awx.main.signals import disable_activity_stream
|
from awx.main.signals import disable_activity_stream
|
||||||
|
|
||||||
with disable_activity_stream():
|
with disable_activity_stream():
|
||||||
self.value = self._saved_value
|
self.value = self._saved_value
|
||||||
self.save(update_fields=['value'])
|
self.save(update_fields=['value'])
|
||||||
@@ -82,6 +72,7 @@ class Setting(CreatedModifiedModel):
|
|||||||
import awx.conf.signals # noqa
|
import awx.conf.signals # noqa
|
||||||
|
|
||||||
from awx.main.registrar import activity_stream_registrar # noqa
|
from awx.main.registrar import activity_stream_registrar # noqa
|
||||||
|
|
||||||
activity_stream_registrar.connect(Setting)
|
activity_stream_registrar.connect(Setting)
|
||||||
|
|
||||||
import awx.conf.access # noqa
|
import awx.conf.access # noqa
|
||||||
|
|||||||
@@ -69,10 +69,7 @@ class SettingsRegistry(object):
|
|||||||
return self._dependent_settings.get(setting, set())
|
return self._dependent_settings.get(setting, set())
|
||||||
|
|
||||||
def get_registered_categories(self):
|
def get_registered_categories(self):
|
||||||
categories = {
|
categories = {'all': _('All'), 'changed': _('Changed')}
|
||||||
'all': _('All'),
|
|
||||||
'changed': _('Changed'),
|
|
||||||
}
|
|
||||||
for setting, kwargs in self._registry.items():
|
for setting, kwargs in self._registry.items():
|
||||||
category_slug = kwargs.get('category_slug', None)
|
category_slug = kwargs.get('category_slug', None)
|
||||||
if category_slug is None or category_slug in categories:
|
if category_slug is None or category_slug in categories:
|
||||||
@@ -95,8 +92,11 @@ class SettingsRegistry(object):
|
|||||||
continue
|
continue
|
||||||
if kwargs.get('category_slug', None) in slugs_to_ignore:
|
if kwargs.get('category_slug', None) in slugs_to_ignore:
|
||||||
continue
|
continue
|
||||||
if (read_only in {True, False} and kwargs.get('read_only', False) != read_only and
|
if (
|
||||||
setting not in ('INSTALL_UUID', 'AWX_ISOLATED_PRIVATE_KEY', 'AWX_ISOLATED_PUBLIC_KEY')):
|
read_only in {True, False}
|
||||||
|
and kwargs.get('read_only', False) != read_only
|
||||||
|
and setting not in ('INSTALL_UUID', 'AWX_ISOLATED_PRIVATE_KEY', 'AWX_ISOLATED_PUBLIC_KEY')
|
||||||
|
):
|
||||||
# Note: Doesn't catch fields that set read_only via __init__;
|
# Note: Doesn't catch fields that set read_only via __init__;
|
||||||
# read-only field kwargs should always include read_only=True.
|
# read-only field kwargs should always include read_only=True.
|
||||||
continue
|
continue
|
||||||
@@ -117,6 +117,7 @@ class SettingsRegistry(object):
|
|||||||
|
|
||||||
def get_setting_field(self, setting, mixin_class=None, for_user=False, **kwargs):
|
def get_setting_field(self, setting, mixin_class=None, for_user=False, **kwargs):
|
||||||
from rest_framework.fields import empty
|
from rest_framework.fields import empty
|
||||||
|
|
||||||
field_kwargs = {}
|
field_kwargs = {}
|
||||||
field_kwargs.update(self._registry[setting])
|
field_kwargs.update(self._registry[setting])
|
||||||
field_kwargs.update(kwargs)
|
field_kwargs.update(kwargs)
|
||||||
@@ -141,11 +142,7 @@ class SettingsRegistry(object):
|
|||||||
field_instance.placeholder = placeholder
|
field_instance.placeholder = placeholder
|
||||||
field_instance.defined_in_file = defined_in_file
|
field_instance.defined_in_file = defined_in_file
|
||||||
if field_instance.defined_in_file:
|
if field_instance.defined_in_file:
|
||||||
field_instance.help_text = (
|
field_instance.help_text = str(_('This value has been set manually in a settings file.')) + '\n\n' + str(field_instance.help_text)
|
||||||
str(_('This value has been set manually in a settings file.')) +
|
|
||||||
'\n\n' +
|
|
||||||
str(field_instance.help_text)
|
|
||||||
)
|
|
||||||
field_instance.encrypted = encrypted
|
field_instance.encrypted = encrypted
|
||||||
original_field_instance = field_instance
|
original_field_instance = field_instance
|
||||||
if field_class != original_field_class:
|
if field_class != original_field_class:
|
||||||
|
|||||||
@@ -30,15 +30,9 @@ class SettingSerializer(BaseSerializer):
|
|||||||
class SettingCategorySerializer(serializers.Serializer):
|
class SettingCategorySerializer(serializers.Serializer):
|
||||||
"""Serialize setting category """
|
"""Serialize setting category """
|
||||||
|
|
||||||
url = serializers.CharField(
|
url = serializers.CharField(read_only=True)
|
||||||
read_only=True,
|
slug = serializers.CharField(read_only=True)
|
||||||
)
|
name = serializers.CharField(read_only=True)
|
||||||
slug = serializers.CharField(
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
name = serializers.CharField(
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SettingFieldMixin(object):
|
class SettingFieldMixin(object):
|
||||||
|
|||||||
@@ -62,12 +62,12 @@ __all__ = ['SettingsWrapper', 'get_settings_to_cache', 'SETTING_CACHE_NOTSET']
|
|||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _ctit_db_wrapper(trans_safe=False):
|
def _ctit_db_wrapper(trans_safe=False):
|
||||||
'''
|
"""
|
||||||
Wrapper to avoid undesired actions by Django ORM when managing settings
|
Wrapper to avoid undesired actions by Django ORM when managing settings
|
||||||
if only getting a setting, can use trans_safe=True, which will avoid
|
if only getting a setting, can use trans_safe=True, which will avoid
|
||||||
throwing errors if the prior context was a broken transaction.
|
throwing errors if the prior context was a broken transaction.
|
||||||
Any database errors will be logged, but exception will be suppressed.
|
Any database errors will be logged, but exception will be suppressed.
|
||||||
'''
|
"""
|
||||||
rollback_set = None
|
rollback_set = None
|
||||||
is_atomic = None
|
is_atomic = None
|
||||||
try:
|
try:
|
||||||
@@ -115,7 +115,6 @@ class TransientSetting(object):
|
|||||||
|
|
||||||
|
|
||||||
class EncryptedCacheProxy(object):
|
class EncryptedCacheProxy(object):
|
||||||
|
|
||||||
def __init__(self, cache, registry, encrypter=None, decrypter=None):
|
def __init__(self, cache, registry, encrypter=None, decrypter=None):
|
||||||
"""
|
"""
|
||||||
This proxy wraps a Django cache backend and overwrites the
|
This proxy wraps a Django cache backend and overwrites the
|
||||||
@@ -145,19 +144,11 @@ class EncryptedCacheProxy(object):
|
|||||||
|
|
||||||
def set(self, key, value, log=True, **kwargs):
|
def set(self, key, value, log=True, **kwargs):
|
||||||
if log is True:
|
if log is True:
|
||||||
logger.debug('cache set(%r, %r, %r)', key, filter_sensitive(self.registry, key, value),
|
logger.debug('cache set(%r, %r, %r)', key, filter_sensitive(self.registry, key, value), SETTING_CACHE_TIMEOUT)
|
||||||
SETTING_CACHE_TIMEOUT)
|
self.cache.set(key, self._handle_encryption(self.encrypter, key, value), **kwargs)
|
||||||
self.cache.set(
|
|
||||||
key,
|
|
||||||
self._handle_encryption(self.encrypter, key, value),
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_many(self, data, **kwargs):
|
def set_many(self, data, **kwargs):
|
||||||
filtered_data = dict(
|
filtered_data = dict((key, filter_sensitive(self.registry, key, value)) for key, value in data.items())
|
||||||
(key, filter_sensitive(self.registry, key, value))
|
|
||||||
for key, value in data.items()
|
|
||||||
)
|
|
||||||
logger.debug('cache set_many(%r, %r)', filtered_data, SETTING_CACHE_TIMEOUT)
|
logger.debug('cache set_many(%r, %r)', filtered_data, SETTING_CACHE_TIMEOUT)
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
self.set(key, value, log=False, **kwargs)
|
self.set(key, value, log=False, **kwargs)
|
||||||
@@ -168,18 +159,11 @@ class EncryptedCacheProxy(object):
|
|||||||
# as part of the AES key when encrypting/decrypting
|
# as part of the AES key when encrypting/decrypting
|
||||||
obj_id = self.cache.get(Setting.get_cache_id_key(key), default=empty)
|
obj_id = self.cache.get(Setting.get_cache_id_key(key), default=empty)
|
||||||
if obj_id is empty:
|
if obj_id is empty:
|
||||||
logger.info('Efficiency notice: Corresponding id not stored in cache %s',
|
logger.info('Efficiency notice: Corresponding id not stored in cache %s', Setting.get_cache_id_key(key))
|
||||||
Setting.get_cache_id_key(key))
|
|
||||||
obj_id = getattr(self._get_setting_from_db(key), 'pk', None)
|
obj_id = getattr(self._get_setting_from_db(key), 'pk', None)
|
||||||
elif obj_id == SETTING_CACHE_NONE:
|
elif obj_id == SETTING_CACHE_NONE:
|
||||||
obj_id = None
|
obj_id = None
|
||||||
return method(
|
return method(TransientSetting(pk=obj_id, value=value), 'value')
|
||||||
TransientSetting(
|
|
||||||
pk=obj_id,
|
|
||||||
value=value
|
|
||||||
),
|
|
||||||
'value'
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the field in question isn't an "encrypted" field, this function is
|
# If the field in question isn't an "encrypted" field, this function is
|
||||||
# a no-op; it just returns the provided value
|
# a no-op; it just returns the provided value
|
||||||
@@ -206,9 +190,9 @@ def get_settings_to_cache(registry):
|
|||||||
|
|
||||||
|
|
||||||
def get_cache_value(value):
|
def get_cache_value(value):
|
||||||
'''Returns the proper special cache setting for a value
|
"""Returns the proper special cache setting for a value
|
||||||
based on instance type.
|
based on instance type.
|
||||||
'''
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
value = SETTING_CACHE_NONE
|
value = SETTING_CACHE_NONE
|
||||||
elif isinstance(value, (list, tuple)) and len(value) == 0:
|
elif isinstance(value, (list, tuple)) and len(value) == 0:
|
||||||
@@ -219,7 +203,6 @@ def get_cache_value(value):
|
|||||||
|
|
||||||
|
|
||||||
class SettingsWrapper(UserSettingsHolder):
|
class SettingsWrapper(UserSettingsHolder):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initialize(cls, cache=None, registry=None):
|
def initialize(cls, cache=None, registry=None):
|
||||||
"""
|
"""
|
||||||
@@ -231,11 +214,7 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
``awx.conf.settings_registry`` is used by default.
|
``awx.conf.settings_registry`` is used by default.
|
||||||
"""
|
"""
|
||||||
if not getattr(settings, '_awx_conf_settings', False):
|
if not getattr(settings, '_awx_conf_settings', False):
|
||||||
settings_wrapper = cls(
|
settings_wrapper = cls(settings._wrapped, cache=cache or django_cache, registry=registry or settings_registry)
|
||||||
settings._wrapped,
|
|
||||||
cache=cache or django_cache,
|
|
||||||
registry=registry or settings_registry
|
|
||||||
)
|
|
||||||
settings._wrapped = settings_wrapper
|
settings._wrapped = settings_wrapper
|
||||||
|
|
||||||
def __init__(self, default_settings, cache, registry):
|
def __init__(self, default_settings, cache, registry):
|
||||||
@@ -322,7 +301,7 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
try:
|
try:
|
||||||
value = decrypt_field(setting, 'value')
|
value = decrypt_field(setting, 'value')
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
#TODO: Remove in Tower 3.3
|
# TODO: Remove in Tower 3.3
|
||||||
logger.debug('encountered error decrypting field: %s - attempting fallback to old', e)
|
logger.debug('encountered error decrypting field: %s - attempting fallback to old', e)
|
||||||
value = old_decrypt_field(setting, 'value')
|
value = old_decrypt_field(setting, 'value')
|
||||||
|
|
||||||
@@ -345,8 +324,7 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
# Generate a cache key for each setting and store them all at once.
|
# Generate a cache key for each setting and store them all at once.
|
||||||
settings_to_cache = dict([(Setting.get_cache_key(k), v) for k, v in settings_to_cache.items()])
|
settings_to_cache = dict([(Setting.get_cache_key(k), v) for k, v in settings_to_cache.items()])
|
||||||
for k, id_val in setting_ids.items():
|
for k, id_val in setting_ids.items():
|
||||||
logger.debug('Saving id in cache for encrypted setting %s, %s',
|
logger.debug('Saving id in cache for encrypted setting %s, %s', Setting.get_cache_id_key(k), id_val)
|
||||||
Setting.get_cache_id_key(k), id_val)
|
|
||||||
self.cache.cache.set(Setting.get_cache_id_key(k), id_val)
|
self.cache.cache.set(Setting.get_cache_id_key(k), id_val)
|
||||||
settings_to_cache['_awx_conf_preload_expires'] = self._awx_conf_preload_expires
|
settings_to_cache['_awx_conf_preload_expires'] = self._awx_conf_preload_expires
|
||||||
self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT)
|
self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT)
|
||||||
@@ -420,9 +398,7 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
else:
|
else:
|
||||||
return value
|
return value
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning('The current value "%r" for setting "%s" is invalid.', value, name, exc_info=True)
|
||||||
'The current value "%r" for setting "%s" is invalid.',
|
|
||||||
value, name, exc_info=True)
|
|
||||||
return empty
|
return empty
|
||||||
|
|
||||||
def _get_default(self, name):
|
def _get_default(self, name):
|
||||||
@@ -453,8 +429,7 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
setting_value = field.run_validation(data)
|
setting_value = field.run_validation(data)
|
||||||
db_value = field.to_representation(setting_value)
|
db_value = field.to_representation(setting_value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception('Unable to assign value "%r" to setting "%s".',
|
logger.exception('Unable to assign value "%r" to setting "%s".', value, name, exc_info=True)
|
||||||
value, name, exc_info=True)
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
setting = Setting.objects.filter(key=name, user__isnull=True).order_by('pk').first()
|
setting = Setting.objects.filter(key=name, user__isnull=True).order_by('pk').first()
|
||||||
@@ -492,8 +467,7 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
def __dir__(self):
|
def __dir__(self):
|
||||||
keys = []
|
keys = []
|
||||||
with _ctit_db_wrapper(trans_safe=True):
|
with _ctit_db_wrapper(trans_safe=True):
|
||||||
for setting in Setting.objects.filter(
|
for setting in Setting.objects.filter(key__in=self.all_supported_settings, user__isnull=True):
|
||||||
key__in=self.all_supported_settings, user__isnull=True):
|
|
||||||
# Skip returning settings that have been overridden but are
|
# Skip returning settings that have been overridden but are
|
||||||
# considered to be "not set".
|
# considered to be "not set".
|
||||||
if setting.value is None and SETTING_CACHE_NOTSET == SETTING_CACHE_NONE:
|
if setting.value is None and SETTING_CACHE_NOTSET == SETTING_CACHE_NONE:
|
||||||
@@ -511,7 +485,7 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
with _ctit_db_wrapper(trans_safe=True):
|
with _ctit_db_wrapper(trans_safe=True):
|
||||||
set_locally = Setting.objects.filter(key=setting, user__isnull=True).exists()
|
set_locally = Setting.objects.filter(key=setting, user__isnull=True).exists()
|
||||||
set_on_default = getattr(self.default_settings, 'is_overridden', lambda s: False)(setting)
|
set_on_default = getattr(self.default_settings, 'is_overridden', lambda s: False)(setting)
|
||||||
return (set_locally or set_on_default)
|
return set_locally or set_on_default
|
||||||
|
|
||||||
|
|
||||||
def __getattr_without_cache__(self, name):
|
def __getattr_without_cache__(self, name):
|
||||||
|
|||||||
@@ -30,12 +30,7 @@ def handle_setting_change(key, for_delete=False):
|
|||||||
|
|
||||||
# Send setting_changed signal with new value for each setting.
|
# Send setting_changed signal with new value for each setting.
|
||||||
for setting_key in setting_keys:
|
for setting_key in setting_keys:
|
||||||
setting_changed.send(
|
setting_changed.send(sender=Setting, setting=setting_key, value=getattr(settings, setting_key, None), enter=not bool(for_delete))
|
||||||
sender=Setting,
|
|
||||||
setting=setting_key,
|
|
||||||
value=getattr(settings, setting_key, None),
|
|
||||||
enter=not bool(for_delete),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Setting)
|
@receiver(post_save, sender=Setting)
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import pytest
|
|||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from rest_framework.test import (
|
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||||
APIRequestFactory,
|
|
||||||
force_authenticate,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -41,4 +38,5 @@ def api_request(admin):
|
|||||||
response = view(request, *view_args, **view_kwargs)
|
response = view(request, *view_args, **view_kwargs)
|
||||||
response.render()
|
response.render()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return rf
|
return rf
|
||||||
|
|||||||
@@ -45,44 +45,19 @@ def dummy_validate():
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_non_admin_user_does_not_see_categories(api_request, dummy_setting, normal_user):
|
def test_non_admin_user_does_not_see_categories(api_request, dummy_setting, normal_user):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'):
|
||||||
'FOO_BAR',
|
response = api_request('get', reverse('api:setting_category_list', kwargs={'version': 'v2'}))
|
||||||
field_class=fields.IntegerField,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
):
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_category_list',
|
|
||||||
kwargs={'version': 'v2'})
|
|
||||||
)
|
|
||||||
assert response.data['results']
|
assert response.data['results']
|
||||||
response = api_request(
|
response = api_request('get', reverse('api:setting_category_list', kwargs={'version': 'v2'}), user=normal_user)
|
||||||
'get',
|
|
||||||
reverse('api:setting_category_list',
|
|
||||||
kwargs={'version': 'v2'}),
|
|
||||||
user=normal_user
|
|
||||||
)
|
|
||||||
assert not response.data['results']
|
assert not response.data['results']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
|
def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR_1', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), dummy_setting(
|
||||||
'FOO_BAR_1',
|
'FOO_BAR_2', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'
|
||||||
field_class=fields.IntegerField,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
), dummy_setting(
|
|
||||||
'FOO_BAR_2',
|
|
||||||
field_class=fields.IntegerField,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
):
|
):
|
||||||
response = api_request(
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert 'FOO_BAR_1' in response.data and response.data['FOO_BAR_1'] is None
|
assert 'FOO_BAR_1' in response.data and response.data['FOO_BAR_1'] is None
|
||||||
assert 'FOO_BAR_2' in response.data and response.data['FOO_BAR_2'] is None
|
assert 'FOO_BAR_2' in response.data and response.data['FOO_BAR_2'] is None
|
||||||
@@ -90,97 +65,43 @@ def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_detail_invalid_retrieve(api_request, dummy_setting, normal_user):
|
def test_setting_singleton_detail_invalid_retrieve(api_request, dummy_setting, normal_user):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR_1', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), dummy_setting(
|
||||||
'FOO_BAR_1',
|
'FOO_BAR_2', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'
|
||||||
field_class=fields.IntegerField,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
), dummy_setting(
|
|
||||||
'FOO_BAR_2',
|
|
||||||
field_class=fields.IntegerField,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
):
|
):
|
||||||
response = api_request(
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'barfoo'}))
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'barfoo'})
|
|
||||||
)
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
response = api_request(
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), user=normal_user)
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
user = normal_user
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_signleton_retrieve_hierachy(api_request, dummy_setting):
|
def test_setting_signleton_retrieve_hierachy(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, default=0, category='FooBar', category_slug='foobar'):
|
||||||
'FOO_BAR',
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
field_class=fields.IntegerField,
|
|
||||||
default=0,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
):
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == 0
|
assert response.data['FOO_BAR'] == 0
|
||||||
s = Setting(key='FOO_BAR', value=1)
|
s = Setting(key='FOO_BAR', value=1)
|
||||||
s.save()
|
s.save()
|
||||||
response = api_request(
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == 1
|
assert response.data['FOO_BAR'] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_retrieve_readonly(api_request, dummy_setting):
|
def test_setting_singleton_retrieve_readonly(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=2, category='FooBar', category_slug='foobar'):
|
||||||
'FOO_BAR',
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
field_class=fields.IntegerField,
|
|
||||||
read_only=True,
|
|
||||||
default=2,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
):
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == 2
|
assert response.data['FOO_BAR'] == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_update(api_request, dummy_setting):
|
def test_setting_singleton_update(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), mock.patch(
|
||||||
'FOO_BAR',
|
'awx.conf.views.handle_setting_changes'
|
||||||
field_class=fields.IntegerField,
|
):
|
||||||
category='FooBar',
|
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 3})
|
||||||
category_slug='foobar'
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
|
||||||
api_request(
|
|
||||||
'patch',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
data={'FOO_BAR': 3}
|
|
||||||
)
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == 3
|
assert response.data['FOO_BAR'] == 3
|
||||||
api_request(
|
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 4})
|
||||||
'patch',
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
data={'FOO_BAR': 4}
|
|
||||||
)
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == 4
|
assert response.data['FOO_BAR'] == 4
|
||||||
|
|
||||||
|
|
||||||
@@ -190,138 +111,70 @@ def test_setting_singleton_update_hybriddictfield_with_forbidden(api_request, du
|
|||||||
# indicating that only the defined fields can be filled in. Make
|
# indicating that only the defined fields can be filled in. Make
|
||||||
# sure that the _Forbidden validator doesn't get used for the
|
# sure that the _Forbidden validator doesn't get used for the
|
||||||
# fields. See also https://github.com/ansible/awx/issues/4099.
|
# fields. See also https://github.com/ansible/awx/issues/4099.
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=sso_fields.SAMLOrgAttrField, category='FooBar', category_slug='foobar'), mock.patch(
|
||||||
'FOO_BAR',
|
'awx.conf.views.handle_setting_changes'
|
||||||
field_class=sso_fields.SAMLOrgAttrField,
|
):
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar',
|
|
||||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
|
||||||
api_request(
|
api_request(
|
||||||
'patch',
|
'patch',
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
||||||
data={'FOO_BAR': {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'}}
|
data={'FOO_BAR': {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'}},
|
||||||
)
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
)
|
||||||
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
assert response.data['FOO_BAR'] == {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'}
|
assert response.data['FOO_BAR'] == {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_update_dont_change_readonly_fields(api_request, dummy_setting):
|
def test_setting_singleton_update_dont_change_readonly_fields(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=4, category='FooBar', category_slug='foobar'), mock.patch(
|
||||||
'FOO_BAR',
|
'awx.conf.views.handle_setting_changes'
|
||||||
field_class=fields.IntegerField,
|
):
|
||||||
read_only=True,
|
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 5})
|
||||||
default=4,
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
|
||||||
api_request(
|
|
||||||
'patch',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
data={'FOO_BAR': 5}
|
|
||||||
)
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == 4
|
assert response.data['FOO_BAR'] == 4
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_update_dont_change_encrypted_mark(api_request, dummy_setting):
|
def test_setting_singleton_update_dont_change_encrypted_mark(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.CharField, encrypted=True, category='FooBar', category_slug='foobar'), mock.patch(
|
||||||
'FOO_BAR',
|
'awx.conf.views.handle_setting_changes'
|
||||||
field_class=fields.CharField,
|
):
|
||||||
encrypted=True,
|
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 'password'})
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
|
||||||
api_request(
|
|
||||||
'patch',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
data={'FOO_BAR': 'password'}
|
|
||||||
)
|
|
||||||
assert Setting.objects.get(key='FOO_BAR').value.startswith('$encrypted$')
|
assert Setting.objects.get(key='FOO_BAR').value.startswith('$encrypted$')
|
||||||
response = api_request(
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == '$encrypted$'
|
assert response.data['FOO_BAR'] == '$encrypted$'
|
||||||
api_request(
|
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': '$encrypted$'})
|
||||||
'patch',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
data={'FOO_BAR': '$encrypted$'}
|
|
||||||
)
|
|
||||||
assert decrypt_field(Setting.objects.get(key='FOO_BAR'), 'value') == 'password'
|
assert decrypt_field(Setting.objects.get(key='FOO_BAR'), 'value') == 'password'
|
||||||
api_request(
|
api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 'new_pw'})
|
||||||
'patch',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
data={'FOO_BAR': 'new_pw'}
|
|
||||||
)
|
|
||||||
assert decrypt_field(Setting.objects.get(key='FOO_BAR'), 'value') == 'new_pw'
|
assert decrypt_field(Setting.objects.get(key='FOO_BAR'), 'value') == 'new_pw'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_update_runs_custom_validate(api_request, dummy_setting, dummy_validate):
|
def test_setting_singleton_update_runs_custom_validate(api_request, dummy_setting, dummy_validate):
|
||||||
|
|
||||||
def func_raising_exception(serializer, attrs):
|
def func_raising_exception(serializer, attrs):
|
||||||
raise serializers.ValidationError('Error')
|
raise serializers.ValidationError('Error')
|
||||||
|
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), dummy_validate(
|
||||||
'FOO_BAR',
|
|
||||||
field_class=fields.IntegerField,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
), dummy_validate(
|
|
||||||
'foobar', func_raising_exception
|
'foobar', func_raising_exception
|
||||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
), mock.patch('awx.conf.views.handle_setting_changes'):
|
||||||
response = api_request(
|
response = api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 23})
|
||||||
'patch',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
|
||||||
data={'FOO_BAR': 23}
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_delete(api_request, dummy_setting):
|
def test_setting_singleton_delete(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, category='FooBar', category_slug='foobar'), mock.patch(
|
||||||
'FOO_BAR',
|
'awx.conf.views.handle_setting_changes'
|
||||||
field_class=fields.IntegerField,
|
):
|
||||||
category='FooBar',
|
api_request('delete', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
category_slug='foobar'
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
|
||||||
api_request(
|
|
||||||
'delete',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert not response.data['FOO_BAR']
|
assert not response.data['FOO_BAR']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_delete_no_read_only_fields(api_request, dummy_setting):
|
def test_setting_singleton_delete_no_read_only_fields(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=23, category='FooBar', category_slug='foobar'), mock.patch(
|
||||||
'FOO_BAR',
|
'awx.conf.views.handle_setting_changes'
|
||||||
field_class=fields.IntegerField,
|
):
|
||||||
read_only=True,
|
api_request('delete', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
default=23,
|
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
|
||||||
api_request(
|
|
||||||
'delete',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
|
||||||
)
|
|
||||||
assert response.data['FOO_BAR'] == 23
|
assert response.data['FOO_BAR'] == 23
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
# Ensure that our autouse overwrites are working
|
# Ensure that our autouse overwrites are working
|
||||||
def test_cache(settings):
|
def test_cache(settings):
|
||||||
assert settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.locmem.LocMemCache'
|
assert settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.locmem.LocMemCache'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from rest_framework.fields import ValidationError
|
|||||||
from awx.conf.fields import StringListBooleanField, StringListPathField, ListTuplesField, URLField
|
from awx.conf.fields import StringListBooleanField, StringListPathField, ListTuplesField, URLField
|
||||||
|
|
||||||
|
|
||||||
class TestStringListBooleanField():
|
class TestStringListBooleanField:
|
||||||
|
|
||||||
FIELD_VALUES = [
|
FIELD_VALUES = [
|
||||||
("hello", "hello"),
|
("hello", "hello"),
|
||||||
@@ -23,10 +23,7 @@ class TestStringListBooleanField():
|
|||||||
("NULL", None),
|
("NULL", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
FIELD_VALUES_INVALID = [
|
FIELD_VALUES_INVALID = [1.245, {"a": "b"}]
|
||||||
1.245,
|
|
||||||
{"a": "b"},
|
|
||||||
]
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
||||||
def test_to_internal_value_valid(self, value_in, value_known):
|
def test_to_internal_value_valid(self, value_in, value_known):
|
||||||
@@ -39,8 +36,7 @@ class TestStringListBooleanField():
|
|||||||
field = StringListBooleanField()
|
field = StringListBooleanField()
|
||||||
with pytest.raises(ValidationError) as e:
|
with pytest.raises(ValidationError) as e:
|
||||||
field.to_internal_value(value)
|
field.to_internal_value(value)
|
||||||
assert e.value.detail[0] == "Expected None, True, False, a string or list " \
|
assert e.value.detail[0] == "Expected None, True, False, a string or list " "of strings but got {} instead.".format(type(value))
|
||||||
"of strings but got {} instead.".format(type(value))
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
||||||
def test_to_representation_valid(self, value_in, value_known):
|
def test_to_representation_valid(self, value_in, value_known):
|
||||||
@@ -53,22 +49,14 @@ class TestStringListBooleanField():
|
|||||||
field = StringListBooleanField()
|
field = StringListBooleanField()
|
||||||
with pytest.raises(ValidationError) as e:
|
with pytest.raises(ValidationError) as e:
|
||||||
field.to_representation(value)
|
field.to_representation(value)
|
||||||
assert e.value.detail[0] == "Expected None, True, False, a string or list " \
|
assert e.value.detail[0] == "Expected None, True, False, a string or list " "of strings but got {} instead.".format(type(value))
|
||||||
"of strings but got {} instead.".format(type(value))
|
|
||||||
|
|
||||||
|
|
||||||
class TestListTuplesField():
|
class TestListTuplesField:
|
||||||
|
|
||||||
FIELD_VALUES = [
|
FIELD_VALUES = [([('a', 'b'), ('abc', '123')], [("a", "b"), ("abc", "123")])]
|
||||||
([('a', 'b'), ('abc', '123')], [("a", "b"), ("abc", "123")]),
|
|
||||||
]
|
|
||||||
|
|
||||||
FIELD_VALUES_INVALID = [
|
FIELD_VALUES_INVALID = [("abc", type("abc")), ([('a', 'b', 'c'), ('abc', '123', '456')], type(('a',))), (['a', 'b'], type('a')), (123, type(123))]
|
||||||
("abc", type("abc")),
|
|
||||||
([('a', 'b', 'c'), ('abc', '123', '456')], type(('a',))),
|
|
||||||
(['a', 'b'], type('a')),
|
|
||||||
(123, type(123)),
|
|
||||||
]
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
||||||
def test_to_internal_value_valid(self, value_in, value_known):
|
def test_to_internal_value_valid(self, value_in, value_known):
|
||||||
@@ -81,11 +69,10 @@ class TestListTuplesField():
|
|||||||
field = ListTuplesField()
|
field = ListTuplesField()
|
||||||
with pytest.raises(ValidationError) as e:
|
with pytest.raises(ValidationError) as e:
|
||||||
field.to_internal_value(value)
|
field.to_internal_value(value)
|
||||||
assert e.value.detail[0] == "Expected a list of tuples of max length 2 " \
|
assert e.value.detail[0] == "Expected a list of tuples of max length 2 " "but got {} instead.".format(t)
|
||||||
"but got {} instead.".format(t)
|
|
||||||
|
|
||||||
|
|
||||||
class TestStringListPathField():
|
class TestStringListPathField:
|
||||||
|
|
||||||
FIELD_VALUES = [
|
FIELD_VALUES = [
|
||||||
((".", "..", "/"), [".", "..", "/"]),
|
((".", "..", "/"), [".", "..", "/"]),
|
||||||
@@ -93,22 +80,12 @@ class TestStringListPathField():
|
|||||||
(("///home///",), ["/home"]),
|
(("///home///",), ["/home"]),
|
||||||
(("/home/././././",), ["/home"]),
|
(("/home/././././",), ["/home"]),
|
||||||
(("/home", "/home", "/home/"), ["/home"]),
|
(("/home", "/home", "/home/"), ["/home"]),
|
||||||
(["/home/", "/home/", "/opt/", "/opt/", "/var/"], ["/home", "/opt", "/var"])
|
(["/home/", "/home/", "/opt/", "/opt/", "/var/"], ["/home", "/opt", "/var"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
FIELD_VALUES_INVALID_TYPE = [
|
FIELD_VALUES_INVALID_TYPE = [1.245, {"a": "b"}, ("/home")]
|
||||||
1.245,
|
|
||||||
{"a": "b"},
|
|
||||||
("/home"),
|
|
||||||
]
|
|
||||||
|
|
||||||
FIELD_VALUES_INVALID_PATH = [
|
FIELD_VALUES_INVALID_PATH = ["", "~/", "home", "/invalid_path", "/home/invalid_path"]
|
||||||
"",
|
|
||||||
"~/",
|
|
||||||
"home",
|
|
||||||
"/invalid_path",
|
|
||||||
"/home/invalid_path",
|
|
||||||
]
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
||||||
def test_to_internal_value_valid(self, value_in, value_known):
|
def test_to_internal_value_valid(self, value_in, value_known):
|
||||||
@@ -131,16 +108,19 @@ class TestStringListPathField():
|
|||||||
assert e.value.detail[0] == "{} is not a valid path choice.".format(value)
|
assert e.value.detail[0] == "{} is not a valid path choice.".format(value)
|
||||||
|
|
||||||
|
|
||||||
class TestURLField():
|
class TestURLField:
|
||||||
regex = "^https://www.example.org$"
|
regex = "^https://www.example.org$"
|
||||||
|
|
||||||
@pytest.mark.parametrize("url,schemes,regex, allow_numbers_in_top_level_domain, expect_no_error",[
|
@pytest.mark.parametrize(
|
||||||
("ldap://www.example.org42", "ldap", None, True, True),
|
"url,schemes,regex, allow_numbers_in_top_level_domain, expect_no_error",
|
||||||
("https://www.example.org42", "https", None, False, False),
|
[
|
||||||
("https://www.example.org", None, regex, None, True),
|
("ldap://www.example.org42", "ldap", None, True, True),
|
||||||
("https://www.example3.org", None, regex, None, False),
|
("https://www.example.org42", "https", None, False, False),
|
||||||
("ftp://www.example.org", "https", None, None, False)
|
("https://www.example.org", None, regex, None, True),
|
||||||
])
|
("https://www.example3.org", None, regex, None, False),
|
||||||
|
("ftp://www.example.org", "https", None, None, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_urls(self, url, schemes, regex, allow_numbers_in_top_level_domain, expect_no_error):
|
def test_urls(self, url, schemes, regex, allow_numbers_in_top_level_domain, expect_no_error):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
kwargs.setdefault("allow_numbers_in_top_level_domain", allow_numbers_in_top_level_domain)
|
kwargs.setdefault("allow_numbers_in_top_level_domain", allow_numbers_in_top_level_domain)
|
||||||
|
|||||||
@@ -33,30 +33,18 @@ def reg(request):
|
|||||||
if marker.name == 'defined_in_file':
|
if marker.name == 'defined_in_file':
|
||||||
settings.configure(**marker.kwargs)
|
settings.configure(**marker.kwargs)
|
||||||
|
|
||||||
settings._wrapped = SettingsWrapper(settings._wrapped,
|
settings._wrapped = SettingsWrapper(settings._wrapped, cache, registry)
|
||||||
cache,
|
|
||||||
registry)
|
|
||||||
return registry
|
return registry
|
||||||
|
|
||||||
|
|
||||||
def test_simple_setting_registration(reg):
|
def test_simple_setting_registration(reg):
|
||||||
assert reg.get_registered_settings() == []
|
assert reg.get_registered_settings() == []
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
assert reg.get_registered_settings() == ['AWX_SOME_SETTING_ENABLED']
|
assert reg.get_registered_settings() == ['AWX_SOME_SETTING_ENABLED']
|
||||||
|
|
||||||
|
|
||||||
def test_simple_setting_unregistration(reg):
|
def test_simple_setting_unregistration(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
assert reg.get_registered_settings() == ['AWX_SOME_SETTING_ENABLED']
|
assert reg.get_registered_settings() == ['AWX_SOME_SETTING_ENABLED']
|
||||||
|
|
||||||
reg.unregister('AWX_SOME_SETTING_ENABLED')
|
reg.unregister('AWX_SOME_SETTING_ENABLED')
|
||||||
@@ -67,12 +55,7 @@ def test_duplicate_setting_registration(reg):
|
|||||||
"ensure that settings cannot be registered twice."
|
"ensure that settings cannot be registered twice."
|
||||||
with pytest.raises(ImproperlyConfigured):
|
with pytest.raises(ImproperlyConfigured):
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_field_class_required_for_registration(reg):
|
def test_field_class_required_for_registration(reg):
|
||||||
@@ -82,110 +65,42 @@ def test_field_class_required_for_registration(reg):
|
|||||||
|
|
||||||
|
|
||||||
def test_get_registered_settings_by_slug(reg):
|
def test_get_registered_settings_by_slug(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
assert reg.get_registered_settings(category_slug='system') == ['AWX_SOME_SETTING_ENABLED']
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
assert reg.get_registered_settings(category_slug='system') == [
|
|
||||||
'AWX_SOME_SETTING_ENABLED'
|
|
||||||
]
|
|
||||||
assert reg.get_registered_settings(category_slug='other') == []
|
assert reg.get_registered_settings(category_slug='other') == []
|
||||||
|
|
||||||
|
|
||||||
def test_get_registered_read_only_settings(reg):
|
def test_get_registered_read_only_settings(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
reg.register('AWX_SOME_READ_ONLY', field_class=fields.BooleanField, category=_('System'), category_slug='system', read_only=True)
|
||||||
field_class=fields.BooleanField,
|
assert reg.get_registered_settings(read_only=True) == ['AWX_SOME_READ_ONLY']
|
||||||
category=_('System'),
|
assert reg.get_registered_settings(read_only=False) == ['AWX_SOME_SETTING_ENABLED']
|
||||||
category_slug='system'
|
assert reg.get_registered_settings() == ['AWX_SOME_SETTING_ENABLED', 'AWX_SOME_READ_ONLY']
|
||||||
)
|
|
||||||
reg.register(
|
|
||||||
'AWX_SOME_READ_ONLY',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
assert reg.get_registered_settings(read_only=True) ==[
|
|
||||||
'AWX_SOME_READ_ONLY'
|
|
||||||
]
|
|
||||||
assert reg.get_registered_settings(read_only=False) == [
|
|
||||||
'AWX_SOME_SETTING_ENABLED'
|
|
||||||
]
|
|
||||||
assert reg.get_registered_settings() == [
|
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
|
||||||
'AWX_SOME_READ_ONLY'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_dependent_settings(reg):
|
def test_get_dependent_settings(reg):
|
||||||
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category=_('System'), category_slug='system')
|
||||||
reg.register(
|
reg.register(
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
'AWX_SOME_DEPENDENT_SETTING', field_class=fields.BooleanField, category=_('System'), category_slug='system', depends_on=['AWX_SOME_SETTING_ENABLED']
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
)
|
||||||
reg.register(
|
assert reg.get_dependent_settings('AWX_SOME_SETTING_ENABLED') == set(['AWX_SOME_DEPENDENT_SETTING'])
|
||||||
'AWX_SOME_DEPENDENT_SETTING',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
depends_on=['AWX_SOME_SETTING_ENABLED']
|
|
||||||
)
|
|
||||||
assert reg.get_dependent_settings('AWX_SOME_SETTING_ENABLED') == set([
|
|
||||||
'AWX_SOME_DEPENDENT_SETTING'
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_registered_categories(reg):
|
def test_get_registered_categories(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
reg.register('AWX_SOME_OTHER_SETTING_ENABLED', field_class=fields.BooleanField, category=_('OtherSystem'), category_slug='other-system')
|
||||||
field_class=fields.BooleanField,
|
assert reg.get_registered_categories() == {'all': _('All'), 'changed': _('Changed'), 'system': _('System'), 'other-system': _('OtherSystem')}
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
reg.register(
|
|
||||||
'AWX_SOME_OTHER_SETTING_ENABLED',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category=_('OtherSystem'),
|
|
||||||
category_slug='other-system'
|
|
||||||
)
|
|
||||||
assert reg.get_registered_categories() == {
|
|
||||||
'all': _('All'),
|
|
||||||
'changed': _('Changed'),
|
|
||||||
'system': _('System'),
|
|
||||||
'other-system': _('OtherSystem'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_setting_encrypted(reg):
|
def test_is_setting_encrypted(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
reg.register('AWX_SOME_ENCRYPTED_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', encrypted=True)
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
reg.register(
|
|
||||||
'AWX_SOME_ENCRYPTED_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
encrypted=True
|
|
||||||
)
|
|
||||||
assert reg.is_setting_encrypted('AWX_SOME_SETTING_ENABLED') is False
|
assert reg.is_setting_encrypted('AWX_SOME_SETTING_ENABLED') is False
|
||||||
assert reg.is_setting_encrypted('AWX_SOME_ENCRYPTED_SETTING') is True
|
assert reg.is_setting_encrypted('AWX_SOME_ENCRYPTED_SETTING') is True
|
||||||
|
|
||||||
|
|
||||||
def test_simple_field(reg):
|
def test_simple_field(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', placeholder='Example Value')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
placeholder='Example Value',
|
|
||||||
)
|
|
||||||
|
|
||||||
field = reg.get_setting_field('AWX_SOME_SETTING')
|
field = reg.get_setting_field('AWX_SOME_SETTING')
|
||||||
assert isinstance(field, fields.CharField)
|
assert isinstance(field, fields.CharField)
|
||||||
@@ -196,31 +111,20 @@ def test_simple_field(reg):
|
|||||||
|
|
||||||
|
|
||||||
def test_field_with_custom_attribute(reg):
|
def test_field_with_custom_attribute(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
|
|
||||||
field = reg.get_setting_field('AWX_SOME_SETTING_ENABLED',
|
field = reg.get_setting_field('AWX_SOME_SETTING_ENABLED', category_slug='other-system')
|
||||||
category_slug='other-system')
|
|
||||||
assert field.category_slug == 'other-system'
|
assert field.category_slug == 'other-system'
|
||||||
|
|
||||||
|
|
||||||
def test_field_with_custom_mixin(reg):
|
def test_field_with_custom_mixin(reg):
|
||||||
class GreatMixin(object):
|
class GreatMixin(object):
|
||||||
|
|
||||||
def is_great(self):
|
def is_great(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING_ENABLED', field_class=fields.BooleanField, category_slug='system')
|
||||||
'AWX_SOME_SETTING_ENABLED',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
|
|
||||||
field = reg.get_setting_field('AWX_SOME_SETTING_ENABLED',
|
field = reg.get_setting_field('AWX_SOME_SETTING_ENABLED', mixin_class=GreatMixin)
|
||||||
mixin_class=GreatMixin)
|
|
||||||
assert isinstance(field, fields.BooleanField)
|
assert isinstance(field, fields.BooleanField)
|
||||||
assert isinstance(field, GreatMixin)
|
assert isinstance(field, GreatMixin)
|
||||||
assert field.is_great() is True
|
assert field.is_great() is True
|
||||||
@@ -228,12 +132,7 @@ def test_field_with_custom_mixin(reg):
|
|||||||
|
|
||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_default_value_from_settings(reg):
|
def test_default_value_from_settings(reg):
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
|
|
||||||
field = reg.get_setting_field('AWX_SOME_SETTING')
|
field = reg.get_setting_field('AWX_SOME_SETTING')
|
||||||
assert field.default == 'DEFAULT'
|
assert field.default == 'DEFAULT'
|
||||||
@@ -242,16 +141,10 @@ def test_default_value_from_settings(reg):
|
|||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_default_value_from_settings_with_custom_representation(reg):
|
def test_default_value_from_settings_with_custom_representation(reg):
|
||||||
class LowercaseCharField(fields.CharField):
|
class LowercaseCharField(fields.CharField):
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
return value.lower()
|
return value.lower()
|
||||||
|
|
||||||
reg.register(
|
reg.register('AWX_SOME_SETTING', field_class=LowercaseCharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=LowercaseCharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
)
|
|
||||||
|
|
||||||
field = reg.get_setting_field('AWX_SOME_SETTING')
|
field = reg.get_setting_field('AWX_SOME_SETTING')
|
||||||
assert field.default == 'default'
|
assert field.default == 'default'
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ def settings(request):
|
|||||||
|
|
||||||
defaults['DEFAULTS_SNAPSHOT'] = {}
|
defaults['DEFAULTS_SNAPSHOT'] = {}
|
||||||
settings.configure(**defaults)
|
settings.configure(**defaults)
|
||||||
settings._wrapped = SettingsWrapper(settings._wrapped,
|
settings._wrapped = SettingsWrapper(settings._wrapped, cache, registry)
|
||||||
cache,
|
|
||||||
registry)
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
@@ -67,14 +65,7 @@ def test_unregistered_setting(settings):
|
|||||||
|
|
||||||
|
|
||||||
def test_read_only_setting(settings):
|
def test_read_only_setting(settings):
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_READ_ONLY', field_class=fields.CharField, category=_('System'), category_slug='system', default='NO-EDITS', read_only=True)
|
||||||
'AWX_READ_ONLY',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
default='NO-EDITS',
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
assert settings.AWX_READ_ONLY == 'NO-EDITS'
|
assert settings.AWX_READ_ONLY == 'NO-EDITS'
|
||||||
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
||||||
settings = settings.registry.get_registered_settings(read_only=True)
|
settings = settings.registry.get_registered_settings(read_only=True)
|
||||||
@@ -85,13 +76,7 @@ def test_read_only_setting(settings):
|
|||||||
@pytest.mark.parametrize('read_only', [True, False])
|
@pytest.mark.parametrize('read_only', [True, False])
|
||||||
def test_setting_defined_in_file(settings, read_only):
|
def test_setting_defined_in_file(settings, read_only):
|
||||||
kwargs = {'read_only': True} if read_only else {}
|
kwargs = {'read_only': True} if read_only else {}
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', **kwargs)
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
||||||
settings = settings.registry.get_registered_settings(read_only=True)
|
settings = settings.registry.get_registered_settings(read_only=True)
|
||||||
@@ -100,13 +85,7 @@ def test_setting_defined_in_file(settings, read_only):
|
|||||||
|
|
||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_setting_defined_in_file_with_empty_default(settings):
|
def test_setting_defined_in_file_with_empty_default(settings):
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', default='')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
default='',
|
|
||||||
)
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
||||||
settings = settings.registry.get_registered_settings(read_only=True)
|
settings = settings.registry.get_registered_settings(read_only=True)
|
||||||
@@ -115,13 +94,7 @@ def test_setting_defined_in_file_with_empty_default(settings):
|
|||||||
|
|
||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_setting_defined_in_file_with_specific_default(settings):
|
def test_setting_defined_in_file_with_specific_default(settings):
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', default=123)
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
default=123
|
|
||||||
)
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
assert len(settings.registry.get_registered_settings(read_only=False)) == 0
|
||||||
settings = settings.registry.get_registered_settings(read_only=True)
|
settings = settings.registry.get_registered_settings(read_only=True)
|
||||||
@@ -131,12 +104,7 @@ def test_setting_defined_in_file_with_specific_default(settings):
|
|||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_read_only_defaults_are_cached(settings):
|
def test_read_only_defaults_are_cached(settings):
|
||||||
"read-only settings are stored in the cache"
|
"read-only settings are stored in the cache"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
assert settings.cache.get('AWX_SOME_SETTING') == 'DEFAULT'
|
assert settings.cache.get('AWX_SOME_SETTING') == 'DEFAULT'
|
||||||
|
|
||||||
@@ -144,12 +112,7 @@ def test_read_only_defaults_are_cached(settings):
|
|||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_cache_respects_timeout(settings):
|
def test_cache_respects_timeout(settings):
|
||||||
"only preload the cache every SETTING_CACHE_TIMEOUT settings"
|
"only preload the cache every SETTING_CACHE_TIMEOUT settings"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
cache_expiration = settings.cache.get('_awx_conf_preload_expires')
|
cache_expiration = settings.cache.get('_awx_conf_preload_expires')
|
||||||
@@ -161,13 +124,7 @@ def test_cache_respects_timeout(settings):
|
|||||||
|
|
||||||
def test_default_setting(settings, mocker):
|
def test_default_setting(settings, mocker):
|
||||||
"settings that specify a default are inserted into the cache"
|
"settings that specify a default are inserted into the cache"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', default='DEFAULT')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
default='DEFAULT'
|
|
||||||
)
|
|
||||||
|
|
||||||
settings_to_cache = mocker.Mock(**{'order_by.return_value': []})
|
settings_to_cache = mocker.Mock(**{'order_by.return_value': []})
|
||||||
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=settings_to_cache):
|
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=settings_to_cache):
|
||||||
@@ -177,24 +134,13 @@ def test_default_setting(settings, mocker):
|
|||||||
|
|
||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_setting_is_from_setting_file(settings, mocker):
|
def test_setting_is_from_setting_file(settings, mocker):
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
assert settings.registry.get_setting_field('AWX_SOME_SETTING').defined_in_file is True
|
assert settings.registry.get_setting_field('AWX_SOME_SETTING').defined_in_file is True
|
||||||
|
|
||||||
|
|
||||||
def test_setting_is_not_from_setting_file(settings, mocker):
|
def test_setting_is_not_from_setting_file(settings, mocker):
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', default='DEFAULT')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
default='DEFAULT'
|
|
||||||
)
|
|
||||||
|
|
||||||
settings_to_cache = mocker.Mock(**{'order_by.return_value': []})
|
settings_to_cache = mocker.Mock(**{'order_by.return_value': []})
|
||||||
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=settings_to_cache):
|
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=settings_to_cache):
|
||||||
@@ -204,19 +150,9 @@ def test_setting_is_not_from_setting_file(settings, mocker):
|
|||||||
|
|
||||||
def test_empty_setting(settings, mocker):
|
def test_empty_setting(settings, mocker):
|
||||||
"settings with no default and no defined value are not valid"
|
"settings with no default and no defined value are not valid"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
|
|
||||||
mocks = mocker.Mock(**{
|
mocks = mocker.Mock(**{'order_by.return_value': mocker.Mock(**{'__iter__': lambda self: iter([]), 'first.return_value': None})})
|
||||||
'order_by.return_value': mocker.Mock(**{
|
|
||||||
'__iter__': lambda self: iter([]),
|
|
||||||
'first.return_value': None
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=mocks):
|
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=mocks):
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
settings.AWX_SOME_SETTING
|
settings.AWX_SOME_SETTING
|
||||||
@@ -225,21 +161,10 @@ def test_empty_setting(settings, mocker):
|
|||||||
|
|
||||||
def test_setting_from_db(settings, mocker):
|
def test_setting_from_db(settings, mocker):
|
||||||
"settings can be loaded from the database"
|
"settings can be loaded from the database"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', default='DEFAULT')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
default='DEFAULT'
|
|
||||||
)
|
|
||||||
|
|
||||||
setting_from_db = mocker.Mock(key='AWX_SOME_SETTING', value='FROM_DB')
|
setting_from_db = mocker.Mock(key='AWX_SOME_SETTING', value='FROM_DB')
|
||||||
mocks = mocker.Mock(**{
|
mocks = mocker.Mock(**{'order_by.return_value': mocker.Mock(**{'__iter__': lambda self: iter([setting_from_db]), 'first.return_value': setting_from_db})})
|
||||||
'order_by.return_value': mocker.Mock(**{
|
|
||||||
'__iter__': lambda self: iter([setting_from_db]),
|
|
||||||
'first.return_value': setting_from_db
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=mocks):
|
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=mocks):
|
||||||
assert settings.AWX_SOME_SETTING == 'FROM_DB'
|
assert settings.AWX_SOME_SETTING == 'FROM_DB'
|
||||||
assert settings.cache.get('AWX_SOME_SETTING') == 'FROM_DB'
|
assert settings.cache.get('AWX_SOME_SETTING') == 'FROM_DB'
|
||||||
@@ -248,12 +173,7 @@ def test_setting_from_db(settings, mocker):
|
|||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_read_only_setting_assignment(settings):
|
def test_read_only_setting_assignment(settings):
|
||||||
"read-only settings cannot be overwritten"
|
"read-only settings cannot be overwritten"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
with pytest.raises(ImproperlyConfigured):
|
with pytest.raises(ImproperlyConfigured):
|
||||||
settings.AWX_SOME_SETTING = 'CHANGED'
|
settings.AWX_SOME_SETTING = 'CHANGED'
|
||||||
@@ -262,41 +182,26 @@ def test_read_only_setting_assignment(settings):
|
|||||||
|
|
||||||
def test_db_setting_create(settings, mocker):
|
def test_db_setting_create(settings, mocker):
|
||||||
"settings are stored in the database when set for the first time"
|
"settings are stored in the database when set for the first time"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
|
|
||||||
setting_list = mocker.Mock(**{'order_by.return_value.first.return_value': None})
|
setting_list = mocker.Mock(**{'order_by.return_value.first.return_value': None})
|
||||||
with apply_patches([
|
with apply_patches(
|
||||||
mocker.patch('awx.conf.models.Setting.objects.filter',
|
[
|
||||||
return_value=setting_list),
|
mocker.patch('awx.conf.models.Setting.objects.filter', return_value=setting_list),
|
||||||
mocker.patch('awx.conf.models.Setting.objects.create', mocker.Mock())
|
mocker.patch('awx.conf.models.Setting.objects.create', mocker.Mock()),
|
||||||
]):
|
]
|
||||||
|
):
|
||||||
settings.AWX_SOME_SETTING = 'NEW-VALUE'
|
settings.AWX_SOME_SETTING = 'NEW-VALUE'
|
||||||
|
|
||||||
models.Setting.objects.create.assert_called_with(
|
models.Setting.objects.create.assert_called_with(key='AWX_SOME_SETTING', user=None, value='NEW-VALUE')
|
||||||
key='AWX_SOME_SETTING',
|
|
||||||
user=None,
|
|
||||||
value='NEW-VALUE'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_db_setting_update(settings, mocker):
|
def test_db_setting_update(settings, mocker):
|
||||||
"settings are updated in the database when their value changes"
|
"settings are updated in the database when their value changes"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_setting = mocker.Mock(key='AWX_SOME_SETTING', value='FROM_DB')
|
existing_setting = mocker.Mock(key='AWX_SOME_SETTING', value='FROM_DB')
|
||||||
setting_list = mocker.Mock(**{
|
setting_list = mocker.Mock(**{'order_by.return_value.first.return_value': existing_setting})
|
||||||
'order_by.return_value.first.return_value': existing_setting
|
|
||||||
})
|
|
||||||
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=setting_list):
|
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=setting_list):
|
||||||
settings.AWX_SOME_SETTING = 'NEW-VALUE'
|
settings.AWX_SOME_SETTING = 'NEW-VALUE'
|
||||||
|
|
||||||
@@ -306,12 +211,7 @@ def test_db_setting_update(settings, mocker):
|
|||||||
|
|
||||||
def test_db_setting_deletion(settings, mocker):
|
def test_db_setting_deletion(settings, mocker):
|
||||||
"settings are auto-deleted from the database"
|
"settings are auto-deleted from the database"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_setting = mocker.Mock(key='AWX_SOME_SETTING', value='FROM_DB')
|
existing_setting = mocker.Mock(key='AWX_SOME_SETTING', value='FROM_DB')
|
||||||
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=[existing_setting]):
|
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=[existing_setting]):
|
||||||
@@ -323,12 +223,7 @@ def test_db_setting_deletion(settings, mocker):
|
|||||||
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT')
|
||||||
def test_read_only_setting_deletion(settings):
|
def test_read_only_setting_deletion(settings):
|
||||||
"read-only settings cannot be deleted"
|
"read-only settings cannot be deleted"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
assert settings.AWX_SOME_SETTING == 'DEFAULT'
|
||||||
with pytest.raises(ImproperlyConfigured):
|
with pytest.raises(ImproperlyConfigured):
|
||||||
del settings.AWX_SOME_SETTING
|
del settings.AWX_SOME_SETTING
|
||||||
@@ -337,36 +232,22 @@ def test_read_only_setting_deletion(settings):
|
|||||||
|
|
||||||
def test_charfield_properly_sets_none(settings, mocker):
|
def test_charfield_properly_sets_none(settings, mocker):
|
||||||
"see: https://github.com/ansible/ansible-tower/issues/5322"
|
"see: https://github.com/ansible/ansible-tower/issues/5322"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_SOME_SETTING', field_class=fields.CharField, category=_('System'), category_slug='system', allow_null=True)
|
||||||
'AWX_SOME_SETTING',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
allow_null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
setting_list = mocker.Mock(**{'order_by.return_value.first.return_value': None})
|
setting_list = mocker.Mock(**{'order_by.return_value.first.return_value': None})
|
||||||
with apply_patches([
|
with apply_patches(
|
||||||
mocker.patch('awx.conf.models.Setting.objects.filter',
|
[
|
||||||
return_value=setting_list),
|
mocker.patch('awx.conf.models.Setting.objects.filter', return_value=setting_list),
|
||||||
mocker.patch('awx.conf.models.Setting.objects.create', mocker.Mock())
|
mocker.patch('awx.conf.models.Setting.objects.create', mocker.Mock()),
|
||||||
]):
|
]
|
||||||
|
):
|
||||||
settings.AWX_SOME_SETTING = None
|
settings.AWX_SOME_SETTING = None
|
||||||
|
|
||||||
models.Setting.objects.create.assert_called_with(
|
models.Setting.objects.create.assert_called_with(key='AWX_SOME_SETTING', user=None, value=None)
|
||||||
key='AWX_SOME_SETTING',
|
|
||||||
user=None,
|
|
||||||
value=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_settings_use_cache(settings, mocker):
|
def test_settings_use_cache(settings, mocker):
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_VAR', field_class=fields.CharField, category=_('System'), category_slug='system')
|
||||||
'AWX_VAR',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system'
|
|
||||||
)
|
|
||||||
settings.cache.set('AWX_VAR', 'foobar')
|
settings.cache.set('AWX_VAR', 'foobar')
|
||||||
settings.cache.set('_awx_conf_preload_expires', 100)
|
settings.cache.set('_awx_conf_preload_expires', 100)
|
||||||
# Will fail test if database is used
|
# Will fail test if database is used
|
||||||
@@ -374,13 +255,7 @@ def test_settings_use_cache(settings, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_settings_use_an_encrypted_cache(settings, mocker):
|
def test_settings_use_an_encrypted_cache(settings, mocker):
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_ENCRYPTED', field_class=fields.CharField, category=_('System'), category_slug='system', encrypted=True)
|
||||||
'AWX_ENCRYPTED',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
encrypted=True
|
|
||||||
)
|
|
||||||
assert isinstance(settings.cache, EncryptedCacheProxy)
|
assert isinstance(settings.cache, EncryptedCacheProxy)
|
||||||
assert settings.cache.__dict__['encrypter'] == encrypt_field
|
assert settings.cache.__dict__['encrypter'] == encrypt_field
|
||||||
assert settings.cache.__dict__['decrypter'] == decrypt_field
|
assert settings.cache.__dict__['decrypter'] == decrypt_field
|
||||||
@@ -393,34 +268,18 @@ def test_settings_use_an_encrypted_cache(settings, mocker):
|
|||||||
|
|
||||||
def test_sensitive_cache_data_is_encrypted(settings, mocker):
|
def test_sensitive_cache_data_is_encrypted(settings, mocker):
|
||||||
"fields marked as `encrypted` are stored in the cache with encryption"
|
"fields marked as `encrypted` are stored in the cache with encryption"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_ENCRYPTED', field_class=fields.CharField, category=_('System'), category_slug='system', encrypted=True)
|
||||||
'AWX_ENCRYPTED',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
encrypted=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def rot13(obj, attribute):
|
def rot13(obj, attribute):
|
||||||
assert obj.pk == 123
|
assert obj.pk == 123
|
||||||
return codecs.encode(getattr(obj, attribute), 'rot_13')
|
return codecs.encode(getattr(obj, attribute), 'rot_13')
|
||||||
|
|
||||||
native_cache = LocMemCache(str(uuid4()), {})
|
native_cache = LocMemCache(str(uuid4()), {})
|
||||||
cache = EncryptedCacheProxy(
|
cache = EncryptedCacheProxy(native_cache, settings.registry, encrypter=rot13, decrypter=rot13)
|
||||||
native_cache,
|
|
||||||
settings.registry,
|
|
||||||
encrypter=rot13,
|
|
||||||
decrypter=rot13
|
|
||||||
)
|
|
||||||
# Insert the setting value into the database; the encryption process will
|
# Insert the setting value into the database; the encryption process will
|
||||||
# use its primary key as part of the encryption key
|
# use its primary key as part of the encryption key
|
||||||
setting_from_db = mocker.Mock(pk=123, key='AWX_ENCRYPTED', value='SECRET!')
|
setting_from_db = mocker.Mock(pk=123, key='AWX_ENCRYPTED', value='SECRET!')
|
||||||
mocks = mocker.Mock(**{
|
mocks = mocker.Mock(**{'order_by.return_value': mocker.Mock(**{'__iter__': lambda self: iter([setting_from_db]), 'first.return_value': setting_from_db})})
|
||||||
'order_by.return_value': mocker.Mock(**{
|
|
||||||
'__iter__': lambda self: iter([setting_from_db]),
|
|
||||||
'first.return_value': setting_from_db
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=mocks):
|
with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=mocks):
|
||||||
cache.set('AWX_ENCRYPTED', 'SECRET!')
|
cache.set('AWX_ENCRYPTED', 'SECRET!')
|
||||||
assert cache.get('AWX_ENCRYPTED') == 'SECRET!'
|
assert cache.get('AWX_ENCRYPTED') == 'SECRET!'
|
||||||
@@ -429,26 +288,14 @@ def test_sensitive_cache_data_is_encrypted(settings, mocker):
|
|||||||
|
|
||||||
def test_readonly_sensitive_cache_data_is_encrypted(settings):
|
def test_readonly_sensitive_cache_data_is_encrypted(settings):
|
||||||
"readonly fields marked as `encrypted` are stored in the cache with encryption"
|
"readonly fields marked as `encrypted` are stored in the cache with encryption"
|
||||||
settings.registry.register(
|
settings.registry.register('AWX_ENCRYPTED', field_class=fields.CharField, category=_('System'), category_slug='system', read_only=True, encrypted=True)
|
||||||
'AWX_ENCRYPTED',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
category=_('System'),
|
|
||||||
category_slug='system',
|
|
||||||
read_only=True,
|
|
||||||
encrypted=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def rot13(obj, attribute):
|
def rot13(obj, attribute):
|
||||||
assert obj.pk is None
|
assert obj.pk is None
|
||||||
return codecs.encode(getattr(obj, attribute), 'rot_13')
|
return codecs.encode(getattr(obj, attribute), 'rot_13')
|
||||||
|
|
||||||
native_cache = LocMemCache(str(uuid4()), {})
|
native_cache = LocMemCache(str(uuid4()), {})
|
||||||
cache = EncryptedCacheProxy(
|
cache = EncryptedCacheProxy(native_cache, settings.registry, encrypter=rot13, decrypter=rot13)
|
||||||
native_cache,
|
|
||||||
settings.registry,
|
|
||||||
encrypter=rot13,
|
|
||||||
decrypter=rot13
|
|
||||||
)
|
|
||||||
cache.set('AWX_ENCRYPTED', 'SECRET!')
|
cache.set('AWX_ENCRYPTED', 'SECRET!')
|
||||||
assert cache.get('AWX_ENCRYPTED') == 'SECRET!'
|
assert cache.get('AWX_ENCRYPTED') == 'SECRET!'
|
||||||
assert native_cache.get('AWX_ENCRYPTED') == 'FRPERG!'
|
assert native_cache.get('AWX_ENCRYPTED') == 'FRPERG!'
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from awx.conf.views import (
|
from awx.conf.views import SettingCategoryList, SettingSingletonDetail, SettingLoggingTest
|
||||||
SettingCategoryList,
|
|
||||||
SettingSingletonDetail,
|
|
||||||
SettingLoggingTest,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|||||||
@@ -7,7 +7,4 @@ __all__ = ['conf_to_dict']
|
|||||||
|
|
||||||
|
|
||||||
def conf_to_dict(obj):
|
def conf_to_dict(obj):
|
||||||
return {
|
return {'category': settings_registry.get_setting_category(obj.key), 'name': obj.key}
|
||||||
'category': settings_registry.get_setting_category(obj.key),
|
|
||||||
'name': obj.key,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,12 +22,7 @@ from rest_framework import serializers
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Tower
|
# Tower
|
||||||
from awx.api.generics import (
|
from awx.api.generics import APIView, GenericAPIView, ListAPIView, RetrieveUpdateDestroyAPIView
|
||||||
APIView,
|
|
||||||
GenericAPIView,
|
|
||||||
ListAPIView,
|
|
||||||
RetrieveUpdateDestroyAPIView,
|
|
||||||
)
|
|
||||||
from awx.api.permissions import IsSuperUser
|
from awx.api.permissions import IsSuperUser
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.utils import camelcase_to_underscore
|
from awx.main.utils import camelcase_to_underscore
|
||||||
@@ -81,9 +76,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
if self.category_slug not in category_slugs:
|
if self.category_slug not in category_slugs:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
registered_settings = settings_registry.get_registered_settings(
|
registered_settings = settings_registry.get_registered_settings(category_slug=self.category_slug, read_only=False)
|
||||||
category_slug=self.category_slug, read_only=False,
|
|
||||||
)
|
|
||||||
if self.category_slug == 'user':
|
if self.category_slug == 'user':
|
||||||
return Setting.objects.filter(key__in=registered_settings, user=self.request.user)
|
return Setting.objects.filter(key__in=registered_settings, user=self.request.user)
|
||||||
else:
|
else:
|
||||||
@@ -91,9 +84,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
settings_qs = self.get_queryset()
|
settings_qs = self.get_queryset()
|
||||||
registered_settings = settings_registry.get_registered_settings(
|
registered_settings = settings_registry.get_registered_settings(category_slug=self.category_slug)
|
||||||
category_slug=self.category_slug,
|
|
||||||
)
|
|
||||||
all_settings = {}
|
all_settings = {}
|
||||||
for setting in settings_qs:
|
for setting in settings_qs:
|
||||||
all_settings[setting.key] = setting.value
|
all_settings[setting.key] = setting.value
|
||||||
@@ -117,9 +108,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
for key, value in serializer.validated_data.items():
|
for key, value in serializer.validated_data.items():
|
||||||
if key == 'LICENSE' or settings_registry.is_setting_read_only(key):
|
if key == 'LICENSE' or settings_registry.is_setting_read_only(key):
|
||||||
continue
|
continue
|
||||||
if settings_registry.is_setting_encrypted(key) and \
|
if settings_registry.is_setting_encrypted(key) and isinstance(value, str) and value.startswith('$encrypted$'):
|
||||||
isinstance(value, str) and \
|
|
||||||
value.startswith('$encrypted$'):
|
|
||||||
continue
|
continue
|
||||||
setattr(serializer.instance, key, value)
|
setattr(serializer.instance, key, value)
|
||||||
setting = settings_qs.filter(key=key).order_by('pk').first()
|
setting = settings_qs.filter(key=key).order_by('pk').first()
|
||||||
@@ -133,7 +122,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
if settings_change_list:
|
if settings_change_list:
|
||||||
connection.on_commit(lambda: handle_setting_changes.delay(settings_change_list))
|
connection.on_commit(lambda: handle_setting_changes.delay(settings_change_list))
|
||||||
|
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
self.perform_destroy(instance)
|
self.perform_destroy(instance)
|
||||||
@@ -184,10 +172,7 @@ class SettingLoggingTest(GenericAPIView):
|
|||||||
protocol = getattr(settings, 'LOG_AGGREGATOR_PROTOCOL', None)
|
protocol = getattr(settings, 'LOG_AGGREGATOR_PROTOCOL', None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_output(
|
subprocess.check_output(['rsyslogd', '-N1', '-f', '/var/lib/awx/rsyslog/rsyslog.conf'], stderr=subprocess.STDOUT)
|
||||||
['rsyslogd', '-N1', '-f', '/var/lib/awx/rsyslog/rsyslog.conf'],
|
|
||||||
stderr=subprocess.STDOUT
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
return Response({'error': exc.output}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': exc.output}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@@ -206,7 +191,7 @@ class SettingLoggingTest(GenericAPIView):
|
|||||||
else:
|
else:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
s.settimeout(.5)
|
s.settimeout(0.5)
|
||||||
s.connect((hostname, int(port)))
|
s.connect((hostname, int(port)))
|
||||||
s.shutdown(SHUT_RDWR)
|
s.shutdown(SHUT_RDWR)
|
||||||
s.close()
|
s.close()
|
||||||
|
|||||||
1002
awx/main/access.py
1002
awx/main/access.py
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ logger = logging.getLogger('awx.analytics.broadcast_websocket')
|
|||||||
|
|
||||||
|
|
||||||
def dt_to_seconds(dt):
|
def dt_to_seconds(dt):
|
||||||
return int((dt - datetime.datetime(1970,1,1)).total_seconds())
|
return int((dt - datetime.datetime(1970, 1, 1)).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
def now_seconds():
|
def now_seconds():
|
||||||
@@ -37,7 +37,7 @@ def safe_name(s):
|
|||||||
|
|
||||||
|
|
||||||
# Second granularity; Per-minute
|
# Second granularity; Per-minute
|
||||||
class FixedSlidingWindow():
|
class FixedSlidingWindow:
|
||||||
def __init__(self, start_time=None):
|
def __init__(self, start_time=None):
|
||||||
self.buckets = dict()
|
self.buckets = dict()
|
||||||
self.start_time = start_time or now_seconds()
|
self.start_time = start_time or now_seconds()
|
||||||
@@ -65,7 +65,7 @@ class FixedSlidingWindow():
|
|||||||
return sum(self.buckets.values()) or 0
|
return sum(self.buckets.values()) or 0
|
||||||
|
|
||||||
|
|
||||||
class BroadcastWebsocketStatsManager():
|
class BroadcastWebsocketStatsManager:
|
||||||
def __init__(self, event_loop, local_hostname):
|
def __init__(self, event_loop, local_hostname):
|
||||||
self._local_hostname = local_hostname
|
self._local_hostname = local_hostname
|
||||||
|
|
||||||
@@ -74,8 +74,7 @@ class BroadcastWebsocketStatsManager():
|
|||||||
self._redis_key = BROADCAST_WEBSOCKET_REDIS_KEY_NAME
|
self._redis_key = BROADCAST_WEBSOCKET_REDIS_KEY_NAME
|
||||||
|
|
||||||
def new_remote_host_stats(self, remote_hostname):
|
def new_remote_host_stats(self, remote_hostname):
|
||||||
self._stats[remote_hostname] = BroadcastWebsocketStats(self._local_hostname,
|
self._stats[remote_hostname] = BroadcastWebsocketStats(self._local_hostname, remote_hostname)
|
||||||
remote_hostname)
|
|
||||||
return self._stats[remote_hostname]
|
return self._stats[remote_hostname]
|
||||||
|
|
||||||
def delete_remote_host_stats(self, remote_hostname):
|
def delete_remote_host_stats(self, remote_hostname):
|
||||||
@@ -100,15 +99,15 @@ class BroadcastWebsocketStatsManager():
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_stats_sync(cls):
|
def get_stats_sync(cls):
|
||||||
'''
|
"""
|
||||||
Stringified verion of all the stats
|
Stringified verion of all the stats
|
||||||
'''
|
"""
|
||||||
redis_conn = redis.Redis.from_url(settings.BROKER_URL)
|
redis_conn = redis.Redis.from_url(settings.BROKER_URL)
|
||||||
stats_str = redis_conn.get(BROADCAST_WEBSOCKET_REDIS_KEY_NAME) or b''
|
stats_str = redis_conn.get(BROADCAST_WEBSOCKET_REDIS_KEY_NAME) or b''
|
||||||
return parser.text_string_to_metric_families(stats_str.decode('UTF-8'))
|
return parser.text_string_to_metric_families(stats_str.decode('UTF-8'))
|
||||||
|
|
||||||
|
|
||||||
class BroadcastWebsocketStats():
|
class BroadcastWebsocketStats:
|
||||||
def __init__(self, local_hostname, remote_hostname):
|
def __init__(self, local_hostname, remote_hostname):
|
||||||
self._local_hostname = local_hostname
|
self._local_hostname = local_hostname
|
||||||
self._remote_hostname = remote_hostname
|
self._remote_hostname = remote_hostname
|
||||||
@@ -118,24 +117,25 @@ class BroadcastWebsocketStats():
|
|||||||
self.name = safe_name(self._local_hostname)
|
self.name = safe_name(self._local_hostname)
|
||||||
self.remote_name = safe_name(self._remote_hostname)
|
self.remote_name = safe_name(self._remote_hostname)
|
||||||
|
|
||||||
self._messages_received_total = Counter(f'awx_{self.remote_name}_messages_received_total',
|
self._messages_received_total = Counter(
|
||||||
'Number of messages received, to be forwarded, by the broadcast websocket system',
|
f'awx_{self.remote_name}_messages_received_total',
|
||||||
registry=self._registry)
|
'Number of messages received, to be forwarded, by the broadcast websocket system',
|
||||||
self._messages_received = Gauge(f'awx_{self.remote_name}_messages_received',
|
registry=self._registry,
|
||||||
'Number forwarded messages received by the broadcast websocket system, for the duration of the current connection',
|
)
|
||||||
registry=self._registry)
|
self._messages_received = Gauge(
|
||||||
self._connection = Enum(f'awx_{self.remote_name}_connection',
|
f'awx_{self.remote_name}_messages_received',
|
||||||
'Websocket broadcast connection',
|
'Number forwarded messages received by the broadcast websocket system, for the duration of the current connection',
|
||||||
states=['disconnected', 'connected'],
|
registry=self._registry,
|
||||||
registry=self._registry)
|
)
|
||||||
|
self._connection = Enum(
|
||||||
|
f'awx_{self.remote_name}_connection', 'Websocket broadcast connection', states=['disconnected', 'connected'], registry=self._registry
|
||||||
|
)
|
||||||
self._connection.state('disconnected')
|
self._connection.state('disconnected')
|
||||||
self._connection_start = Gauge(f'awx_{self.remote_name}_connection_start',
|
self._connection_start = Gauge(f'awx_{self.remote_name}_connection_start', 'Time the connection was established', registry=self._registry)
|
||||||
'Time the connection was established',
|
|
||||||
registry=self._registry)
|
|
||||||
|
|
||||||
self._messages_received_per_minute = Gauge(f'awx_{self.remote_name}_messages_received_per_minute',
|
self._messages_received_per_minute = Gauge(
|
||||||
'Messages received per minute',
|
f'awx_{self.remote_name}_messages_received_per_minute', 'Messages received per minute', registry=self._registry
|
||||||
registry=self._registry)
|
)
|
||||||
self._internal_messages_received_per_minute = FixedSlidingWindow()
|
self._internal_messages_received_per_minute = FixedSlidingWindow()
|
||||||
|
|
||||||
def unregister(self):
|
def unregister(self):
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from awx.conf.license import get_license
|
from awx.conf.license import get_license
|
||||||
from awx.main.utils import (get_awx_version, get_ansible_version,
|
from awx.main.utils import get_awx_version, get_ansible_version, get_custom_venv_choices, camelcase_to_underscore
|
||||||
get_custom_venv_choices, camelcase_to_underscore)
|
|
||||||
from awx.main import models
|
from awx.main import models
|
||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
from awx.main.analytics import register
|
from awx.main.analytics import register
|
||||||
@@ -68,18 +67,24 @@ def config(since, **kwargs):
|
|||||||
@register('counts', '1.0', description=_('Counts of objects such as organizations, inventories, and projects'))
|
@register('counts', '1.0', description=_('Counts of objects such as organizations, inventories, and projects'))
|
||||||
def counts(since, **kwargs):
|
def counts(since, **kwargs):
|
||||||
counts = {}
|
counts = {}
|
||||||
for cls in (models.Organization, models.Team, models.User,
|
for cls in (
|
||||||
models.Inventory, models.Credential, models.Project,
|
models.Organization,
|
||||||
models.JobTemplate, models.WorkflowJobTemplate,
|
models.Team,
|
||||||
models.Host, models.Schedule, models.CustomInventoryScript,
|
models.User,
|
||||||
models.NotificationTemplate):
|
models.Inventory,
|
||||||
|
models.Credential,
|
||||||
|
models.Project,
|
||||||
|
models.JobTemplate,
|
||||||
|
models.WorkflowJobTemplate,
|
||||||
|
models.Host,
|
||||||
|
models.Schedule,
|
||||||
|
models.CustomInventoryScript,
|
||||||
|
models.NotificationTemplate,
|
||||||
|
):
|
||||||
counts[camelcase_to_underscore(cls.__name__)] = cls.objects.count()
|
counts[camelcase_to_underscore(cls.__name__)] = cls.objects.count()
|
||||||
|
|
||||||
venvs = get_custom_venv_choices()
|
venvs = get_custom_venv_choices()
|
||||||
counts['custom_virtualenvs'] = len([
|
counts['custom_virtualenvs'] = len([v for v in venvs if os.path.basename(v.rstrip('/')) != 'ansible'])
|
||||||
v for v in venvs
|
|
||||||
if os.path.basename(v.rstrip('/')) != 'ansible'
|
|
||||||
])
|
|
||||||
|
|
||||||
inv_counts = dict(models.Inventory.objects.order_by().values_list('kind').annotate(Count('kind')))
|
inv_counts = dict(models.Inventory.objects.order_by().values_list('kind').annotate(Count('kind')))
|
||||||
inv_counts['normal'] = inv_counts.get('', 0)
|
inv_counts['normal'] = inv_counts.get('', 0)
|
||||||
@@ -87,7 +92,7 @@ def counts(since, **kwargs):
|
|||||||
inv_counts['smart'] = inv_counts.get('smart', 0)
|
inv_counts['smart'] = inv_counts.get('smart', 0)
|
||||||
counts['inventories'] = inv_counts
|
counts['inventories'] = inv_counts
|
||||||
|
|
||||||
counts['unified_job'] = models.UnifiedJob.objects.exclude(launch_type='sync').count() # excludes implicit project_updates
|
counts['unified_job'] = models.UnifiedJob.objects.exclude(launch_type='sync').count() # excludes implicit project_updates
|
||||||
counts['active_host_count'] = models.Host.objects.active_count()
|
counts['active_host_count'] = models.Host.objects.active_count()
|
||||||
active_sessions = Session.objects.filter(expire_date__gte=now()).count()
|
active_sessions = Session.objects.filter(expire_date__gte=now()).count()
|
||||||
active_user_sessions = models.UserSessionMembership.objects.select_related('session').filter(session__expire_date__gte=now()).count()
|
active_user_sessions = models.UserSessionMembership.objects.select_related('session').filter(session__expire_date__gte=now()).count()
|
||||||
@@ -95,7 +100,16 @@ def counts(since, **kwargs):
|
|||||||
counts['active_sessions'] = active_sessions
|
counts['active_sessions'] = active_sessions
|
||||||
counts['active_user_sessions'] = active_user_sessions
|
counts['active_user_sessions'] = active_user_sessions
|
||||||
counts['active_anonymous_sessions'] = active_anonymous_sessions
|
counts['active_anonymous_sessions'] = active_anonymous_sessions
|
||||||
counts['running_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').filter(status__in=('running', 'waiting',)).count()
|
counts['running_jobs'] = (
|
||||||
|
models.UnifiedJob.objects.exclude(launch_type='sync')
|
||||||
|
.filter(
|
||||||
|
status__in=(
|
||||||
|
'running',
|
||||||
|
'waiting',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
counts['pending_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').filter(status__in=('pending',)).count()
|
counts['pending_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').filter(status__in=('pending',)).count()
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
@@ -103,61 +117,49 @@ def counts(since, **kwargs):
|
|||||||
@register('org_counts', '1.0', description=_('Counts of users and teams by organization'))
|
@register('org_counts', '1.0', description=_('Counts of users and teams by organization'))
|
||||||
def org_counts(since, **kwargs):
|
def org_counts(since, **kwargs):
|
||||||
counts = {}
|
counts = {}
|
||||||
for org in models.Organization.objects.annotate(num_users=Count('member_role__members', distinct=True),
|
for org in models.Organization.objects.annotate(num_users=Count('member_role__members', distinct=True), num_teams=Count('teams', distinct=True)).values(
|
||||||
num_teams=Count('teams', distinct=True)).values('name', 'id', 'num_users', 'num_teams'):
|
'name', 'id', 'num_users', 'num_teams'
|
||||||
counts[org['id']] = {'name': org['name'],
|
):
|
||||||
'users': org['num_users'],
|
counts[org['id']] = {'name': org['name'], 'users': org['num_users'], 'teams': org['num_teams']}
|
||||||
'teams': org['num_teams']
|
|
||||||
}
|
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
@register('cred_type_counts', '1.0', description=_('Counts of credentials by credential type'))
|
@register('cred_type_counts', '1.0', description=_('Counts of credentials by credential type'))
|
||||||
def cred_type_counts(since, **kwargs):
|
def cred_type_counts(since, **kwargs):
|
||||||
counts = {}
|
counts = {}
|
||||||
for cred_type in models.CredentialType.objects.annotate(num_credentials=Count(
|
for cred_type in models.CredentialType.objects.annotate(num_credentials=Count('credentials', distinct=True)).values(
|
||||||
'credentials', distinct=True)).values('name', 'id', 'managed_by_tower', 'num_credentials'):
|
'name', 'id', 'managed_by_tower', 'num_credentials'
|
||||||
counts[cred_type['id']] = {'name': cred_type['name'],
|
):
|
||||||
'credential_count': cred_type['num_credentials'],
|
counts[cred_type['id']] = {
|
||||||
'managed_by_tower': cred_type['managed_by_tower']
|
'name': cred_type['name'],
|
||||||
}
|
'credential_count': cred_type['num_credentials'],
|
||||||
|
'managed_by_tower': cred_type['managed_by_tower'],
|
||||||
|
}
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
@register('inventory_counts', '1.2', description=_('Inventories, their inventory sources, and host counts'))
|
@register('inventory_counts', '1.2', description=_('Inventories, their inventory sources, and host counts'))
|
||||||
def inventory_counts(since, **kwargs):
|
def inventory_counts(since, **kwargs):
|
||||||
counts = {}
|
counts = {}
|
||||||
for inv in models.Inventory.objects.filter(kind='').annotate(num_sources=Count('inventory_sources', distinct=True),
|
for inv in (
|
||||||
num_hosts=Count('hosts', distinct=True)).only('id', 'name', 'kind'):
|
models.Inventory.objects.filter(kind='')
|
||||||
|
.annotate(num_sources=Count('inventory_sources', distinct=True), num_hosts=Count('hosts', distinct=True))
|
||||||
|
.only('id', 'name', 'kind')
|
||||||
|
):
|
||||||
source_list = []
|
source_list = []
|
||||||
for source in inv.inventory_sources.filter().annotate(num_hosts=Count('hosts', distinct=True)).values('name','source', 'num_hosts'):
|
for source in inv.inventory_sources.filter().annotate(num_hosts=Count('hosts', distinct=True)).values('name', 'source', 'num_hosts'):
|
||||||
source_list.append(source)
|
source_list.append(source)
|
||||||
counts[inv.id] = {'name': inv.name,
|
counts[inv.id] = {'name': inv.name, 'kind': inv.kind, 'hosts': inv.num_hosts, 'sources': inv.num_sources, 'source_list': source_list}
|
||||||
'kind': inv.kind,
|
|
||||||
'hosts': inv.num_hosts,
|
|
||||||
'sources': inv.num_sources,
|
|
||||||
'source_list': source_list
|
|
||||||
}
|
|
||||||
|
|
||||||
for smart_inv in models.Inventory.objects.filter(kind='smart'):
|
for smart_inv in models.Inventory.objects.filter(kind='smart'):
|
||||||
counts[smart_inv.id] = {'name': smart_inv.name,
|
counts[smart_inv.id] = {'name': smart_inv.name, 'kind': smart_inv.kind, 'hosts': smart_inv.hosts.count(), 'sources': 0, 'source_list': []}
|
||||||
'kind': smart_inv.kind,
|
|
||||||
'hosts': smart_inv.hosts.count(),
|
|
||||||
'sources': 0,
|
|
||||||
'source_list': []
|
|
||||||
}
|
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
@register('projects_by_scm_type', '1.0', description=_('Counts of projects by source control type'))
|
@register('projects_by_scm_type', '1.0', description=_('Counts of projects by source control type'))
|
||||||
def projects_by_scm_type(since, **kwargs):
|
def projects_by_scm_type(since, **kwargs):
|
||||||
counts = dict(
|
counts = dict((t[0] or 'manual', 0) for t in models.Project.SCM_TYPE_CHOICES)
|
||||||
(t[0] or 'manual', 0)
|
for result in models.Project.objects.values('scm_type').annotate(count=Count('scm_type')).order_by('scm_type'):
|
||||||
for t in models.Project.SCM_TYPE_CHOICES
|
|
||||||
)
|
|
||||||
for result in models.Project.objects.values('scm_type').annotate(
|
|
||||||
count=Count('scm_type')
|
|
||||||
).order_by('scm_type'):
|
|
||||||
counts[result['scm_type'] or 'manual'] = result['count']
|
counts[result['scm_type'] or 'manual'] = result['count']
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
@@ -172,10 +174,10 @@ def _get_isolated_datetime(last_check):
|
|||||||
def instance_info(since, include_hostnames=False, **kwargs):
|
def instance_info(since, include_hostnames=False, **kwargs):
|
||||||
info = {}
|
info = {}
|
||||||
instances = models.Instance.objects.values_list('hostname').values(
|
instances = models.Instance.objects.values_list('hostname').values(
|
||||||
'uuid', 'version', 'capacity', 'cpu', 'memory', 'managed_by_policy', 'hostname', 'last_isolated_check', 'enabled')
|
'uuid', 'version', 'capacity', 'cpu', 'memory', 'managed_by_policy', 'hostname', 'last_isolated_check', 'enabled'
|
||||||
|
)
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
consumed_capacity = sum(x.task_impact for x in models.UnifiedJob.objects.filter(execution_node=instance['hostname'],
|
consumed_capacity = sum(x.task_impact for x in models.UnifiedJob.objects.filter(execution_node=instance['hostname'], status__in=('running', 'waiting')))
|
||||||
status__in=('running', 'waiting')))
|
|
||||||
instance_info = {
|
instance_info = {
|
||||||
'uuid': instance['uuid'],
|
'uuid': instance['uuid'],
|
||||||
'version': instance['version'],
|
'version': instance['version'],
|
||||||
@@ -186,7 +188,7 @@ def instance_info(since, include_hostnames=False, **kwargs):
|
|||||||
'last_isolated_check': _get_isolated_datetime(instance['last_isolated_check']),
|
'last_isolated_check': _get_isolated_datetime(instance['last_isolated_check']),
|
||||||
'enabled': instance['enabled'],
|
'enabled': instance['enabled'],
|
||||||
'consumed_capacity': consumed_capacity,
|
'consumed_capacity': consumed_capacity,
|
||||||
'remaining_capacity': instance['capacity'] - consumed_capacity
|
'remaining_capacity': instance['capacity'] - consumed_capacity,
|
||||||
}
|
}
|
||||||
if include_hostnames is True:
|
if include_hostnames is True:
|
||||||
instance_info['hostname'] = instance['hostname']
|
instance_info['hostname'] = instance['hostname']
|
||||||
@@ -198,20 +200,22 @@ def job_counts(since, **kwargs):
|
|||||||
counts = {}
|
counts = {}
|
||||||
counts['total_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').count()
|
counts['total_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').count()
|
||||||
counts['status'] = dict(models.UnifiedJob.objects.exclude(launch_type='sync').values_list('status').annotate(Count('status')).order_by())
|
counts['status'] = dict(models.UnifiedJob.objects.exclude(launch_type='sync').values_list('status').annotate(Count('status')).order_by())
|
||||||
counts['launch_type'] = dict(models.UnifiedJob.objects.exclude(launch_type='sync').values_list(
|
counts['launch_type'] = dict(models.UnifiedJob.objects.exclude(launch_type='sync').values_list('launch_type').annotate(Count('launch_type')).order_by())
|
||||||
'launch_type').annotate(Count('launch_type')).order_by())
|
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
def job_instance_counts(since, **kwargs):
|
def job_instance_counts(since, **kwargs):
|
||||||
counts = {}
|
counts = {}
|
||||||
job_types = models.UnifiedJob.objects.exclude(launch_type='sync').values_list(
|
job_types = (
|
||||||
'execution_node', 'launch_type').annotate(job_launch_type=Count('launch_type')).order_by()
|
models.UnifiedJob.objects.exclude(launch_type='sync')
|
||||||
|
.values_list('execution_node', 'launch_type')
|
||||||
|
.annotate(job_launch_type=Count('launch_type'))
|
||||||
|
.order_by()
|
||||||
|
)
|
||||||
for job in job_types:
|
for job in job_types:
|
||||||
counts.setdefault(job[0], {}).setdefault('launch_type', {})[job[1]] = job[2]
|
counts.setdefault(job[0], {}).setdefault('launch_type', {})[job[1]] = job[2]
|
||||||
|
|
||||||
job_statuses = models.UnifiedJob.objects.exclude(launch_type='sync').values_list(
|
job_statuses = models.UnifiedJob.objects.exclude(launch_type='sync').values_list('execution_node', 'status').annotate(job_status=Count('status')).order_by()
|
||||||
'execution_node', 'status').annotate(job_status=Count('status')).order_by()
|
|
||||||
for job in job_statuses:
|
for job in job_statuses:
|
||||||
counts.setdefault(job[0], {}).setdefault('status', {})[job[1]] = job[2]
|
counts.setdefault(job[0], {}).setdefault('status', {})[job[1]] = job[2]
|
||||||
return counts
|
return counts
|
||||||
@@ -261,12 +265,12 @@ class FileSplitter(io.StringIO):
|
|||||||
self.files = self.files[:-1]
|
self.files = self.files[:-1]
|
||||||
# If we only have one file, remove the suffix
|
# If we only have one file, remove the suffix
|
||||||
if len(self.files) == 1:
|
if len(self.files) == 1:
|
||||||
os.rename(self.files[0],self.files[0].replace('_split0',''))
|
os.rename(self.files[0], self.files[0].replace('_split0', ''))
|
||||||
return self.files
|
return self.files
|
||||||
|
|
||||||
def write(self, s):
|
def write(self, s):
|
||||||
if not self.header:
|
if not self.header:
|
||||||
self.header = s[0:s.index('\n')]
|
self.header = s[0 : s.index('\n')]
|
||||||
self.counter += self.currentfile.write(s)
|
self.counter += self.currentfile.write(s)
|
||||||
if self.counter >= MAX_TABLE_SIZE:
|
if self.counter >= MAX_TABLE_SIZE:
|
||||||
self.cycle_file()
|
self.cycle_file()
|
||||||
@@ -307,7 +311,9 @@ def events_table(since, full_path, until, **kwargs):
|
|||||||
FROM main_jobevent
|
FROM main_jobevent
|
||||||
WHERE (main_jobevent.created > '{}' AND main_jobevent.created <= '{}')
|
WHERE (main_jobevent.created > '{}' AND main_jobevent.created <= '{}')
|
||||||
ORDER BY main_jobevent.id ASC) TO STDOUT WITH CSV HEADER
|
ORDER BY main_jobevent.id ASC) TO STDOUT WITH CSV HEADER
|
||||||
'''.format(since.isoformat(),until.isoformat())
|
'''.format(
|
||||||
|
since.isoformat(), until.isoformat()
|
||||||
|
)
|
||||||
return _copy_table(table='events', query=events_query, path=full_path)
|
return _copy_table(table='events', query=events_query, path=full_path)
|
||||||
|
|
||||||
|
|
||||||
@@ -346,7 +352,9 @@ def unified_jobs_table(since, full_path, until, **kwargs):
|
|||||||
OR (main_unifiedjob.finished > '{0}' AND main_unifiedjob.finished <= '{1}'))
|
OR (main_unifiedjob.finished > '{0}' AND main_unifiedjob.finished <= '{1}'))
|
||||||
AND main_unifiedjob.launch_type != 'sync'
|
AND main_unifiedjob.launch_type != 'sync'
|
||||||
ORDER BY main_unifiedjob.id ASC) TO STDOUT WITH CSV HEADER
|
ORDER BY main_unifiedjob.id ASC) TO STDOUT WITH CSV HEADER
|
||||||
'''.format(since.isoformat(),until.isoformat())
|
'''.format(
|
||||||
|
since.isoformat(), until.isoformat()
|
||||||
|
)
|
||||||
return _copy_table(table='unified_jobs', query=unified_job_query, path=full_path)
|
return _copy_table(table='unified_jobs', query=unified_job_query, path=full_path)
|
||||||
|
|
||||||
|
|
||||||
@@ -405,7 +413,9 @@ def workflow_job_node_table(since, full_path, until, **kwargs):
|
|||||||
) always_nodes ON main_workflowjobnode.id = always_nodes.from_workflowjobnode_id
|
) always_nodes ON main_workflowjobnode.id = always_nodes.from_workflowjobnode_id
|
||||||
WHERE (main_workflowjobnode.modified > '{}' AND main_workflowjobnode.modified <= '{}')
|
WHERE (main_workflowjobnode.modified > '{}' AND main_workflowjobnode.modified <= '{}')
|
||||||
ORDER BY main_workflowjobnode.id ASC) TO STDOUT WITH CSV HEADER
|
ORDER BY main_workflowjobnode.id ASC) TO STDOUT WITH CSV HEADER
|
||||||
'''.format(since.isoformat(),until.isoformat())
|
'''.format(
|
||||||
|
since.isoformat(), until.isoformat()
|
||||||
|
)
|
||||||
return _copy_table(table='workflow_job_node', query=workflow_job_node_query, path=full_path)
|
return _copy_table(table='workflow_job_node', query=workflow_job_node_query, path=full_path)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def all_collectors():
|
|||||||
key = func.__awx_analytics_key__
|
key = func.__awx_analytics_key__
|
||||||
desc = func.__awx_analytics_description__ or ''
|
desc = func.__awx_analytics_description__ or ''
|
||||||
version = func.__awx_analytics_version__
|
version = func.__awx_analytics_version__
|
||||||
collector_dict[key] = { 'name': key, 'version': version, 'description': desc}
|
collector_dict[key] = {'name': key, 'version': version, 'description': desc}
|
||||||
return collector_dict
|
return collector_dict
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ def register(key, version, description=None, format='json', expensive=False):
|
|||||||
return decorate
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
def gather(dest=None, module=None, subset = None, since = None, until = now(), collection_type='scheduled'):
|
def gather(dest=None, module=None, subset=None, since=None, until=now(), collection_type='scheduled'):
|
||||||
"""
|
"""
|
||||||
Gather all defined metrics and write them as JSON files in a .tgz
|
Gather all defined metrics and write them as JSON files in a .tgz
|
||||||
|
|
||||||
@@ -90,6 +90,7 @@ def gather(dest=None, module=None, subset = None, since = None, until = now(), c
|
|||||||
:param module: the module to search for registered analytic collector
|
:param module: the module to search for registered analytic collector
|
||||||
functions; defaults to awx.main.analytics.collectors
|
functions; defaults to awx.main.analytics.collectors
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _write_manifest(destdir, manifest):
|
def _write_manifest(destdir, manifest):
|
||||||
path = os.path.join(destdir, 'manifest.json')
|
path = os.path.join(destdir, 'manifest.json')
|
||||||
with open(path, 'w', encoding='utf-8') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
@@ -116,13 +117,10 @@ def gather(dest=None, module=None, subset = None, since = None, until = now(), c
|
|||||||
collector_module = module
|
collector_module = module
|
||||||
else:
|
else:
|
||||||
from awx.main.analytics import collectors
|
from awx.main.analytics import collectors
|
||||||
|
|
||||||
collector_module = collectors
|
collector_module = collectors
|
||||||
for name, func in inspect.getmembers(collector_module):
|
for name, func in inspect.getmembers(collector_module):
|
||||||
if (
|
if inspect.isfunction(func) and hasattr(func, '__awx_analytics_key__') and (not subset or name in subset):
|
||||||
inspect.isfunction(func) and
|
|
||||||
hasattr(func, '__awx_analytics_key__') and
|
|
||||||
(not subset or name in subset)
|
|
||||||
):
|
|
||||||
collector_list.append((name, func))
|
collector_list.append((name, func))
|
||||||
|
|
||||||
manifest = dict()
|
manifest = dict()
|
||||||
@@ -162,6 +160,7 @@ def gather(dest=None, module=None, subset = None, since = None, until = now(), c
|
|||||||
# Always include config.json if we're using our collectors
|
# Always include config.json if we're using our collectors
|
||||||
if 'config.json' not in manifest.keys() and not module:
|
if 'config.json' not in manifest.keys() and not module:
|
||||||
from awx.main.analytics import collectors
|
from awx.main.analytics import collectors
|
||||||
|
|
||||||
config = collectors.config
|
config = collectors.config
|
||||||
path = '{}.json'.format(os.path.join(gather_dir, config.__awx_analytics_key__))
|
path = '{}.json'.format(os.path.join(gather_dir, config.__awx_analytics_key__))
|
||||||
with open(path, 'w', encoding='utf-8') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
@@ -204,22 +203,14 @@ def gather(dest=None, module=None, subset = None, since = None, until = now(), c
|
|||||||
for i in range(0, len(stage_dirs)):
|
for i in range(0, len(stage_dirs)):
|
||||||
stage_dir = stage_dirs[i]
|
stage_dir = stage_dirs[i]
|
||||||
# can't use isoformat() since it has colons, which GNU tar doesn't like
|
# can't use isoformat() since it has colons, which GNU tar doesn't like
|
||||||
tarname = '_'.join([
|
tarname = '_'.join([settings.SYSTEM_UUID, until.strftime('%Y-%m-%d-%H%M%S%z'), str(i)])
|
||||||
settings.SYSTEM_UUID,
|
tgz = shutil.make_archive(os.path.join(os.path.dirname(dest), tarname), 'gztar', stage_dir)
|
||||||
until.strftime('%Y-%m-%d-%H%M%S%z'),
|
|
||||||
str(i)
|
|
||||||
])
|
|
||||||
tgz = shutil.make_archive(
|
|
||||||
os.path.join(os.path.dirname(dest), tarname),
|
|
||||||
'gztar',
|
|
||||||
stage_dir
|
|
||||||
)
|
|
||||||
tarfiles.append(tgz)
|
tarfiles.append(tgz)
|
||||||
except Exception:
|
except Exception:
|
||||||
shutil.rmtree(stage_dir, ignore_errors = True)
|
shutil.rmtree(stage_dir, ignore_errors=True)
|
||||||
logger.exception("Failed to write analytics archive file")
|
logger.exception("Failed to write analytics archive file")
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(dest, ignore_errors = True)
|
shutil.rmtree(dest, ignore_errors=True)
|
||||||
return tarfiles
|
return tarfiles
|
||||||
|
|
||||||
|
|
||||||
@@ -253,16 +244,17 @@ def ship(path):
|
|||||||
s.headers = get_awx_http_client_headers()
|
s.headers = get_awx_http_client_headers()
|
||||||
s.headers.pop('Content-Type')
|
s.headers.pop('Content-Type')
|
||||||
with set_environ(**settings.AWX_TASK_ENV):
|
with set_environ(**settings.AWX_TASK_ENV):
|
||||||
response = s.post(url,
|
response = s.post(
|
||||||
files=files,
|
url,
|
||||||
verify="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
files=files,
|
||||||
auth=(rh_user, rh_password),
|
verify="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
||||||
headers=s.headers,
|
auth=(rh_user, rh_password),
|
||||||
timeout=(31, 31))
|
headers=s.headers,
|
||||||
|
timeout=(31, 31),
|
||||||
|
)
|
||||||
# Accept 2XX status_codes
|
# Accept 2XX status_codes
|
||||||
if response.status_code >= 300:
|
if response.status_code >= 300:
|
||||||
return logger.exception('Upload failed with status {}, {}'.format(response.status_code,
|
return logger.exception('Upload failed with status {}, {}'.format(response.status_code, response.text))
|
||||||
response.text))
|
|
||||||
finally:
|
finally:
|
||||||
# cleanup tar.gz
|
# cleanup tar.gz
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from prometheus_client import (
|
from prometheus_client import REGISTRY, PROCESS_COLLECTOR, PLATFORM_COLLECTOR, GC_COLLECTOR, Gauge, Info, generate_latest
|
||||||
REGISTRY,
|
|
||||||
PROCESS_COLLECTOR,
|
|
||||||
PLATFORM_COLLECTOR,
|
|
||||||
GC_COLLECTOR,
|
|
||||||
Gauge,
|
|
||||||
Info,
|
|
||||||
generate_latest
|
|
||||||
)
|
|
||||||
|
|
||||||
from awx.conf.license import get_license
|
from awx.conf.license import get_license
|
||||||
from awx.main.utils import (get_awx_version, get_ansible_version)
|
from awx.main.utils import get_awx_version, get_ansible_version
|
||||||
from awx.main.analytics.collectors import (
|
from awx.main.analytics.collectors import (
|
||||||
counts,
|
counts,
|
||||||
instance_info,
|
instance_info,
|
||||||
@@ -31,23 +23,97 @@ INV_COUNT = Gauge('awx_inventories_total', 'Number of inventories')
|
|||||||
PROJ_COUNT = Gauge('awx_projects_total', 'Number of projects')
|
PROJ_COUNT = Gauge('awx_projects_total', 'Number of projects')
|
||||||
JT_COUNT = Gauge('awx_job_templates_total', 'Number of job templates')
|
JT_COUNT = Gauge('awx_job_templates_total', 'Number of job templates')
|
||||||
WFJT_COUNT = Gauge('awx_workflow_job_templates_total', 'Number of workflow job templates')
|
WFJT_COUNT = Gauge('awx_workflow_job_templates_total', 'Number of workflow job templates')
|
||||||
HOST_COUNT = Gauge('awx_hosts_total', 'Number of hosts', ['type',])
|
HOST_COUNT = Gauge(
|
||||||
|
'awx_hosts_total',
|
||||||
|
'Number of hosts',
|
||||||
|
[
|
||||||
|
'type',
|
||||||
|
],
|
||||||
|
)
|
||||||
SCHEDULE_COUNT = Gauge('awx_schedules_total', 'Number of schedules')
|
SCHEDULE_COUNT = Gauge('awx_schedules_total', 'Number of schedules')
|
||||||
INV_SCRIPT_COUNT = Gauge('awx_inventory_scripts_total', 'Number of invetory scripts')
|
INV_SCRIPT_COUNT = Gauge('awx_inventory_scripts_total', 'Number of invetory scripts')
|
||||||
USER_SESSIONS = Gauge('awx_sessions_total', 'Number of sessions', ['type',])
|
USER_SESSIONS = Gauge(
|
||||||
|
'awx_sessions_total',
|
||||||
|
'Number of sessions',
|
||||||
|
[
|
||||||
|
'type',
|
||||||
|
],
|
||||||
|
)
|
||||||
CUSTOM_VENVS = Gauge('awx_custom_virtualenvs_total', 'Number of virtualenvs')
|
CUSTOM_VENVS = Gauge('awx_custom_virtualenvs_total', 'Number of virtualenvs')
|
||||||
RUNNING_JOBS = Gauge('awx_running_jobs_total', 'Number of running jobs on the Tower system')
|
RUNNING_JOBS = Gauge('awx_running_jobs_total', 'Number of running jobs on the Tower system')
|
||||||
PENDING_JOBS = Gauge('awx_pending_jobs_total', 'Number of pending jobs on the Tower system')
|
PENDING_JOBS = Gauge('awx_pending_jobs_total', 'Number of pending jobs on the Tower system')
|
||||||
STATUS = Gauge('awx_status_total', 'Status of Job launched', ['status',])
|
STATUS = Gauge(
|
||||||
|
'awx_status_total',
|
||||||
|
'Status of Job launched',
|
||||||
|
[
|
||||||
|
'status',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
INSTANCE_CAPACITY = Gauge('awx_instance_capacity', 'Capacity of each node in a Tower system', ['hostname', 'instance_uuid',])
|
INSTANCE_CAPACITY = Gauge(
|
||||||
INSTANCE_CPU = Gauge('awx_instance_cpu', 'CPU cores on each node in a Tower system', ['hostname', 'instance_uuid',])
|
'awx_instance_capacity',
|
||||||
INSTANCE_MEMORY = Gauge('awx_instance_memory', 'RAM (Kb) on each node in a Tower system', ['hostname', 'instance_uuid',])
|
'Capacity of each node in a Tower system',
|
||||||
INSTANCE_INFO = Info('awx_instance', 'Info about each node in a Tower system', ['hostname', 'instance_uuid',])
|
[
|
||||||
INSTANCE_LAUNCH_TYPE = Gauge('awx_instance_launch_type_total', 'Type of Job launched', ['node', 'launch_type',])
|
'hostname',
|
||||||
INSTANCE_STATUS = Gauge('awx_instance_status_total', 'Status of Job launched', ['node', 'status',])
|
'instance_uuid',
|
||||||
INSTANCE_CONSUMED_CAPACITY = Gauge('awx_instance_consumed_capacity', 'Consumed capacity of each node in a Tower system', ['hostname', 'instance_uuid',])
|
],
|
||||||
INSTANCE_REMAINING_CAPACITY = Gauge('awx_instance_remaining_capacity', 'Remaining capacity of each node in a Tower system', ['hostname', 'instance_uuid',])
|
)
|
||||||
|
INSTANCE_CPU = Gauge(
|
||||||
|
'awx_instance_cpu',
|
||||||
|
'CPU cores on each node in a Tower system',
|
||||||
|
[
|
||||||
|
'hostname',
|
||||||
|
'instance_uuid',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
INSTANCE_MEMORY = Gauge(
|
||||||
|
'awx_instance_memory',
|
||||||
|
'RAM (Kb) on each node in a Tower system',
|
||||||
|
[
|
||||||
|
'hostname',
|
||||||
|
'instance_uuid',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
INSTANCE_INFO = Info(
|
||||||
|
'awx_instance',
|
||||||
|
'Info about each node in a Tower system',
|
||||||
|
[
|
||||||
|
'hostname',
|
||||||
|
'instance_uuid',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
INSTANCE_LAUNCH_TYPE = Gauge(
|
||||||
|
'awx_instance_launch_type_total',
|
||||||
|
'Type of Job launched',
|
||||||
|
[
|
||||||
|
'node',
|
||||||
|
'launch_type',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
INSTANCE_STATUS = Gauge(
|
||||||
|
'awx_instance_status_total',
|
||||||
|
'Status of Job launched',
|
||||||
|
[
|
||||||
|
'node',
|
||||||
|
'status',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
INSTANCE_CONSUMED_CAPACITY = Gauge(
|
||||||
|
'awx_instance_consumed_capacity',
|
||||||
|
'Consumed capacity of each node in a Tower system',
|
||||||
|
[
|
||||||
|
'hostname',
|
||||||
|
'instance_uuid',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
INSTANCE_REMAINING_CAPACITY = Gauge(
|
||||||
|
'awx_instance_remaining_capacity',
|
||||||
|
'Remaining capacity of each node in a Tower system',
|
||||||
|
[
|
||||||
|
'hostname',
|
||||||
|
'instance_uuid',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
LICENSE_INSTANCE_TOTAL = Gauge('awx_license_instance_total', 'Total number of managed hosts provided by your license')
|
LICENSE_INSTANCE_TOTAL = Gauge('awx_license_instance_total', 'Total number of managed hosts provided by your license')
|
||||||
LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining managed hosts provided by your license')
|
LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining managed hosts provided by your license')
|
||||||
@@ -55,18 +121,20 @@ LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining
|
|||||||
|
|
||||||
def metrics():
|
def metrics():
|
||||||
license_info = get_license()
|
license_info = get_license()
|
||||||
SYSTEM_INFO.info({
|
SYSTEM_INFO.info(
|
||||||
'install_uuid': settings.INSTALL_UUID,
|
{
|
||||||
'insights_analytics': str(settings.INSIGHTS_TRACKING_STATE),
|
'install_uuid': settings.INSTALL_UUID,
|
||||||
'tower_url_base': settings.TOWER_URL_BASE,
|
'insights_analytics': str(settings.INSIGHTS_TRACKING_STATE),
|
||||||
'tower_version': get_awx_version(),
|
'tower_url_base': settings.TOWER_URL_BASE,
|
||||||
'ansible_version': get_ansible_version(),
|
'tower_version': get_awx_version(),
|
||||||
'license_type': license_info.get('license_type', 'UNLICENSED'),
|
'ansible_version': get_ansible_version(),
|
||||||
'license_expiry': str(license_info.get('time_remaining', 0)),
|
'license_type': license_info.get('license_type', 'UNLICENSED'),
|
||||||
'pendo_tracking': settings.PENDO_TRACKING_STATE,
|
'license_expiry': str(license_info.get('time_remaining', 0)),
|
||||||
'external_logger_enabled': str(settings.LOG_AGGREGATOR_ENABLED),
|
'pendo_tracking': settings.PENDO_TRACKING_STATE,
|
||||||
'external_logger_type': getattr(settings, 'LOG_AGGREGATOR_TYPE', 'None')
|
'external_logger_enabled': str(settings.LOG_AGGREGATOR_ENABLED),
|
||||||
})
|
'external_logger_type': getattr(settings, 'LOG_AGGREGATOR_TYPE', 'None'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
LICENSE_INSTANCE_TOTAL.set(str(license_info.get('instance_count', 0)))
|
LICENSE_INSTANCE_TOTAL.set(str(license_info.get('instance_count', 0)))
|
||||||
LICENSE_INSTANCE_FREE.set(str(license_info.get('free_instances', 0)))
|
LICENSE_INSTANCE_FREE.set(str(license_info.get('free_instances', 0)))
|
||||||
@@ -108,12 +176,14 @@ def metrics():
|
|||||||
INSTANCE_MEMORY.labels(hostname=hostname, instance_uuid=uuid).set(instance_data[uuid]['memory'])
|
INSTANCE_MEMORY.labels(hostname=hostname, instance_uuid=uuid).set(instance_data[uuid]['memory'])
|
||||||
INSTANCE_CONSUMED_CAPACITY.labels(hostname=hostname, instance_uuid=uuid).set(instance_data[uuid]['consumed_capacity'])
|
INSTANCE_CONSUMED_CAPACITY.labels(hostname=hostname, instance_uuid=uuid).set(instance_data[uuid]['consumed_capacity'])
|
||||||
INSTANCE_REMAINING_CAPACITY.labels(hostname=hostname, instance_uuid=uuid).set(instance_data[uuid]['remaining_capacity'])
|
INSTANCE_REMAINING_CAPACITY.labels(hostname=hostname, instance_uuid=uuid).set(instance_data[uuid]['remaining_capacity'])
|
||||||
INSTANCE_INFO.labels(hostname=hostname, instance_uuid=uuid).info({
|
INSTANCE_INFO.labels(hostname=hostname, instance_uuid=uuid).info(
|
||||||
'enabled': str(instance_data[uuid]['enabled']),
|
{
|
||||||
'last_isolated_check': getattr(instance_data[uuid], 'last_isolated_check', 'None'),
|
'enabled': str(instance_data[uuid]['enabled']),
|
||||||
'managed_by_policy': str(instance_data[uuid]['managed_by_policy']),
|
'last_isolated_check': getattr(instance_data[uuid], 'last_isolated_check', 'None'),
|
||||||
'version': instance_data[uuid]['version']
|
'managed_by_policy': str(instance_data[uuid]['managed_by_policy']),
|
||||||
})
|
'version': instance_data[uuid]['version'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
instance_data = job_instance_counts(None)
|
instance_data = job_instance_counts(None)
|
||||||
for node in instance_data:
|
for node in instance_data:
|
||||||
@@ -127,7 +197,6 @@ def metrics():
|
|||||||
for status, value in statuses.items():
|
for status, value in statuses.items():
|
||||||
INSTANCE_STATUS.labels(node=node, status=status).set(value)
|
INSTANCE_STATUS.labels(node=node, status=status).set(value)
|
||||||
|
|
||||||
|
|
||||||
return generate_latest()
|
return generate_latest()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
231
awx/main/conf.py
231
awx/main/conf.py
@@ -37,8 +37,7 @@ register(
|
|||||||
'ORG_ADMINS_CAN_SEE_ALL_USERS',
|
'ORG_ADMINS_CAN_SEE_ALL_USERS',
|
||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
label=_('All Users Visible to Organization Admins'),
|
label=_('All Users Visible to Organization Admins'),
|
||||||
help_text=_('Controls whether any Organization Admin can view all users and teams, '
|
help_text=_('Controls whether any Organization Admin can view all users and teams, ' 'even those not associated with their Organization.'),
|
||||||
'even those not associated with their Organization.'),
|
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
)
|
)
|
||||||
@@ -47,8 +46,10 @@ register(
|
|||||||
'MANAGE_ORGANIZATION_AUTH',
|
'MANAGE_ORGANIZATION_AUTH',
|
||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
label=_('Organization Admins Can Manage Users and Teams'),
|
label=_('Organization Admins Can Manage Users and Teams'),
|
||||||
help_text=_('Controls whether any Organization Admin has the privileges to create and manage users and teams. '
|
help_text=_(
|
||||||
'You may want to disable this ability if you are using an LDAP or SAML integration.'),
|
'Controls whether any Organization Admin has the privileges to create and manage users and teams. '
|
||||||
|
'You may want to disable this ability if you are using an LDAP or SAML integration.'
|
||||||
|
),
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
)
|
)
|
||||||
@@ -59,8 +60,7 @@ register(
|
|||||||
schemes=('http', 'https'),
|
schemes=('http', 'https'),
|
||||||
allow_plain_hostname=True, # Allow hostname only without TLD.
|
allow_plain_hostname=True, # Allow hostname only without TLD.
|
||||||
label=_('Base URL of the Tower host'),
|
label=_('Base URL of the Tower host'),
|
||||||
help_text=_('This setting is used by services like notifications to render '
|
help_text=_('This setting is used by services like notifications to render ' 'a valid url to the Tower host.'),
|
||||||
'a valid url to the Tower host.'),
|
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
)
|
)
|
||||||
@@ -69,11 +69,13 @@ register(
|
|||||||
'REMOTE_HOST_HEADERS',
|
'REMOTE_HOST_HEADERS',
|
||||||
field_class=fields.StringListField,
|
field_class=fields.StringListField,
|
||||||
label=_('Remote Host Headers'),
|
label=_('Remote Host Headers'),
|
||||||
help_text=_('HTTP headers and meta keys to search to determine remote host '
|
help_text=_(
|
||||||
'name or IP. Add additional items to this list, such as '
|
'HTTP headers and meta keys to search to determine remote host '
|
||||||
'"HTTP_X_FORWARDED_FOR", if behind a reverse proxy. '
|
'name or IP. Add additional items to this list, such as '
|
||||||
'See the "Proxy Support" section of the Adminstrator guide for '
|
'"HTTP_X_FORWARDED_FOR", if behind a reverse proxy. '
|
||||||
'more details.'),
|
'See the "Proxy Support" section of the Adminstrator guide for '
|
||||||
|
'more details.'
|
||||||
|
),
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
)
|
)
|
||||||
@@ -82,11 +84,13 @@ register(
|
|||||||
'PROXY_IP_ALLOWED_LIST',
|
'PROXY_IP_ALLOWED_LIST',
|
||||||
field_class=fields.StringListField,
|
field_class=fields.StringListField,
|
||||||
label=_('Proxy IP Allowed List'),
|
label=_('Proxy IP Allowed List'),
|
||||||
help_text=_("If Tower is behind a reverse proxy/load balancer, use this setting "
|
help_text=_(
|
||||||
"to configure the proxy IP addresses from which Tower should trust "
|
"If Tower is behind a reverse proxy/load balancer, use this setting "
|
||||||
"custom REMOTE_HOST_HEADERS header values. "
|
"to configure the proxy IP addresses from which Tower should trust "
|
||||||
"If this setting is an empty list (the default), the headers specified by "
|
"custom REMOTE_HOST_HEADERS header values. "
|
||||||
"REMOTE_HOST_HEADERS will be trusted unconditionally')"),
|
"If this setting is an empty list (the default), the headers specified by "
|
||||||
|
"REMOTE_HOST_HEADERS will be trusted unconditionally')"
|
||||||
|
),
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
)
|
)
|
||||||
@@ -97,9 +101,7 @@ register(
|
|||||||
field_class=fields.DictField,
|
field_class=fields.DictField,
|
||||||
default=lambda: {},
|
default=lambda: {},
|
||||||
label=_('License'),
|
label=_('License'),
|
||||||
help_text=_('The license controls which features and functionality are '
|
help_text=_('The license controls which features and functionality are ' 'enabled. Use /api/v2/config/ to update or change ' 'the license.'),
|
||||||
'enabled. Use /api/v2/config/ to update or change '
|
|
||||||
'the license.'),
|
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
)
|
)
|
||||||
@@ -193,8 +195,7 @@ register(
|
|||||||
'CUSTOM_VENV_PATHS',
|
'CUSTOM_VENV_PATHS',
|
||||||
field_class=fields.StringListPathField,
|
field_class=fields.StringListPathField,
|
||||||
label=_('Custom virtual environment paths'),
|
label=_('Custom virtual environment paths'),
|
||||||
help_text=_('Paths where Tower will look for custom virtual environments '
|
help_text=_('Paths where Tower will look for custom virtual environments ' '(in addition to /var/lib/awx/venv/). Enter one path per line.'),
|
||||||
'(in addition to /var/lib/awx/venv/). Enter one path per line.'),
|
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
default=[],
|
default=[],
|
||||||
@@ -244,9 +245,11 @@ register(
|
|||||||
'AWX_PROOT_BASE_PATH',
|
'AWX_PROOT_BASE_PATH',
|
||||||
field_class=fields.CharField,
|
field_class=fields.CharField,
|
||||||
label=_('Job execution path'),
|
label=_('Job execution path'),
|
||||||
help_text=_('The directory in which Tower will create new temporary '
|
help_text=_(
|
||||||
'directories for job execution and isolation '
|
'The directory in which Tower will create new temporary '
|
||||||
'(such as credential files and custom inventory scripts).'),
|
'directories for job execution and isolation '
|
||||||
|
'(such as credential files and custom inventory scripts).'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
@@ -287,8 +290,10 @@ register(
|
|||||||
field_class=fields.IntegerField,
|
field_class=fields.IntegerField,
|
||||||
min_value=0,
|
min_value=0,
|
||||||
label=_('Isolated launch timeout'),
|
label=_('Isolated launch timeout'),
|
||||||
help_text=_('The timeout (in seconds) for launching jobs on isolated instances. '
|
help_text=_(
|
||||||
'This includes the time needed to copy source control files (playbooks) to the isolated instance.'),
|
'The timeout (in seconds) for launching jobs on isolated instances. '
|
||||||
|
'This includes the time needed to copy source control files (playbooks) to the isolated instance.'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -300,8 +305,10 @@ register(
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
default=10,
|
default=10,
|
||||||
label=_('Isolated connection timeout'),
|
label=_('Isolated connection timeout'),
|
||||||
help_text=_('Ansible SSH connection timeout (in seconds) to use when communicating with isolated instances. '
|
help_text=_(
|
||||||
'Value should be substantially greater than expected network latency.'),
|
'Ansible SSH connection timeout (in seconds) to use when communicating with isolated instances. '
|
||||||
|
'Value should be substantially greater than expected network latency.'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -314,7 +321,7 @@ register(
|
|||||||
help_text=_('When set to True, AWX will enforce strict host key checking for communication with isolated nodes.'),
|
help_text=_('When set to True, AWX will enforce strict host key checking for communication with isolated nodes.'),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
default=False
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -322,9 +329,11 @@ register(
|
|||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
default=True,
|
default=True,
|
||||||
label=_('Generate RSA keys for isolated instances'),
|
label=_('Generate RSA keys for isolated instances'),
|
||||||
help_text=_('If set, a random RSA key will be generated and distributed to '
|
help_text=_(
|
||||||
'isolated instances. To disable this behavior and manage authentication '
|
'If set, a random RSA key will be generated and distributed to '
|
||||||
'for isolated instances outside of Tower, disable this setting.'), # noqa
|
'isolated instances. To disable this behavior and manage authentication '
|
||||||
|
'for isolated instances outside of Tower, disable this setting.'
|
||||||
|
), # noqa
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
@@ -359,8 +368,7 @@ register(
|
|||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
default=False,
|
default=False,
|
||||||
label=_('Enable detailed resource profiling on all playbook runs'),
|
label=_('Enable detailed resource profiling on all playbook runs'),
|
||||||
help_text=_('If set, detailed resource profiling data will be collected on all jobs. '
|
help_text=_('If set, detailed resource profiling data will be collected on all jobs. ' 'This data can be gathered with `sosreport`.'), # noqa
|
||||||
'This data can be gathered with `sosreport`.'), # noqa
|
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
@@ -370,8 +378,7 @@ register(
|
|||||||
field_class=FloatField,
|
field_class=FloatField,
|
||||||
default='0.25',
|
default='0.25',
|
||||||
label=_('Interval (in seconds) between polls for cpu usage.'),
|
label=_('Interval (in seconds) between polls for cpu usage.'),
|
||||||
help_text=_('Interval (in seconds) between polls for cpu usage. '
|
help_text=_('Interval (in seconds) between polls for cpu usage. ' 'Setting this lower than the default will affect playbook performance.'),
|
||||||
'Setting this lower than the default will affect playbook performance.'),
|
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
required=False,
|
required=False,
|
||||||
@@ -382,8 +389,7 @@ register(
|
|||||||
field_class=FloatField,
|
field_class=FloatField,
|
||||||
default='0.25',
|
default='0.25',
|
||||||
label=_('Interval (in seconds) between polls for memory usage.'),
|
label=_('Interval (in seconds) between polls for memory usage.'),
|
||||||
help_text=_('Interval (in seconds) between polls for memory usage. '
|
help_text=_('Interval (in seconds) between polls for memory usage. ' 'Setting this lower than the default will affect playbook performance.'),
|
||||||
'Setting this lower than the default will affect playbook performance.'),
|
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
required=False,
|
required=False,
|
||||||
@@ -394,8 +400,7 @@ register(
|
|||||||
field_class=FloatField,
|
field_class=FloatField,
|
||||||
default='0.25',
|
default='0.25',
|
||||||
label=_('Interval (in seconds) between polls for PID count.'),
|
label=_('Interval (in seconds) between polls for PID count.'),
|
||||||
help_text=_('Interval (in seconds) between polls for PID count. '
|
help_text=_('Interval (in seconds) between polls for PID count. ' 'Setting this lower than the default will affect playbook performance.'),
|
||||||
'Setting this lower than the default will affect playbook performance.'),
|
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
required=False,
|
required=False,
|
||||||
@@ -469,10 +474,9 @@ register(
|
|||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
default=False,
|
default=False,
|
||||||
label=_('Ignore Ansible Galaxy SSL Certificate Verification'),
|
label=_('Ignore Ansible Galaxy SSL Certificate Verification'),
|
||||||
help_text=_('If set to true, certificate validation will not be done when '
|
help_text=_('If set to true, certificate validation will not be done when ' 'installing content from any Galaxy server.'),
|
||||||
'installing content from any Galaxy server.'),
|
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs'
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -491,7 +495,8 @@ register(
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
label=_('Job Event Standard Output Maximum Display Size'),
|
label=_('Job Event Standard Output Maximum Display Size'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
u'Maximum Size of Standard Output in bytes to display for a single job or ad hoc command event. `stdout` will end with `\u2026` when truncated.'),
|
u'Maximum Size of Standard Output in bytes to display for a single job or ad hoc command event. `stdout` will end with `\u2026` when truncated.'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
@@ -522,8 +527,10 @@ register(
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
default=0,
|
default=0,
|
||||||
label=_('Default Job Timeout'),
|
label=_('Default Job Timeout'),
|
||||||
help_text=_('Maximum time in seconds to allow jobs to run. Use value of 0 to indicate that no '
|
help_text=_(
|
||||||
'timeout should be imposed. A timeout set on an individual job template will override this.'),
|
'Maximum time in seconds to allow jobs to run. Use value of 0 to indicate that no '
|
||||||
|
'timeout should be imposed. A timeout set on an individual job template will override this.'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -535,8 +542,10 @@ register(
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
default=0,
|
default=0,
|
||||||
label=_('Default Inventory Update Timeout'),
|
label=_('Default Inventory Update Timeout'),
|
||||||
help_text=_('Maximum time in seconds to allow inventory updates to run. Use value of 0 to indicate that no '
|
help_text=_(
|
||||||
'timeout should be imposed. A timeout set on an individual inventory source will override this.'),
|
'Maximum time in seconds to allow inventory updates to run. Use value of 0 to indicate that no '
|
||||||
|
'timeout should be imposed. A timeout set on an individual inventory source will override this.'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -548,8 +557,10 @@ register(
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
default=0,
|
default=0,
|
||||||
label=_('Default Project Update Timeout'),
|
label=_('Default Project Update Timeout'),
|
||||||
help_text=_('Maximum time in seconds to allow project updates to run. Use value of 0 to indicate that no '
|
help_text=_(
|
||||||
'timeout should be imposed. A timeout set on an individual project will override this.'),
|
'Maximum time in seconds to allow project updates to run. Use value of 0 to indicate that no '
|
||||||
|
'timeout should be imposed. A timeout set on an individual project will override this.'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -561,10 +572,12 @@ register(
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
default=0,
|
default=0,
|
||||||
label=_('Per-Host Ansible Fact Cache Timeout'),
|
label=_('Per-Host Ansible Fact Cache Timeout'),
|
||||||
help_text=_('Maximum time, in seconds, that stored Ansible facts are considered valid since '
|
help_text=_(
|
||||||
'the last time they were modified. Only valid, non-stale, facts will be accessible by '
|
'Maximum time, in seconds, that stored Ansible facts are considered valid since '
|
||||||
'a playbook. Note, this does not influence the deletion of ansible_facts from the database. '
|
'the last time they were modified. Only valid, non-stale, facts will be accessible by '
|
||||||
'Use a value of 0 to indicate that no timeout should be imposed.'),
|
'a playbook. Note, this does not influence the deletion of ansible_facts from the database. '
|
||||||
|
'Use a value of 0 to indicate that no timeout should be imposed.'
|
||||||
|
),
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -576,8 +589,7 @@ register(
|
|||||||
allow_null=False,
|
allow_null=False,
|
||||||
default=200,
|
default=200,
|
||||||
label=_('Maximum number of forks per job'),
|
label=_('Maximum number of forks per job'),
|
||||||
help_text=_('Saving a Job Template with more than this number of forks will result in an error. '
|
help_text=_('Saving a Job Template with more than this number of forks will result in an error. ' 'When set to 0, no limit is applied.'),
|
||||||
'When set to 0, no limit is applied.'),
|
|
||||||
category=_('Jobs'),
|
category=_('Jobs'),
|
||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
@@ -598,11 +610,10 @@ register(
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
default=None,
|
default=None,
|
||||||
label=_('Logging Aggregator Port'),
|
label=_('Logging Aggregator Port'),
|
||||||
help_text=_('Port on Logging Aggregator to send logs to (if required and not'
|
help_text=_('Port on Logging Aggregator to send logs to (if required and not' ' provided in Logging Aggregator).'),
|
||||||
' provided in Logging Aggregator).'),
|
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
required=False
|
required=False,
|
||||||
)
|
)
|
||||||
register(
|
register(
|
||||||
'LOG_AGGREGATOR_TYPE',
|
'LOG_AGGREGATOR_TYPE',
|
||||||
@@ -643,12 +654,14 @@ register(
|
|||||||
field_class=fields.StringListField,
|
field_class=fields.StringListField,
|
||||||
default=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
default=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
||||||
label=_('Loggers Sending Data to Log Aggregator Form'),
|
label=_('Loggers Sending Data to Log Aggregator Form'),
|
||||||
help_text=_('List of loggers that will send HTTP logs to the collector, these can '
|
help_text=_(
|
||||||
'include any or all of: \n'
|
'List of loggers that will send HTTP logs to the collector, these can '
|
||||||
'awx - service logs\n'
|
'include any or all of: \n'
|
||||||
'activity_stream - activity stream records\n'
|
'awx - service logs\n'
|
||||||
'job_events - callback data from Ansible job events\n'
|
'activity_stream - activity stream records\n'
|
||||||
'system_tracking - facts gathered from scan jobs.'),
|
'job_events - callback data from Ansible job events\n'
|
||||||
|
'system_tracking - facts gathered from scan jobs.'
|
||||||
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
@@ -657,10 +670,12 @@ register(
|
|||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
default=False,
|
default=False,
|
||||||
label=_('Log System Tracking Facts Individually'),
|
label=_('Log System Tracking Facts Individually'),
|
||||||
help_text=_('If set, system tracking facts will be sent for each package, service, or '
|
help_text=_(
|
||||||
'other item found in a scan, allowing for greater search query granularity. '
|
'If set, system tracking facts will be sent for each package, service, or '
|
||||||
'If unset, facts will be sent as a single dictionary, allowing for greater '
|
'other item found in a scan, allowing for greater search query granularity. '
|
||||||
'efficiency in fact processing.'),
|
'If unset, facts will be sent as a single dictionary, allowing for greater '
|
||||||
|
'efficiency in fact processing.'
|
||||||
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
@@ -689,9 +704,11 @@ register(
|
|||||||
choices=[('https', 'HTTPS/HTTP'), ('tcp', 'TCP'), ('udp', 'UDP')],
|
choices=[('https', 'HTTPS/HTTP'), ('tcp', 'TCP'), ('udp', 'UDP')],
|
||||||
default='https',
|
default='https',
|
||||||
label=_('Logging Aggregator Protocol'),
|
label=_('Logging Aggregator Protocol'),
|
||||||
help_text=_('Protocol used to communicate with log aggregator. '
|
help_text=_(
|
||||||
'HTTPS/HTTP assumes HTTPS unless http:// is explicitly used in '
|
'Protocol used to communicate with log aggregator. '
|
||||||
'the Logging Aggregator hostname.'),
|
'HTTPS/HTTP assumes HTTPS unless http:// is explicitly used in '
|
||||||
|
'the Logging Aggregator hostname.'
|
||||||
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
@@ -700,9 +717,7 @@ register(
|
|||||||
field_class=fields.IntegerField,
|
field_class=fields.IntegerField,
|
||||||
default=5,
|
default=5,
|
||||||
label=_('TCP Connection Timeout'),
|
label=_('TCP Connection Timeout'),
|
||||||
help_text=_('Number of seconds for a TCP connection to external log '
|
help_text=_('Number of seconds for a TCP connection to external log ' 'aggregator to timeout. Applies to HTTPS and TCP log ' 'aggregator protocols.'),
|
||||||
'aggregator to timeout. Applies to HTTPS and TCP log '
|
|
||||||
'aggregator protocols.'),
|
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -712,10 +727,12 @@ register(
|
|||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
default=True,
|
default=True,
|
||||||
label=_('Enable/disable HTTPS certificate verification'),
|
label=_('Enable/disable HTTPS certificate verification'),
|
||||||
help_text=_('Flag to control enable/disable of certificate verification'
|
help_text=_(
|
||||||
' when LOG_AGGREGATOR_PROTOCOL is "https". If enabled, Tower\'s'
|
'Flag to control enable/disable of certificate verification'
|
||||||
' log handler will verify certificate sent by external log aggregator'
|
' when LOG_AGGREGATOR_PROTOCOL is "https". If enabled, Tower\'s'
|
||||||
' before establishing connection.'),
|
' log handler will verify certificate sent by external log aggregator'
|
||||||
|
' before establishing connection.'
|
||||||
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
@@ -725,10 +742,12 @@ register(
|
|||||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||||
default='WARNING',
|
default='WARNING',
|
||||||
label=_('Logging Aggregator Level Threshold'),
|
label=_('Logging Aggregator Level Threshold'),
|
||||||
help_text=_('Level threshold used by log handler. Severities from lowest to highest'
|
help_text=_(
|
||||||
' are DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe '
|
'Level threshold used by log handler. Severities from lowest to highest'
|
||||||
'than the threshold will be ignored by log handler. (messages under category '
|
' are DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe '
|
||||||
'awx.anlytics ignore this setting)'),
|
'than the threshold will be ignored by log handler. (messages under category '
|
||||||
|
'awx.anlytics ignore this setting)'
|
||||||
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
@@ -738,9 +757,11 @@ register(
|
|||||||
default=1,
|
default=1,
|
||||||
min_value=1,
|
min_value=1,
|
||||||
label=_('Maximum disk persistance for external log aggregation (in GB)'),
|
label=_('Maximum disk persistance for external log aggregation (in GB)'),
|
||||||
help_text=_('Amount of data to store (in gigabytes) during an outage of '
|
help_text=_(
|
||||||
'the external log aggregator (defaults to 1). '
|
'Amount of data to store (in gigabytes) during an outage of '
|
||||||
'Equivalent to the rsyslogd queue.maxdiskspace setting.'),
|
'the external log aggregator (defaults to 1). '
|
||||||
|
'Equivalent to the rsyslogd queue.maxdiskspace setting.'
|
||||||
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
@@ -749,9 +770,11 @@ register(
|
|||||||
field_class=fields.CharField,
|
field_class=fields.CharField,
|
||||||
default='/var/lib/awx',
|
default='/var/lib/awx',
|
||||||
label=_('File system location for rsyslogd disk persistence'),
|
label=_('File system location for rsyslogd disk persistence'),
|
||||||
help_text=_('Location to persist logs that should be retried after an outage '
|
help_text=_(
|
||||||
'of the external log aggregator (defaults to /var/lib/awx). '
|
'Location to persist logs that should be retried after an outage '
|
||||||
'Equivalent to the rsyslogd queue.spoolDirectory setting.'),
|
'of the external log aggregator (defaults to /var/lib/awx). '
|
||||||
|
'Equivalent to the rsyslogd queue.spoolDirectory setting.'
|
||||||
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
@@ -760,21 +783,19 @@ register(
|
|||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
default=False,
|
default=False,
|
||||||
label=_('Enable rsyslogd debugging'),
|
label=_('Enable rsyslogd debugging'),
|
||||||
help_text=_('Enabled high verbosity debugging for rsyslogd. '
|
help_text=_('Enabled high verbosity debugging for rsyslogd. ' 'Useful for debugging connection issues for external log aggregation.'),
|
||||||
'Useful for debugging connection issues for external log aggregation.'),
|
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
register(
|
register(
|
||||||
'AUTOMATION_ANALYTICS_LAST_GATHER',
|
'AUTOMATION_ANALYTICS_LAST_GATHER',
|
||||||
field_class=fields.DateTimeField,
|
field_class=fields.DateTimeField,
|
||||||
label=_('Last gather date for Automation Analytics.'),
|
label=_('Last gather date for Automation Analytics.'),
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system'
|
category_slug='system',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -783,8 +804,8 @@ register(
|
|||||||
field_class=fields.IntegerField,
|
field_class=fields.IntegerField,
|
||||||
label=_('Automation Analytics Gather Interval'),
|
label=_('Automation Analytics Gather Interval'),
|
||||||
help_text=_('Interval (in seconds) between data gathering.'),
|
help_text=_('Interval (in seconds) between data gathering.'),
|
||||||
default=14400, # every 4 hours
|
default=14400, # every 4 hours
|
||||||
min_value=1800, # every 30 minutes
|
min_value=1800, # every 30 minutes
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
unit=_('seconds'),
|
unit=_('seconds'),
|
||||||
@@ -792,17 +813,23 @@ register(
|
|||||||
|
|
||||||
|
|
||||||
def logging_validate(serializer, attrs):
|
def logging_validate(serializer, attrs):
|
||||||
if not serializer.instance or \
|
if not serializer.instance or not hasattr(serializer.instance, 'LOG_AGGREGATOR_HOST') or not hasattr(serializer.instance, 'LOG_AGGREGATOR_TYPE'):
|
||||||
not hasattr(serializer.instance, 'LOG_AGGREGATOR_HOST') or \
|
|
||||||
not hasattr(serializer.instance, 'LOG_AGGREGATOR_TYPE'):
|
|
||||||
return attrs
|
return attrs
|
||||||
errors = []
|
errors = []
|
||||||
if attrs.get('LOG_AGGREGATOR_ENABLED', False):
|
if attrs.get('LOG_AGGREGATOR_ENABLED', False):
|
||||||
if not serializer.instance.LOG_AGGREGATOR_HOST and not attrs.get('LOG_AGGREGATOR_HOST', None) or\
|
if (
|
||||||
serializer.instance.LOG_AGGREGATOR_HOST and not attrs.get('LOG_AGGREGATOR_HOST', True):
|
not serializer.instance.LOG_AGGREGATOR_HOST
|
||||||
|
and not attrs.get('LOG_AGGREGATOR_HOST', None)
|
||||||
|
or serializer.instance.LOG_AGGREGATOR_HOST
|
||||||
|
and not attrs.get('LOG_AGGREGATOR_HOST', True)
|
||||||
|
):
|
||||||
errors.append('Cannot enable log aggregator without providing host.')
|
errors.append('Cannot enable log aggregator without providing host.')
|
||||||
if not serializer.instance.LOG_AGGREGATOR_TYPE and not attrs.get('LOG_AGGREGATOR_TYPE', None) or\
|
if (
|
||||||
serializer.instance.LOG_AGGREGATOR_TYPE and not attrs.get('LOG_AGGREGATOR_TYPE', True):
|
not serializer.instance.LOG_AGGREGATOR_TYPE
|
||||||
|
and not attrs.get('LOG_AGGREGATOR_TYPE', None)
|
||||||
|
or serializer.instance.LOG_AGGREGATOR_TYPE
|
||||||
|
and not attrs.get('LOG_AGGREGATOR_TYPE', True)
|
||||||
|
):
|
||||||
errors.append('Cannot enable log aggregator without providing type.')
|
errors.append('Cannot enable log aggregator without providing type.')
|
||||||
if errors:
|
if errors:
|
||||||
raise serializers.ValidationError(_('\n'.join(errors)))
|
raise serializers.ValidationError(_('\n'.join(errors)))
|
||||||
|
|||||||
@@ -6,17 +6,33 @@ import re
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'CLOUD_PROVIDERS', 'SCHEDULEABLE_PROVIDERS', 'PRIVILEGE_ESCALATION_METHODS',
|
'CLOUD_PROVIDERS',
|
||||||
'ANSI_SGR_PATTERN', 'CAN_CANCEL', 'ACTIVE_STATES', 'STANDARD_INVENTORY_UPDATE_ENV'
|
'SCHEDULEABLE_PROVIDERS',
|
||||||
|
'PRIVILEGE_ESCALATION_METHODS',
|
||||||
|
'ANSI_SGR_PATTERN',
|
||||||
|
'CAN_CANCEL',
|
||||||
|
'ACTIVE_STATES',
|
||||||
|
'STANDARD_INVENTORY_UPDATE_ENV',
|
||||||
]
|
]
|
||||||
|
|
||||||
CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'rhv', 'satellite6', 'tower')
|
CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'rhv', 'satellite6', 'tower')
|
||||||
SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom', 'scm',)
|
SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + (
|
||||||
|
'custom',
|
||||||
|
'scm',
|
||||||
|
)
|
||||||
PRIVILEGE_ESCALATION_METHODS = [
|
PRIVILEGE_ESCALATION_METHODS = [
|
||||||
('sudo', _('Sudo')), ('su', _('Su')), ('pbrun', _('Pbrun')), ('pfexec', _('Pfexec')),
|
('sudo', _('Sudo')),
|
||||||
('dzdo', _('DZDO')), ('pmrun', _('Pmrun')), ('runas', _('Runas')),
|
('su', _('Su')),
|
||||||
('enable', _('Enable')), ('doas', _('Doas')), ('ksu', _('Ksu')),
|
('pbrun', _('Pbrun')),
|
||||||
('machinectl', _('Machinectl')), ('sesu', _('Sesu')),
|
('pfexec', _('Pfexec')),
|
||||||
|
('dzdo', _('DZDO')),
|
||||||
|
('pmrun', _('Pmrun')),
|
||||||
|
('runas', _('Runas')),
|
||||||
|
('enable', _('Enable')),
|
||||||
|
('doas', _('Doas')),
|
||||||
|
('ksu', _('Ksu')),
|
||||||
|
('machinectl', _('Machinectl')),
|
||||||
|
('sesu', _('Sesu')),
|
||||||
]
|
]
|
||||||
CHOICES_PRIVILEGE_ESCALATION_METHODS = [('', _('None'))] + PRIVILEGE_ESCALATION_METHODS
|
CHOICES_PRIVILEGE_ESCALATION_METHODS = [('', _('None'))] + PRIVILEGE_ESCALATION_METHODS
|
||||||
ANSI_SGR_PATTERN = re.compile(r'\x1b\[[0-9;]*m')
|
ANSI_SGR_PATTERN = re.compile(r'\x1b\[[0-9;]*m')
|
||||||
@@ -26,19 +42,35 @@ STANDARD_INVENTORY_UPDATE_ENV = {
|
|||||||
# Always use the --export option for ansible-inventory
|
# Always use the --export option for ansible-inventory
|
||||||
'ANSIBLE_INVENTORY_EXPORT': 'True',
|
'ANSIBLE_INVENTORY_EXPORT': 'True',
|
||||||
# Redirecting output to stderr allows JSON parsing to still work with -vvv
|
# Redirecting output to stderr allows JSON parsing to still work with -vvv
|
||||||
'ANSIBLE_VERBOSE_TO_STDERR': 'True'
|
'ANSIBLE_VERBOSE_TO_STDERR': 'True',
|
||||||
}
|
}
|
||||||
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
||||||
ACTIVE_STATES = CAN_CANCEL
|
ACTIVE_STATES = CAN_CANCEL
|
||||||
CENSOR_VALUE = '************'
|
CENSOR_VALUE = '************'
|
||||||
ENV_BLOCKLIST = frozenset((
|
ENV_BLOCKLIST = frozenset(
|
||||||
'VIRTUAL_ENV', 'PATH', 'PYTHONPATH', 'PROOT_TMP_DIR', 'JOB_ID',
|
(
|
||||||
'INVENTORY_ID', 'INVENTORY_SOURCE_ID', 'INVENTORY_UPDATE_ID',
|
'VIRTUAL_ENV',
|
||||||
'AD_HOC_COMMAND_ID', 'REST_API_URL', 'REST_API_TOKEN', 'MAX_EVENT_RES',
|
'PATH',
|
||||||
'CALLBACK_QUEUE', 'CALLBACK_CONNECTION', 'CACHE',
|
'PYTHONPATH',
|
||||||
'JOB_CALLBACK_DEBUG', 'INVENTORY_HOSTVARS',
|
'PROOT_TMP_DIR',
|
||||||
'AWX_HOST', 'PROJECT_REVISION', 'SUPERVISOR_WEB_CONFIG_PATH'
|
'JOB_ID',
|
||||||
))
|
'INVENTORY_ID',
|
||||||
|
'INVENTORY_SOURCE_ID',
|
||||||
|
'INVENTORY_UPDATE_ID',
|
||||||
|
'AD_HOC_COMMAND_ID',
|
||||||
|
'REST_API_URL',
|
||||||
|
'REST_API_TOKEN',
|
||||||
|
'MAX_EVENT_RES',
|
||||||
|
'CALLBACK_QUEUE',
|
||||||
|
'CALLBACK_CONNECTION',
|
||||||
|
'CACHE',
|
||||||
|
'JOB_CALLBACK_DEBUG',
|
||||||
|
'INVENTORY_HOSTVARS',
|
||||||
|
'AWX_HOST',
|
||||||
|
'PROJECT_REVISION',
|
||||||
|
'SUPERVISOR_WEB_CONFIG_PATH',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# loggers that may be called in process of emitting a log
|
# loggers that may be called in process of emitting a log
|
||||||
LOGGER_BLOCKLIST = (
|
LOGGER_BLOCKLIST = (
|
||||||
@@ -48,5 +80,5 @@ LOGGER_BLOCKLIST = (
|
|||||||
'awx.main.utils.encryption',
|
'awx.main.utils.encryption',
|
||||||
'awx.main.utils.log',
|
'awx.main.utils.log',
|
||||||
# loggers that may be called getting logging settings
|
# loggers that may be called getting logging settings
|
||||||
'awx.conf'
|
'awx.conf',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,19 +30,13 @@ class WebsocketSecretAuthHelper:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def construct_secret(cls):
|
def construct_secret(cls):
|
||||||
nonce_serialized = f"{int(time.time())}"
|
nonce_serialized = f"{int(time.time())}"
|
||||||
payload_dict = {
|
payload_dict = {'secret': settings.BROADCAST_WEBSOCKET_SECRET, 'nonce': nonce_serialized}
|
||||||
'secret': settings.BROADCAST_WEBSOCKET_SECRET,
|
|
||||||
'nonce': nonce_serialized
|
|
||||||
}
|
|
||||||
payload_serialized = json.dumps(payload_dict)
|
payload_serialized = json.dumps(payload_dict)
|
||||||
|
|
||||||
secret_serialized = hmac.new(force_bytes(settings.BROADCAST_WEBSOCKET_SECRET),
|
secret_serialized = hmac.new(force_bytes(settings.BROADCAST_WEBSOCKET_SECRET), msg=force_bytes(payload_serialized), digestmod='sha256').hexdigest()
|
||||||
msg=force_bytes(payload_serialized),
|
|
||||||
digestmod='sha256').hexdigest()
|
|
||||||
|
|
||||||
return 'HMAC-SHA256 {}:{}'.format(nonce_serialized, secret_serialized)
|
return 'HMAC-SHA256 {}:{}'.format(nonce_serialized, secret_serialized)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def verify_secret(cls, s, nonce_tolerance=300):
|
def verify_secret(cls, s, nonce_tolerance=300):
|
||||||
try:
|
try:
|
||||||
@@ -62,9 +56,7 @@ class WebsocketSecretAuthHelper:
|
|||||||
except Exception:
|
except Exception:
|
||||||
raise ValueError("Failed to create hash to compare to secret.")
|
raise ValueError("Failed to create hash to compare to secret.")
|
||||||
|
|
||||||
secret_serialized = hmac.new(force_bytes(settings.BROADCAST_WEBSOCKET_SECRET),
|
secret_serialized = hmac.new(force_bytes(settings.BROADCAST_WEBSOCKET_SECRET), msg=force_bytes(payload_serialized), digestmod='sha256').hexdigest()
|
||||||
msg=force_bytes(payload_serialized),
|
|
||||||
digestmod='sha256').hexdigest()
|
|
||||||
|
|
||||||
if secret_serialized != secret_parsed:
|
if secret_serialized != secret_parsed:
|
||||||
raise ValueError("Invalid secret")
|
raise ValueError("Invalid secret")
|
||||||
@@ -90,7 +82,6 @@ class WebsocketSecretAuthHelper:
|
|||||||
|
|
||||||
|
|
||||||
class BroadcastConsumer(AsyncJsonWebsocketConsumer):
|
class BroadcastConsumer(AsyncJsonWebsocketConsumer):
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
try:
|
try:
|
||||||
WebsocketSecretAuthHelper.is_authorized(self.scope)
|
WebsocketSecretAuthHelper.is_authorized(self.scope)
|
||||||
@@ -151,13 +142,10 @@ class EventConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
async def receive_json(self, data):
|
async def receive_json(self, data):
|
||||||
from awx.main.access import consumer_access
|
from awx.main.access import consumer_access
|
||||||
|
|
||||||
user = self.scope['user']
|
user = self.scope['user']
|
||||||
xrftoken = data.get('xrftoken')
|
xrftoken = data.get('xrftoken')
|
||||||
if (
|
if not xrftoken or XRF_KEY not in self.scope["session"] or xrftoken != self.scope["session"][XRF_KEY]:
|
||||||
not xrftoken or
|
|
||||||
XRF_KEY not in self.scope["session"] or
|
|
||||||
xrftoken != self.scope["session"][XRF_KEY]
|
|
||||||
):
|
|
||||||
logger.error(f"access denied to channel, XRF mismatch for {user.username}")
|
logger.error(f"access denied to channel, XRF mismatch for {user.username}")
|
||||||
await self.send_json({"error": "access denied to channel"})
|
await self.send_json({"error": "access denied to channel"})
|
||||||
return
|
return
|
||||||
@@ -166,7 +154,7 @@ class EventConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
groups = data['groups']
|
groups = data['groups']
|
||||||
new_groups = set()
|
new_groups = set()
|
||||||
current_groups = set(self.scope['session'].pop('groups') if 'groups' in self.scope['session'] else [])
|
current_groups = set(self.scope['session'].pop('groups') if 'groups' in self.scope['session'] else [])
|
||||||
for group_name,v in groups.items():
|
for group_name, v in groups.items():
|
||||||
if type(v) is list:
|
if type(v) is list:
|
||||||
for oid in v:
|
for oid in v:
|
||||||
name = '{}-{}'.format(group_name, oid)
|
name = '{}-{}'.format(group_name, oid)
|
||||||
@@ -191,16 +179,9 @@ class EventConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
new_groups_exclusive = new_groups - current_groups
|
new_groups_exclusive = new_groups - current_groups
|
||||||
for group_name in new_groups_exclusive:
|
for group_name in new_groups_exclusive:
|
||||||
await self.channel_layer.group_add(
|
await self.channel_layer.group_add(group_name, self.channel_name)
|
||||||
group_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
self.scope['session']['groups'] = new_groups
|
self.scope['session']['groups'] = new_groups
|
||||||
await self.send_json({
|
await self.send_json({"groups_current": list(new_groups), "groups_left": list(old_groups), "groups_joined": list(new_groups_exclusive)})
|
||||||
"groups_current": list(new_groups),
|
|
||||||
"groups_left": list(old_groups),
|
|
||||||
"groups_joined": list(new_groups_exclusive)
|
|
||||||
})
|
|
||||||
|
|
||||||
async def internal_message(self, event):
|
async def internal_message(self, event):
|
||||||
await self.send(event['text'])
|
await self.send(event['text'])
|
||||||
@@ -221,7 +202,7 @@ def _dump_payload(payload):
|
|||||||
|
|
||||||
|
|
||||||
def emit_channel_notification(group, payload):
|
def emit_channel_notification(group, payload):
|
||||||
from awx.main.wsbroadcast import wrap_broadcast_msg # noqa
|
from awx.main.wsbroadcast import wrap_broadcast_msg # noqa
|
||||||
|
|
||||||
payload_dumped = _dump_payload(payload)
|
payload_dumped = _dump_payload(payload)
|
||||||
if payload_dumped is None:
|
if payload_dumped is None:
|
||||||
@@ -229,18 +210,19 @@ def emit_channel_notification(group, payload):
|
|||||||
|
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
|
|
||||||
run_sync(channel_layer.group_send(
|
run_sync(
|
||||||
group,
|
channel_layer.group_send(
|
||||||
{
|
group,
|
||||||
"type": "internal.message",
|
{"type": "internal.message", "text": payload_dumped},
|
||||||
"text": payload_dumped
|
)
|
||||||
},
|
)
|
||||||
))
|
|
||||||
|
|
||||||
run_sync(channel_layer.group_send(
|
run_sync(
|
||||||
settings.BROADCAST_WEBSOCKET_GROUP_NAME,
|
channel_layer.group_send(
|
||||||
{
|
settings.BROADCAST_WEBSOCKET_GROUP_NAME,
|
||||||
"type": "internal.message",
|
{
|
||||||
"text": wrap_broadcast_msg(group, payload_dumped),
|
"type": "internal.message",
|
||||||
},
|
"text": wrap_broadcast_msg(group, payload_dumped),
|
||||||
))
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,51 +6,55 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
aim_inputs = {
|
aim_inputs = {
|
||||||
'fields': [{
|
'fields': [
|
||||||
'id': 'url',
|
{
|
||||||
'label': _('CyberArk AIM URL'),
|
'id': 'url',
|
||||||
'type': 'string',
|
'label': _('CyberArk AIM URL'),
|
||||||
'format': 'url',
|
'type': 'string',
|
||||||
}, {
|
'format': 'url',
|
||||||
'id': 'app_id',
|
},
|
||||||
'label': _('Application ID'),
|
{
|
||||||
'type': 'string',
|
'id': 'app_id',
|
||||||
'secret': True,
|
'label': _('Application ID'),
|
||||||
}, {
|
'type': 'string',
|
||||||
'id': 'client_key',
|
'secret': True,
|
||||||
'label': _('Client Key'),
|
},
|
||||||
'type': 'string',
|
{
|
||||||
'secret': True,
|
'id': 'client_key',
|
||||||
'multiline': True,
|
'label': _('Client Key'),
|
||||||
}, {
|
'type': 'string',
|
||||||
'id': 'client_cert',
|
'secret': True,
|
||||||
'label': _('Client Certificate'),
|
'multiline': True,
|
||||||
'type': 'string',
|
},
|
||||||
'secret': True,
|
{
|
||||||
'multiline': True,
|
'id': 'client_cert',
|
||||||
}, {
|
'label': _('Client Certificate'),
|
||||||
'id': 'verify',
|
'type': 'string',
|
||||||
'label': _('Verify SSL Certificates'),
|
'secret': True,
|
||||||
'type': 'boolean',
|
'multiline': True,
|
||||||
'default': True,
|
},
|
||||||
}],
|
{
|
||||||
'metadata': [{
|
'id': 'verify',
|
||||||
'id': 'object_query',
|
'label': _('Verify SSL Certificates'),
|
||||||
'label': _('Object Query'),
|
'type': 'boolean',
|
||||||
'type': 'string',
|
'default': True,
|
||||||
'help_text': _('Lookup query for the object. Ex: Safe=TestSafe;Object=testAccountName123'),
|
},
|
||||||
}, {
|
],
|
||||||
'id': 'object_query_format',
|
'metadata': [
|
||||||
'label': _('Object Query Format'),
|
{
|
||||||
'type': 'string',
|
'id': 'object_query',
|
||||||
'default': 'Exact',
|
'label': _('Object Query'),
|
||||||
'choices': ['Exact', 'Regexp']
|
'type': 'string',
|
||||||
}, {
|
'help_text': _('Lookup query for the object. Ex: Safe=TestSafe;Object=testAccountName123'),
|
||||||
'id': 'reason',
|
},
|
||||||
'label': _('Reason'),
|
{'id': 'object_query_format', 'label': _('Object Query Format'), 'type': 'string', 'default': 'Exact', 'choices': ['Exact', 'Regexp']},
|
||||||
'type': 'string',
|
{
|
||||||
'help_text': _('Object request reason. This is only needed if it is required by the object\'s policy.')
|
'id': 'reason',
|
||||||
}],
|
'label': _('Reason'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('Object request reason. This is only needed if it is required by the object\'s policy.'),
|
||||||
|
},
|
||||||
|
],
|
||||||
'required': ['url', 'app_id', 'object_query'],
|
'required': ['url', 'app_id', 'object_query'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +92,4 @@ def aim_backend(**kwargs):
|
|||||||
return res.json()['Content']
|
return res.json()['Content']
|
||||||
|
|
||||||
|
|
||||||
aim_plugin = CredentialPlugin(
|
aim_plugin = CredentialPlugin('CyberArk AIM Central Credential Provider Lookup', inputs=aim_inputs, backend=aim_backend)
|
||||||
'CyberArk AIM Central Credential Provider Lookup',
|
|
||||||
inputs=aim_inputs,
|
|
||||||
backend=aim_backend
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -7,51 +7,48 @@ from msrestazure import azure_cloud
|
|||||||
|
|
||||||
|
|
||||||
# https://github.com/Azure/msrestazure-for-python/blob/master/msrestazure/azure_cloud.py
|
# https://github.com/Azure/msrestazure-for-python/blob/master/msrestazure/azure_cloud.py
|
||||||
clouds = [
|
clouds = [vars(azure_cloud)[n] for n in dir(azure_cloud) if n.startswith("AZURE_") and n.endswith("_CLOUD")]
|
||||||
vars(azure_cloud)[n]
|
|
||||||
for n in dir(azure_cloud)
|
|
||||||
if n.startswith("AZURE_") and n.endswith("_CLOUD")
|
|
||||||
]
|
|
||||||
default_cloud = vars(azure_cloud)["AZURE_PUBLIC_CLOUD"]
|
default_cloud = vars(azure_cloud)["AZURE_PUBLIC_CLOUD"]
|
||||||
|
|
||||||
|
|
||||||
azure_keyvault_inputs = {
|
azure_keyvault_inputs = {
|
||||||
'fields': [{
|
'fields': [
|
||||||
'id': 'url',
|
{
|
||||||
'label': _('Vault URL (DNS Name)'),
|
'id': 'url',
|
||||||
'type': 'string',
|
'label': _('Vault URL (DNS Name)'),
|
||||||
'format': 'url',
|
'type': 'string',
|
||||||
}, {
|
'format': 'url',
|
||||||
'id': 'client',
|
},
|
||||||
'label': _('Client ID'),
|
{'id': 'client', 'label': _('Client ID'), 'type': 'string'},
|
||||||
'type': 'string'
|
{
|
||||||
}, {
|
'id': 'secret',
|
||||||
'id': 'secret',
|
'label': _('Client Secret'),
|
||||||
'label': _('Client Secret'),
|
'type': 'string',
|
||||||
'type': 'string',
|
'secret': True,
|
||||||
'secret': True,
|
},
|
||||||
}, {
|
{'id': 'tenant', 'label': _('Tenant ID'), 'type': 'string'},
|
||||||
'id': 'tenant',
|
{
|
||||||
'label': _('Tenant ID'),
|
'id': 'cloud_name',
|
||||||
'type': 'string'
|
'label': _('Cloud Environment'),
|
||||||
}, {
|
'help_text': _('Specify which azure cloud environment to use.'),
|
||||||
'id': 'cloud_name',
|
'choices': list(set([default_cloud.name] + [c.name for c in clouds])),
|
||||||
'label': _('Cloud Environment'),
|
'default': default_cloud.name,
|
||||||
'help_text': _('Specify which azure cloud environment to use.'),
|
},
|
||||||
'choices': list(set([default_cloud.name] + [c.name for c in clouds])),
|
],
|
||||||
'default': default_cloud.name
|
'metadata': [
|
||||||
}],
|
{
|
||||||
'metadata': [{
|
'id': 'secret_field',
|
||||||
'id': 'secret_field',
|
'label': _('Secret Name'),
|
||||||
'label': _('Secret Name'),
|
'type': 'string',
|
||||||
'type': 'string',
|
'help_text': _('The name of the secret to look up.'),
|
||||||
'help_text': _('The name of the secret to look up.'),
|
},
|
||||||
}, {
|
{
|
||||||
'id': 'secret_version',
|
'id': 'secret_version',
|
||||||
'label': _('Secret Version'),
|
'label': _('Secret Version'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'help_text': _('Used to specify a specific secret version (if left empty, the latest version will be used).'),
|
'help_text': _('Used to specify a specific secret version (if left empty, the latest version will be used).'),
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
'required': ['url', 'client', 'secret', 'tenant', 'secret_field'],
|
'required': ['url', 'client', 'secret', 'tenant', 'secret_field'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +59,11 @@ def azure_keyvault_backend(**kwargs):
|
|||||||
|
|
||||||
def auth_callback(server, resource, scope):
|
def auth_callback(server, resource, scope):
|
||||||
credentials = ServicePrincipalCredentials(
|
credentials = ServicePrincipalCredentials(
|
||||||
url = url,
|
url=url,
|
||||||
client_id = kwargs['client'],
|
client_id=kwargs['client'],
|
||||||
secret = kwargs['secret'],
|
secret=kwargs['secret'],
|
||||||
tenant = kwargs['tenant'],
|
tenant=kwargs['tenant'],
|
||||||
resource = f"https://{cloud.suffixes.keyvault_dns.split('.', 1).pop()}",
|
resource=f"https://{cloud.suffixes.keyvault_dns.split('.', 1).pop()}",
|
||||||
)
|
)
|
||||||
token = credentials.token
|
token = credentials.token
|
||||||
return token['token_type'], token['access_token']
|
return token['token_type'], token['access_token']
|
||||||
@@ -75,8 +72,4 @@ def azure_keyvault_backend(**kwargs):
|
|||||||
return kv.get_secret(url, kwargs['secret_field'], kwargs.get('secret_version', '')).value
|
return kv.get_secret(url, kwargs['secret_field'], kwargs.get('secret_version', '')).value
|
||||||
|
|
||||||
|
|
||||||
azure_keyvault_plugin = CredentialPlugin(
|
azure_keyvault_plugin = CredentialPlugin('Microsoft Azure Key Vault', inputs=azure_keyvault_inputs, backend=azure_keyvault_backend)
|
||||||
'Microsoft Azure Key Vault',
|
|
||||||
inputs=azure_keyvault_inputs,
|
|
||||||
backend=azure_keyvault_backend
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -2,66 +2,66 @@ from .plugin import CredentialPlugin, raise_for_status
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
import requests
|
import requests
|
||||||
pas_inputs = {
|
|
||||||
'fields': [{
|
|
||||||
'id': 'url',
|
|
||||||
'label': _('Centrify Tenant URL'),
|
|
||||||
'type': 'string',
|
|
||||||
'help_text': _('Centrify Tenant URL'),
|
|
||||||
'format': 'url',
|
|
||||||
}, {
|
|
||||||
'id':'client_id',
|
|
||||||
'label':_('Centrify API User'),
|
|
||||||
'type':'string',
|
|
||||||
'help_text': _('Centrify API User, having necessary permissions as mentioned in support doc'),
|
|
||||||
|
|
||||||
}, {
|
pas_inputs = {
|
||||||
'id':'client_password',
|
'fields': [
|
||||||
'label':_('Centrify API Password'),
|
{
|
||||||
'type':'string',
|
'id': 'url',
|
||||||
'help_text': _('Password of Centrify API User with necessary permissions'),
|
'label': _('Centrify Tenant URL'),
|
||||||
'secret':True,
|
'type': 'string',
|
||||||
},{
|
'help_text': _('Centrify Tenant URL'),
|
||||||
'id':'oauth_application_id',
|
'format': 'url',
|
||||||
'label':_('OAuth2 Application ID'),
|
},
|
||||||
'type':'string',
|
{
|
||||||
'help_text': _('Application ID of the configured OAuth2 Client (defaults to \'awx\')'),
|
'id': 'client_id',
|
||||||
'default': 'awx',
|
'label': _('Centrify API User'),
|
||||||
},{
|
'type': 'string',
|
||||||
'id':'oauth_scope',
|
'help_text': _('Centrify API User, having necessary permissions as mentioned in support doc'),
|
||||||
'label':_('OAuth2 Scope'),
|
},
|
||||||
'type':'string',
|
{
|
||||||
'help_text': _('Scope of the configured OAuth2 Client (defaults to \'awx\')'),
|
'id': 'client_password',
|
||||||
'default': 'awx',
|
'label': _('Centrify API Password'),
|
||||||
}],
|
'type': 'string',
|
||||||
'metadata': [{
|
'help_text': _('Password of Centrify API User with necessary permissions'),
|
||||||
'id': 'account-name',
|
'secret': True,
|
||||||
'label': _('Account Name'),
|
},
|
||||||
'type': 'string',
|
{
|
||||||
'help_text': _('Local system account or Domain account name enrolled in Centrify Vault. eg. (root or DOMAIN/Administrator)'),
|
'id': 'oauth_application_id',
|
||||||
},{
|
'label': _('OAuth2 Application ID'),
|
||||||
'id': 'system-name',
|
'type': 'string',
|
||||||
'label': _('System Name'),
|
'help_text': _('Application ID of the configured OAuth2 Client (defaults to \'awx\')'),
|
||||||
'type': 'string',
|
'default': 'awx',
|
||||||
'help_text': _('Machine Name enrolled with in Centrify Portal'),
|
},
|
||||||
}],
|
{
|
||||||
'required': ['url', 'account-name', 'system-name','client_id','client_password'],
|
'id': 'oauth_scope',
|
||||||
|
'label': _('OAuth2 Scope'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('Scope of the configured OAuth2 Client (defaults to \'awx\')'),
|
||||||
|
'default': 'awx',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'metadata': [
|
||||||
|
{
|
||||||
|
'id': 'account-name',
|
||||||
|
'label': _('Account Name'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('Local system account or Domain account name enrolled in Centrify Vault. eg. (root or DOMAIN/Administrator)'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'system-name',
|
||||||
|
'label': _('System Name'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('Machine Name enrolled with in Centrify Portal'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'required': ['url', 'account-name', 'system-name', 'client_id', 'client_password'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# generate bearer token to authenticate with PAS portal, Input : Client ID, Client Secret
|
# generate bearer token to authenticate with PAS portal, Input : Client ID, Client Secret
|
||||||
def handle_auth(**kwargs):
|
def handle_auth(**kwargs):
|
||||||
post_data = {
|
post_data = {"grant_type": "client_credentials", "scope": kwargs['oauth_scope']}
|
||||||
"grant_type": "client_credentials",
|
response = requests.post(kwargs['endpoint'], data=post_data, auth=(kwargs['client_id'], kwargs['client_password']), verify=True, timeout=(5, 30))
|
||||||
"scope": kwargs['oauth_scope']
|
|
||||||
}
|
|
||||||
response = requests.post(
|
|
||||||
kwargs['endpoint'],
|
|
||||||
data = post_data,
|
|
||||||
auth = (kwargs['client_id'],kwargs['client_password']),
|
|
||||||
verify = True,
|
|
||||||
timeout = (5, 30)
|
|
||||||
)
|
|
||||||
raise_for_status(response)
|
raise_for_status(response)
|
||||||
try:
|
try:
|
||||||
return response.json()['access_token']
|
return response.json()['access_token']
|
||||||
@@ -71,20 +71,11 @@ def handle_auth(**kwargs):
|
|||||||
|
|
||||||
# fetch the ID of system with RedRock query, Input : System Name, Account Name
|
# fetch the ID of system with RedRock query, Input : System Name, Account Name
|
||||||
def get_ID(**kwargs):
|
def get_ID(**kwargs):
|
||||||
endpoint = urljoin(kwargs['url'],'/Redrock/query')
|
endpoint = urljoin(kwargs['url'], '/Redrock/query')
|
||||||
name=" Name='{0}' and User='{1}'".format(kwargs['system_name'],kwargs['acc_name'])
|
name = " Name='{0}' and User='{1}'".format(kwargs['system_name'], kwargs['acc_name'])
|
||||||
query = 'Select ID from VaultAccount where {0}'.format(name)
|
query = 'Select ID from VaultAccount where {0}'.format(name)
|
||||||
post_headers = {
|
post_headers = {"Authorization": "Bearer " + kwargs['access_token'], "X-CENTRIFY-NATIVE-CLIENT": "true"}
|
||||||
"Authorization": "Bearer " + kwargs['access_token'],
|
response = requests.post(endpoint, json={'Script': query}, headers=post_headers, verify=True, timeout=(5, 30))
|
||||||
"X-CENTRIFY-NATIVE-CLIENT":"true"
|
|
||||||
}
|
|
||||||
response = requests.post(
|
|
||||||
endpoint,
|
|
||||||
json = {'Script': query},
|
|
||||||
headers = post_headers,
|
|
||||||
verify = True,
|
|
||||||
timeout = (5, 30)
|
|
||||||
)
|
|
||||||
raise_for_status(response)
|
raise_for_status(response)
|
||||||
try:
|
try:
|
||||||
result_str = response.json()["Result"]["Results"]
|
result_str = response.json()["Result"]["Results"]
|
||||||
@@ -95,18 +86,9 @@ def get_ID(**kwargs):
|
|||||||
|
|
||||||
# CheckOut Password from Centrify Vault, Input : ID
|
# CheckOut Password from Centrify Vault, Input : ID
|
||||||
def get_passwd(**kwargs):
|
def get_passwd(**kwargs):
|
||||||
endpoint = urljoin(kwargs['url'],'/ServerManage/CheckoutPassword')
|
endpoint = urljoin(kwargs['url'], '/ServerManage/CheckoutPassword')
|
||||||
post_headers = {
|
post_headers = {"Authorization": "Bearer " + kwargs['access_token'], "X-CENTRIFY-NATIVE-CLIENT": "true"}
|
||||||
"Authorization": "Bearer " + kwargs['access_token'],
|
response = requests.post(endpoint, json={'ID': kwargs['acc_id']}, headers=post_headers, verify=True, timeout=(5, 30))
|
||||||
"X-CENTRIFY-NATIVE-CLIENT":"true"
|
|
||||||
}
|
|
||||||
response = requests.post(
|
|
||||||
endpoint,
|
|
||||||
json = {'ID': kwargs['acc_id']},
|
|
||||||
headers = post_headers,
|
|
||||||
verify = True,
|
|
||||||
timeout = (5, 30)
|
|
||||||
)
|
|
||||||
raise_for_status(response)
|
raise_for_status(response)
|
||||||
try:
|
try:
|
||||||
return response.json()["Result"]["Password"]
|
return response.json()["Result"]["Password"]
|
||||||
@@ -122,21 +104,12 @@ def centrify_backend(**kwargs):
|
|||||||
client_password = kwargs.get('client_password')
|
client_password = kwargs.get('client_password')
|
||||||
app_id = kwargs.get('oauth_application_id', 'awx')
|
app_id = kwargs.get('oauth_application_id', 'awx')
|
||||||
endpoint = urljoin(url, f'/oauth2/token/{app_id}')
|
endpoint = urljoin(url, f'/oauth2/token/{app_id}')
|
||||||
endpoint = {
|
endpoint = {'endpoint': endpoint, 'client_id': client_id, 'client_password': client_password, 'oauth_scope': kwargs.get('oauth_scope', 'awx')}
|
||||||
'endpoint': endpoint,
|
|
||||||
'client_id': client_id,
|
|
||||||
'client_password': client_password,
|
|
||||||
'oauth_scope': kwargs.get('oauth_scope', 'awx')
|
|
||||||
}
|
|
||||||
token = handle_auth(**endpoint)
|
token = handle_auth(**endpoint)
|
||||||
get_id_args = {'system_name':system_name,'acc_name':acc_name,'url':url,'access_token':token}
|
get_id_args = {'system_name': system_name, 'acc_name': acc_name, 'url': url, 'access_token': token}
|
||||||
acc_id = get_ID(**get_id_args)
|
acc_id = get_ID(**get_id_args)
|
||||||
get_pwd_args = {'url':url,'acc_id':acc_id,'access_token':token}
|
get_pwd_args = {'url': url, 'acc_id': acc_id, 'access_token': token}
|
||||||
return get_passwd(**get_pwd_args)
|
return get_passwd(**get_pwd_args)
|
||||||
|
|
||||||
|
|
||||||
centrify_plugin = CredentialPlugin(
|
centrify_plugin = CredentialPlugin('Centrify Vault Credential Provider Lookup', inputs=pas_inputs, backend=centrify_backend)
|
||||||
'Centrify Vault Credential Provider Lookup',
|
|
||||||
inputs=pas_inputs,
|
|
||||||
backend=centrify_backend
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -8,41 +8,45 @@ import requests
|
|||||||
|
|
||||||
|
|
||||||
conjur_inputs = {
|
conjur_inputs = {
|
||||||
'fields': [{
|
'fields': [
|
||||||
'id': 'url',
|
{
|
||||||
'label': _('Conjur URL'),
|
'id': 'url',
|
||||||
'type': 'string',
|
'label': _('Conjur URL'),
|
||||||
'format': 'url',
|
'type': 'string',
|
||||||
}, {
|
'format': 'url',
|
||||||
'id': 'api_key',
|
},
|
||||||
'label': _('API Key'),
|
{
|
||||||
'type': 'string',
|
'id': 'api_key',
|
||||||
'secret': True,
|
'label': _('API Key'),
|
||||||
}, {
|
'type': 'string',
|
||||||
'id': 'account',
|
'secret': True,
|
||||||
'label': _('Account'),
|
},
|
||||||
'type': 'string',
|
{
|
||||||
}, {
|
'id': 'account',
|
||||||
'id': 'username',
|
'label': _('Account'),
|
||||||
'label': _('Username'),
|
'type': 'string',
|
||||||
'type': 'string',
|
},
|
||||||
}, {
|
{
|
||||||
'id': 'cacert',
|
'id': 'username',
|
||||||
'label': _('Public Key Certificate'),
|
'label': _('Username'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'multiline': True
|
},
|
||||||
}],
|
{'id': 'cacert', 'label': _('Public Key Certificate'), 'type': 'string', 'multiline': True},
|
||||||
'metadata': [{
|
],
|
||||||
'id': 'secret_path',
|
'metadata': [
|
||||||
'label': _('Secret Identifier'),
|
{
|
||||||
'type': 'string',
|
'id': 'secret_path',
|
||||||
'help_text': _('The identifier for the secret e.g., /some/identifier'),
|
'label': _('Secret Identifier'),
|
||||||
}, {
|
'type': 'string',
|
||||||
'id': 'secret_version',
|
'help_text': _('The identifier for the secret e.g., /some/identifier'),
|
||||||
'label': _('Secret Version'),
|
},
|
||||||
'type': 'string',
|
{
|
||||||
'help_text': _('Used to specify a specific secret version (if left empty, the latest version will be used).'),
|
'id': 'secret_version',
|
||||||
}],
|
'label': _('Secret Version'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('Used to specify a specific secret version (if left empty, the latest version will be used).'),
|
||||||
|
},
|
||||||
|
],
|
||||||
'required': ['url', 'api_key', 'account', 'username'],
|
'required': ['url', 'api_key', 'account', 'username'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +69,7 @@ def conjur_backend(**kwargs):
|
|||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
# https://www.conjur.org/api.html#authentication-authenticate-post
|
# https://www.conjur.org/api.html#authentication-authenticate-post
|
||||||
auth_kwargs['verify'] = cert
|
auth_kwargs['verify'] = cert
|
||||||
resp = requests.post(
|
resp = requests.post(urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), **auth_kwargs)
|
||||||
urljoin(url, '/'.join(['authn', account, username, 'authenticate'])),
|
|
||||||
**auth_kwargs
|
|
||||||
)
|
|
||||||
raise_for_status(resp)
|
raise_for_status(resp)
|
||||||
token = base64.b64encode(resp.content).decode('utf-8')
|
token = base64.b64encode(resp.content).decode('utf-8')
|
||||||
|
|
||||||
@@ -78,12 +79,7 @@ def conjur_backend(**kwargs):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# https://www.conjur.org/api.html#secrets-retrieve-a-secret-get
|
# https://www.conjur.org/api.html#secrets-retrieve-a-secret-get
|
||||||
path = urljoin(url, '/'.join([
|
path = urljoin(url, '/'.join(['secrets', account, 'variable', secret_path]))
|
||||||
'secrets',
|
|
||||||
account,
|
|
||||||
'variable',
|
|
||||||
secret_path
|
|
||||||
]))
|
|
||||||
if version:
|
if version:
|
||||||
path = '?'.join([path, version])
|
path = '?'.join([path, version])
|
||||||
|
|
||||||
@@ -94,8 +90,4 @@ def conjur_backend(**kwargs):
|
|||||||
return resp.text
|
return resp.text
|
||||||
|
|
||||||
|
|
||||||
conjur_plugin = CredentialPlugin(
|
conjur_plugin = CredentialPlugin('CyberArk Conjur Secret Lookup', inputs=conjur_inputs, backend=conjur_backend)
|
||||||
'CyberArk Conjur Secret Lookup',
|
|
||||||
inputs=conjur_inputs,
|
|
||||||
backend=conjur_backend
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -9,110 +9,131 @@ import requests
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
base_inputs = {
|
base_inputs = {
|
||||||
'fields': [{
|
'fields': [
|
||||||
'id': 'url',
|
{
|
||||||
'label': _('Server URL'),
|
'id': 'url',
|
||||||
'type': 'string',
|
'label': _('Server URL'),
|
||||||
'format': 'url',
|
'type': 'string',
|
||||||
'help_text': _('The URL to the HashiCorp Vault'),
|
'format': 'url',
|
||||||
}, {
|
'help_text': _('The URL to the HashiCorp Vault'),
|
||||||
'id': 'token',
|
},
|
||||||
'label': _('Token'),
|
{
|
||||||
'type': 'string',
|
'id': 'token',
|
||||||
'secret': True,
|
'label': _('Token'),
|
||||||
'help_text': _('The access token used to authenticate to the Vault server'),
|
'type': 'string',
|
||||||
}, {
|
'secret': True,
|
||||||
'id': 'cacert',
|
'help_text': _('The access token used to authenticate to the Vault server'),
|
||||||
'label': _('CA Certificate'),
|
},
|
||||||
'type': 'string',
|
{
|
||||||
'multiline': True,
|
'id': 'cacert',
|
||||||
'help_text': _('The CA certificate used to verify the SSL certificate of the Vault server')
|
'label': _('CA Certificate'),
|
||||||
}, {
|
'type': 'string',
|
||||||
'id': 'role_id',
|
'multiline': True,
|
||||||
'label': _('AppRole role_id'),
|
'help_text': _('The CA certificate used to verify the SSL certificate of the Vault server'),
|
||||||
'type': 'string',
|
},
|
||||||
'multiline': False,
|
{'id': 'role_id', 'label': _('AppRole role_id'), 'type': 'string', 'multiline': False, 'help_text': _('The Role ID for AppRole Authentication')},
|
||||||
'help_text': _('The Role ID for AppRole Authentication')
|
{
|
||||||
}, {
|
'id': 'secret_id',
|
||||||
'id': 'secret_id',
|
'label': _('AppRole secret_id'),
|
||||||
'label': _('AppRole secret_id'),
|
'type': 'string',
|
||||||
'type': 'string',
|
'multiline': False,
|
||||||
'multiline': False,
|
'secret': True,
|
||||||
'secret': True,
|
'help_text': _('The Secret ID for AppRole Authentication'),
|
||||||
'help_text': _('The Secret ID for AppRole Authentication')
|
},
|
||||||
}, {
|
{
|
||||||
'id': 'namespace',
|
'id': 'namespace',
|
||||||
'label': _('Namespace name (Vault Enterprise only)'),
|
'label': _('Namespace name (Vault Enterprise only)'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'multiline': False,
|
'multiline': False,
|
||||||
'help_text': _('Name of the namespace to use when authenticate and retrieve secrets')
|
'help_text': _('Name of the namespace to use when authenticate and retrieve secrets'),
|
||||||
}, {
|
},
|
||||||
'id': 'default_auth_path',
|
{
|
||||||
'label': _('Path to Approle Auth'),
|
'id': 'default_auth_path',
|
||||||
'type': 'string',
|
'label': _('Path to Approle Auth'),
|
||||||
'multiline': False,
|
'type': 'string',
|
||||||
'default': 'approle',
|
'multiline': False,
|
||||||
'help_text': _('The AppRole Authentication path to use if one isn\'t provided in the metadata when linking to an input field. Defaults to \'approle\'')
|
'default': 'approle',
|
||||||
}
|
'help_text': _(
|
||||||
|
'The AppRole Authentication path to use if one isn\'t provided in the metadata when linking to an input field. Defaults to \'approle\''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'metadata': [
|
||||||
|
{
|
||||||
|
'id': 'secret_path',
|
||||||
|
'label': _('Path to Secret'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('The path to the secret stored in the secret backend e.g, /some/secret/'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'auth_path',
|
||||||
|
'label': _('Path to Auth'),
|
||||||
|
'type': 'string',
|
||||||
|
'multiline': False,
|
||||||
|
'help_text': _('The path where the Authentication method is mounted e.g, approle'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'metadata': [{
|
|
||||||
'id': 'secret_path',
|
|
||||||
'label': _('Path to Secret'),
|
|
||||||
'type': 'string',
|
|
||||||
'help_text': _('The path to the secret stored in the secret backend e.g, /some/secret/')
|
|
||||||
}, {
|
|
||||||
'id': 'auth_path',
|
|
||||||
'label': _('Path to Auth'),
|
|
||||||
'type': 'string',
|
|
||||||
'multiline': False,
|
|
||||||
'help_text': _('The path where the Authentication method is mounted e.g, approle')
|
|
||||||
}],
|
|
||||||
'required': ['url', 'secret_path'],
|
'required': ['url', 'secret_path'],
|
||||||
}
|
}
|
||||||
|
|
||||||
hashi_kv_inputs = copy.deepcopy(base_inputs)
|
hashi_kv_inputs = copy.deepcopy(base_inputs)
|
||||||
hashi_kv_inputs['fields'].append({
|
hashi_kv_inputs['fields'].append(
|
||||||
'id': 'api_version',
|
{
|
||||||
'label': _('API Version'),
|
'id': 'api_version',
|
||||||
'choices': ['v1', 'v2'],
|
'label': _('API Version'),
|
||||||
'help_text': _('API v1 is for static key/value lookups. API v2 is for versioned key/value lookups.'),
|
'choices': ['v1', 'v2'],
|
||||||
'default': 'v1',
|
'help_text': _('API v1 is for static key/value lookups. API v2 is for versioned key/value lookups.'),
|
||||||
})
|
'default': 'v1',
|
||||||
hashi_kv_inputs['metadata'] = [{
|
}
|
||||||
'id': 'secret_backend',
|
)
|
||||||
'label': _('Name of Secret Backend'),
|
hashi_kv_inputs['metadata'] = (
|
||||||
'type': 'string',
|
[
|
||||||
'help_text': _('The name of the kv secret backend (if left empty, the first segment of the secret path will be used).')
|
{
|
||||||
}] + hashi_kv_inputs['metadata'] + [{
|
'id': 'secret_backend',
|
||||||
'id': 'secret_key',
|
'label': _('Name of Secret Backend'),
|
||||||
'label': _('Key Name'),
|
'type': 'string',
|
||||||
'type': 'string',
|
'help_text': _('The name of the kv secret backend (if left empty, the first segment of the secret path will be used).'),
|
||||||
'help_text': _('The name of the key to look up in the secret.'),
|
}
|
||||||
}, {
|
]
|
||||||
'id': 'secret_version',
|
+ hashi_kv_inputs['metadata']
|
||||||
'label': _('Secret Version (v2 only)'),
|
+ [
|
||||||
'type': 'string',
|
{
|
||||||
'help_text': _('Used to specify a specific secret version (if left empty, the latest version will be used).'),
|
'id': 'secret_key',
|
||||||
}]
|
'label': _('Key Name'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('The name of the key to look up in the secret.'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'secret_version',
|
||||||
|
'label': _('Secret Version (v2 only)'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('Used to specify a specific secret version (if left empty, the latest version will be used).'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
hashi_kv_inputs['required'].extend(['api_version', 'secret_key'])
|
hashi_kv_inputs['required'].extend(['api_version', 'secret_key'])
|
||||||
|
|
||||||
hashi_ssh_inputs = copy.deepcopy(base_inputs)
|
hashi_ssh_inputs = copy.deepcopy(base_inputs)
|
||||||
hashi_ssh_inputs['metadata'] = [{
|
hashi_ssh_inputs['metadata'] = (
|
||||||
'id': 'public_key',
|
[
|
||||||
'label': _('Unsigned Public Key'),
|
{
|
||||||
'type': 'string',
|
'id': 'public_key',
|
||||||
'multiline': True,
|
'label': _('Unsigned Public Key'),
|
||||||
}] + hashi_ssh_inputs['metadata'] + [{
|
'type': 'string',
|
||||||
'id': 'role',
|
'multiline': True,
|
||||||
'label': _('Role Name'),
|
}
|
||||||
'type': 'string',
|
]
|
||||||
'help_text': _('The name of the role used to sign.')
|
+ hashi_ssh_inputs['metadata']
|
||||||
}, {
|
+ [
|
||||||
'id': 'valid_principals',
|
{'id': 'role', 'label': _('Role Name'), 'type': 'string', 'help_text': _('The name of the role used to sign.')},
|
||||||
'label': _('Valid Principals'),
|
{
|
||||||
'type': 'string',
|
'id': 'valid_principals',
|
||||||
'help_text': _('Valid principals (either usernames or hostnames) that the certificate should be signed for.'),
|
'label': _('Valid Principals'),
|
||||||
}]
|
'type': 'string',
|
||||||
|
'help_text': _('Valid principals (either usernames or hostnames) that the certificate should be signed for.'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
hashi_ssh_inputs['required'].extend(['public_key', 'role'])
|
hashi_ssh_inputs['required'].extend(['public_key', 'role'])
|
||||||
|
|
||||||
|
|
||||||
@@ -209,9 +230,7 @@ def kv_backend(**kwargs):
|
|||||||
try:
|
try:
|
||||||
return json['data'][secret_key]
|
return json['data'][secret_key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise RuntimeError(
|
raise RuntimeError('{} is not present at {}'.format(secret_key, secret_path))
|
||||||
'{} is not present at {}'.format(secret_key, secret_path)
|
|
||||||
)
|
|
||||||
return json['data']
|
return json['data']
|
||||||
|
|
||||||
|
|
||||||
@@ -248,14 +267,6 @@ def ssh_backend(**kwargs):
|
|||||||
return resp.json()['data']['signed_key']
|
return resp.json()['data']['signed_key']
|
||||||
|
|
||||||
|
|
||||||
hashivault_kv_plugin = CredentialPlugin(
|
hashivault_kv_plugin = CredentialPlugin('HashiCorp Vault Secret Lookup', inputs=hashi_kv_inputs, backend=kv_backend)
|
||||||
'HashiCorp Vault Secret Lookup',
|
|
||||||
inputs=hashi_kv_inputs,
|
|
||||||
backend=kv_backend
|
|
||||||
)
|
|
||||||
|
|
||||||
hashivault_ssh_plugin = CredentialPlugin(
|
hashivault_ssh_plugin = CredentialPlugin('HashiCorp Vault Signed SSH', inputs=hashi_ssh_inputs, backend=ssh_backend)
|
||||||
'HashiCorp Vault Signed SSH',
|
|
||||||
inputs=hashi_ssh_inputs,
|
|
||||||
backend=ssh_backend
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def raise_for_status(resp):
|
|||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
class CertFiles():
|
class CertFiles:
|
||||||
"""
|
"""
|
||||||
A context manager used for writing a certificate and (optional) key
|
A context manager used for writing a certificate and (optional) key
|
||||||
to $TMPDIR, and cleaning up afterwards.
|
to $TMPDIR, and cleaning up afterwards.
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ __all__ = ['DatabaseWrapper']
|
|||||||
|
|
||||||
|
|
||||||
class RecordedQueryLog(object):
|
class RecordedQueryLog(object):
|
||||||
|
|
||||||
def __init__(self, log, db, dest='/var/log/tower/profile'):
|
def __init__(self, log, db, dest='/var/log/tower/profile'):
|
||||||
self.log = log
|
self.log = log
|
||||||
self.db = db
|
self.db = db
|
||||||
@@ -70,10 +69,7 @@ class RecordedQueryLog(object):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
progname = os.path.basename(sys.argv[0])
|
progname = os.path.basename(sys.argv[0])
|
||||||
filepath = os.path.join(
|
filepath = os.path.join(self.dest, '{}.sqlite'.format(progname))
|
||||||
self.dest,
|
|
||||||
'{}.sqlite'.format(progname)
|
|
||||||
)
|
|
||||||
version = pkg_resources.get_distribution('awx').version
|
version = pkg_resources.get_distribution('awx').version
|
||||||
log = sqlite3.connect(filepath, timeout=3)
|
log = sqlite3.connect(filepath, timeout=3)
|
||||||
log.execute(
|
log.execute(
|
||||||
@@ -91,9 +87,8 @@ class RecordedQueryLog(object):
|
|||||||
)
|
)
|
||||||
log.commit()
|
log.commit()
|
||||||
log.execute(
|
log.execute(
|
||||||
'INSERT INTO queries (pid, version, argv, time, sql, explain, bt) '
|
'INSERT INTO queries (pid, version, argv, time, sql, explain, bt) ' 'VALUES (?, ?, ?, ?, ?, ?, ?);',
|
||||||
'VALUES (?, ?, ?, ?, ?, ?, ?);',
|
(os.getpid(), version, ' '.join(sys.argv), seconds, sql, explain, bt),
|
||||||
(os.getpid(), version, ' ' .join(sys.argv), seconds, sql, explain, bt)
|
|
||||||
)
|
)
|
||||||
log.commit()
|
log.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -47,16 +47,9 @@ class PubSub(object):
|
|||||||
@contextmanager
|
@contextmanager
|
||||||
def pg_bus_conn():
|
def pg_bus_conn():
|
||||||
conf = settings.DATABASES['default']
|
conf = settings.DATABASES['default']
|
||||||
conn = psycopg2.connect(dbname=conf['NAME'],
|
conn = psycopg2.connect(dbname=conf['NAME'], host=conf['HOST'], user=conf['USER'], password=conf['PASSWORD'], port=conf['PORT'], **conf.get("OPTIONS", {}))
|
||||||
host=conf['HOST'],
|
|
||||||
user=conf['USER'],
|
|
||||||
password=conf['PASSWORD'],
|
|
||||||
port=conf['PORT'],
|
|
||||||
**conf.get("OPTIONS", {}))
|
|
||||||
# Django connection.cursor().connection doesn't have autocommit=True on
|
# Django connection.cursor().connection doesn't have autocommit=True on
|
||||||
conn.set_session(autocommit=True)
|
conn.set_session(autocommit=True)
|
||||||
pubsub = PubSub(conn)
|
pubsub = PubSub(conn)
|
||||||
yield pubsub
|
yield pubsub
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ class Control(object):
|
|||||||
|
|
||||||
with pg_bus_conn() as conn:
|
with pg_bus_conn() as conn:
|
||||||
conn.listen(reply_queue)
|
conn.listen(reply_queue)
|
||||||
conn.notify(self.queuename,
|
conn.notify(self.queuename, json.dumps({'control': command, 'reply_to': reply_queue}))
|
||||||
json.dumps({'control': command, 'reply_to': reply_queue}))
|
|
||||||
|
|
||||||
for reply in conn.events(select_timeout=timeout, yield_timeouts=True):
|
for reply in conn.events(select_timeout=timeout, yield_timeouts=True):
|
||||||
if reply is None:
|
if reply is None:
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ logger = logging.getLogger('awx.main.dispatch.periodic')
|
|||||||
|
|
||||||
|
|
||||||
class Scheduler(Scheduler):
|
class Scheduler(Scheduler):
|
||||||
|
|
||||||
def run_continuously(self):
|
def run_continuously(self):
|
||||||
idle_seconds = max(
|
idle_seconds = max(1, min(self.jobs).period.total_seconds() / 2)
|
||||||
1,
|
|
||||||
min(self.jobs).period.total_seconds() / 2
|
|
||||||
)
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
ppid = os.getppid()
|
ppid = os.getppid()
|
||||||
@@ -39,9 +35,7 @@ class Scheduler(Scheduler):
|
|||||||
GuidMiddleware.set_guid(GuidMiddleware._generate_guid())
|
GuidMiddleware.set_guid(GuidMiddleware._generate_guid())
|
||||||
self.run_pending()
|
self.run_pending()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception('encountered an error while scheduling periodic tasks')
|
||||||
'encountered an error while scheduling periodic tasks'
|
|
||||||
)
|
|
||||||
time.sleep(idle_seconds)
|
time.sleep(idle_seconds)
|
||||||
|
|
||||||
process = Process(target=run)
|
process = Process(target=run)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user