mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 11:20:39 -03:30
Merge branch 'rbac' of github.com:ansible/ansible-tower into rbac
This commit is contained in:
commit
39a1e893fb
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,7 +34,7 @@ __pycache__
|
||||
/tar-build
|
||||
/setup-bundle-build
|
||||
/dist
|
||||
*.egg-info
|
||||
/*.egg-info
|
||||
*.py[c,o]
|
||||
|
||||
# JavaScript
|
||||
|
||||
8
Makefile
8
Makefile
@ -273,9 +273,9 @@ version_file:
|
||||
# Do any one-time init tasks.
|
||||
init:
|
||||
@if [ "$(VIRTUAL_ENV)" ]; then \
|
||||
awx-manage register_instance --primary --hostname=127.0.0.1; \
|
||||
tower-manage register_instance --primary --hostname=127.0.0.1; \
|
||||
else \
|
||||
sudo awx-manage register_instance --primary --hostname=127.0.0.1; \
|
||||
sudo tower-manage register_instance --primary --hostname=127.0.0.1; \
|
||||
fi
|
||||
|
||||
# Refresh development environment after pulling new code.
|
||||
@ -291,7 +291,7 @@ migrate:
|
||||
|
||||
# Run after making changes to the models to create a new migration.
|
||||
dbchange:
|
||||
$(PYTHON) manage.py schemamigration main v14_changes --auto
|
||||
$(PYTHON) manage.py makemigrations
|
||||
|
||||
# access database shell, asks for password
|
||||
dbshell:
|
||||
@ -358,7 +358,7 @@ pylint: reports
|
||||
@(set -o pipefail && $@ | reports/$@.report)
|
||||
|
||||
check: flake8 pep8 # pyflakes pylint
|
||||
|
||||
|
||||
# Run all API unit tests.
|
||||
test:
|
||||
py.test awx/main/tests awx/api/tests awx/fact/tests
|
||||
|
||||
@ -32,7 +32,8 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
||||
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
||||
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
|
||||
'MongoAPIView', 'MongoListAPIView']
|
||||
'SubDetailAPIView',
|
||||
'ParentMixin',]
|
||||
|
||||
logger = logging.getLogger('awx.api.generics')
|
||||
|
||||
@ -219,28 +220,6 @@ class GenericAPIView(generics.GenericAPIView, APIView):
|
||||
d['settings'] = settings
|
||||
return d
|
||||
|
||||
|
||||
class MongoAPIView(GenericAPIView):
|
||||
|
||||
def get_parent_object(self):
|
||||
parent_filter = {
|
||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
||||
}
|
||||
return get_object_or_404(self.parent_model, **parent_filter)
|
||||
|
||||
def check_parent_access(self, parent=None):
|
||||
parent = parent or self.get_parent_object()
|
||||
parent_access = getattr(self, 'parent_access', 'read')
|
||||
if parent_access in ('read', 'delete'):
|
||||
args = (self.parent_model, parent_access, parent)
|
||||
else:
|
||||
args = (self.parent_model, parent_access, parent, None)
|
||||
if not self.request.user.can_access(*args):
|
||||
raise PermissionDenied()
|
||||
|
||||
class MongoListAPIView(generics.ListAPIView, MongoAPIView):
|
||||
pass
|
||||
|
||||
class SimpleListAPIView(generics.ListAPIView, GenericAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
@ -277,7 +256,25 @@ class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
||||
# Base class for a list view that allows creating new objects.
|
||||
pass
|
||||
|
||||
class SubListAPIView(ListAPIView):
|
||||
class ParentMixin(object):
|
||||
|
||||
def get_parent_object(self):
|
||||
parent_filter = {
|
||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
||||
}
|
||||
return get_object_or_404(self.parent_model, **parent_filter)
|
||||
|
||||
def check_parent_access(self, parent=None):
|
||||
parent = parent or self.get_parent_object()
|
||||
parent_access = getattr(self, 'parent_access', 'read')
|
||||
if parent_access in ('read', 'delete'):
|
||||
args = (self.parent_model, parent_access, parent)
|
||||
else:
|
||||
args = (self.parent_model, parent_access, parent, None)
|
||||
if not self.request.user.can_access(*args):
|
||||
raise PermissionDenied()
|
||||
|
||||
class SubListAPIView(ListAPIView, ParentMixin):
|
||||
# Base class for a read-only sublist view.
|
||||
|
||||
# Subclasses should define at least:
|
||||
@ -297,22 +294,6 @@ class SubListAPIView(ListAPIView):
|
||||
})
|
||||
return d
|
||||
|
||||
def get_parent_object(self):
|
||||
parent_filter = {
|
||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
||||
}
|
||||
return get_object_or_404(self.parent_model, **parent_filter)
|
||||
|
||||
def check_parent_access(self, parent=None):
|
||||
parent = parent or self.get_parent_object()
|
||||
parent_access = getattr(self, 'parent_access', 'read')
|
||||
if parent_access in ('read', 'delete'):
|
||||
args = (self.parent_model, parent_access, parent)
|
||||
else:
|
||||
args = (self.parent_model, parent_access, parent, None)
|
||||
if not self.request.user.can_access(*args):
|
||||
raise PermissionDenied()
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
@ -449,6 +430,9 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
else:
|
||||
return self.attach(request, *args, **kwargs)
|
||||
|
||||
class SubDetailAPIView(generics.RetrieveAPIView, GenericAPIView, ParentMixin):
|
||||
pass
|
||||
|
||||
class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
|
||||
pass
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ from rest_framework import serializers
|
||||
from rest_framework.request import clone_request
|
||||
|
||||
# Ansible Tower
|
||||
from awx.main.models import InventorySource
|
||||
from awx.main.models import InventorySource, Notifier
|
||||
|
||||
|
||||
class Metadata(metadata.SimpleMetadata):
|
||||
@ -76,6 +76,12 @@ class Metadata(metadata.SimpleMetadata):
|
||||
get_group_by_choices = getattr(InventorySource, 'get_%s_group_by_choices' % cp)
|
||||
field_info['%s_group_by_choices' % cp] = get_group_by_choices()
|
||||
|
||||
# Special handling of notification configuration where the required properties
|
||||
# are conditional on the type selected.
|
||||
if field.field_name == 'notification_configuration':
|
||||
for (notification_type_name, notification_tr_name, notification_type_class) in Notifier.NOTIFICATION_TYPES:
|
||||
field_info[notification_type_name] = notification_type_class.init_parameters
|
||||
|
||||
# Update type of fields returned...
|
||||
if field.field_name == 'type':
|
||||
field_info['type'] = 'multiple choice'
|
||||
|
||||
@ -9,8 +9,6 @@ import logging
|
||||
from collections import OrderedDict
|
||||
from dateutil import rrule
|
||||
|
||||
from rest_framework_mongoengine.serializers import DocumentSerializer
|
||||
|
||||
# PyYAML
|
||||
import yaml
|
||||
|
||||
@ -46,12 +44,10 @@ from awx.main.conf import tower_settings
|
||||
from awx.api.license import feature_enabled
|
||||
from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, EncryptedPasswordField, VerbatimField
|
||||
|
||||
from awx.fact.models import * # noqa
|
||||
|
||||
logger = logging.getLogger('awx.api.serializers')
|
||||
|
||||
# Fields that should be summarized regardless of object type.
|
||||
DEFAULT_SUMMARY_FIELDS = ('name', 'description')# , 'created_by', 'modified_by')#, 'type')
|
||||
DEFAULT_SUMMARY_FIELDS = ('id', 'name', 'description')# , 'created_by', 'modified_by')#, 'type')
|
||||
|
||||
# Keys are fields (foreign keys) where, if found on an instance, summary info
|
||||
# should be added to the serialized data. Values are a tuple of field names on
|
||||
@ -362,6 +358,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
roles[field.name] = {
|
||||
'id': role.id,
|
||||
'name': role.name,
|
||||
'description': role.description,
|
||||
'url': role.get_absolute_url(),
|
||||
}
|
||||
if len(roles) > 0:
|
||||
@ -555,19 +552,19 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
class EmptySerializer(serializers.Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class BaseFactSerializer(DocumentSerializer):
|
||||
class BaseFactSerializer(BaseSerializer):
|
||||
|
||||
__metaclass__ = BaseSerializerMetaclass
|
||||
|
||||
def get_fields(self):
|
||||
ret = super(BaseFactSerializer, self).get_fields()
|
||||
if 'module' in ret and feature_enabled('system_tracking'):
|
||||
choices = [(o, o.title()) for o in FactVersion.objects.all().only('module').distinct('module')]
|
||||
ret['module'] = serializers.ChoiceField(source='module', choices=choices, read_only=True, required=False)
|
||||
if 'module' in ret:
|
||||
# TODO: the values_list may pull in a LOT of entries before the distinct is called
|
||||
modules = Fact.objects.all().values_list('module', flat=True).distinct()
|
||||
choices = [(o, o.title()) for o in modules]
|
||||
ret['module'] = serializers.ChoiceField(choices=choices, read_only=True, required=False)
|
||||
return ret
|
||||
|
||||
|
||||
class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
@ -868,7 +865,11 @@ class OrganizationSerializer(BaseSerializer):
|
||||
users = reverse('api:organization_users_list', args=(obj.pk,)),
|
||||
admins = reverse('api:organization_admins_list', args=(obj.pk,)),
|
||||
teams = reverse('api:organization_teams_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,))
|
||||
activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,)),
|
||||
notifiers = reverse('api:organization_notifiers_list', args=(obj.pk,)),
|
||||
notifiers_any = reverse('api:organization_notifiers_any_list', args=(obj.pk,)),
|
||||
notifiers_success = reverse('api:organization_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:organization_notifiers_error_list', args=(obj.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
@ -938,6 +939,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
||||
project_updates = reverse('api:project_updates_list', args=(obj.pk,)),
|
||||
schedules = reverse('api:project_schedules_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:project_activity_stream_list', args=(obj.pk,)),
|
||||
notifiers_any = reverse('api:project_notifiers_any_list', args=(obj.pk,)),
|
||||
notifiers_success = reverse('api:project_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:project_notifiers_error_list', args=(obj.pk,)),
|
||||
))
|
||||
# Backwards compatibility.
|
||||
if obj.current_update:
|
||||
@ -983,6 +987,7 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
|
||||
res.update(dict(
|
||||
project = reverse('api:project_detail', args=(obj.project.pk,)),
|
||||
cancel = reverse('api:project_update_cancel', args=(obj.pk,)),
|
||||
notifications = reverse('api:project_update_notifications_list', args=(obj.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
@ -1390,6 +1395,9 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
|
||||
hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)),
|
||||
groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)),
|
||||
notifiers_any = reverse('api:inventory_source_notifiers_any_list', args=(obj.pk,)),
|
||||
notifiers_success = reverse('api:inventory_source_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:inventory_source_notifiers_error_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
@ -1434,6 +1442,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
||||
res.update(dict(
|
||||
inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)),
|
||||
cancel = reverse('api:inventory_update_cancel', args=(obj.pk,)),
|
||||
notifications = reverse('api:inventory_update_notifications_list', args=(obj.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
@ -1532,7 +1541,7 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
||||
ret['summary_fields']['permissions'] = resource.get_permissions(user)
|
||||
|
||||
def format_role_perm(role):
|
||||
role_dict = { 'id': role.id, 'name': role.name}
|
||||
role_dict = { 'id': role.id, 'name': role.name, 'description': role.description}
|
||||
try:
|
||||
role_dict['resource_name'] = role.content_object.name
|
||||
role_dict['resource_type'] = role.content_type.name
|
||||
@ -1672,6 +1681,9 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
schedules = reverse('api:job_template_schedules_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:job_template_activity_stream_list', args=(obj.pk,)),
|
||||
launch = reverse('api:job_template_launch', args=(obj.pk,)),
|
||||
notifiers_any = reverse('api:job_template_notifiers_any_list', args=(obj.pk,)),
|
||||
notifiers_success = reverse('api:job_template_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:job_template_notifiers_error_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.host_config_key:
|
||||
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
|
||||
@ -1726,6 +1738,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
job_tasks = reverse('api:job_job_tasks_list', args=(obj.pk,)),
|
||||
job_host_summaries = reverse('api:job_job_host_summaries_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:job_activity_stream_list', args=(obj.pk,)),
|
||||
notifications = reverse('api:job_notifications_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.job_template and obj.job_template.active:
|
||||
res['job_template'] = reverse('api:job_template_detail',
|
||||
@ -2141,6 +2154,79 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
||||
return attrs
|
||||
|
||||
class NotifierSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Notifier
|
||||
fields = ('*', 'organization', 'notification_type', 'notification_configuration')
|
||||
|
||||
type_map = {"string": (str, unicode),
|
||||
"int": (int,),
|
||||
"bool": (bool,),
|
||||
"list": (list,),
|
||||
"password": (str, unicode),
|
||||
"object": (dict, OrderedDict)}
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(NotifierSerializer, self).to_representation(obj)
|
||||
for field in obj.notification_class.init_parameters:
|
||||
if field in ret['notification_configuration'] and \
|
||||
force_text(ret['notification_configuration'][field]).startswith('$encrypted$'):
|
||||
ret['notification_configuration'][field] = '$encrypted$'
|
||||
return ret
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(NotifierSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
test = reverse('api:notifier_test', args=(obj.pk,)),
|
||||
notifications = reverse('api:notifier_notification_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.organization and obj.organization.active:
|
||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
||||
return res
|
||||
|
||||
def validate(self, attrs):
|
||||
notification_class = Notifier.CLASS_FOR_NOTIFICATION_TYPE[attrs['notification_type']]
|
||||
missing_fields = []
|
||||
incorrect_type_fields = []
|
||||
if 'notification_configuration' not in attrs:
|
||||
return attrs
|
||||
for field in notification_class.init_parameters:
|
||||
if field not in attrs['notification_configuration']:
|
||||
missing_fields.append(field)
|
||||
continue
|
||||
field_val = attrs['notification_configuration'][field]
|
||||
field_type = notification_class.init_parameters[field]['type']
|
||||
expected_types = self.type_map[field_type]
|
||||
if not type(field_val) in expected_types:
|
||||
incorrect_type_fields.append((field, field_type))
|
||||
continue
|
||||
if field_type == "password" and field_val.startswith('$encrypted$'):
|
||||
missing_fields.append(field)
|
||||
error_list = []
|
||||
if missing_fields:
|
||||
error_list.append("Missing required fields for Notification Configuration: {}".format(missing_fields))
|
||||
if incorrect_type_fields:
|
||||
for type_field_error in incorrect_type_fields:
|
||||
error_list.append("Configuration field '{}' incorrect type, expected {}".format(type_field_error[0],
|
||||
type_field_error[1]))
|
||||
if error_list:
|
||||
raise serializers.ValidationError(error_list)
|
||||
return attrs
|
||||
|
||||
class NotificationSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = ('*', '-name', '-description', 'notifier', 'error', 'status', 'notifications_sent',
|
||||
'notification_type', 'recipients', 'subject')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(NotificationSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
notifier = reverse('api:notifier_detail', args=(obj.notifier.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
class ScheduleSerializer(BaseSerializer):
|
||||
|
||||
@ -2391,28 +2477,31 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class FactVersionSerializer(BaseFactSerializer):
|
||||
related = serializers.SerializerMethodField('get_related')
|
||||
|
||||
class Meta:
|
||||
model = FactVersion
|
||||
fields = ('related', 'module', 'timestamp',)
|
||||
model = Fact
|
||||
fields = ('related', 'module', 'timestamp')
|
||||
read_only_fields = ('*',)
|
||||
|
||||
def get_related(self, obj):
|
||||
host_obj = self.context.get('host_obj')
|
||||
res = {}
|
||||
res = super(FactVersionSerializer, self).get_related(obj)
|
||||
params = {
|
||||
'datetime': timestamp_apiformat(obj.timestamp),
|
||||
'module': obj.module,
|
||||
}
|
||||
res.update(dict(
|
||||
fact_view = build_url('api:host_fact_compare_view', args=(host_obj.pk,), get=params),
|
||||
))
|
||||
res['fact_view'] = build_url('api:host_fact_compare_view', args=(obj.host.pk,), get=params)
|
||||
return res
|
||||
|
||||
|
||||
class FactSerializer(BaseFactSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Fact
|
||||
depth = 2
|
||||
fields = ('timestamp', 'host', 'module', 'fact')
|
||||
# TODO: Consider adding in host to the fields list ?
|
||||
fields = ('related', 'timestamp', 'module', 'facts', 'id', 'summary_fields', 'host')
|
||||
read_only_fields = ('*',)
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(FactSerializer, self).get_related(obj)
|
||||
res['host'] = obj.host.get_absolute_url()
|
||||
return res
|
||||
|
||||
|
||||
@ -20,6 +20,10 @@ organization_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/projects/$', 'organization_projects_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/teams/$', 'organization_teams_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'organization_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers/$', 'organization_notifiers_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'organization_notifiers_any_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'organization_notifiers_error_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'organization_notifiers_success_list'),
|
||||
)
|
||||
|
||||
user_urls = patterns('awx.api.views',
|
||||
@ -44,12 +48,16 @@ project_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/project_updates/$', 'project_updates_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'project_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/schedules/$', 'project_schedules_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'project_notifiers_any_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'project_notifiers_error_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'project_notifiers_success_list'),
|
||||
)
|
||||
|
||||
project_update_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'),
|
||||
url(r'^(?P<pk>[0-9]+)/stdout/$', 'project_update_stdout'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifications/$', 'project_update_notifications_list'),
|
||||
)
|
||||
|
||||
team_urls = patterns('awx.api.views',
|
||||
@ -92,8 +100,8 @@ host_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'host_ad_hoc_commands_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_command_events/$', 'host_ad_hoc_command_events_list'),
|
||||
#url(r'^(?P<pk>[0-9]+)/single_fact/$', 'host_single_fact_view'),
|
||||
url(r'^(?P<pk>[0-9]+)/fact_versions/$', 'host_fact_versions_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/fact_view/$', 'host_fact_compare_view'),
|
||||
url(r'^(?P<pk>[0-9]+)/fact_versions/$', 'host_fact_versions_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/fact_view/$', 'host_fact_compare_view'),
|
||||
)
|
||||
|
||||
group_urls = patterns('awx.api.views',
|
||||
@ -121,12 +129,16 @@ inventory_source_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/schedules/$', 'inventory_source_schedules_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_source_groups_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/hosts/$', 'inventory_source_hosts_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'inventory_source_notifiers_any_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'inventory_source_notifiers_error_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'inventory_source_notifiers_success_list'),
|
||||
)
|
||||
|
||||
inventory_update_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'),
|
||||
url(r'^(?P<pk>[0-9]+)/stdout/$', 'inventory_update_stdout'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifications/$', 'inventory_update_notifications_list'),
|
||||
)
|
||||
|
||||
inventory_script_urls = patterns('awx.api.views',
|
||||
@ -168,6 +180,9 @@ job_template_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/schedules/$', 'job_template_schedules_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/survey_spec/$', 'job_template_survey_spec'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_template_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'job_template_notifiers_any_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'job_template_notifiers_error_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'job_template_notifiers_success_list'),
|
||||
)
|
||||
|
||||
job_urls = patterns('awx.api.views',
|
||||
@ -182,6 +197,7 @@ job_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/job_tasks/$', 'job_job_tasks_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/stdout/$', 'job_stdout'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifications/$', 'job_notifications_list'),
|
||||
)
|
||||
|
||||
job_host_summary_urls = patterns('awx.api.views',
|
||||
@ -224,6 +240,18 @@ system_job_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/cancel/$', 'system_job_cancel'),
|
||||
)
|
||||
|
||||
notifier_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'notifier_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'notifier_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/test/$', 'notifier_test'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifications/$', 'notifier_notification_list'),
|
||||
)
|
||||
|
||||
notification_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'notification_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'notification_detail'),
|
||||
)
|
||||
|
||||
schedule_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'schedule_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'schedule_detail'),
|
||||
@ -273,6 +301,8 @@ v1_urls = patterns('awx.api.views',
|
||||
url(r'^ad_hoc_command_events/', include(ad_hoc_command_event_urls)),
|
||||
url(r'^system_job_templates/', include(system_job_template_urls)),
|
||||
url(r'^system_jobs/', include(system_job_urls)),
|
||||
url(r'^notifiers/', include(notifier_urls)),
|
||||
url(r'^notifications/', include(notification_urls)),
|
||||
url(r'^unified_job_templates/$', 'unified_job_template_list'),
|
||||
url(r'^unified_jobs/$', 'unified_job_list'),
|
||||
url(r'^activity_stream/', include(activity_stream_urls)),
|
||||
|
||||
387
awx/api/views.py
387
awx/api/views.py
@ -42,9 +42,6 @@ from rest_framework import status
|
||||
from rest_framework_yaml.parsers import YAMLParser
|
||||
from rest_framework_yaml.renderers import YAMLRenderer
|
||||
|
||||
# MongoEngine
|
||||
import mongoengine
|
||||
|
||||
# QSStats
|
||||
import qsstats
|
||||
|
||||
@ -56,12 +53,11 @@ from social.backends.utils import load_backends
|
||||
|
||||
# AWX
|
||||
from awx.main.task_engine import TaskSerializer, TASK_FILE, TEMPORARY_TASK_FILE
|
||||
from awx.main.tasks import mongodb_control
|
||||
from awx.main.tasks import mongodb_control, send_notifications
|
||||
from awx.main.access import get_user_queryset
|
||||
from awx.main.ha import is_ha_environment
|
||||
from awx.api.authentication import TaskAuthentication, TokenGetAuthentication
|
||||
from awx.api.utils.decorators import paginated
|
||||
from awx.api.filters import MongoFilterBackend
|
||||
from awx.api.generics import get_view_name
|
||||
from awx.api.generics import * # noqa
|
||||
from awx.api.license import feature_enabled, feature_exists, LicenseForbids
|
||||
@ -70,7 +66,6 @@ from awx.main.utils import * # noqa
|
||||
from awx.api.permissions import * # noqa
|
||||
from awx.api.renderers import * # noqa
|
||||
from awx.api.serializers import * # noqa
|
||||
from awx.fact.models import * # noqa
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.conf import tower_settings
|
||||
|
||||
@ -137,6 +132,8 @@ class ApiV1RootView(APIView):
|
||||
data['schedules'] = reverse('api:schedule_list')
|
||||
data['roles'] = reverse('api:role_list')
|
||||
data['resources'] = reverse('api:resource_list')
|
||||
data['notifiers'] = reverse('api:notifier_list')
|
||||
data['notifications'] = reverse('api:notification_list')
|
||||
data['unified_job_templates'] = reverse('api:unified_job_template_list')
|
||||
data['unified_jobs'] = reverse('api:unified_job_list')
|
||||
data['activity_stream'] = reverse('api:activity_stream_list')
|
||||
@ -252,32 +249,12 @@ class ApiV1ConfigView(APIView):
|
||||
# FIX: Log
|
||||
return Response({"error": "Invalid License"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Sanity check: If this license includes system tracking, make
|
||||
# sure that we have a valid MongoDB to point to, and complain if
|
||||
# we do not.
|
||||
if ('features' in license_data and 'system_tracking' in license_data['features'] and
|
||||
license_data['features']['system_tracking'] and settings.MONGO_HOST == NotImplemented):
|
||||
return Response({
|
||||
'error': 'This license supports system tracking, which '
|
||||
'requires MongoDB to be installed. Since you are '
|
||||
'running in an HA environment, you will need to '
|
||||
'provide a MongoDB instance. Please re-run the '
|
||||
'installer prior to installing this license.'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# If the license is valid, write it to disk.
|
||||
if license_data['valid_key']:
|
||||
tower_settings.LICENSE = data_actual
|
||||
|
||||
# Spawn a task to ensure that MongoDB is started (or stopped)
|
||||
# as appropriate, based on whether the license uses it.
|
||||
if license_data['features']['system_tracking']:
|
||||
mongodb_control.delay('start')
|
||||
else:
|
||||
mongodb_control.delay('stop')
|
||||
|
||||
# Done; return the response.
|
||||
tower_settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host())
|
||||
return Response(license_data)
|
||||
|
||||
return Response({"error": "Invalid license"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request):
|
||||
@ -698,6 +675,35 @@ class OrganizationActivityStreamList(SubListAPIView):
|
||||
# Okay, let it through.
|
||||
return super(type(self), self).get(request, *args, **kwargs)
|
||||
|
||||
class OrganizationNotifiersList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'notifiers'
|
||||
parent_key = 'organization'
|
||||
|
||||
class OrganizationNotifiersAnyList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'notifiers_any'
|
||||
|
||||
class OrganizationNotifiersErrorList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'notifiers_error'
|
||||
|
||||
class OrganizationNotifiersSuccessList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'notifiers_success'
|
||||
|
||||
class TeamList(ListCreateAPIView):
|
||||
|
||||
model = Team
|
||||
@ -724,11 +730,8 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
|
||||
relationship='member_role.children'
|
||||
|
||||
def get_queryset(self):
|
||||
# XXX: This needs to be the intersection between
|
||||
# what roles the user has and what roles the viewer
|
||||
# has access to see.
|
||||
team = Team.objects.get(pk=self.kwargs['pk'])
|
||||
return team.member_role.children
|
||||
return team.member_role.children.filter(id__in=Role.visible_roles(self.request.user))
|
||||
|
||||
# XXX: Need to enforce permissions
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -868,6 +871,26 @@ class ProjectActivityStreamList(SubListAPIView):
|
||||
return qs.filter(project=parent)
|
||||
return qs.filter(Q(project=parent) | Q(credential__in=parent.credential))
|
||||
|
||||
class ProjectNotifiersAnyList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = Project
|
||||
relationship = 'notifiers_any'
|
||||
|
||||
class ProjectNotifiersErrorList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = Project
|
||||
relationship = 'notifiers_error'
|
||||
|
||||
class ProjectNotifiersSuccessList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = Project
|
||||
relationship = 'notifiers_success'
|
||||
|
||||
class ProjectUpdatesList(SubListAPIView):
|
||||
|
||||
@ -918,6 +941,12 @@ class ProjectUpdateCancel(RetrieveAPIView):
|
||||
else:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
class ProjectUpdateNotificationsList(SubListAPIView):
|
||||
|
||||
model = Notification
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = Project
|
||||
relationship = 'notifications'
|
||||
|
||||
class UserList(ListCreateAPIView):
|
||||
|
||||
@ -947,13 +976,11 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = User
|
||||
relationship='roles'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get_queryset(self):
|
||||
# XXX: This needs to be the intersection between
|
||||
# what roles the user has and what roles the viewer
|
||||
# has access to see.
|
||||
u = User.objects.get(pk=self.kwargs['pk'])
|
||||
return u.roles
|
||||
#u = User.objects.get(pk=self.kwargs['pk'])
|
||||
return Role.visible_roles(self.request.user).filter(members__in=[int(self.kwargs['pk']), ])
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Forbid implicit role creation here
|
||||
@ -963,6 +990,10 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return super(type(self), self).post(request, *args, **kwargs)
|
||||
|
||||
def check_parent_access(self, parent=None):
|
||||
# We hide roles that shouldn't be seen in our queryset
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class UserProjectsList(SubListAPIView):
|
||||
@ -1172,33 +1203,6 @@ class InventoryScanJobTemplateList(SubListAPIView):
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(job_type=PERM_INVENTORY_SCAN, inventory=parent)
|
||||
|
||||
class InventorySingleFactView(MongoAPIView):
|
||||
|
||||
model = Fact
|
||||
parent_model = Inventory
|
||||
new_in_220 = True
|
||||
serializer_class = FactSerializer
|
||||
filter_backends = (MongoFilterBackend,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
'of system tracking.')
|
||||
|
||||
fact_key = request.query_params.get("fact_key", None)
|
||||
fact_value = request.query_params.get("fact_value", None)
|
||||
datetime_spec = request.query_params.get("timestamp", None)
|
||||
module_spec = request.query_params.get("module", None)
|
||||
|
||||
if fact_key is None or fact_value is None or module_spec is None:
|
||||
return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
|
||||
inventory_obj = self.get_parent_object()
|
||||
fact_data = Fact.get_single_facts([h.name for h in inventory_obj.hosts.all()], fact_key, fact_value, datetime_actual, module_spec)
|
||||
return Response(dict(results=FactSerializer(fact_data).data if fact_data is not None else []))
|
||||
|
||||
|
||||
class HostList(ListCreateAPIView):
|
||||
|
||||
model = Host
|
||||
@ -1285,102 +1289,59 @@ class HostActivityStreamList(SubListAPIView):
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
|
||||
|
||||
class HostFactVersionsList(MongoListAPIView):
|
||||
class SystemTrackingEnforcementMixin(APIView):
|
||||
'''
|
||||
Use check_permissions instead of initial() because it's in the OPTION's path as well
|
||||
'''
|
||||
def check_permissions(self, request):
|
||||
if not feature_enabled("system_tracking"):
|
||||
raise LicenseForbids("Your license does not permit use "
|
||||
"of system tracking.")
|
||||
return super(SystemTrackingEnforcementMixin, self).check_permissions(request)
|
||||
|
||||
class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMixin):
|
||||
|
||||
model = Fact
|
||||
serializer_class = FactVersionSerializer
|
||||
parent_model = Host
|
||||
new_in_220 = True
|
||||
filter_backends = (MongoFilterBackend,)
|
||||
|
||||
def get_queryset(self):
|
||||
from_spec = self.request.query_params.get('from', None)
|
||||
to_spec = self.request.query_params.get('to', None)
|
||||
module_spec = self.request.query_params.get('module', None)
|
||||
|
||||
if not feature_enabled("system_tracking"):
|
||||
raise LicenseForbids("Your license does not permit use "
|
||||
"of system tracking.")
|
||||
if from_spec:
|
||||
from_spec = dateutil.parser.parse(from_spec)
|
||||
if to_spec:
|
||||
to_spec = dateutil.parser.parse(to_spec)
|
||||
|
||||
host = self.get_parent_object()
|
||||
self.check_parent_access(host)
|
||||
host_obj = self.get_parent_object()
|
||||
|
||||
try:
|
||||
fact_host = FactHost.objects.get(hostname=host.name, inventory_id=host.inventory.pk)
|
||||
except FactHost.DoesNotExist:
|
||||
return None
|
||||
except mongoengine.ConnectionError:
|
||||
return Response(dict(error="System Tracking Database is disabled"), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
kv = {
|
||||
'host': fact_host.id,
|
||||
}
|
||||
if module_spec is not None:
|
||||
kv['module'] = module_spec
|
||||
if from_spec is not None:
|
||||
from_actual = dateutil.parser.parse(from_spec)
|
||||
kv['timestamp__gt'] = from_actual
|
||||
if to_spec is not None:
|
||||
to_actual = dateutil.parser.parse(to_spec)
|
||||
kv['timestamp__lte'] = to_actual
|
||||
|
||||
return FactVersion.objects.filter(**kv).order_by("-timestamp")
|
||||
return Fact.get_timeline(host_obj.id, module=module_spec, ts_from=from_spec, ts_to=to_spec)
|
||||
|
||||
def list(self, *args, **kwargs):
|
||||
queryset = self.get_queryset() or []
|
||||
try:
|
||||
serializer = FactVersionSerializer(queryset, many=True, context=dict(host_obj=self.get_parent_object()))
|
||||
except mongoengine.ConnectionError:
|
||||
return Response(dict(error="System Tracking Database is disabled"), status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(dict(results=serializer.data))
|
||||
return Response(dict(results=self.serializer_class(queryset, many=True).data))
|
||||
|
||||
class HostSingleFactView(MongoAPIView):
|
||||
class HostFactCompareView(SubDetailAPIView, SystemTrackingEnforcementMixin):
|
||||
|
||||
model = Fact
|
||||
parent_model = Host
|
||||
new_in_220 = True
|
||||
serializer_class = FactSerializer
|
||||
filter_backends = (MongoFilterBackend,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
'of system tracking.')
|
||||
|
||||
fact_key = request.query_params.get("fact_key", None)
|
||||
fact_value = request.query_params.get("fact_value", None)
|
||||
datetime_spec = request.query_params.get("timestamp", None)
|
||||
module_spec = request.query_params.get("module", None)
|
||||
|
||||
if fact_key is None or fact_value is None or module_spec is None:
|
||||
return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
|
||||
host_obj = self.get_parent_object()
|
||||
fact_data = Fact.get_single_facts([host_obj.name], fact_key, fact_value, datetime_actual, module_spec)
|
||||
return Response(dict(results=FactSerializer(fact_data).data if fact_data is not None else []))
|
||||
|
||||
class HostFactCompareView(MongoAPIView):
|
||||
|
||||
new_in_220 = True
|
||||
parent_model = Host
|
||||
serializer_class = FactSerializer
|
||||
filter_backends = (MongoFilterBackend,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
'of system tracking.')
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
datetime_spec = request.query_params.get('datetime', None)
|
||||
module_spec = request.query_params.get('module', "ansible")
|
||||
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
|
||||
|
||||
host_obj = self.get_parent_object()
|
||||
fact_entry = Fact.get_host_version(host_obj.name, host_obj.inventory.pk, datetime_actual, module_spec)
|
||||
host_data = FactSerializer(fact_entry).data if fact_entry is not None else {}
|
||||
|
||||
return Response(host_data)
|
||||
fact_entry = Fact.get_host_fact(host_obj.id, module_spec, datetime_actual)
|
||||
if not fact_entry:
|
||||
return Response({'detail': 'Fact not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(self.serializer_class(instance=fact_entry).data)
|
||||
|
||||
class GroupList(ListCreateAPIView):
|
||||
|
||||
@ -1549,33 +1510,6 @@ class GroupDetail(RetrieveUpdateDestroyAPIView):
|
||||
obj.mark_inactive_recursive()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class GroupSingleFactView(MongoAPIView):
|
||||
|
||||
model = Fact
|
||||
parent_model = Group
|
||||
new_in_220 = True
|
||||
serializer_class = FactSerializer
|
||||
filter_backends = (MongoFilterBackend,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
'of system tracking.')
|
||||
|
||||
fact_key = request.query_params.get("fact_key", None)
|
||||
fact_value = request.query_params.get("fact_value", None)
|
||||
datetime_spec = request.query_params.get("timestamp", None)
|
||||
module_spec = request.query_params.get("module", None)
|
||||
|
||||
if fact_key is None or fact_value is None or module_spec is None:
|
||||
return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
|
||||
group_obj = self.get_parent_object()
|
||||
fact_data = Fact.get_single_facts([h.name for h in group_obj.hosts.all()], fact_key, fact_value, datetime_actual, module_spec)
|
||||
return Response(dict(results=FactSerializer(fact_data).data if fact_data is not None else []))
|
||||
|
||||
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
@ -1803,6 +1737,27 @@ class InventorySourceActivityStreamList(SubListAPIView):
|
||||
# Okay, let it through.
|
||||
return super(type(self), self).get(request, *args, **kwargs)
|
||||
|
||||
class InventorySourceNotifiersAnyList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = InventorySource
|
||||
relationship = 'notifiers_any'
|
||||
|
||||
class InventorySourceNotifiersErrorList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = InventorySource
|
||||
relationship = 'notifiers_error'
|
||||
|
||||
class InventorySourceNotifiersSuccessList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = InventorySource
|
||||
relationship = 'notifiers_success'
|
||||
|
||||
class InventorySourceHostsList(SubListAPIView):
|
||||
|
||||
model = Host
|
||||
@ -1867,6 +1822,13 @@ class InventoryUpdateCancel(RetrieveAPIView):
|
||||
else:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
class InventoryUpdateNotificationsList(SubListAPIView):
|
||||
|
||||
model = Notification
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = InventoryUpdate
|
||||
relationship = 'notifications'
|
||||
|
||||
class JobTemplateList(ListCreateAPIView):
|
||||
|
||||
model = JobTemplate
|
||||
@ -2036,6 +1998,27 @@ class JobTemplateActivityStreamList(SubListAPIView):
|
||||
# Okay, let it through.
|
||||
return super(type(self), self).get(request, *args, **kwargs)
|
||||
|
||||
class JobTemplateNotifiersAnyList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = JobTemplate
|
||||
relationship = 'notifiers_any'
|
||||
|
||||
class JobTemplateNotifiersErrorList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = JobTemplate
|
||||
relationship = 'notifiers_error'
|
||||
|
||||
class JobTemplateNotifiersSuccessList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
parent_model = JobTemplate
|
||||
relationship = 'notifiers_success'
|
||||
|
||||
class JobTemplateCallback(GenericAPIView):
|
||||
|
||||
model = JobTemplate
|
||||
@ -2369,6 +2352,13 @@ class JobRelaunch(RetrieveAPIView, GenericAPIView):
|
||||
headers = {'Location': new_job.get_absolute_url()}
|
||||
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
class JobNotificationsList(SubListAPIView):
|
||||
|
||||
model = Notification
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = Job
|
||||
relationship = 'notifications'
|
||||
|
||||
class BaseJobHostSummariesList(SubListAPIView):
|
||||
|
||||
model = JobHostSummary
|
||||
@ -3022,6 +3012,58 @@ class AdHocCommandStdout(UnifiedJobStdout):
|
||||
model = AdHocCommand
|
||||
new_in_220 = True
|
||||
|
||||
class NotifierList(ListCreateAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
new_in_300 = True
|
||||
|
||||
class NotifierDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = Notifier
|
||||
serializer_class = NotifierSerializer
|
||||
new_in_300 = True
|
||||
|
||||
class NotifierTest(GenericAPIView):
|
||||
|
||||
view_name = 'Notifier Test'
|
||||
model = Notifier
|
||||
serializer_class = EmptySerializer
|
||||
new_in_300 = True
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
notification = obj.generate_notification("Tower Notification Test {} {}".format(obj.id, tower_settings.TOWER_URL_BASE),
|
||||
{"body": "Ansible Tower Test Notification {} {}".format(obj.id, tower_settings.TOWER_URL_BASE)})
|
||||
if not notification:
|
||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
send_notifications.delay([notification.id])
|
||||
headers = {'Location': notification.get_absolute_url()}
|
||||
return Response({"notification": notification.id},
|
||||
headers=headers,
|
||||
status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
class NotifierNotificationList(SubListAPIView):
|
||||
|
||||
model = Notification
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = Notifier
|
||||
relationship = 'notifications'
|
||||
parent_key = 'notifier'
|
||||
|
||||
class NotificationList(ListAPIView):
|
||||
|
||||
model = Notification
|
||||
serializer_class = NotificationSerializer
|
||||
new_in_300 = True
|
||||
|
||||
class NotificationDetail(RetrieveAPIView):
|
||||
|
||||
model = Notification
|
||||
serializer_class = NotificationSerializer
|
||||
new_in_300 = True
|
||||
|
||||
class ActivityStreamList(SimpleListAPIView):
|
||||
|
||||
model = ActivityStream
|
||||
@ -3120,29 +3162,27 @@ class SettingsReset(APIView):
|
||||
TowerSettings.objects.filter(key=settings_key).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
#class RoleList(ListCreateAPIView):
|
||||
|
||||
class RoleList(ListAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
# XXX: Permissions - only roles the user has access to see should be listed here
|
||||
def get_queryset(self):
|
||||
return Role.objects
|
||||
if self.request.user.is_superuser:
|
||||
return Role.objects
|
||||
return Role.visible_roles(self.request.user)
|
||||
|
||||
# XXX: Need to define who can create custom roles, and then restrict access
|
||||
# appropriately
|
||||
# XXX: Need to define how we want to deal with administration of custom roles.
|
||||
|
||||
class RoleDetail(RetrieveUpdateAPIView):
|
||||
class RoleDetail(RetrieveAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
# XXX: Permissions - only appropriate people should be able to change these
|
||||
|
||||
|
||||
class RoleUsersList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
@ -3150,6 +3190,8 @@ class RoleUsersList(SubListCreateAttachDetachAPIView):
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Role
|
||||
relationship = 'members'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
# XXX: Access control
|
||||
@ -3171,6 +3213,8 @@ class RoleTeamsList(ListAPIView):
|
||||
serializer_class = TeamSerializer
|
||||
parent_model = Role
|
||||
relationship = 'member_role.parents'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
# TODO: Check
|
||||
@ -3201,6 +3245,8 @@ class RoleParentsList(SubListAPIView):
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Role
|
||||
relationship = 'parents'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
# XXX: This should be the intersection between the roles of the user
|
||||
@ -3214,6 +3260,8 @@ class RoleChildrenList(SubListAPIView):
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Role
|
||||
relationship = 'children'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
# XXX: This should be the intersection between the roles of the user
|
||||
@ -3225,6 +3273,7 @@ class ResourceDetail(RetrieveAPIView):
|
||||
|
||||
model = Resource
|
||||
serializer_class = ResourceSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
# XXX: Permissions - only roles the user has access to see should be listed here
|
||||
@ -3235,6 +3284,7 @@ class ResourceList(ListAPIView):
|
||||
|
||||
model = Resource
|
||||
serializer_class = ResourceSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
@ -3244,6 +3294,7 @@ class ResourceAccessList(ListAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = ResourceAccessListElementSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
from django.conf import settings
|
||||
from mongoengine import connect
|
||||
from mongoengine.connection import ConnectionError
|
||||
from pymongo.errors import AutoReconnect
|
||||
|
||||
def test_mongo_connection():
|
||||
# Connect to Mongo
|
||||
try:
|
||||
# Sanity check: If we have intentionally invalid settings, then we
|
||||
# know we cannot connect.
|
||||
if settings.MONGO_HOST == NotImplemented:
|
||||
raise ConnectionError
|
||||
|
||||
# Attempt to connect to the MongoDB database.
|
||||
db = connect(settings.MONGO_DB,
|
||||
host=settings.MONGO_HOST,
|
||||
port=int(settings.MONGO_PORT),
|
||||
username=settings.MONGO_USERNAME,
|
||||
password=settings.MONGO_PASSWORD,
|
||||
tz_aware=settings.USE_TZ)
|
||||
db[settings.MONGO_DB].command('ping')
|
||||
return True
|
||||
except (ConnectionError, AutoReconnect):
|
||||
return False
|
||||
|
||||
@ -1291,6 +1291,31 @@ class ScheduleAccess(BaseAccess):
|
||||
else:
|
||||
return False
|
||||
|
||||
class NotifierAccess(BaseAccess):
|
||||
'''
|
||||
I can see/use a notifier if I have permission to
|
||||
'''
|
||||
model = Notifier
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.model.objects.filter(active=True).distinct()
|
||||
if self.user.is_superuser:
|
||||
return qs
|
||||
return qs
|
||||
|
||||
class NotificationAccess(BaseAccess):
|
||||
'''
|
||||
I can see/use a notification if I have permission to
|
||||
'''
|
||||
model = Notification
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.model.objects.distinct()
|
||||
if self.user.is_superuser:
|
||||
return qs
|
||||
return qs
|
||||
|
||||
|
||||
class ActivityStreamAccess(BaseAccess):
|
||||
'''
|
||||
I can see activity stream events only when I have permission on all objects included in the event
|
||||
@ -1475,23 +1500,31 @@ class RoleAccess(BaseAccess):
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser:
|
||||
return self.model.objects.all()
|
||||
return self.model.objects.none()
|
||||
return self.model.visible_roles(self.user)
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.user.is_superuser
|
||||
|
||||
def can_add(self, obj, data):
|
||||
return self.user.is_superuser
|
||||
# Unsupported for now
|
||||
return False
|
||||
|
||||
def can_attach(self, obj, sub_obj, relationship, data,
|
||||
skip_sub_obj_read_check=False):
|
||||
return self.user.is_superuser
|
||||
return self.can_unattach(obj, sub_obj, relationship)
|
||||
|
||||
def can_unattach(self, obj, sub_obj, relationship):
|
||||
return self.user.is_superuser
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if obj.object_id and \
|
||||
isinstance(obj.content_object, ResourceMixin) and \
|
||||
obj.content_object.accessible_by(self.user, {'write': True}):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.user.is_superuser
|
||||
# Unsupported for now
|
||||
return False
|
||||
|
||||
|
||||
class ResourceAccess(BaseAccess):
|
||||
@ -1550,3 +1583,5 @@ register_access(CustomInventoryScript, CustomInventoryScriptAccess)
|
||||
register_access(TowerSettings, TowerSettingsAccess)
|
||||
register_access(Role, RoleAccess)
|
||||
register_access(Resource, ResourceAccess)
|
||||
register_access(Notifier, NotifierAccess)
|
||||
register_access(Notification, NotificationAccess)
|
||||
|
||||
@ -134,8 +134,9 @@ def resolve_role_field(obj, field):
|
||||
class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
"""Descriptor Implict Role Fields. Auto-creates the appropriate role entry on first access"""
|
||||
|
||||
def __init__(self, role_name, permissions, parent_role, *args, **kwargs):
|
||||
def __init__(self, role_name, role_description, permissions, parent_role, *args, **kwargs):
|
||||
self.role_name = role_name
|
||||
self.role_description = role_description if role_description else ""
|
||||
self.permissions = permissions
|
||||
self.parent_role = parent_role
|
||||
|
||||
@ -152,7 +153,7 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
if connection.needs_rollback:
|
||||
raise TransactionManagementError('Current transaction has failed, cannot create implicit role')
|
||||
|
||||
role = Role.objects.create(name=self.role_name, content_object=instance)
|
||||
role = Role.objects.create(name=self.role_name, description=self.role_description, content_object=instance)
|
||||
if self.parent_role:
|
||||
|
||||
# Add all non-null parent roles as parents
|
||||
@ -195,8 +196,9 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
class ImplicitRoleField(models.ForeignKey):
|
||||
"""Implicitly creates a role entry for a resource"""
|
||||
|
||||
def __init__(self, role_name=None, permissions=None, parent_role=None, *args, **kwargs):
|
||||
def __init__(self, role_name=None, role_description=None, permissions=None, parent_role=None, *args, **kwargs):
|
||||
self.role_name = role_name
|
||||
self.role_description = role_description
|
||||
self.permissions = permissions
|
||||
self.parent_role = parent_role
|
||||
|
||||
@ -211,6 +213,7 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
self.name,
|
||||
ImplicitRoleDescriptor(
|
||||
self.role_name,
|
||||
self.role_description,
|
||||
self.permissions,
|
||||
self.parent_role,
|
||||
self
|
||||
|
||||
@ -12,7 +12,7 @@ from django.db import transaction
|
||||
from django.utils.timezone import now
|
||||
|
||||
# AWX
|
||||
from awx.fact.models.fact import * # noqa
|
||||
from awx.main.models.fact import Fact
|
||||
from awx.api.license import feature_enabled
|
||||
|
||||
OLDER_THAN = 'older_than'
|
||||
@ -31,7 +31,7 @@ class CleanupFacts(object):
|
||||
# pivot -= granularity
|
||||
# group by host
|
||||
def cleanup(self, older_than_abs, granularity, module=None):
|
||||
fact_oldest = FactVersion.objects.all().order_by('timestamp').first()
|
||||
fact_oldest = Fact.objects.all().order_by('timestamp').first()
|
||||
if not fact_oldest:
|
||||
return 0
|
||||
|
||||
@ -44,7 +44,10 @@ class CleanupFacts(object):
|
||||
# Special case, granularity=0x where x is d, w, or y
|
||||
# The intent is to delete all facts < older_than_abs
|
||||
if granularity == relativedelta():
|
||||
return FactVersion.objects.filter(**kv).order_by('-timestamp').delete()
|
||||
qs = Fact.objects.filter(**kv)
|
||||
count = qs.count()
|
||||
qs.delete()
|
||||
return count
|
||||
|
||||
total = 0
|
||||
|
||||
@ -61,18 +64,17 @@ class CleanupFacts(object):
|
||||
kv['module'] = module
|
||||
|
||||
|
||||
fact_version_objs = FactVersion.objects.filter(**kv).order_by('-timestamp').limit(1)
|
||||
if fact_version_objs:
|
||||
fact_version_obj = fact_version_objs[0]
|
||||
fact_version_obj = Fact.objects.filter(**kv).order_by('-timestamp').first()
|
||||
if fact_version_obj:
|
||||
kv = {
|
||||
'timestamp__lt': fact_version_obj.timestamp,
|
||||
'timestamp__gt': date_pivot_next
|
||||
}
|
||||
if module:
|
||||
kv['module'] = module
|
||||
count = FactVersion.objects.filter(**kv).delete()
|
||||
# FIXME: These two deletes should be a transaction
|
||||
count = Fact.objects.filter(**kv).delete()
|
||||
qs = Fact.objects.filter(**kv)
|
||||
count = qs.count()
|
||||
qs.delete()
|
||||
total += count
|
||||
|
||||
date_pivot = date_pivot_next
|
||||
|
||||
@ -9,9 +9,11 @@ from datetime import datetime
|
||||
# Django
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.conf import settings
|
||||
#from django.core.exceptions import Does
|
||||
|
||||
# AWX
|
||||
from awx.fact.models.fact import * # noqa
|
||||
from awx.main.models.fact import Fact
|
||||
from awx.main.models.inventory import Host
|
||||
from awx.main.socket import Socket
|
||||
|
||||
logger = logging.getLogger('awx.main.commands.run_fact_cache_receiver')
|
||||
@ -47,35 +49,34 @@ class FactCacheReceiver(object):
|
||||
# ansible v2 will not emit this message. Thus, this can be removed at that time.
|
||||
if 'module_setup' in facts_data and len(facts_data) == 1:
|
||||
logger.info('Received module_setup message')
|
||||
return
|
||||
return None
|
||||
|
||||
try:
|
||||
host = FactHost.objects.get(hostname=hostname, inventory_id=inventory_id)
|
||||
except FactHost.DoesNotExist:
|
||||
logger.info('Creating new host <hostname, inventory_id> <%s, %s>' % (hostname, inventory_id))
|
||||
host = FactHost(hostname=hostname, inventory_id=inventory_id)
|
||||
host.save()
|
||||
logger.info('Created new host <%s>' % (host.id))
|
||||
except FactHost.MultipleObjectsReturned:
|
||||
query = "db['fact_host'].find(hostname=%s, inventory_id=%s)" % (hostname, inventory_id)
|
||||
logger.warn('Database inconsistent. Multiple FactHost "%s" exist. Try the query %s to find the records.' % (hostname, query))
|
||||
host_obj = Host.objects.get(name=hostname, inventory__id=inventory_id)
|
||||
except Fact.DoesNotExist:
|
||||
logger.warn('Failed to intake fact. Host does not exist <hostname, inventory_id> <%s, %s>' % (hostname, inventory_id))
|
||||
return
|
||||
except Fact.MultipleObjectsReturned:
|
||||
logger.warn('Database inconsistent. Multiple Hosts found for <hostname, inventory_id> <%s, %s>.' % (hostname, inventory_id))
|
||||
return None
|
||||
except Exception, e:
|
||||
logger.error("Exception communicating with Fact Cache Database: %s" % str(e))
|
||||
return
|
||||
return None
|
||||
|
||||
(module, facts) = self.process_facts(facts_data)
|
||||
(module_name, facts) = self.process_facts(facts_data)
|
||||
self.timestamp = datetime.fromtimestamp(date_key, None)
|
||||
|
||||
try:
|
||||
# Update existing Fact entry
|
||||
version_obj = FactVersion.objects.get(timestamp=self.timestamp, host=host, module=module)
|
||||
Fact.objects(id=version_obj.fact.id).update_one(fact=facts)
|
||||
logger.info('Updated existing fact <%s>' % (version_obj.fact.id))
|
||||
except FactVersion.DoesNotExist:
|
||||
# Update existing Fact entry
|
||||
fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp)
|
||||
if fact_obj:
|
||||
fact_obj.facts = facts
|
||||
fact_obj.save()
|
||||
logger.info('Updated existing fact <%s>' % (fact_obj.id))
|
||||
else:
|
||||
# Create new Fact entry
|
||||
(fact_obj, version_obj) = Fact.add_fact(self.timestamp, facts, host, module)
|
||||
logger.info('Created new fact <fact, fact_version> <%s, %s>' % (fact_obj.id, version_obj.id))
|
||||
fact_obj = Fact.add_fact(host_obj.id, module_name, self.timestamp, facts)
|
||||
logger.info('Created new fact <fact_id, module> <%s, %s>' % (fact_obj.id, module_name))
|
||||
return fact_obj
|
||||
|
||||
def run_receiver(self, use_processing_threads=True):
|
||||
with Socket('fact_cache', 'r') as facts:
|
||||
|
||||
@ -15,7 +15,7 @@ from django.core.management.base import NoArgsCommand
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.queue import FifoQueue
|
||||
from awx.main.tasks import handle_work_error
|
||||
from awx.main.tasks import handle_work_error, handle_work_success
|
||||
from awx.main.utils import get_system_task_capacity
|
||||
|
||||
# Celery
|
||||
@ -265,14 +265,15 @@ def process_graph(graph, task_capacity):
|
||||
[{'type': graph.get_node_type(n['node_object']),
|
||||
'id': n['node_object'].id} for n in node_dependencies]
|
||||
error_handler = handle_work_error.s(subtasks=dependent_nodes)
|
||||
start_status = node_obj.start(error_callback=error_handler)
|
||||
success_handler = handle_work_success.s(task_actual={'type': graph.get_node_type(node_obj),
|
||||
'id': node_obj.id})
|
||||
start_status = node_obj.start(error_callback=error_handler, success_callback=success_handler)
|
||||
if not start_status:
|
||||
node_obj.status = 'failed'
|
||||
if node_obj.job_explanation:
|
||||
node_obj.job_explanation += ' '
|
||||
node_obj.job_explanation += 'Task failed pre-start check.'
|
||||
node_obj.save()
|
||||
# TODO: Run error handler
|
||||
continue
|
||||
remaining_volume -= impact
|
||||
running_impact += impact
|
||||
|
||||
@ -43,7 +43,7 @@ class Migration(migrations.Migration):
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('host_name', models.CharField(default=b'', max_length=1024, editable=False)),
|
||||
('event', models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_unreachable', 'Host Unreachable')])),
|
||||
('event', models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_unreachable', 'Host Unreachable'), (b'runner_on_skipped', 'Host Skipped')])),
|
||||
('event_data', jsonfield.fields.JSONField(default={}, blank=True)),
|
||||
('failed', models.BooleanField(default=False, editable=False)),
|
||||
('changed', models.BooleanField(default=False, editable=False)),
|
||||
|
||||
105
awx/main/migrations/0003_v300_changes.py
Normal file
105
awx/main/migrations/0003_v300_changes.py
Normal file
@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import jsonfield.fields
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('main', '0002_v300_changes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('status', models.CharField(default=b'pending', max_length=20, editable=False, choices=[(b'pending', 'Pending'), (b'successful', 'Successful'), (b'failed', 'Failed')])),
|
||||
('error', models.TextField(default=b'', editable=False, blank=True)),
|
||||
('notifications_sent', models.IntegerField(default=0, editable=False)),
|
||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'irc', 'IRC')])),
|
||||
('recipients', models.TextField(default=b'', editable=False, blank=True)),
|
||||
('subject', models.TextField(default=b'', editable=False, blank=True)),
|
||||
('body', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('pk',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notifier',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('description', models.TextField(default=b'', blank=True)),
|
||||
('active', models.BooleanField(default=True, editable=False)),
|
||||
('name', models.CharField(unique=True, max_length=512)),
|
||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'irc', 'IRC')])),
|
||||
('notification_configuration', jsonfield.fields.JSONField(default=dict)),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'notifier', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'notifier', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('organization', models.ForeignKey(related_name='notifiers', on_delete=django.db.models.deletion.SET_NULL, to='main.Organization', null=True)),
|
||||
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='notifier',
|
||||
field=models.ForeignKey(related_name='notifications', editable=False, to='main.Notifier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activitystream',
|
||||
name='notification',
|
||||
field=models.ManyToManyField(to='main.Notification', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activitystream',
|
||||
name='notifier',
|
||||
field=models.ManyToManyField(to='main.Notifier', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='notifiers_any',
|
||||
field=models.ManyToManyField(related_name='organization_notifiers_for_any', to='main.Notifier', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='notifiers_error',
|
||||
field=models.ManyToManyField(related_name='organization_notifiers_for_errors', to='main.Notifier', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='notifiers_success',
|
||||
field=models.ManyToManyField(related_name='organization_notifiers_for_success', to='main.Notifier', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unifiedjob',
|
||||
name='notifications',
|
||||
field=models.ManyToManyField(related_name='unifiedjob_notifications', editable=False, to='main.Notification'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unifiedjobtemplate',
|
||||
name='notifiers_any',
|
||||
field=models.ManyToManyField(related_name='unifiedjobtemplate_notifiers_for_any', to='main.Notifier', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unifiedjobtemplate',
|
||||
name='notifiers_error',
|
||||
field=models.ManyToManyField(related_name='unifiedjobtemplate_notifiers_for_errors', to='main.Notifier', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unifiedjobtemplate',
|
||||
name='notifiers_success',
|
||||
field=models.ManyToManyField(related_name='unifiedjobtemplate_notifiers_for_success', to='main.Notifier', blank=True),
|
||||
),
|
||||
]
|
||||
29
awx/main/migrations/0004_v300_changes.py
Normal file
29
awx/main/migrations/0004_v300_changes.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import jsonbfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0003_v300_changes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Fact',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('timestamp', models.DateTimeField(default=None, help_text='Date and time of the corresponding fact scan gathering time.', editable=False)),
|
||||
('module', models.CharField(max_length=128)),
|
||||
('facts', jsonbfield.fields.JSONField(default={}, help_text='Arbitrary JSON structure of module facts captured at timestamp for a single host.', blank=True)),
|
||||
('host', models.ForeignKey(related_name='facts', to='main.Host', help_text='Host for the facts that the fact scan captured.')),
|
||||
],
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='fact',
|
||||
index_together=set([('timestamp', 'module', 'host')]),
|
||||
),
|
||||
]
|
||||
16
awx/main/migrations/0005_v300_active_field_changes.py
Normal file
16
awx/main/migrations/0005_v300_active_field_changes.py
Normal file
@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0004_v300_changes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# This is a placeholder for our future active flag removal work
|
||||
]
|
||||
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('main', '0002_v300_changes'),
|
||||
('main', '0005_v300_active_field_changes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@ -8,7 +8,7 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0003_rbac_changes'),
|
||||
('main', '0006_v300_rbac_changes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@ -20,6 +20,8 @@ from awx.main.models.configuration import * # noqa
|
||||
from awx.main.models.rbac import * # noqa
|
||||
from awx.main.models.user import * # noqa
|
||||
from awx.main.models.mixins import * # noqa
|
||||
from awx.main.models.notifications import * # noqa
|
||||
from awx.main.models.fact import * # noqa
|
||||
|
||||
# Monkeypatch Django serializer to ignore django-taggit fields (which break
|
||||
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155).
|
||||
@ -62,3 +64,5 @@ activity_stream_registrar.connect(AdHocCommand)
|
||||
activity_stream_registrar.connect(Schedule)
|
||||
activity_stream_registrar.connect(CustomInventoryScript)
|
||||
activity_stream_registrar.connect(TowerSettings)
|
||||
activity_stream_registrar.connect(Notifier)
|
||||
activity_stream_registrar.connect(Notification)
|
||||
|
||||
@ -53,6 +53,8 @@ class ActivityStream(models.Model):
|
||||
ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True)
|
||||
schedule = models.ManyToManyField("Schedule", blank=True)
|
||||
custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True)
|
||||
notifier = models.ManyToManyField("Notifier", blank=True)
|
||||
notification = models.ManyToManyField("Notification", blank=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:activity_stream_detail', args=(self.pk,))
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from urlparse import urljoin
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -139,6 +140,9 @@ class AdHocCommand(UnifiedJob):
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:ad_hoc_command_detail', args=(self.pk,))
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(tower_settings.TOWER_URL_BASE, "/#/ad_hoc_commands/{}".format(self.pk))
|
||||
|
||||
@property
|
||||
def task_auth_token(self):
|
||||
'''Return temporary auth token used for task requests via API.'''
|
||||
@ -221,8 +225,9 @@ class AdHocCommandEvent(CreatedModifiedModel):
|
||||
('runner_on_unreachable', _('Host Unreachable'), True),
|
||||
# Tower won't see no_hosts (check is done earlier without callback).
|
||||
#('runner_on_no_hosts', _('No Hosts Matched'), False),
|
||||
# Tower should probably never see skipped (no conditionals).
|
||||
#('runner_on_skipped', _('Host Skipped'), False),
|
||||
# Tower will see skipped (when running in check mode for a module that
|
||||
# does not support check mode).
|
||||
('runner_on_skipped', _('Host Skipped'), False),
|
||||
# Tower does not support async for ad hoc commands.
|
||||
#('runner_on_async_poll', _('Host Polling'), False),
|
||||
#('runner_on_async_ok', _('Host Async OK'), False),
|
||||
|
||||
@ -25,7 +25,7 @@ from awx.main.utils import encrypt_field
|
||||
|
||||
__all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
|
||||
'PasswordFieldsModel', 'PrimordialModel', 'CommonModel',
|
||||
'CommonModelNameNotUnique',
|
||||
'CommonModelNameNotUnique', 'NotificationFieldsModel',
|
||||
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
|
||||
'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN',
|
||||
'PERM_INVENTORY_CHECK', 'PERM_JOBTEMPLATE_CREATE', 'JOB_TYPE_CHOICES',
|
||||
@ -337,3 +337,26 @@ class CommonModelNameNotUnique(PrimordialModel):
|
||||
max_length=512,
|
||||
unique=False,
|
||||
)
|
||||
|
||||
class NotificationFieldsModel(BaseModel):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
notifiers_error = models.ManyToManyField(
|
||||
"Notifier",
|
||||
blank=True,
|
||||
related_name='%(class)s_notifiers_for_errors'
|
||||
)
|
||||
|
||||
notifiers_success = models.ManyToManyField(
|
||||
"Notifier",
|
||||
blank=True,
|
||||
related_name='%(class)s_notifiers_for_success'
|
||||
)
|
||||
|
||||
notifiers_any = models.ManyToManyField(
|
||||
"Notifier",
|
||||
blank=True,
|
||||
related_name='%(class)s_notifiers_for_any'
|
||||
)
|
||||
|
||||
@ -157,11 +157,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
)
|
||||
owner_role = ImplicitRoleField(
|
||||
role_name='Credential Owner',
|
||||
role_description='Owner of the credential',
|
||||
parent_role='team.admin_role',
|
||||
permissions = {'all': True}
|
||||
)
|
||||
usage_role = ImplicitRoleField(
|
||||
role_name='Credential User',
|
||||
role_description='May use this credential, but not read sensitive portions or modify it',
|
||||
parent_role= 'team.member_role',
|
||||
permissions = {'use': True}
|
||||
)
|
||||
|
||||
64
awx/main/models/fact.py
Normal file
64
awx/main/models/fact.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from jsonbfield.fields import JSONField
|
||||
|
||||
__all__ = ('Fact', )
|
||||
|
||||
class Fact(models.Model):
|
||||
"""A model representing a fact returned from Ansible.
|
||||
Facts are stored as JSON dictionaries.
|
||||
"""
|
||||
host = models.ForeignKey(
|
||||
'Host',
|
||||
related_name='facts',
|
||||
db_index=True,
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_('Host for the facts that the fact scan captured.'),
|
||||
)
|
||||
timestamp = models.DateTimeField(
|
||||
default=None,
|
||||
editable=False,
|
||||
help_text=_('Date and time of the corresponding fact scan gathering time.')
|
||||
)
|
||||
module = models.CharField(max_length=128)
|
||||
facts = JSONField(blank=True, default={}, help_text=_('Arbitrary JSON structure of module facts captured at timestamp for a single host.'))
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
index_together = [
|
||||
["timestamp", "module", "host"],
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_host_fact(host_id, module, timestamp):
|
||||
qs = Fact.objects.filter(host__id=host_id, module=module, timestamp__lte=timestamp).order_by('-timestamp')
|
||||
if qs:
|
||||
return qs[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_timeline(host_id, module=None, ts_from=None, ts_to=None):
|
||||
kwargs = {
|
||||
'host__id': host_id,
|
||||
}
|
||||
if module:
|
||||
kwargs['module'] = module
|
||||
if ts_from and ts_to and ts_from == ts_to:
|
||||
kwargs['timestamp'] = ts_from
|
||||
else:
|
||||
if ts_from:
|
||||
kwargs['timestamp__gt'] = ts_from
|
||||
if ts_to:
|
||||
kwargs['timestamp__lte'] = ts_to
|
||||
return Fact.objects.filter(**kwargs).order_by('-timestamp').only('timestamp', 'module').order_by('-timestamp', 'module')
|
||||
|
||||
@staticmethod
|
||||
def add_fact(host_id, module, timestamp, facts):
|
||||
fact_obj = Fact.objects.create(host_id=host_id, module=module, timestamp=timestamp, facts=facts)
|
||||
fact_obj.save()
|
||||
return fact_obj
|
||||
@ -6,6 +6,7 @@ import datetime
|
||||
import logging
|
||||
import re
|
||||
import copy
|
||||
from urlparse import urljoin
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -24,7 +25,9 @@ from awx.main.models.base import * # noqa
|
||||
from awx.main.models.jobs import Job
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.main.models.notifications import Notifier
|
||||
from awx.main.utils import ignore_inventory_computed_fields, _inventory_updates
|
||||
from awx.main.conf import tower_settings
|
||||
|
||||
__all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', 'CustomInventoryScript']
|
||||
|
||||
@ -95,19 +98,23 @@ class Inventory(CommonModel, ResourceMixin):
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Inventory Administrator',
|
||||
role_description='May manage this inventory',
|
||||
parent_role='organization.admin_role',
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Inventory Auditor',
|
||||
role_description='May view but not modify this inventory',
|
||||
parent_role='organization.auditor_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
updater_role = ImplicitRoleField(
|
||||
role_name='Inventory Updater',
|
||||
role_description='May update the inventory',
|
||||
)
|
||||
executor_role = ImplicitRoleField(
|
||||
role_name='Inventory Executor',
|
||||
role_description='May execute jobs against this inventory',
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
@ -1217,6 +1224,14 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, ResourceMixin)
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def notifiers(self):
|
||||
base_notifiers = Notifier.objects.filter(active=True)
|
||||
error_notifiers = list(base_notifiers.filter(organization_notifiers_for_errors=self.inventory.organization))
|
||||
success_notifiers = list(base_notifiers.filter(organization_notifiers_for_success=self.inventory.organization))
|
||||
any_notifiers = list(base_notifiers.filter(organization_notifiers_for_any=self.inventory.organization))
|
||||
return dict(error=error_notifiers, success=success_notifiers, any=any_notifiers)
|
||||
|
||||
def clean_source(self):
|
||||
source = self.source
|
||||
if source and self.group:
|
||||
@ -1276,6 +1291,9 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions):
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:inventory_update_detail', args=(self.pk,))
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(tower_settings.TOWER_URL_BASE, "/#/inventory_sync/{}".format(self.pk))
|
||||
|
||||
def is_blocked_by(self, obj):
|
||||
if type(obj) == InventoryUpdate:
|
||||
if self.inventory_source.inventory == obj.inventory_source.inventory:
|
||||
|
||||
@ -6,6 +6,7 @@ import hmac
|
||||
import json
|
||||
import yaml
|
||||
import logging
|
||||
from urlparse import urljoin
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -22,6 +23,7 @@ from jsonfield import JSONField
|
||||
from awx.main.constants import CLOUD_PROVIDERS
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.models.notifications import Notifier
|
||||
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.redact import PlainTextCleaner
|
||||
@ -183,16 +185,19 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Job Template Administrator',
|
||||
role_description='Full access to all settings',
|
||||
parent_role='project.admin_role',
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Job Template Auditor',
|
||||
role_description='Read-only access to all settings',
|
||||
parent_role='project.auditor_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
executor_role = ImplicitRoleField(
|
||||
role_name='Job Template Executor',
|
||||
role_name='Job Template Runner',
|
||||
role_description='May run the job template',
|
||||
permissions = {'read': True, 'execute': True}
|
||||
)
|
||||
|
||||
@ -347,6 +352,20 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
def _can_update(self):
|
||||
return self.can_start_without_user_input()
|
||||
|
||||
@property
|
||||
def notifiers(self):
|
||||
# Return all notifiers defined on the Job Template, on the Project, and on the Organization for each trigger type
|
||||
# TODO: Currently there is no org fk on project so this will need to be added once that is
|
||||
# available after the rbac pr
|
||||
base_notifiers = Notifier.objects.filter(active=True)
|
||||
error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_errors__in=[self, self.project]))
|
||||
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success__in=[self, self.project]))
|
||||
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any__in=[self, self.project]))
|
||||
# Get Organization Notifiers
|
||||
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.project.organizations.all())))
|
||||
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.project.organizations.all())))
|
||||
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.project.organizations.all())))
|
||||
return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers))
|
||||
|
||||
class Job(UnifiedJob, JobOptions):
|
||||
'''
|
||||
@ -386,6 +405,9 @@ class Job(UnifiedJob, JobOptions):
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:job_detail', args=(self.pk,))
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(tower_settings.TOWER_URL_BASE, "/#/jobs/{}".format(self.pk))
|
||||
|
||||
@property
|
||||
def task_auth_token(self):
|
||||
'''Return temporary auth token used for task requests via API.'''
|
||||
@ -502,6 +524,26 @@ class Job(UnifiedJob, JobOptions):
|
||||
dependencies.append(source.create_inventory_update(launch_type='dependency'))
|
||||
return dependencies
|
||||
|
||||
def notification_data(self):
|
||||
data = super(Job, self).notification_data()
|
||||
all_hosts = {}
|
||||
for h in self.job_host_summaries.all():
|
||||
all_hosts[h.host.name] = dict(failed=h.failed,
|
||||
changed=h.changed,
|
||||
dark=h.dark,
|
||||
failures=h.failures,
|
||||
ok=h.ok,
|
||||
processed=h.processed,
|
||||
skipped=h.skipped)
|
||||
data.update(dict(inventory=self.inventory.name,
|
||||
project=self.project.name,
|
||||
playbook=self.playbook,
|
||||
credential=self.credential.name,
|
||||
limit=self.limit,
|
||||
extra_vars=self.extra_vars,
|
||||
hosts=all_hosts))
|
||||
return data
|
||||
|
||||
def handle_extra_data(self, extra_data):
|
||||
extra_vars = {}
|
||||
if isinstance(extra_data, dict):
|
||||
@ -1082,6 +1124,9 @@ class SystemJob(UnifiedJob, SystemJobOptions):
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:system_job_detail', args=(self.pk,))
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(tower_settings.TOWER_URL_BASE, "/#/management_jobs/{}".format(self.pk))
|
||||
|
||||
def is_blocked_by(self, obj):
|
||||
return True
|
||||
|
||||
|
||||
172
awx/main/models/notifications.py
Normal file
172
awx/main/models/notifications.py
Normal file
@ -0,0 +1,172 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.mail.message import EmailMessage
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.utils import encrypt_field, decrypt_field
|
||||
from awx.main.notifications.email_backend import CustomEmailBackend
|
||||
from awx.main.notifications.slack_backend import SlackBackend
|
||||
from awx.main.notifications.twilio_backend import TwilioBackend
|
||||
from awx.main.notifications.pagerduty_backend import PagerDutyBackend
|
||||
from awx.main.notifications.hipchat_backend import HipChatBackend
|
||||
from awx.main.notifications.webhook_backend import WebhookBackend
|
||||
from awx.main.notifications.irc_backend import IrcBackend
|
||||
|
||||
# Django-JSONField
|
||||
from jsonfield import JSONField
|
||||
|
||||
logger = logging.getLogger('awx.main.models.notifications')
|
||||
|
||||
__all__ = ['Notifier', 'Notification']
|
||||
|
||||
class Notifier(CommonModel):
|
||||
|
||||
NOTIFICATION_TYPES = [('email', _('Email'), CustomEmailBackend),
|
||||
('slack', _('Slack'), SlackBackend),
|
||||
('twilio', _('Twilio'), TwilioBackend),
|
||||
('pagerduty', _('Pagerduty'), PagerDutyBackend),
|
||||
('hipchat', _('HipChat'), HipChatBackend),
|
||||
('webhook', _('Webhook'), WebhookBackend),
|
||||
('irc', _('IRC'), IrcBackend)]
|
||||
NOTIFICATION_TYPE_CHOICES = [(x[0], x[1]) for x in NOTIFICATION_TYPES]
|
||||
CLASS_FOR_NOTIFICATION_TYPE = dict([(x[0], x[2]) for x in NOTIFICATION_TYPES])
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
|
||||
organization = models.ForeignKey(
|
||||
'Organization',
|
||||
blank=False,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='notifiers',
|
||||
)
|
||||
|
||||
notification_type = models.CharField(
|
||||
max_length = 32,
|
||||
choices=NOTIFICATION_TYPE_CHOICES,
|
||||
)
|
||||
|
||||
notification_configuration = JSONField(blank=False)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:notifier_detail', args=(self.pk,))
|
||||
|
||||
@property
|
||||
def notification_class(self):
|
||||
return self.CLASS_FOR_NOTIFICATION_TYPE[self.notification_type]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_instance = not bool(self.pk)
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password",
|
||||
self.notification_class.init_parameters):
|
||||
if new_instance:
|
||||
value = self.notification_configuration[field]
|
||||
setattr(self, '_saved_{}_{}'.format("config", field), value)
|
||||
self.notification_configuration[field] = ''
|
||||
else:
|
||||
encrypted = encrypt_field(self, 'notification_configuration', subfield=field)
|
||||
self.notification_configuration[field] = encrypted
|
||||
if 'notification_configuration' not in update_fields:
|
||||
update_fields.append('notification_configuration')
|
||||
super(Notifier, self).save(*args, **kwargs)
|
||||
if new_instance:
|
||||
update_fields = []
|
||||
for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password",
|
||||
self.notification_class.init_parameters):
|
||||
saved_value = getattr(self, '_saved_{}_{}'.format("config", field), '')
|
||||
self.notification_configuration[field] = saved_value
|
||||
#setattr(self.notification_configuration, field, saved_value)
|
||||
if 'notification_configuration' not in update_fields:
|
||||
update_fields.append('notification_configuration')
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
return self.notification_configuration[self.notification_class.recipient_parameter]
|
||||
|
||||
def generate_notification(self, subject, message):
|
||||
notification = Notification(notifier=self,
|
||||
notification_type=self.notification_type,
|
||||
recipients=smart_str(self.recipients),
|
||||
subject=subject,
|
||||
body=message)
|
||||
notification.save()
|
||||
return notification
|
||||
|
||||
def send(self, subject, body):
|
||||
for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password",
|
||||
self.notification_class.init_parameters):
|
||||
self.notification_configuration[field] = decrypt_field(self,
|
||||
'notification_configuration',
|
||||
subfield=field)
|
||||
recipients = self.notification_configuration.pop(self.notification_class.recipient_parameter)
|
||||
if not isinstance(recipients, list):
|
||||
recipients = [recipients]
|
||||
sender = self.notification_configuration.pop(self.notification_class.sender_parameter, None)
|
||||
backend_obj = self.notification_class(**self.notification_configuration)
|
||||
notification_obj = EmailMessage(subject, backend_obj.format_body(body), sender, recipients)
|
||||
return backend_obj.send_messages([notification_obj])
|
||||
|
||||
class Notification(CreatedModifiedModel):
|
||||
'''
|
||||
A notification event emitted when a Notifier is run
|
||||
'''
|
||||
|
||||
NOTIFICATION_STATE_CHOICES = [
|
||||
('pending', _('Pending')),
|
||||
('successful', _('Successful')),
|
||||
('failed', _('Failed')),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
ordering = ('pk',)
|
||||
|
||||
notifier = models.ForeignKey(
|
||||
'Notifier',
|
||||
related_name='notifications',
|
||||
on_delete=models.CASCADE,
|
||||
editable=False
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=NOTIFICATION_STATE_CHOICES,
|
||||
default='pending',
|
||||
editable=False,
|
||||
)
|
||||
error = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
notifications_sent = models.IntegerField(
|
||||
default=0,
|
||||
editable=False,
|
||||
)
|
||||
notification_type = models.CharField(
|
||||
max_length = 32,
|
||||
choices=Notifier.NOTIFICATION_TYPE_CHOICES,
|
||||
)
|
||||
recipients = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
subject = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
body = JSONField(blank=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:notification_detail', args=(self.pk,))
|
||||
@ -29,7 +29,7 @@ from awx.main.conf import tower_settings
|
||||
__all__ = ['Organization', 'Team', 'Permission', 'Profile', 'AuthToken']
|
||||
|
||||
|
||||
class Organization(CommonModel, ResourceMixin):
|
||||
class Organization(CommonModel, NotificationFieldsModel, ResourceMixin):
|
||||
'''
|
||||
An organization is the basic unit of multi-tenancy divisions
|
||||
'''
|
||||
@ -55,16 +55,20 @@ class Organization(CommonModel, ResourceMixin):
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Organization Administrator',
|
||||
role_description='May manage all aspects of this organization',
|
||||
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
permissions = ALL_PERMISSIONS,
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Organization Auditor',
|
||||
role_description='May read all settings associated with this organization',
|
||||
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
permissions = {'read': True}
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Organization Member',
|
||||
role_description='A member of this organization',
|
||||
parent_role='admin_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
|
||||
@ -111,16 +115,19 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Team Administrator',
|
||||
role_description='May manage this team',
|
||||
parent_role='organization.admin_role',
|
||||
permissions = ALL_PERMISSIONS,
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Team Auditor',
|
||||
role_description='May read all settings associated with this team',
|
||||
parent_role='organization.auditor_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Team Member',
|
||||
role_description='A member of this team',
|
||||
parent_role='admin_role',
|
||||
permissions = {'read':True},
|
||||
)
|
||||
|
||||
@ -20,10 +20,12 @@ from django.utils.timezone import now, make_aware, get_default_timezone
|
||||
from awx.lib.compat import slugify
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.jobs import Job
|
||||
from awx.main.models.notifications import Notifier
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.main.utils import update_scm_url
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.conf import tower_settings
|
||||
|
||||
__all__ = ['Project', 'ProjectUpdate']
|
||||
|
||||
@ -209,20 +211,24 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Project Administrator',
|
||||
role_description='May manage this project',
|
||||
parent_role='organizations.admin_role',
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Project Auditor',
|
||||
role_description='May read all settings associated with this project',
|
||||
parent_role='organizations.auditor_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Project Member',
|
||||
role_description='Implies membership within this project',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
scm_update_role = ImplicitRoleField(
|
||||
role_name='Project Updater',
|
||||
role_description='May update this project from the source control management system',
|
||||
parent_role='admin_role',
|
||||
permissions = {'scm_update': True}
|
||||
)
|
||||
@ -330,6 +336,18 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def notifiers(self):
|
||||
base_notifiers = Notifier.objects.filter(active=True)
|
||||
error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_errors=self))
|
||||
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success=self))
|
||||
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any=self))
|
||||
# Get Organization Notifiers
|
||||
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.organizations.all())))
|
||||
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.organizations.all())))
|
||||
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.organizations.all())))
|
||||
return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:project_detail', args=(self.pk,))
|
||||
|
||||
@ -391,6 +409,9 @@ class ProjectUpdate(UnifiedJob, ProjectOptions):
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:project_update_detail', args=(self.pk,))
|
||||
|
||||
def get_ui_url(self):
|
||||
return urlparse.urljoin(tower_settings.TOWER_URL_BASE, "/#/scm_update/{}".format(self.pk))
|
||||
|
||||
def _update_parent_instance(self):
|
||||
parent_instance = self._get_parent_instance()
|
||||
if parent_instance:
|
||||
|
||||
@ -6,6 +6,7 @@ import logging
|
||||
|
||||
# Django
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.aggregates import Max
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -128,6 +129,10 @@ class Role(CommonModelNameNotUnique):
|
||||
setattr(permission, k, int(permissions[k]))
|
||||
permission.save()
|
||||
|
||||
@staticmethod
|
||||
def visible_roles(user):
|
||||
return Role.objects.filter(Q(descendents__in=user.roles.filter()) | Q(ancestors__in=user.roles.filter()))
|
||||
|
||||
@staticmethod
|
||||
def singleton(name):
|
||||
try:
|
||||
|
||||
@ -17,6 +17,7 @@ from django.db import models
|
||||
from django.core.exceptions import NON_FIELD_ERRORS
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
# Django-JSONField
|
||||
from jsonfield import JSONField
|
||||
@ -40,7 +41,7 @@ logger = logging.getLogger('awx.main.models.unified_jobs')
|
||||
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
||||
|
||||
|
||||
class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique):
|
||||
class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, NotificationFieldsModel):
|
||||
'''
|
||||
Concrete base class for unified job templates.
|
||||
'''
|
||||
@ -297,6 +298,14 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique):
|
||||
'''
|
||||
return kwargs # Override if needed in subclass.
|
||||
|
||||
@property
|
||||
def notifiers(self):
|
||||
'''
|
||||
Return notifiers relevant to this Unified Job Template
|
||||
'''
|
||||
# NOTE: Derived classes should implement
|
||||
return Notifier.objects.none()
|
||||
|
||||
def create_unified_job(self, **kwargs):
|
||||
'''
|
||||
Create a new unified job based on this unified job template.
|
||||
@ -385,6 +394,11 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
editable=False,
|
||||
related_name='%(class)s_blocked_jobs+',
|
||||
)
|
||||
notifications = models.ManyToManyField(
|
||||
'Notification',
|
||||
editable=False,
|
||||
related_name='%(class)s_notifications',
|
||||
)
|
||||
cancel_flag = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
@ -470,6 +484,13 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
else:
|
||||
return ''
|
||||
|
||||
def get_ui_url(self):
|
||||
real_instance = self.get_real_instance()
|
||||
if real_instance != self:
|
||||
return real_instance.get_ui_url()
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def _get_task_class(cls):
|
||||
raise NotImplementedError # Implement in subclasses.
|
||||
@ -717,7 +738,17 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
tasks that might preclude creating one'''
|
||||
return []
|
||||
|
||||
def start(self, error_callback, **kwargs):
|
||||
def notification_data(self):
|
||||
return dict(id=self.id,
|
||||
name=self.name,
|
||||
url=self.get_ui_url(),
|
||||
created_by=smart_text(self.created_by),
|
||||
started=self.started.isoformat(),
|
||||
finished=self.finished.isoformat(),
|
||||
status=self.status,
|
||||
traceback=self.result_traceback)
|
||||
|
||||
def start(self, error_callback, success_callback, **kwargs):
|
||||
'''
|
||||
Start the task running via Celery.
|
||||
'''
|
||||
@ -743,7 +774,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
# if field not in needed])
|
||||
if 'extra_vars' in kwargs:
|
||||
self.handle_extra_data(kwargs['extra_vars'])
|
||||
task_class().apply_async((self.pk,), opts, link_error=error_callback)
|
||||
task_class().apply_async((self.pk,), opts, link_error=error_callback, link=success_callback)
|
||||
return True
|
||||
|
||||
def signal_start(self, **kwargs):
|
||||
@ -765,7 +796,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
|
||||
# Sanity check: If we are running unit tests, then run synchronously.
|
||||
if getattr(settings, 'CELERY_UNIT_TEST', False):
|
||||
return self.start(None, **kwargs)
|
||||
return self.start(None, None, **kwargs)
|
||||
|
||||
# Save the pending status, and inform the SocketIO listener.
|
||||
self.update_fields(start_args=json.dumps(kwargs), status='pending')
|
||||
|
||||
@ -26,5 +26,6 @@ class UserResource(CommonModelNameNotUnique, ResourceMixin):
|
||||
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='User Administrator',
|
||||
role_description='May manage this user',
|
||||
permissions = {'all': True},
|
||||
)
|
||||
|
||||
0
awx/main/notifications/__init__.py
Normal file
0
awx/main/notifications/__init__.py
Normal file
20
awx/main/notifications/base.py
Normal file
20
awx/main/notifications/base.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import pprint
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
|
||||
class TowerBaseEmailBackend(BaseEmailBackend):
|
||||
|
||||
def format_body(self, body):
|
||||
if "body" in body:
|
||||
body_actual = body['body']
|
||||
else:
|
||||
body_actual = smart_text("{} #{} had status {} on Ansible Tower, view details at {}\n\n".format(body['friendly_name'],
|
||||
body['id'],
|
||||
body['status'],
|
||||
body['url']))
|
||||
body_actual += pprint.pformat(body, indent=4)
|
||||
return body_actual
|
||||
28
awx/main/notifications/email_backend.py
Normal file
28
awx/main/notifications/email_backend.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import pprint
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
|
||||
class CustomEmailBackend(EmailBackend):
|
||||
|
||||
init_parameters = {"host": {"label": "Host", "type": "string"},
|
||||
"port": {"label": "Port", "type": "int"},
|
||||
"username": {"label": "Username", "type": "string"},
|
||||
"password": {"label": "Password", "type": "password"},
|
||||
"use_tls": {"label": "Use TLS", "type": "bool"},
|
||||
"use_ssl": {"label": "Use SSL", "type": "bool"},
|
||||
"sender": {"label": "Sender Email", "type": "string"},
|
||||
"recipients": {"label": "Recipient List", "type": "list"}}
|
||||
recipient_parameter = "recipients"
|
||||
sender_parameter = "sender"
|
||||
|
||||
def format_body(self, body):
|
||||
body_actual = smart_text("{} #{} had status {} on Ansible Tower, view details at {}\n\n".format(body['friendly_name'],
|
||||
body['id'],
|
||||
body['status'],
|
||||
body['url']))
|
||||
body_actual += pprint.pformat(body, indent=4)
|
||||
return body_actual
|
||||
49
awx/main/notifications/hipchat_backend.py
Normal file
49
awx/main/notifications/hipchat_backend.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from awx.main.notifications.base import TowerBaseEmailBackend
|
||||
|
||||
logger = logging.getLogger('awx.main.notifications.hipchat_backend')
|
||||
|
||||
class HipChatBackend(TowerBaseEmailBackend):
|
||||
|
||||
init_parameters = {"token": {"label": "Token", "type": "password"},
|
||||
"channels": {"label": "Destination Channels", "type": "list"},
|
||||
"color": {"label": "Notification Color", "type": "string"},
|
||||
"api_url": {"label": "API Url (e.g: https://mycompany.hipchat.com)", "type": "string"},
|
||||
"notify": {"label": "Notify channel", "type": "bool"},
|
||||
"message_from": {"label": "Label to be shown with notification", "type": "string"}}
|
||||
recipient_parameter = "channels"
|
||||
sender_parameter = "message_from"
|
||||
|
||||
def __init__(self, token, color, api_url, notify, fail_silently=False, **kwargs):
|
||||
super(HipChatBackend, self).__init__(fail_silently=fail_silently)
|
||||
self.token = token
|
||||
self.color = color
|
||||
self.api_url = api_url
|
||||
self.notify = notify
|
||||
|
||||
def send_messages(self, messages):
|
||||
sent_messages = 0
|
||||
|
||||
for m in messages:
|
||||
for rcp in m.recipients():
|
||||
r = requests.post("{}/v2/room/{}/notification".format(self.api_url, rcp),
|
||||
params={"auth_token": self.token},
|
||||
json={"color": self.color,
|
||||
"message": m.subject,
|
||||
"notify": self.notify,
|
||||
"from": m.from_email,
|
||||
"message_format": "text"})
|
||||
if r.status_code != 204:
|
||||
logger.error(smart_text("Error sending messages: {}".format(r.text)))
|
||||
if not self.fail_silently:
|
||||
raise Exception(smart_text("Error sending message to hipchat: {}".format(r.text)))
|
||||
sent_messages += 1
|
||||
return sent_messages
|
||||
95
awx/main/notifications/irc_backend.py
Normal file
95
awx/main/notifications/irc_backend.py
Normal file
@ -0,0 +1,95 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import time
|
||||
import ssl
|
||||
import logging
|
||||
|
||||
import irc.client
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from awx.main.notifications.base import TowerBaseEmailBackend
|
||||
|
||||
logger = logging.getLogger('awx.main.notifications.irc_backend')
|
||||
|
||||
class IrcBackend(TowerBaseEmailBackend):
|
||||
|
||||
init_parameters = {"server": {"label": "IRC Server Address", "type": "string"},
|
||||
"port": {"label": "IRC Server Port", "type": "int"},
|
||||
"nickname": {"label": "IRC Nick", "type": "string"},
|
||||
"password": {"label": "IRC Server Password", "type": "password"},
|
||||
"use_ssl": {"label": "SSL Connection", "type": "bool"},
|
||||
"targets": {"label": "Destination Channels or Users", "type": "list"}}
|
||||
recipient_parameter = "targets"
|
||||
sender_parameter = None
|
||||
|
||||
def __init__(self, server, port, nickname, password, use_ssl, fail_silently=False, **kwargs):
|
||||
super(IrcBackend, self).__init__(fail_silently=fail_silently)
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.nickname = nickname
|
||||
self.password = password if password != "" else None
|
||||
self.use_ssl = use_ssl
|
||||
self.connection = None
|
||||
|
||||
def open(self):
|
||||
if self.connection is not None:
|
||||
return False
|
||||
if self.use_ssl:
|
||||
connection_factory = irc.connection.Factory(wrapper=ssl.wrap_socket)
|
||||
else:
|
||||
connection_factory = irc.connection.Factory()
|
||||
try:
|
||||
self.reactor = irc.client.Reactor()
|
||||
self.connection = self.reactor.server().connect(
|
||||
self.server,
|
||||
self.port,
|
||||
self.nickname,
|
||||
password=self.password,
|
||||
connect_factory=connection_factory,
|
||||
)
|
||||
except irc.client.ServerConnectionError as e:
|
||||
logger.error(smart_text("Exception connecting to irc server: {}".format(e)))
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
if self.connection is None:
|
||||
return
|
||||
self.connection = None
|
||||
|
||||
def on_connect(self, connection, event):
|
||||
for c in self.channels:
|
||||
if irc.client.is_channel(c):
|
||||
connection.join(c)
|
||||
else:
|
||||
for m in self.channels[c]:
|
||||
connection.privmsg(c, m.subject)
|
||||
self.channels_sent += 1
|
||||
|
||||
def on_join(self, connection, event):
|
||||
for m in self.channels[event.target]:
|
||||
connection.privmsg(event.target, m.subject)
|
||||
self.channels_sent += 1
|
||||
|
||||
def send_messages(self, messages):
|
||||
if self.connection is None:
|
||||
self.open()
|
||||
self.channels = {}
|
||||
self.channels_sent = 0
|
||||
for m in messages:
|
||||
for r in m.recipients():
|
||||
if r not in self.channels:
|
||||
self.channels[r] = []
|
||||
self.channels[r].append(m)
|
||||
self.connection.add_global_handler("welcome", self.on_connect)
|
||||
self.connection.add_global_handler("join", self.on_join)
|
||||
start_time = time.time()
|
||||
process_time = time.time()
|
||||
while self.channels_sent < len(self.channels) and (process_time - start_time) < 60:
|
||||
self.reactor.process_once(0.1)
|
||||
process_time = time.time()
|
||||
self.reactor.disconnect_all()
|
||||
return self.channels_sent
|
||||
49
awx/main/notifications/pagerduty_backend.py
Normal file
49
awx/main/notifications/pagerduty_backend.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import logging
|
||||
import pygerduty
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from awx.main.notifications.base import TowerBaseEmailBackend
|
||||
|
||||
logger = logging.getLogger('awx.main.notifications.pagerduty_backend')
|
||||
|
||||
class PagerDutyBackend(TowerBaseEmailBackend):
|
||||
|
||||
init_parameters = {"subdomain": {"label": "Pagerduty subdomain", "type": "string"},
|
||||
"token": {"label": "API Token", "type": "password"},
|
||||
"service_key": {"label": "API Service/Integration Key", "type": "string"},
|
||||
"client_name": {"label": "Client Identifier", "type": "string"}}
|
||||
recipient_parameter = "service_key"
|
||||
sender_parameter = "client_name"
|
||||
|
||||
def __init__(self, subdomain, token, fail_silently=False, **kwargs):
|
||||
super(PagerDutyBackend, self).__init__(fail_silently=fail_silently)
|
||||
self.subdomain = subdomain
|
||||
self.token = token
|
||||
|
||||
def format_body(self, body):
|
||||
return body
|
||||
|
||||
def send_messages(self, messages):
|
||||
sent_messages = 0
|
||||
|
||||
try:
|
||||
pager = pygerduty.PagerDuty(self.subdomain, self.token)
|
||||
except Exception as e:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
logger.error(smart_text("Exception connecting to PagerDuty: {}".format(e)))
|
||||
for m in messages:
|
||||
try:
|
||||
pager.trigger_incident(m.recipients()[0],
|
||||
description=m.subject,
|
||||
details=m.body,
|
||||
client=m.from_email)
|
||||
except Exception as e:
|
||||
logger.error(smart_text("Exception sending messages: {}".format(e)))
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return sent_messages
|
||||
52
awx/main/notifications/slack_backend.py
Normal file
52
awx/main/notifications/slack_backend.py
Normal file
@ -0,0 +1,52 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import logging
|
||||
from slackclient import SlackClient
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from awx.main.notifications.base import TowerBaseEmailBackend
|
||||
|
||||
logger = logging.getLogger('awx.main.notifications.slack_backend')
|
||||
|
||||
class SlackBackend(TowerBaseEmailBackend):
|
||||
|
||||
init_parameters = {"token": {"label": "Token", "type": "password"},
|
||||
"channels": {"label": "Destination Channels", "type": "list"}}
|
||||
recipient_parameter = "channels"
|
||||
sender_parameter = None
|
||||
|
||||
def __init__(self, token, fail_silently=False, **kwargs):
|
||||
super(SlackBackend, self).__init__(fail_silently=fail_silently)
|
||||
self.token = token
|
||||
self.connection = None
|
||||
|
||||
def open(self):
|
||||
if self.connection is not None:
|
||||
return False
|
||||
self.connection = SlackClient(self.token)
|
||||
if not self.connection.rtm_connect():
|
||||
if not self.fail_silently:
|
||||
raise Exception("Slack Notification Token is invalid")
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
if self.connection is None:
|
||||
return
|
||||
self.connection = None
|
||||
|
||||
def send_messages(self, messages):
|
||||
if self.connection is None:
|
||||
self.open()
|
||||
sent_messages = 0
|
||||
for m in messages:
|
||||
try:
|
||||
for r in m.recipients():
|
||||
self.connection.rtm_send_message(r, m.subject)
|
||||
sent_messages += 1
|
||||
except Exception as e:
|
||||
logger.error(smart_text("Exception sending messages: {}".format(e)))
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return sent_messages
|
||||
48
awx/main/notifications/twilio_backend.py
Normal file
48
awx/main/notifications/twilio_backend.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import logging
|
||||
|
||||
from twilio.rest import TwilioRestClient
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from awx.main.notifications.base import TowerBaseEmailBackend
|
||||
|
||||
logger = logging.getLogger('awx.main.notifications.twilio_backend')
|
||||
|
||||
class TwilioBackend(TowerBaseEmailBackend):
|
||||
|
||||
init_parameters = {"account_sid": {"label": "Account SID", "type": "string"},
|
||||
"account_token": {"label": "Account Token", "type": "password"},
|
||||
"from_number": {"label": "Source Phone Number", "type": "string"},
|
||||
"to_numbers": {"label": "Destination SMS Numbers", "type": "list"}}
|
||||
recipient_parameter = "to_numbers"
|
||||
sender_parameter = "from_number"
|
||||
|
||||
def __init__(self, account_sid, account_token, fail_silently=False, **kwargs):
|
||||
super(TwilioBackend, self).__init__(fail_silently=fail_silently)
|
||||
self.account_sid = account_sid
|
||||
self.account_token = account_token
|
||||
|
||||
def send_messages(self, messages):
|
||||
sent_messages = 0
|
||||
try:
|
||||
connection = TwilioRestClient(self.account_sid, self.account_token)
|
||||
except Exception as e:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
logger.error(smart_text("Exception connecting to Twilio: {}".format(e)))
|
||||
|
||||
for m in messages:
|
||||
try:
|
||||
connection.messages.create(
|
||||
to=m.to,
|
||||
from_=m.from_email,
|
||||
body=m.subject)
|
||||
sent_messages += 1
|
||||
except Exception as e:
|
||||
logger.error(smart_text("Exception sending messages: {}".format(e)))
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return sent_messages
|
||||
39
awx/main/notifications/webhook_backend.py
Normal file
39
awx/main/notifications/webhook_backend.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from awx.main.notifications.base import TowerBaseEmailBackend
|
||||
|
||||
logger = logging.getLogger('awx.main.notifications.webhook_backend')
|
||||
|
||||
class WebhookBackend(TowerBaseEmailBackend):
|
||||
|
||||
init_parameters = {"url": {"label": "Target URL", "type": "string"},
|
||||
"headers": {"label": "HTTP Headers", "type": "object"}}
|
||||
recipient_parameter = "url"
|
||||
sender_parameter = None
|
||||
|
||||
def __init__(self, headers, fail_silently=False, **kwargs):
|
||||
self.headers = headers
|
||||
super(WebhookBackend, self).__init__(fail_silently=fail_silently)
|
||||
|
||||
def format_body(self, body):
|
||||
return body
|
||||
|
||||
def send_messages(self, messages):
|
||||
sent_messages = 0
|
||||
for m in messages:
|
||||
r = requests.post("{}".format(m.recipients()[0]),
|
||||
data=json.dumps(m.body),
|
||||
headers=self.headers)
|
||||
if r.status_code >= 400:
|
||||
logger.error(smart_text("Error sending notification webhook: {}".format(r.text)))
|
||||
if not self.fail_silently:
|
||||
raise Exception(smart_text("Error sending notification webhook: {}".format(r.text)))
|
||||
sent_messages += 1
|
||||
return sent_messages
|
||||
@ -332,6 +332,8 @@ model_serializer_mapping = {
|
||||
Job: JobSerializer,
|
||||
AdHocCommand: AdHocCommandSerializer,
|
||||
TowerSettings: TowerSettingsSerializer,
|
||||
Notifier: NotifierSerializer,
|
||||
Notification: NotificationSerializer,
|
||||
}
|
||||
|
||||
def activity_stream_create(sender, instance, created, **kwargs):
|
||||
|
||||
@ -39,6 +39,9 @@ from celery import Task, task
|
||||
from django.conf import settings
|
||||
from django.db import transaction, DatabaseError
|
||||
from django.utils.timezone import now
|
||||
from django.utils.encoding import smart_text
|
||||
from django.core.mail import send_mail
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# AWX
|
||||
from awx.lib.metrics import task_timer
|
||||
@ -46,13 +49,14 @@ from awx.main.constants import CLOUD_PROVIDERS
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.queue import FifoQueue
|
||||
from awx.main.conf import tower_settings
|
||||
from awx.main.task_engine import TaskSerializer, TASK_TIMEOUT_INTERVAL
|
||||
from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url,
|
||||
ignore_inventory_computed_fields, emit_websocket_notification,
|
||||
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot)
|
||||
from awx.fact.utils.connection import test_mongo_connection
|
||||
|
||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||
'RunAdHocCommand', 'handle_work_error', 'update_inventory_computed_fields']
|
||||
'RunAdHocCommand', 'handle_work_error', 'handle_work_success',
|
||||
'update_inventory_computed_fields', 'send_notifications', 'run_administrative_checks']
|
||||
|
||||
HIDDEN_PASSWORD = '**********'
|
||||
|
||||
@ -64,6 +68,48 @@ Try upgrading OpenSSH or providing your private key in an different format. \
|
||||
|
||||
logger = logging.getLogger('awx.main.tasks')
|
||||
|
||||
@task()
|
||||
def send_notifications(notification_list, job_id=None):
|
||||
if not isinstance(notification_list, list):
|
||||
raise TypeError("notification_list should be of type list")
|
||||
if job_id is not None:
|
||||
job_actual = UnifiedJob.objects.get(id=job_id)
|
||||
for notification_id in notification_list:
|
||||
notification = Notification.objects.get(id=notification_id)
|
||||
try:
|
||||
sent = notification.notifier.send(notification.subject, notification.body)
|
||||
notification.status = "successful"
|
||||
notification.notifications_sent = sent
|
||||
except Exception as e:
|
||||
logger.error("Send Notification Failed {}".format(e))
|
||||
notification.status = "failed"
|
||||
notification.error = smart_text(e)
|
||||
finally:
|
||||
notification.save()
|
||||
if job_id is not None:
|
||||
job_actual.notifications.add(notification)
|
||||
|
||||
@task(bind=True)
|
||||
def run_administrative_checks(self):
|
||||
if not tower_settings.TOWER_ADMIN_ALERTS:
|
||||
return
|
||||
reader = TaskSerializer()
|
||||
validation_info = reader.from_database()
|
||||
if validation_info.get('instance_count', 0) < 1:
|
||||
return
|
||||
used_percentage = float(validation_info.get('current_instances', 0)) / float(validation_info.get('instance_count', 100))
|
||||
tower_admin_emails = User.objects.filter(is_superuser=True).values_list('email', flat=True)
|
||||
if (used_percentage * 100) > 90:
|
||||
send_mail("Ansible Tower host usage over 90%",
|
||||
"Ansible Tower host usage over 90%",
|
||||
tower_admin_emails,
|
||||
fail_silently=True)
|
||||
if validation_info.get('time_remaining', 0) < TASK_TIMEOUT_INTERVAL:
|
||||
send_mail("Ansible Tower license will expire soon",
|
||||
"Ansible Tower license will expire soon",
|
||||
tower_admin_emails,
|
||||
fail_silently=True)
|
||||
|
||||
@task()
|
||||
def bulk_inventory_element_delete(inventory, hosts=[], groups=[]):
|
||||
from awx.main.signals import disable_activity_stream
|
||||
@ -134,7 +180,6 @@ def notify_task_runner(metadata_dict):
|
||||
queue = FifoQueue('tower_task_manager')
|
||||
queue.push(metadata_dict)
|
||||
|
||||
|
||||
@task()
|
||||
def mongodb_control(cmd):
|
||||
# Sanity check: Do not send arbitrary commands.
|
||||
@ -159,6 +204,39 @@ def mongodb_control(cmd):
|
||||
p = subprocess.Popen('sudo mongod --shutdown -f /etc/mongod.conf', shell=True)
|
||||
p.wait()
|
||||
|
||||
@task(bind=True)
|
||||
def handle_work_success(self, result, task_actual):
|
||||
if task_actual['type'] == 'project_update':
|
||||
instance = ProjectUpdate.objects.get(id=task_actual['id'])
|
||||
instance_name = instance.name
|
||||
notifiers = instance.project.notifiers
|
||||
friendly_name = "Project Update"
|
||||
elif task_actual['type'] == 'inventory_update':
|
||||
instance = InventoryUpdate.objects.get(id=task_actual['id'])
|
||||
instance_name = instance.name
|
||||
notifiers = instance.inventory_source.notifiers
|
||||
friendly_name = "Inventory Update"
|
||||
elif task_actual['type'] == 'job':
|
||||
instance = Job.objects.get(id=task_actual['id'])
|
||||
instance_name = instance.job_template.name
|
||||
notifiers = instance.job_template.notifiers
|
||||
friendly_name = "Job"
|
||||
elif task_actual['type'] == 'ad_hoc_command':
|
||||
instance = AdHocCommand.objects.get(id=task_actual['id'])
|
||||
instance_name = instance.module_name
|
||||
notifiers = [] # TODO: Ad-hoc commands need to notify someone
|
||||
friendly_name = "AdHoc Command"
|
||||
else:
|
||||
return
|
||||
notification_body = instance.notification_data()
|
||||
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
|
||||
task_actual['id'],
|
||||
instance_name,
|
||||
notification_body['url'])
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body)
|
||||
for n in set(notifiers.get('success', []) + notifiers.get('any', []))],
|
||||
job_id=task_actual['id'])
|
||||
|
||||
@task(bind=True)
|
||||
def handle_work_error(self, task_id, subtasks=None):
|
||||
print('Executing error task id %s, subtasks: %s' %
|
||||
@ -173,15 +251,23 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
if each_task['type'] == 'project_update':
|
||||
instance = ProjectUpdate.objects.get(id=each_task['id'])
|
||||
instance_name = instance.name
|
||||
notifiers = instance.project.notifiers
|
||||
friendly_name = "Project Update"
|
||||
elif each_task['type'] == 'inventory_update':
|
||||
instance = InventoryUpdate.objects.get(id=each_task['id'])
|
||||
instance_name = instance.name
|
||||
notifiers = instance.inventory_source.notifiers
|
||||
friendly_name = "Inventory Update"
|
||||
elif each_task['type'] == 'job':
|
||||
instance = Job.objects.get(id=each_task['id'])
|
||||
instance_name = instance.job_template.name
|
||||
notifiers = instance.job_template.notifiers
|
||||
friendly_name = "Job"
|
||||
elif each_task['type'] == 'ad_hoc_command':
|
||||
instance = AdHocCommand.objects.get(id=each_task['id'])
|
||||
instance_name = instance.module_name
|
||||
notifiers = []
|
||||
friendly_name = "AdHoc Command"
|
||||
else:
|
||||
# Unknown task type
|
||||
break
|
||||
@ -190,6 +276,7 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
first_task_id = instance.id
|
||||
first_task_type = each_task['type']
|
||||
first_task_name = instance_name
|
||||
first_task_friendly_name = friendly_name
|
||||
if instance.celery_task_id != task_id:
|
||||
instance.status = 'failed'
|
||||
instance.failed = True
|
||||
@ -197,6 +284,16 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
(first_task_type, first_task_name, first_task_id)
|
||||
instance.save()
|
||||
instance.socketio_emit_status("failed")
|
||||
notification_body = first_task.notification_data()
|
||||
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name,
|
||||
first_task_id,
|
||||
first_task_name,
|
||||
notification_body['url'])
|
||||
notification_body['friendly_name'] = first_task_friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
for n in set(notifiers.get('error', []) + notifiers.get('any', []))],
|
||||
job_id=first_task_id)
|
||||
|
||||
|
||||
@task()
|
||||
def update_inventory_computed_fields(inventory_id, should_update_hosts=True):
|
||||
@ -861,11 +958,6 @@ class RunJob(BaseTask):
|
||||
'''
|
||||
return getattr(tower_settings, 'AWX_PROOT_ENABLED', False)
|
||||
|
||||
def pre_run_hook(self, job, **kwargs):
|
||||
if job.job_type == PERM_INVENTORY_SCAN:
|
||||
if not test_mongo_connection():
|
||||
raise RuntimeError("Fact Scan Database is offline")
|
||||
|
||||
def post_run_hook(self, job, **kwargs):
|
||||
'''
|
||||
Hook for actions to run after job/task has completed.
|
||||
|
||||
283
awx/main/tests/functional/ansible.json
Normal file
283
awx/main/tests/functional/ansible.json
Normal file
@ -0,0 +1,283 @@
|
||||
{
|
||||
"ansible_all_ipv4_addresses": [
|
||||
"172.17.0.7"
|
||||
],
|
||||
"ansible_all_ipv6_addresses": [
|
||||
"fe80::42:acff:fe11:7"
|
||||
],
|
||||
"ansible_architecture": "x86_64",
|
||||
"ansible_bios_date": "12/01/2006",
|
||||
"ansible_bios_version": "VirtualBox",
|
||||
"ansible_cmdline": {
|
||||
"BOOT_IMAGE": "/boot/vmlinuz64",
|
||||
"base": true,
|
||||
"console": "tty0",
|
||||
"initrd": "/boot/initrd.img",
|
||||
"loglevel": "3",
|
||||
"noembed": true,
|
||||
"nomodeset": true,
|
||||
"norestore": true,
|
||||
"user": "docker",
|
||||
"waitusb": "10:LABEL=boot2docker-data"
|
||||
},
|
||||
"ansible_date_time": {
|
||||
"date": "2016-02-02",
|
||||
"day": "02",
|
||||
"epoch": "1454424257",
|
||||
"hour": "14",
|
||||
"iso8601": "2016-02-02T14:44:17Z",
|
||||
"iso8601_basic": "20160202T144417348424",
|
||||
"iso8601_basic_short": "20160202T144417",
|
||||
"iso8601_micro": "2016-02-02T14:44:17.348496Z",
|
||||
"minute": "44",
|
||||
"month": "02",
|
||||
"second": "17",
|
||||
"time": "14:44:17",
|
||||
"tz": "UTC",
|
||||
"tz_offset": "+0000",
|
||||
"weekday": "Tuesday",
|
||||
"weekday_number": "2",
|
||||
"weeknumber": "05",
|
||||
"year": "2016"
|
||||
},
|
||||
"ansible_default_ipv4": {
|
||||
"address": "172.17.0.7",
|
||||
"alias": "eth0",
|
||||
"broadcast": "global",
|
||||
"gateway": "172.17.0.1",
|
||||
"interface": "eth0",
|
||||
"macaddress": "02:42:ac:11:00:07",
|
||||
"mtu": 1500,
|
||||
"netmask": "255.255.0.0",
|
||||
"network": "172.17.0.0",
|
||||
"type": "ether"
|
||||
},
|
||||
"ansible_default_ipv6": {},
|
||||
"ansible_devices": {
|
||||
"sda": {
|
||||
"holders": [],
|
||||
"host": "",
|
||||
"model": "VBOX HARDDISK",
|
||||
"partitions": {
|
||||
"sda1": {
|
||||
"sectors": "510015555",
|
||||
"sectorsize": 512,
|
||||
"size": "243.19 GB",
|
||||
"start": "1975995"
|
||||
},
|
||||
"sda2": {
|
||||
"sectors": "1975932",
|
||||
"sectorsize": 512,
|
||||
"size": "964.81 MB",
|
||||
"start": "63"
|
||||
}
|
||||
},
|
||||
"removable": "0",
|
||||
"rotational": "0",
|
||||
"scheduler_mode": "deadline",
|
||||
"sectors": "512000000",
|
||||
"sectorsize": "512",
|
||||
"size": "244.14 GB",
|
||||
"support_discard": "0",
|
||||
"vendor": "ATA"
|
||||
},
|
||||
"sr0": {
|
||||
"holders": [],
|
||||
"host": "",
|
||||
"model": "CD-ROM",
|
||||
"partitions": {},
|
||||
"removable": "1",
|
||||
"rotational": "1",
|
||||
"scheduler_mode": "deadline",
|
||||
"sectors": "61440",
|
||||
"sectorsize": "2048",
|
||||
"size": "120.00 MB",
|
||||
"support_discard": "0",
|
||||
"vendor": "VBOX"
|
||||
}
|
||||
},
|
||||
"ansible_distribution": "Ubuntu",
|
||||
"ansible_distribution_major_version": "14",
|
||||
"ansible_distribution_release": "trusty",
|
||||
"ansible_distribution_version": "14.04",
|
||||
"ansible_dns": {
|
||||
"nameservers": [
|
||||
"8.8.8.8"
|
||||
]
|
||||
},
|
||||
"ansible_domain": "",
|
||||
"ansible_env": {
|
||||
"HOME": "/root",
|
||||
"HOSTNAME": "ede894599989",
|
||||
"LANG": "en_US.UTF-8",
|
||||
"LC_ALL": "en_US.UTF-8",
|
||||
"LC_MESSAGES": "en_US.UTF-8",
|
||||
"LESSCLOSE": "/usr/bin/lesspipe %s %s",
|
||||
"LESSOPEN": "| /usr/bin/lesspipe %s",
|
||||
"LS_COLORS": "",
|
||||
"OLDPWD": "/ansible",
|
||||
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"PWD": "/ansible/examples",
|
||||
"SHLVL": "1",
|
||||
"_": "/usr/local/bin/ansible",
|
||||
"container": "docker"
|
||||
},
|
||||
"ansible_eth0": {
|
||||
"active": true,
|
||||
"device": "eth0",
|
||||
"ipv4": {
|
||||
"address": "172.17.0.7",
|
||||
"broadcast": "global",
|
||||
"netmask": "255.255.0.0",
|
||||
"network": "172.17.0.0"
|
||||
},
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "fe80::42:acff:fe11:7",
|
||||
"prefix": "64",
|
||||
"scope": "link"
|
||||
}
|
||||
],
|
||||
"macaddress": "02:42:ac:11:00:07",
|
||||
"mtu": 1500,
|
||||
"promisc": false,
|
||||
"type": "ether"
|
||||
},
|
||||
"ansible_fips": false,
|
||||
"ansible_form_factor": "Other",
|
||||
"ansible_fqdn": "ede894599989",
|
||||
"ansible_hostname": "ede894599989",
|
||||
"ansible_interfaces": [
|
||||
"lo",
|
||||
"eth0"
|
||||
],
|
||||
"ansible_kernel": "4.1.12-boot2docker",
|
||||
"ansible_lo": {
|
||||
"active": true,
|
||||
"device": "lo",
|
||||
"ipv4": {
|
||||
"address": "127.0.0.1",
|
||||
"broadcast": "host",
|
||||
"netmask": "255.0.0.0",
|
||||
"network": "127.0.0.0"
|
||||
},
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "::1",
|
||||
"prefix": "128",
|
||||
"scope": "host"
|
||||
}
|
||||
],
|
||||
"mtu": 65536,
|
||||
"promisc": false,
|
||||
"type": "loopback"
|
||||
},
|
||||
"ansible_lsb": {
|
||||
"codename": "trusty",
|
||||
"description": "Ubuntu 14.04.3 LTS",
|
||||
"id": "Ubuntu",
|
||||
"major_release": "14",
|
||||
"release": "14.04"
|
||||
},
|
||||
"ansible_machine": "x86_64",
|
||||
"ansible_memfree_mb": 3746,
|
||||
"ansible_memory_mb": {
|
||||
"nocache": {
|
||||
"free": 8896,
|
||||
"used": 3638
|
||||
},
|
||||
"real": {
|
||||
"free": 3746,
|
||||
"total": 12534,
|
||||
"used": 8788
|
||||
},
|
||||
"swap": {
|
||||
"cached": 0,
|
||||
"free": 4048,
|
||||
"total": 4048,
|
||||
"used": 0
|
||||
}
|
||||
},
|
||||
"ansible_memtotal_mb": 12534,
|
||||
"ansible_mounts": [
|
||||
{
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"mount": "/etc/resolv.conf",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 201281392640,
|
||||
"size_total": 256895700992,
|
||||
"uuid": "NA"
|
||||
},
|
||||
{
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"mount": "/etc/hostname",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 201281392640,
|
||||
"size_total": 256895700992,
|
||||
"uuid": "NA"
|
||||
},
|
||||
{
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"mount": "/etc/hosts",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 201281392640,
|
||||
"size_total": 256895700992,
|
||||
"uuid": "NA"
|
||||
}
|
||||
],
|
||||
"ansible_nodename": "ede894599989",
|
||||
"ansible_os_family": "Debian",
|
||||
"ansible_pkg_mgr": "apt",
|
||||
"ansible_processor": [
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz"
|
||||
],
|
||||
"ansible_processor_cores": 8,
|
||||
"ansible_processor_count": 1,
|
||||
"ansible_processor_threads_per_core": 1,
|
||||
"ansible_processor_vcpus": 8,
|
||||
"ansible_product_name": "VirtualBox",
|
||||
"ansible_product_serial": "0",
|
||||
"ansible_product_uuid": "25C5EA5A-1DF1-48D9-A2C6-81227DA153C0",
|
||||
"ansible_product_version": "1.2",
|
||||
"ansible_python_version": "2.7.6",
|
||||
"ansible_selinux": false,
|
||||
"ansible_service_mgr": "upstart",
|
||||
"ansible_ssh_host_key_dsa_public": "AAAAB3NzaC1kc3MAAACBALF0xsM8UMXgSKiWNw4t19wxbxLnxQX742t/dIM0O8YLx+/lIP+Q69Dv5uoVt0zKV39eFziRlCh96qj2KYkGEJ6XfVZFnhpculL2Pv2CPpSwKuQ1vTbDO/xxUrvY+bHpfNJf9Rh69bFEE2pTsjomFPCgp8M0qGaFtwg6czSaeBONAAAAFQCGEfVtj97JiexTVRqgQITYlFp/eQAAAIEAg+S9qWn+AIb3amwVoLL/usQYOPCmZY9RVPzpkjJ6OG+HI4B7cXeauPtNTJwT0f9vGEqzf4mPpmS+aCShj6iwdmJ+cOwR5+SJlNalab3CMBoXKVLbT1J2XWFlK0szKKnoReP96IDbkAkGQ3fkm4jz0z6Wy0u6wOQVNcd4G5cwLZ4AAACAFvBm+H1LwNrwWBjWio+ayhglZ4Y25mLMEn2+dqBz0gLK5szEbft1HMPOWIVHvl6vi3v34pAJHKpxXpkLlNliTn8iw9BzCOrgP4V8sp2/85mxEuCdI1w/QERj9cHu5iS2pZ0cUwDE3pfuuGBB3IEliaJyaapowdrM8lN12jQl11E=",
|
||||
"ansible_ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHiYp4e9RfXpxDcEWpK4EuXPHW9++xcFI9hiB0TYAZgxEF9RIgwfucpPawFk7HIFoNc7EXQMlryilLSbg155KWM=",
|
||||
"ansible_ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAILclD2JaC654azEsAfcHRIOA2Ig9/Qk6MX80i/VCEdSH",
|
||||
"ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDeSUGxZaZsgBsezld0mj3HcbAwx6aykGnejceBjcs6lVwSGMHevofzSXIQDPYBhZoyWNl0PYAHv6AsQ8+3khd2SitUMJAuHSz1ZjgHCCGQP9ijXTKHn+lWCKA8rhLG/dwYwiouoOPZfn1G+erbKO6XiVbELrrf2RadnMGuMinESIOKVj3IunXsaGRMsDOQferOnUf7MvH7xpQnoySyQ1+p4rGruaohWG+Y2cDo7+B2FylPVbrpRDDJkfbt4J96WHx0KOdD0qzOicQP8JqDflqQPJJCWcgrvjQOSe4gXdPB6GZDtBl2qgQRwt1IgizPMm+b7Bwbd2VDe1TeWV2gT/7H",
|
||||
"ansible_swapfree_mb": 4048,
|
||||
"ansible_swaptotal_mb": 4048,
|
||||
"ansible_system": "Linux",
|
||||
"ansible_system_vendor": "innotek GmbH",
|
||||
"ansible_uptime_seconds": 178398,
|
||||
"ansible_user_dir": "/root",
|
||||
"ansible_user_gecos": "root",
|
||||
"ansible_user_gid": 0,
|
||||
"ansible_user_id": "root",
|
||||
"ansible_user_shell": "/bin/bash",
|
||||
"ansible_user_uid": 0,
|
||||
"ansible_userspace_architecture": "x86_64",
|
||||
"ansible_userspace_bits": "64",
|
||||
"ansible_virtualization_role": "guest",
|
||||
"ansible_virtualization_type": "docker",
|
||||
"module_setup": true
|
||||
}
|
||||
255
awx/main/tests/functional/api/test_fact_versions.py
Normal file
255
awx/main/tests/functional/api/test_fact_versions.py
Normal file
@ -0,0 +1,255 @@
|
||||
# Python
|
||||
import mock
|
||||
import pytest
|
||||
from datetime import timedelta
|
||||
import urlparse
|
||||
import urllib
|
||||
|
||||
# AWX
|
||||
from awx.main.models.fact import Fact
|
||||
from awx.main.utils import timestamp_apiformat
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
def mock_feature_enabled(feature, bypass_database=None):
|
||||
return True
|
||||
|
||||
def mock_feature_disabled(feature, bypass_database=None):
|
||||
return False
|
||||
|
||||
def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params={}, host_count=1):
|
||||
hosts = hosts(host_count=host_count)
|
||||
fact_scans(fact_scans=3, timestamp_epoch=epoch)
|
||||
|
||||
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True), data=get_params)
|
||||
|
||||
return (hosts[0], response)
|
||||
|
||||
def check_url(url1_full, fact_known, module):
|
||||
url1_split = urlparse.urlsplit(url1_full)
|
||||
url1 = url1_split.path
|
||||
url1_params = urlparse.parse_qsl(url1_split.query)
|
||||
|
||||
url2 = reverse('api:host_fact_compare_view', args=(fact_known.host.pk,))
|
||||
url2_params = [('module', module), ('datetime', timestamp_apiformat(fact_known.timestamp))]
|
||||
|
||||
assert url1 == url2
|
||||
assert urllib.urlencode(url1_params) == urllib.urlencode(url2_params)
|
||||
|
||||
def check_response_facts(facts_known, response):
|
||||
for i, fact_known in enumerate(facts_known):
|
||||
assert fact_known.module == response.data['results'][i]['module']
|
||||
assert timestamp_apiformat(fact_known.timestamp) == response.data['results'][i]['timestamp']
|
||||
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
|
||||
|
||||
def check_system_tracking_feature_forbidden(response):
|
||||
assert 402 == response.status_code
|
||||
assert 'Your license does not permit use of system tracking.' == response.data['detail']
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.license_feature
|
||||
def test_system_tracking_license_get(hosts, get, user):
|
||||
hosts = hosts(host_count=1)
|
||||
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
check_system_tracking_feature_forbidden(response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.license_feature
|
||||
def test_system_tracking_license_options(hosts, options, user):
|
||||
hosts = hosts(host_count=1)
|
||||
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
|
||||
response = options(url, None, user('admin', True))
|
||||
|
||||
check_system_tracking_feature_forbidden(response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.license_feature
|
||||
def test_no_facts_db(hosts, get, user):
|
||||
hosts = hosts(host_count=1)
|
||||
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
response_expected = {
|
||||
'results': []
|
||||
}
|
||||
assert response_expected == response.data
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_basic_fields(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
search = {
|
||||
'from': epoch,
|
||||
'to': epoch,
|
||||
}
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, get_params=search)
|
||||
|
||||
results = response.data['results']
|
||||
assert 'related' in results[0]
|
||||
assert 'timestamp' in results[0]
|
||||
assert 'module' in results[0]
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.license_feature
|
||||
def test_basic_options_fields(hosts, fact_scans, options, user):
|
||||
hosts = hosts(host_count=1)
|
||||
fact_scans(fact_scans=1)
|
||||
|
||||
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
|
||||
response = options(url, None, user('admin', True), pk=hosts[0].id)
|
||||
|
||||
assert 'related' in response.data['actions']['GET']
|
||||
assert 'module' in response.data['actions']['GET']
|
||||
assert ("ansible", "Ansible") in response.data['actions']['GET']['module']['choices']
|
||||
assert ("services", "Services") in response.data['actions']['GET']['module']['choices']
|
||||
assert ("packages", "Packages") in response.data['actions']['GET']['module']['choices']
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_related_fact_view(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch)
|
||||
facts_known = Fact.get_timeline(host.id)
|
||||
assert 9 == len(facts_known)
|
||||
assert 9 == len(response.data['results'])
|
||||
|
||||
for i, fact_known in enumerate(facts_known):
|
||||
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_multiple_hosts(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, host_count=3)
|
||||
facts_known = Fact.get_timeline(host.id)
|
||||
assert 9 == len(facts_known)
|
||||
assert 9 == len(response.data['results'])
|
||||
|
||||
for i, fact_known in enumerate(facts_known):
|
||||
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_param_to_from(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
search = {
|
||||
'from': epoch - timedelta(days=10),
|
||||
'to': epoch + timedelta(days=10),
|
||||
}
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, get_params=search)
|
||||
facts_known = Fact.get_timeline(host.id, ts_from=search['from'], ts_to=search['to'])
|
||||
assert 9 == len(facts_known)
|
||||
assert 9 == len(response.data['results'])
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_param_module(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
search = {
|
||||
'module': 'packages',
|
||||
}
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, get_params=search)
|
||||
facts_known = Fact.get_timeline(host.id, module=search['module'])
|
||||
assert 3 == len(facts_known)
|
||||
assert 3 == len(response.data['results'])
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_param_from(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
search = {
|
||||
'from': epoch + timedelta(days=1),
|
||||
}
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, get_params=search)
|
||||
facts_known = Fact.get_timeline(host.id, ts_from=search['from'])
|
||||
assert 3 == len(facts_known)
|
||||
assert 3 == len(response.data['results'])
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_param_to(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
search = {
|
||||
'to': epoch + timedelta(days=1),
|
||||
}
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, get_params=search)
|
||||
facts_known = Fact.get_timeline(host.id, ts_to=search['to'])
|
||||
assert 6 == len(facts_known)
|
||||
assert 6 == len(response.data['results'])
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
|
||||
hosts = hosts(host_count=1)
|
||||
fact_scans(fact_scans=1)
|
||||
|
||||
team_obj.users.add(user_obj)
|
||||
|
||||
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
|
||||
response = get(url, user_obj)
|
||||
return response
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_normal_user_403(hosts, fact_scans, get, user, team):
|
||||
user_bob = user('bob', False)
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_bob, team)
|
||||
|
||||
assert 403 == response.status_code
|
||||
assert "You do not have permission to perform this action." == response.data['detail']
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_super_user_ok(hosts, fact_scans, get, user, team):
|
||||
user_super = user('bob', True)
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_super, team)
|
||||
|
||||
assert 200 == response.status_code
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
organization.admins.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
assert 200 == response.status_code
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
org2 = organizations(1)
|
||||
org2[0].admins.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
assert 403 == response.status_code
|
||||
|
||||
182
awx/main/tests/functional/api/test_fact_view.py
Normal file
182
awx/main/tests/functional/api/test_fact_view.py
Normal file
@ -0,0 +1,182 @@
|
||||
import mock
|
||||
import pytest
|
||||
import json
|
||||
|
||||
from awx.main.utils import timestamp_apiformat
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
def mock_feature_enabled(feature, bypass_database=None):
|
||||
return True
|
||||
|
||||
def mock_feature_disabled(feature, bypass_database=None):
|
||||
return False
|
||||
|
||||
# TODO: Consider making the fact_scan() fixture a Class, instead of a function, and move this method into it
|
||||
def find_fact(facts, host_id, module_name, timestamp):
|
||||
for f in facts:
|
||||
if f.host_id == host_id and f.module == module_name and f.timestamp == timestamp:
|
||||
return f
|
||||
raise RuntimeError('fact <%s, %s, %s> not found in %s', (host_id, module_name, timestamp, facts))
|
||||
|
||||
def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), module_name='ansible', get_params={}):
|
||||
hosts = hosts(host_count=1)
|
||||
facts = fact_scans(fact_scans=1, timestamp_epoch=epoch)
|
||||
|
||||
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True), data=get_params)
|
||||
|
||||
fact_known = find_fact(facts, hosts[0].id, module_name, epoch)
|
||||
return (fact_known, response)
|
||||
|
||||
def check_system_tracking_feature_forbidden(response):
|
||||
assert 402 == response.status_code
|
||||
assert 'Your license does not permit use of system tracking.' == response.data['detail']
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.license_feature
|
||||
def test_system_tracking_license_get(hosts, get, user):
|
||||
hosts = hosts(host_count=1)
|
||||
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
check_system_tracking_feature_forbidden(response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.license_feature
|
||||
def test_system_tracking_license_options(hosts, options, user):
|
||||
hosts = hosts(host_count=1)
|
||||
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
|
||||
response = options(url, None, user('admin', True))
|
||||
|
||||
check_system_tracking_feature_forbidden(response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_no_fact_found(hosts, get, user):
|
||||
hosts = hosts(host_count=1)
|
||||
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
expected_response = {
|
||||
"detail": "Fact not found"
|
||||
}
|
||||
assert 404 == response.status_code
|
||||
assert expected_response == response.data
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_basic_fields(hosts, fact_scans, get, user):
|
||||
hosts = hosts(host_count=1)
|
||||
fact_scans(fact_scans=1)
|
||||
|
||||
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert 'related' in response.data
|
||||
assert 'id' in response.data
|
||||
assert 'facts' in response.data
|
||||
assert 'module' in response.data
|
||||
assert 'host' in response.data
|
||||
assert isinstance(response.data['host'], int)
|
||||
assert 'summary_fields' in response.data
|
||||
assert 'host' in response.data['summary_fields']
|
||||
assert 'name' in response.data['summary_fields']['host']
|
||||
assert 'description' in response.data['summary_fields']['host']
|
||||
assert 'host' in response.data['related']
|
||||
assert reverse('api:host_detail', args=(hosts[0].pk,)) == response.data['related']['host']
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_content(hosts, fact_scans, get, user, fact_ansible_json):
|
||||
(fact_known, response) = setup_common(hosts, fact_scans, get, user)
|
||||
|
||||
assert fact_known.host_id == response.data['host']
|
||||
assert fact_ansible_json == json.loads(response.data['facts'])
|
||||
assert timestamp_apiformat(fact_known.timestamp) == response.data['timestamp']
|
||||
assert fact_known.module == response.data['module']
|
||||
|
||||
def _test_search_by_module(hosts, fact_scans, get, user, fact_json, module_name):
|
||||
params = {
|
||||
'module': module_name
|
||||
}
|
||||
(fact_known, response) = setup_common(hosts, fact_scans, get, user, module_name=module_name, get_params=params)
|
||||
|
||||
assert fact_json == json.loads(response.data['facts'])
|
||||
assert timestamp_apiformat(fact_known.timestamp) == response.data['timestamp']
|
||||
assert module_name == response.data['module']
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_search_by_module_packages(hosts, fact_scans, get, user, fact_packages_json):
|
||||
_test_search_by_module(hosts, fact_scans, get, user, fact_packages_json, 'packages')
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_search_by_module_services(hosts, fact_scans, get, user, fact_services_json):
|
||||
_test_search_by_module(hosts, fact_scans, get, user, fact_services_json, 'services')
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_search_by_timestamp_and_module(hosts, fact_scans, get, user, fact_packages_json):
|
||||
epoch = timezone.now()
|
||||
module_name = 'packages'
|
||||
|
||||
(fact_known, response) = setup_common(hosts, fact_scans, get, user, module_name=module_name, epoch=epoch, get_params=dict(module=module_name, datetime=epoch))
|
||||
|
||||
assert fact_known.id == response.data['id']
|
||||
|
||||
def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
|
||||
hosts = hosts(host_count=1)
|
||||
fact_scans(fact_scans=1)
|
||||
|
||||
team_obj.users.add(user_obj)
|
||||
|
||||
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
|
||||
response = get(url, user_obj)
|
||||
return response
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_normal_user_403(hosts, fact_scans, get, user, team):
|
||||
user_bob = user('bob', False)
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_bob, team)
|
||||
|
||||
assert 403 == response.status_code
|
||||
assert "You do not have permission to perform this action." == response.data['detail']
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_super_user_ok(hosts, fact_scans, get, user, team):
|
||||
user_super = user('bob', True)
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_super, team)
|
||||
|
||||
assert 200 == response.status_code
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
organization.admins.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
assert 200 == response.status_code
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.ac
|
||||
@pytest.mark.django_db
|
||||
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
org2 = organizations(1)
|
||||
org2[0].admins.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
assert 403 == response.status_code
|
||||
|
||||
17
awx/main/tests/functional/api/test_host_detail.py
Normal file
17
awx/main/tests/functional/api/test_host_detail.py
Normal file
@ -0,0 +1,17 @@
|
||||
# TODO: As of writing this our only concern is ensuring that the fact feature is reflected in the Host endpoint.
|
||||
# Other host tests should live here to make this test suite more complete.
|
||||
import pytest
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_basic_fields(hosts, fact_scans, get, user):
|
||||
hosts = hosts(host_count=1)
|
||||
|
||||
url = reverse('api:host_detail', args=(hosts[0].pk,))
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert 'related' in response.data
|
||||
assert 'fact_versions' in response.data['related']
|
||||
assert reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) == response.data['related']['fact_versions']
|
||||
|
||||
0
awx/main/tests/functional/commands/__init__.py
Normal file
0
awx/main/tests/functional/commands/__init__.py
Normal file
109
awx/main/tests/functional/commands/conftest.py
Normal file
109
awx/main/tests/functional/commands/conftest.py
Normal file
@ -0,0 +1,109 @@
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@pytest.fixture
|
||||
def fact_msg_base(inventory, hosts):
|
||||
host_objs = hosts(1)
|
||||
return {
|
||||
'host': host_objs[0].name,
|
||||
'date_key': time.mktime(datetime.utcnow().timetuple()),
|
||||
'facts' : { },
|
||||
'inventory_id': inventory.id
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def fact_msg_small(fact_msg_base):
|
||||
fact_msg_base['facts'] = {
|
||||
'packages': {
|
||||
"accountsservice": [
|
||||
{
|
||||
"architecture": "amd64",
|
||||
"name": "accountsservice",
|
||||
"source": "apt",
|
||||
"version": "0.6.35-0ubuntu7.1"
|
||||
}
|
||||
],
|
||||
"acpid": [
|
||||
{
|
||||
"architecture": "amd64",
|
||||
"name": "acpid",
|
||||
"source": "apt",
|
||||
"version": "1:2.0.21-1ubuntu2"
|
||||
}
|
||||
],
|
||||
"adduser": [
|
||||
{
|
||||
"architecture": "all",
|
||||
"name": "adduser",
|
||||
"source": "apt",
|
||||
"version": "3.113+nmu3ubuntu3"
|
||||
}
|
||||
],
|
||||
},
|
||||
'services': [
|
||||
{
|
||||
"name": "acpid",
|
||||
"source": "sysv",
|
||||
"state": "running"
|
||||
},
|
||||
{
|
||||
"name": "apparmor",
|
||||
"source": "sysv",
|
||||
"state": "stopped"
|
||||
},
|
||||
{
|
||||
"name": "atd",
|
||||
"source": "sysv",
|
||||
"state": "running"
|
||||
},
|
||||
{
|
||||
"name": "cron",
|
||||
"source": "sysv",
|
||||
"state": "running"
|
||||
}
|
||||
],
|
||||
'ansible': {
|
||||
'ansible_fact_simple': 'hello world',
|
||||
'ansible_fact_complex': {
|
||||
'foo': 'bar',
|
||||
'hello': [
|
||||
'scooby',
|
||||
'dooby',
|
||||
'doo'
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
return fact_msg_base
|
||||
|
||||
|
||||
'''
|
||||
Facts sent from ansible to our fact cache reciever.
|
||||
The fact module type is implicit i.e
|
||||
|
||||
Note: The 'ansible' module is an expection to this rule.
|
||||
It is NOT nested in a dict, and thus does NOT contain a first-level
|
||||
key of 'ansible'
|
||||
|
||||
{
|
||||
'fact_module_name': { ... },
|
||||
}
|
||||
'''
|
||||
|
||||
@pytest.fixture
|
||||
def fact_msg_ansible(fact_msg_base, fact_ansible_json):
|
||||
fact_msg_base['facts'] = fact_ansible_json
|
||||
return fact_msg_base
|
||||
|
||||
@pytest.fixture
|
||||
def fact_msg_packages(fact_msg_base, fact_packages_json):
|
||||
fact_msg_base['facts']['packages'] = fact_packages_json
|
||||
return fact_msg_base
|
||||
|
||||
@pytest.fixture
|
||||
def fact_msg_services(fact_msg_base, fact_services_json):
|
||||
fact_msg_base['facts']['services'] = fact_services_json
|
||||
return fact_msg_base
|
||||
|
||||
200
awx/main/tests/functional/commands/test_cleanup_facts.py
Normal file
200
awx/main/tests/functional/commands/test_cleanup_facts.py
Normal file
@ -0,0 +1,200 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
import mock
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import timedelta
|
||||
|
||||
# Django
|
||||
from django.utils import timezone
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
# AWX
|
||||
from awx.main.management.commands.cleanup_facts import CleanupFacts, Command
|
||||
from awx.main.models.fact import Fact
|
||||
from awx.main.models.inventory import Host
|
||||
|
||||
def mock_feature_enabled(feature, bypass_database=None):
|
||||
return True
|
||||
|
||||
def mock_feature_disabled(feature, bypass_database=None):
|
||||
return False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cleanup_granularity(fact_scans, hosts):
|
||||
epoch = timezone.now()
|
||||
hosts(5)
|
||||
fact_scans(10, timestamp_epoch=epoch)
|
||||
fact_newest = Fact.objects.all().order_by('-timestamp').first()
|
||||
timestamp_future = fact_newest.timestamp + timedelta(days=365)
|
||||
granularity = relativedelta(days=2)
|
||||
|
||||
cleanup_facts = CleanupFacts()
|
||||
deleted_count = cleanup_facts.cleanup(timestamp_future, granularity)
|
||||
assert 60 == deleted_count
|
||||
|
||||
'''
|
||||
Delete half of the scans
|
||||
'''
|
||||
@pytest.mark.django_db
|
||||
def test_cleanup_older_than(fact_scans, hosts):
|
||||
epoch = timezone.now()
|
||||
hosts(5)
|
||||
fact_scans(28, timestamp_epoch=epoch)
|
||||
qs = Fact.objects.all().order_by('-timestamp')
|
||||
fact_middle = qs[qs.count() / 2]
|
||||
granularity = relativedelta()
|
||||
|
||||
cleanup_facts = CleanupFacts()
|
||||
deleted_count = cleanup_facts.cleanup(fact_middle.timestamp, granularity)
|
||||
assert 210 == deleted_count
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cleanup_older_than_granularity_module(fact_scans, hosts):
|
||||
epoch = timezone.now()
|
||||
hosts(5)
|
||||
fact_scans(10, timestamp_epoch=epoch)
|
||||
fact_newest = Fact.objects.all().order_by('-timestamp').first()
|
||||
timestamp_future = fact_newest.timestamp + timedelta(days=365)
|
||||
granularity = relativedelta(days=2)
|
||||
|
||||
cleanup_facts = CleanupFacts()
|
||||
deleted_count = cleanup_facts.cleanup(timestamp_future, granularity, module='ansible')
|
||||
assert 20 == deleted_count
|
||||
|
||||
|
||||
'''
|
||||
Reduce the granularity of half of the facts scans, by half.
|
||||
'''
|
||||
@pytest.mark.django_db
|
||||
def test_cleanup_logic(fact_scans, hosts):
|
||||
epoch = timezone.now()
|
||||
hosts = hosts(5)
|
||||
fact_scans(60, timestamp_epoch=epoch)
|
||||
timestamp_middle = epoch + timedelta(days=30)
|
||||
granularity = relativedelta(days=2)
|
||||
module = 'ansible'
|
||||
|
||||
cleanup_facts = CleanupFacts()
|
||||
cleanup_facts.cleanup(timestamp_middle, granularity, module=module)
|
||||
|
||||
|
||||
host_ids = Host.objects.all().values_list('id', flat=True)
|
||||
host_facts = {}
|
||||
for host_id in host_ids:
|
||||
facts = Fact.objects.filter(host__id=host_id, module=module, timestamp__lt=timestamp_middle).order_by('-timestamp')
|
||||
host_facts[host_id] = facts
|
||||
|
||||
for host_id, facts in host_facts.iteritems():
|
||||
assert 15 == len(facts)
|
||||
|
||||
timestamp_pivot = timestamp_middle
|
||||
for fact in facts:
|
||||
timestamp_pivot -= granularity
|
||||
assert fact.timestamp == timestamp_pivot
|
||||
|
||||
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_disabled)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.license_feature
|
||||
def test_system_tracking_feature_disabled(mocker):
|
||||
cmd = Command()
|
||||
with pytest.raises(CommandError) as err:
|
||||
cmd.handle(None)
|
||||
assert 'The System Tracking feature is not enabled for your Tower instance' in err.value
|
||||
|
||||
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_parameters_ok(mocker):
|
||||
run = mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
|
||||
kv = {
|
||||
'older_than': '1d',
|
||||
'granularity': '1d',
|
||||
'module': None,
|
||||
}
|
||||
cmd = Command()
|
||||
cmd.handle(None, **kv)
|
||||
run.assert_called_once_with(relativedelta(days=1), relativedelta(days=1), module=None)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_string_time_to_timestamp_ok():
|
||||
kvs = [
|
||||
{
|
||||
'time': '2w',
|
||||
'timestamp': relativedelta(weeks=2),
|
||||
'msg': '2 weeks',
|
||||
},
|
||||
{
|
||||
'time': '23d',
|
||||
'timestamp': relativedelta(days=23),
|
||||
'msg': '23 days',
|
||||
},
|
||||
{
|
||||
'time': '11m',
|
||||
'timestamp': relativedelta(months=11),
|
||||
'msg': '11 months',
|
||||
},
|
||||
{
|
||||
'time': '14y',
|
||||
'timestamp': relativedelta(years=14),
|
||||
'msg': '14 years',
|
||||
},
|
||||
]
|
||||
for kv in kvs:
|
||||
cmd = Command()
|
||||
res = cmd.string_time_to_timestamp(kv['time'])
|
||||
assert kv['timestamp'] == res
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_string_time_to_timestamp_invalid():
|
||||
kvs = [
|
||||
{
|
||||
'time': '2weeks',
|
||||
'msg': 'weeks instead of w',
|
||||
},
|
||||
{
|
||||
'time': '2days',
|
||||
'msg': 'days instead of d',
|
||||
},
|
||||
{
|
||||
'time': '23',
|
||||
'msg': 'no unit specified',
|
||||
},
|
||||
{
|
||||
'time': None,
|
||||
'msg': 'no value specified',
|
||||
},
|
||||
{
|
||||
'time': 'zigzag',
|
||||
'msg': 'random string specified',
|
||||
},
|
||||
]
|
||||
for kv in kvs:
|
||||
cmd = Command()
|
||||
res = cmd.string_time_to_timestamp(kv['time'])
|
||||
assert res is None
|
||||
|
||||
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_parameters_fail(mocker):
|
||||
# Mock run() just in case, but it should never get called because an error should be thrown
|
||||
mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
|
||||
kvs = [
|
||||
{
|
||||
'older_than': '1week',
|
||||
'granularity': '1d',
|
||||
'msg': '--older_than invalid value "1week"',
|
||||
},
|
||||
{
|
||||
'older_than': '1d',
|
||||
'granularity': '1year',
|
||||
'msg': '--granularity invalid value "1year"',
|
||||
}
|
||||
]
|
||||
for kv in kvs:
|
||||
cmd = Command()
|
||||
with pytest.raises(CommandError) as err:
|
||||
cmd.handle(None, older_than=kv['older_than'], granularity=kv['granularity'])
|
||||
assert kv['msg'] in err.value
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
# Django
|
||||
|
||||
# AWX
|
||||
from awx.main.management.commands.run_fact_cache_receiver import FactCacheReceiver
|
||||
from awx.main.models.fact import Fact
|
||||
from awx.main.models.inventory import Host
|
||||
|
||||
# TODO: Check that timestamp and other attributes are as expected
|
||||
def check_process_fact_message_module(fact_returned, data, module_name):
|
||||
date_key = data['date_key']
|
||||
|
||||
# Ensure 1, and only 1, fact created
|
||||
timestamp = datetime.fromtimestamp(date_key, None)
|
||||
assert 1 == Fact.objects.all().count()
|
||||
|
||||
host_obj = Host.objects.get(name=data['host'], inventory__id=data['inventory_id'])
|
||||
assert host_obj is not None
|
||||
fact_known = Fact.get_host_fact(host_obj.id, module_name, timestamp)
|
||||
assert fact_known is not None
|
||||
assert fact_known == fact_returned
|
||||
|
||||
assert host_obj == fact_returned.host
|
||||
if module_name == 'ansible':
|
||||
assert data['facts'] == fact_returned.facts
|
||||
else:
|
||||
assert data['facts'][module_name] == fact_returned.facts
|
||||
assert timestamp == fact_returned.timestamp
|
||||
assert module_name == fact_returned.module
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_process_fact_message_ansible(fact_msg_ansible):
|
||||
receiver = FactCacheReceiver()
|
||||
fact_returned = receiver.process_fact_message(fact_msg_ansible)
|
||||
|
||||
check_process_fact_message_module(fact_returned, fact_msg_ansible, 'ansible')
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_process_fact_message_packages(fact_msg_packages):
|
||||
receiver = FactCacheReceiver()
|
||||
fact_returned = receiver.process_fact_message(fact_msg_packages)
|
||||
|
||||
check_process_fact_message_module(fact_returned, fact_msg_packages, 'packages')
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_process_fact_message_services(fact_msg_services):
|
||||
receiver = FactCacheReceiver()
|
||||
fact_returned = receiver.process_fact_message(fact_msg_services)
|
||||
|
||||
check_process_fact_message_module(fact_returned, fact_msg_services, 'services')
|
||||
|
||||
'''
|
||||
We pickypack our fact sending onto the Ansible fact interface.
|
||||
The interface is <hostname, facts>. Where facts is a json blob of all the facts.
|
||||
This makes it hard to decipher what facts are new/changed.
|
||||
Because of this, we handle the same fact module data being sent multiple times
|
||||
and just keep the newest version.
|
||||
'''
|
||||
@pytest.mark.django_db
|
||||
def test_process_facts_message_ansible_overwrite(fact_scans, fact_msg_ansible):
|
||||
#epoch = timezone.now()
|
||||
epoch = datetime.fromtimestamp(fact_msg_ansible['date_key'])
|
||||
fact_scans(fact_scans=1, timestamp_epoch=epoch)
|
||||
key = 'ansible.overwrite'
|
||||
value = 'hello world'
|
||||
|
||||
receiver = FactCacheReceiver()
|
||||
receiver.process_fact_message(fact_msg_ansible)
|
||||
|
||||
fact_msg_ansible['facts'][key] = value
|
||||
fact_returned = receiver.process_fact_message(fact_msg_ansible)
|
||||
|
||||
fact_obj = Fact.objects.get(id=fact_returned.id)
|
||||
assert key in fact_obj.facts
|
||||
assert json.loads(fact_obj.facts) == fact_msg_ansible['facts']
|
||||
assert value == json.loads(fact_obj.facts)[key]
|
||||
|
||||
# Ensure that the message flows from the socket through to process_fact_message()
|
||||
@pytest.mark.django_db
|
||||
def test_run_receiver(mocker, fact_msg_ansible):
|
||||
mocker.patch("awx.main.socket.Socket.listen", return_value=[fact_msg_ansible])
|
||||
|
||||
receiver = FactCacheReceiver()
|
||||
mocker.patch.object(receiver, 'process_fact_message', return_value=None)
|
||||
|
||||
receiver.run_receiver(use_processing_threads=False)
|
||||
|
||||
receiver.process_fact_message.assert_called_once_with(fact_msg_ansible)
|
||||
@ -1,8 +1,23 @@
|
||||
import pytest
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
import mock
|
||||
import json
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import resolve
|
||||
from django.utils.six.moves.urllib.parse import urlparse
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
|
||||
# AWX
|
||||
from awx.main.models.projects import Project
|
||||
from awx.main.models.base import PERM_INVENTORY_READ
|
||||
from awx.main.models.ha import Instance
|
||||
from awx.main.models.fact import Fact
|
||||
|
||||
from rest_framework.test import (
|
||||
APIRequestFactory,
|
||||
@ -10,20 +25,34 @@ from rest_framework.test import (
|
||||
)
|
||||
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.models.projects import Project
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models.ha import Instance
|
||||
from awx.main.models.inventory import (
|
||||
Inventory,
|
||||
Group,
|
||||
)
|
||||
from awx.main.models.organization import (
|
||||
Organization,
|
||||
Team,
|
||||
Permission,
|
||||
)
|
||||
|
||||
from awx.main.models.rbac import Role
|
||||
|
||||
'''
|
||||
Disable all django model signals.
|
||||
'''
|
||||
@pytest.fixture(scope="session", autouse=False)
|
||||
def disable_signals():
|
||||
mocked = mock.patch('django.dispatch.Signal.send', autospec=True)
|
||||
mocked.start()
|
||||
|
||||
'''
|
||||
FIXME: Not sure how "far" just setting the BROKER_URL will get us.
|
||||
We may need to incluence CELERY's configuration like we do in the old unit tests (see base.py)
|
||||
|
||||
Allows django signal code to execute without the need for redis
|
||||
'''
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def celery_memory_broker():
|
||||
settings.BROKER_URL='memory://localhost/'
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
@ -60,11 +89,15 @@ def deploy_jobtemplate(project, inventory, credential):
|
||||
|
||||
@pytest.fixture
|
||||
def team(organization):
|
||||
return Team.objects.create(organization=organization, name='test-team')
|
||||
return organization.teams.create(name='test-team')
|
||||
|
||||
@pytest.fixture
|
||||
def project(organization):
|
||||
prj = Project.objects.create(name="test-project", description="test-project-desc")
|
||||
@mock.patch.object(Project, "update", lambda self, **kwargs: None)
|
||||
def project(instance, organization):
|
||||
prj = Project.objects.create(name="test-proj",
|
||||
description="test-proj-desc",
|
||||
scm_type="git",
|
||||
scm_url="https://github.com/jlaska/ansible-playbooks")
|
||||
prj.organizations.add(organization)
|
||||
return prj
|
||||
|
||||
@ -87,7 +120,7 @@ def credential():
|
||||
|
||||
@pytest.fixture
|
||||
def inventory(organization):
|
||||
return Inventory.objects.create(name="test-inventory", organization=organization)
|
||||
return organization.inventories.create(name="test-inv")
|
||||
|
||||
@pytest.fixture
|
||||
def role():
|
||||
@ -105,12 +138,43 @@ def alice(user):
|
||||
def bob(user):
|
||||
return user('bob', False)
|
||||
|
||||
@pytest.fixture
|
||||
def organizations(instance):
|
||||
def rf(organization_count=1):
|
||||
orgs = []
|
||||
for i in xrange(0, organization_count):
|
||||
o = Organization.objects.create(name="test-org-%d" % i, description="test-org-desc")
|
||||
orgs.append(o)
|
||||
return orgs
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
def group(inventory):
|
||||
def g(name):
|
||||
return Group.objects.create(inventory=inventory, name=name)
|
||||
try:
|
||||
return Group.objects.get(name=name, inventory=inventory)
|
||||
except:
|
||||
return Group.objects.create(inventory=inventory, name=name)
|
||||
return g
|
||||
|
||||
@pytest.fixture
|
||||
def hosts(group):
|
||||
group1 = group('group-1')
|
||||
|
||||
def rf(host_count=1):
|
||||
hosts = []
|
||||
for i in xrange(0, host_count):
|
||||
name = '%s-host-%s' % (group1.name, i)
|
||||
(host, created) = group1.inventory.hosts.get_or_create(name=name)
|
||||
if created:
|
||||
group1.hosts.add(host)
|
||||
hosts.append(host)
|
||||
return hosts
|
||||
return rf
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def permissions():
|
||||
return {
|
||||
@ -244,7 +308,48 @@ def options():
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def celery_memory_broker():
|
||||
from django.conf import settings
|
||||
settings.BROKER_URL='memory://localhost/'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fact_scans(group, fact_ansible_json, fact_packages_json, fact_services_json):
|
||||
group1 = group('group-1')
|
||||
|
||||
def rf(fact_scans=1, timestamp_epoch=timezone.now()):
|
||||
facts_json = {}
|
||||
facts = []
|
||||
module_names = ['ansible', 'services', 'packages']
|
||||
timestamp_current = timestamp_epoch
|
||||
|
||||
facts_json['ansible'] = fact_ansible_json
|
||||
facts_json['packages'] = fact_packages_json
|
||||
facts_json['services'] = fact_services_json
|
||||
|
||||
for i in xrange(0, fact_scans):
|
||||
for host in group1.hosts.all():
|
||||
for module_name in module_names:
|
||||
facts.append(Fact.objects.create(host=host, timestamp=timestamp_current, module=module_name, facts=facts_json[module_name]))
|
||||
timestamp_current += timedelta(days=1)
|
||||
return facts
|
||||
return rf
|
||||
|
||||
def _fact_json(module_name):
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
with open('%s/%s.json' % (current_dir, module_name)) as f:
|
||||
return json.load(f)
|
||||
|
||||
@pytest.fixture
|
||||
def fact_ansible_json():
|
||||
return _fact_json('ansible')
|
||||
|
||||
@pytest.fixture
|
||||
def fact_packages_json():
|
||||
return _fact_json('packages')
|
||||
|
||||
@pytest.fixture
|
||||
def fact_services_json():
|
||||
return _fact_json('services')
|
||||
|
||||
@pytest.fixture
|
||||
def permission_inv_read(organization, inventory, team):
|
||||
return Permission.objects.create(inventory=inventory, team=team, permission_type=PERM_INVENTORY_READ)
|
||||
|
||||
|
||||
111
awx/main/tests/functional/models/fact/test_get_host_fact.py
Normal file
111
awx/main/tests/functional/models/fact/test_get_host_fact.py
Normal file
@ -0,0 +1,111 @@
|
||||
import pytest
|
||||
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from awx.main.models import Fact
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_newest_scan_exact(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
hosts = hosts(host_count=2)
|
||||
facts = fact_scans(fact_scans=3, timestamp_epoch=epoch)
|
||||
|
||||
fact_known = None
|
||||
for f in facts:
|
||||
if f.host_id == hosts[0].id and f.module == 'ansible' and f.timestamp == epoch:
|
||||
fact_known = f
|
||||
break
|
||||
fact_found = Fact.get_host_fact(hosts[0].id, 'ansible', epoch)
|
||||
|
||||
assert fact_found == fact_known
|
||||
|
||||
'''
|
||||
Show me the most recent state of the sytem at any point of time.
|
||||
or, said differently
|
||||
For any timestamp, get the first scan that is <= the timestamp.
|
||||
'''
|
||||
|
||||
'''
|
||||
Ensure most recent scan run is the scan returned.
|
||||
Query by future date.
|
||||
'''
|
||||
@pytest.mark.django_db
|
||||
def test_newest_scan_less_than(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
timestamp_future = epoch + timedelta(days=10)
|
||||
hosts = hosts(host_count=2)
|
||||
facts = fact_scans(fact_scans=3, timestamp_epoch=epoch)
|
||||
|
||||
fact_known = None
|
||||
for f in facts:
|
||||
if f.host_id == hosts[0].id and f.module == 'ansible' and f.timestamp == epoch + timedelta(days=2):
|
||||
fact_known = f
|
||||
break
|
||||
assert fact_known is not None
|
||||
|
||||
fact_found = Fact.get_host_fact(hosts[0].id, 'ansible', timestamp_future)
|
||||
|
||||
assert fact_found == fact_known
|
||||
|
||||
'''
|
||||
Tests query Fact that is in the middle of the fact scan timeline, but not an exact timestamp.
|
||||
'''
|
||||
@pytest.mark.django_db
|
||||
def test_query_middle_of_timeline(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
timestamp_middle = epoch + timedelta(days=1, hours=3)
|
||||
hosts = hosts(host_count=2)
|
||||
facts = fact_scans(fact_scans=3, timestamp_epoch=epoch)
|
||||
|
||||
fact_known = None
|
||||
for f in facts:
|
||||
if f.host_id == hosts[0].id and f.module == 'ansible' and f.timestamp == epoch + timedelta(days=1):
|
||||
fact_known = f
|
||||
break
|
||||
assert fact_known is not None
|
||||
|
||||
fact_found = Fact.get_host_fact(hosts[0].id, 'ansible', timestamp_middle)
|
||||
|
||||
assert fact_found == fact_known
|
||||
|
||||
'''
|
||||
Query time less than any fact scan. Should return None
|
||||
'''
|
||||
@pytest.mark.django_db
|
||||
def test_query_result_empty(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
timestamp_less = epoch - timedelta(days=1)
|
||||
hosts = hosts(host_count=2)
|
||||
fact_scans(fact_scans=3, timestamp_epoch=epoch)
|
||||
|
||||
fact_found = Fact.get_host_fact(hosts[0].id, 'ansible', timestamp_less)
|
||||
|
||||
assert fact_found is None
|
||||
|
||||
'''
|
||||
Query by fact module other than 'ansible'
|
||||
'''
|
||||
@pytest.mark.django_db
|
||||
def test_by_module(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
hosts = hosts(host_count=2)
|
||||
facts = fact_scans(fact_scans=3, timestamp_epoch=epoch)
|
||||
|
||||
fact_known_services = None
|
||||
fact_known_packages = None
|
||||
for f in facts:
|
||||
if f.host_id == hosts[0].id:
|
||||
if f.module == 'services' and f.timestamp == epoch:
|
||||
fact_known_services = f
|
||||
elif f.module == 'packages' and f.timestamp == epoch:
|
||||
fact_known_packages = f
|
||||
assert fact_known_services is not None
|
||||
assert fact_known_packages is not None
|
||||
|
||||
fact_found_services = Fact.get_host_fact(hosts[0].id, 'services', epoch)
|
||||
fact_found_packages = Fact.get_host_fact(hosts[0].id, 'packages', epoch)
|
||||
|
||||
assert fact_found_services == fact_known_services
|
||||
assert fact_found_packages == fact_known_packages
|
||||
|
||||
129
awx/main/tests/functional/models/fact/test_get_timeline.py
Normal file
129
awx/main/tests/functional/models/fact/test_get_timeline.py
Normal file
@ -0,0 +1,129 @@
|
||||
import pytest
|
||||
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from awx.main.models import Fact
|
||||
|
||||
def setup_common(hosts, fact_scans, ts_from=None, ts_to=None, epoch=timezone.now(), module_name='ansible', ts_known=None):
|
||||
hosts = hosts(host_count=2)
|
||||
facts = fact_scans(fact_scans=3, timestamp_epoch=epoch)
|
||||
|
||||
facts_known = []
|
||||
for f in facts:
|
||||
if f.host.id == hosts[0].id:
|
||||
if module_name and f.module != module_name:
|
||||
continue
|
||||
if ts_known and f.timestamp != ts_known:
|
||||
continue
|
||||
facts_known.append(f)
|
||||
fact_objs = Fact.get_timeline(hosts[0].id, module=module_name, ts_from=ts_from, ts_to=ts_to)
|
||||
return (facts_known, fact_objs)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_all(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
ts_from = epoch - timedelta(days=1)
|
||||
ts_to = epoch + timedelta(days=10)
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from, ts_to, module_name=None, epoch=epoch)
|
||||
assert 9 == len(facts_known)
|
||||
assert 9 == len(fact_objs)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_all_ansible(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
ts_from = epoch - timedelta(days=1)
|
||||
ts_to = epoch + timedelta(days=10)
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from, ts_to, epoch=epoch)
|
||||
assert 3 == len(facts_known)
|
||||
assert 3 == len(fact_objs)
|
||||
|
||||
for i in xrange(len(facts_known) - 1, 0):
|
||||
assert facts_known[i].id == fact_objs[i].id
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_empty_db(hosts, fact_scans):
|
||||
hosts = hosts(host_count=2)
|
||||
epoch = timezone.now()
|
||||
ts_from = epoch - timedelta(days=1)
|
||||
ts_to = epoch + timedelta(days=10)
|
||||
|
||||
fact_objs = Fact.get_timeline(hosts[0].id, 'ansible', ts_from, ts_to)
|
||||
|
||||
assert 0 == len(fact_objs)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_no_results(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
ts_from = epoch - timedelta(days=100)
|
||||
ts_to = epoch - timedelta(days=50)
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from, ts_to, epoch=epoch)
|
||||
assert 0 == len(fact_objs)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_exact_same_equal(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
ts_to = ts_from = epoch + timedelta(days=1)
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from, ts_to, ts_known=ts_to, epoch=epoch)
|
||||
assert 1 == len(facts_known)
|
||||
assert 1 == len(fact_objs)
|
||||
|
||||
assert facts_known[0].id == fact_objs[0].id
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_exact_from_exclusive_to_inclusive(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
ts_from = epoch + timedelta(days=1)
|
||||
ts_to = epoch + timedelta(days=2)
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from, ts_to, ts_known=ts_to, epoch=epoch)
|
||||
|
||||
assert 1 == len(facts_known)
|
||||
assert 1 == len(fact_objs)
|
||||
|
||||
assert facts_known[0].id == fact_objs[0].id
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_to_lte(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
ts_to = epoch + timedelta(days=1)
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from=None, ts_to=ts_to, epoch=epoch)
|
||||
facts_known_subset = filter(lambda x: x.timestamp <= ts_to, facts_known)
|
||||
|
||||
assert 2 == len(facts_known_subset)
|
||||
assert 2 == len(fact_objs)
|
||||
|
||||
for i in xrange(0, len(fact_objs)):
|
||||
assert facts_known_subset[len(facts_known_subset) - i - 1].id == fact_objs[i].id
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_from_gt(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
ts_from = epoch
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from=ts_from, ts_to=None, epoch=epoch)
|
||||
facts_known_subset = filter(lambda x: x.timestamp > ts_from, facts_known)
|
||||
|
||||
assert 2 == len(facts_known_subset)
|
||||
assert 2 == len(fact_objs)
|
||||
|
||||
for i in xrange(0, len(fact_objs)):
|
||||
assert facts_known_subset[len(facts_known_subset) - i - 1].id == fact_objs[i].id
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_no_ts(hosts, fact_scans):
|
||||
epoch = timezone.now()
|
||||
|
||||
(facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from=None, ts_to=None, epoch=epoch)
|
||||
assert 3 == len(facts_known)
|
||||
assert 3 == len(fact_objs)
|
||||
|
||||
for i in xrange(len(facts_known) - 1, 0):
|
||||
assert facts_known[i].id == fact_objs[i].id
|
||||
|
||||
|
||||
2922
awx/main/tests/functional/packages.json
Normal file
2922
awx/main/tests/functional/packages.json
Normal file
File diff suppressed because it is too large
Load Diff
697
awx/main/tests/functional/services.json
Normal file
697
awx/main/tests/functional/services.json
Normal file
@ -0,0 +1,697 @@
|
||||
[
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "iprdump"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "iprinit"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "iprupdate"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "stopped",
|
||||
"name": "netconsole"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "network"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "arp-ethers.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "auditd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "autovt@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "avahi-daemon.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "blk-availability.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "brandbot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "console-getty.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "console-shell.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "cpupower.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "crond.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.fedoraproject.FirewallD1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.freedesktop.Avahi.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.hostname1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.locale1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.login1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.machine1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.freedesktop.NetworkManager.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.freedesktop.nm-dispatcher.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.timedate1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "debug-shell.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dhcpd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dhcpd6.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dhcrelay.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dm-event.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dnsmasq.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-cmdline.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-initqueue.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-mount.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-mount.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-pivot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-trigger.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-udev.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-shutdown.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "ebtables.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "emergency.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "firewalld.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "getty@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "halt-local.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-cleanup.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-parse-etc.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-switch-root.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-udevadm-cleanup-db.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "irqbalance.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "kdump.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "kmod-static-nodes.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "lvm2-lvmetad.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "lvm2-monitor.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "lvm2-pvscan@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "messagebus.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "microcode.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "named-setup-rndc.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "named.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "NetworkManager-dispatcher.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "NetworkManager-wait-online.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "NetworkManager.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "ntpd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "ntpdate.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "openvpn@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-halt.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-kexec.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-poweroff.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-quit-wait.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-quit.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-read-write.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-reboot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-start.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-switch-root.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "polkit.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "postfix.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "quotaon.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rc-local.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rdisc.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rescue.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-autorelabel-mark.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-autorelabel.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-configure.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-dmesg.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-domainname.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-import-state.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-loadmodules.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-readonly.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "rsyslog.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "serial-getty@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "sshd-keygen.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "sshd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "sshd@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-ask-password-console.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-ask-password-plymouth.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-ask-password-wall.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-backlight@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-binfmt.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-fsck-root.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-fsck@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-halt.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-hibernate.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-hostnamed.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-hybrid-sleep.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-initctl.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-journal-flush.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-journald.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-kexec.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-localed.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-logind.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-machined.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-modules-load.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-nspawn@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-poweroff.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-quotacheck.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-random-seed.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "systemd-readahead-collect.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-readahead-done.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "systemd-readahead-drop.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "systemd-readahead-replay.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-reboot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-remount-fs.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-shutdownd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-suspend.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-sysctl.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-timedated.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-tmpfiles-clean.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-tmpfiles-setup-dev.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-tmpfiles-setup.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-udev-settle.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-udev-trigger.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-udevd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-update-utmp-runlevel.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-update-utmp.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-user-sessions.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-vconsole-setup.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "teamd@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "tuned.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "vmtoolsd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "wpa_supplicant.service"
|
||||
}
|
||||
]
|
||||
115
awx/main/tests/functional/test_notifications.py
Normal file
115
awx/main/tests/functional/test_notifications.py
Normal file
@ -0,0 +1,115 @@
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from awx.main.models.notifications import Notifier
|
||||
from awx.main.models.inventory import Inventory, Group
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
@pytest.fixture
|
||||
def notifier():
|
||||
return Notifier.objects.create(name="test-notification",
|
||||
notification_type="webhook",
|
||||
notification_configuration=dict(url="http://localhost",
|
||||
headers={"Test": "Header"}))
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_notifier_list(get, user, notifier):
|
||||
url = reverse('api:notifier_list')
|
||||
response = get(url, user('admin', True))
|
||||
assert response.status_code == 200
|
||||
assert len(response.data['results']) == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_basic_parameterization(get, post, user, organization):
|
||||
u = user('admin-poster', True)
|
||||
url = reverse('api:notifier_list')
|
||||
response = post(url,
|
||||
dict(name="test-webhook",
|
||||
description="test webhook",
|
||||
organization=1,
|
||||
notification_type="webhook",
|
||||
notification_configuration=dict(url="http://localhost",
|
||||
headers={"Test": "Header"})),
|
||||
u)
|
||||
assert response.status_code == 201
|
||||
url = reverse('api:notifier_detail', args=(response.data['id'],))
|
||||
response = get(url, u)
|
||||
assert 'related' in response.data
|
||||
assert 'organization' in response.data['related']
|
||||
assert 'summary_fields' in response.data
|
||||
assert 'organization' in response.data['summary_fields']
|
||||
assert 'notifications' in response.data['related']
|
||||
assert 'notification_configuration' in response.data
|
||||
assert 'url' in response.data['notification_configuration']
|
||||
assert 'headers' in response.data['notification_configuration']
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_encrypted_subfields(get, post, user, organization):
|
||||
def assert_send(self, messages):
|
||||
assert self.account_token == "shouldhide"
|
||||
return 1
|
||||
u = user('admin-poster', True)
|
||||
url = reverse('api:notifier_list')
|
||||
response = post(url,
|
||||
dict(name="test-twilio",
|
||||
description="test twilio",
|
||||
organization=organization.id,
|
||||
notification_type="twilio",
|
||||
notification_configuration=dict(account_sid="dummy",
|
||||
account_token="shouldhide",
|
||||
from_number="+19999999999",
|
||||
to_numbers=["9998887777"])),
|
||||
u)
|
||||
assert response.status_code == 201
|
||||
notifier_actual = Notifier.objects.get(id=response.data['id'])
|
||||
url = reverse('api:notifier_detail', args=(response.data['id'],))
|
||||
response = get(url, u)
|
||||
assert response.data['notification_configuration']['account_token'] == "$encrypted$"
|
||||
with mock.patch.object(notifier_actual.notification_class, "send_messages", assert_send):
|
||||
notifier_actual.send("Test", {'body': "Test"})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inherited_notifiers(get, post, user, organization, project):
|
||||
u = user('admin-poster', True)
|
||||
url = reverse('api:notifier_list')
|
||||
notifiers = []
|
||||
for nfiers in xrange(3):
|
||||
response = post(url,
|
||||
dict(name="test-webhook-{}".format(nfiers),
|
||||
description="test webhook {}".format(nfiers),
|
||||
organization=1,
|
||||
notification_type="webhook",
|
||||
notification_configuration=dict(url="http://localhost",
|
||||
headers={"Test": "Header"})),
|
||||
u)
|
||||
assert response.status_code == 201
|
||||
notifiers.append(response.data['id'])
|
||||
organization.projects.add(project)
|
||||
i = Inventory.objects.create(name='test', organization=organization)
|
||||
i.save()
|
||||
g = Group.objects.create(name='test', inventory=i)
|
||||
g.save()
|
||||
jt = JobTemplate.objects.create(name='test', inventory=i, project=project, playbook='debug.yml')
|
||||
jt.save()
|
||||
url = reverse('api:organization_notifiers_any_list', args=(organization.id,))
|
||||
response = post(url, dict(id=notifiers[0]), u)
|
||||
assert response.status_code == 204
|
||||
url = reverse('api:project_notifiers_any_list', args=(project.id,))
|
||||
response = post(url, dict(id=notifiers[1]), u)
|
||||
assert response.status_code == 204
|
||||
url = reverse('api:job_template_notifiers_any_list', args=(jt.id,))
|
||||
response = post(url, dict(id=notifiers[2]), u)
|
||||
assert response.status_code == 204
|
||||
assert len(jt.notifiers['any']) == 3
|
||||
assert len(project.notifiers['any']) == 2
|
||||
assert len(g.inventory_source.notifiers['any']) == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notifier_merging(get, post, user, organization, project, notifier):
|
||||
user('admin-poster', True)
|
||||
organization.projects.add(project)
|
||||
organization.notifiers_any.add(notifier)
|
||||
project.notifiers_any.add(notifier)
|
||||
assert len(project.notifiers['any']) == 1
|
||||
@ -2,7 +2,7 @@ import mock # noqa
|
||||
import pytest
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from awx.main.models.rbac import Role
|
||||
from awx.main.models.rbac import Role, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
|
||||
|
||||
def mock_feature_enabled(feature, bypass_database=None):
|
||||
return True
|
||||
@ -24,39 +24,55 @@ def test_get_roles_list_admin(organization, get, admin):
|
||||
assert roles['count'] > 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Unimplemented')
|
||||
def test_get_roles_list_user(organization, get, user):
|
||||
def test_get_roles_list_user(organization, inventory, team, get, user):
|
||||
'Users can see all roles they have access to, but not all roles'
|
||||
assert False
|
||||
this_user = user('user-test_get_roles_list_user')
|
||||
organization.member_role.members.add(this_user)
|
||||
custom_role = Role.objects.create(name='custom_role-test_get_roles_list_user')
|
||||
organization.member_role.children.add(custom_role)
|
||||
|
||||
url = reverse('api:role_list')
|
||||
response = get(url, this_user)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0
|
||||
assert roles['count'] == len(roles['results']) # just to make sure the tests below are valid
|
||||
|
||||
role_hash = {}
|
||||
|
||||
for r in roles['results']:
|
||||
role_hash[r['id']] = r
|
||||
|
||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id in role_hash
|
||||
assert organization.admin_role.id in role_hash
|
||||
assert organization.member_role.id in role_hash
|
||||
assert this_user.resource.admin_role.id in role_hash
|
||||
assert custom_role.id in role_hash
|
||||
|
||||
assert inventory.admin_role.id not in role_hash
|
||||
assert team.member_role.id not in role_hash
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_create_role(post, admin):
|
||||
'Admins can create new roles'
|
||||
#u = user('admin', True)
|
||||
def test_cant_create_role(post, admin):
|
||||
"Ensure we can't create new roles through the api"
|
||||
# Some day we might want to do this, but until that is speced out, lets
|
||||
# ensure we don't slip up and allow this implicitly through some helper or
|
||||
# another
|
||||
response = post(reverse('api:role_list'), {'name': 'New Role'}, admin)
|
||||
assert response.status_code == 201
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_delete_role(post, admin):
|
||||
'Admins can delete a custom role'
|
||||
assert False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_user_create_role(organization, get, user):
|
||||
'User can create custom roles'
|
||||
assert False
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_user_delete_role(organization, get, user):
|
||||
'User can delete their custom roles, but not any old row'
|
||||
assert False
|
||||
def test_cant_delete_role(delete, admin):
|
||||
"Ensure we can't delete roles through the api"
|
||||
# Some day we might want to do this, but until that is speced out, lets
|
||||
# ensure we don't slip up and allow this implicitly through some helper or
|
||||
# another
|
||||
response = delete(reverse('api:role_detail', args=(admin.resource.admin_role.id,)), admin)
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
|
||||
@ -72,6 +88,53 @@ def test_get_user_roles_list(get, admin):
|
||||
roles = response.data
|
||||
assert roles['count'] > 0 # 'System Administrator' role if nothing else
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob):
|
||||
'Users can see roles for other users, but only the roles that that user has access to see as well'
|
||||
organization.member_role.members.add(alice)
|
||||
organization.admins.add(bob)
|
||||
custom_role = Role.objects.create(name='custom_role-test_user_view_admin_roles_list')
|
||||
organization.member_role.children.add(custom_role)
|
||||
team.users.add(bob)
|
||||
|
||||
# alice and bob are in the same org and can see some child role of that org.
|
||||
# Bob is an org admin, alice can see this.
|
||||
# Bob is in a team that alice is not, alice cannot see that bob is a member of that team.
|
||||
|
||||
url = reverse('api:user_roles_list', args=(bob.id,))
|
||||
response = get(url, alice)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0
|
||||
assert roles['count'] == len(roles['results']) # just to make sure the tests below are valid
|
||||
|
||||
role_hash = {}
|
||||
for r in roles['results']:
|
||||
role_hash[r['id']] = r['name']
|
||||
|
||||
assert organization.admin_role.id in role_hash
|
||||
assert custom_role.id not in role_hash # doesn't show up in the user roles list, not an explicit grant
|
||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id not in role_hash
|
||||
assert inventory.admin_role.id not in role_hash
|
||||
assert team.member_role.id not in role_hash # alice can't see this
|
||||
|
||||
# again but this time alice is part of the team, and should be able to see the team role
|
||||
team.users.add(alice)
|
||||
response = get(url, alice)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0
|
||||
assert roles['count'] == len(roles['results']) # just to make sure the tests below are valid
|
||||
|
||||
role_hash = {}
|
||||
for r in roles['results']:
|
||||
role_hash[r['id']] = r['name']
|
||||
|
||||
assert team.member_role.id in role_hash # Alice can now see this
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_role_to_user(role, post, admin):
|
||||
assert admin.roles.filter(id=role.id).count() == 0
|
||||
@ -165,15 +228,15 @@ def test_get_role(get, admin, role):
|
||||
def test_put_role(put, admin, role):
|
||||
url = reverse('api:role_detail', args=(role.id,))
|
||||
response = put(url, {'name': 'Some new name'}, admin)
|
||||
assert response.status_code == 200
|
||||
r = Role.objects.get(id=role.id)
|
||||
assert r.name == 'Some new name'
|
||||
assert response.status_code == 405
|
||||
#r = Role.objects.get(id=role.id)
|
||||
#assert r.name == 'Some new name'
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_put_role_access_denied(put, alice, admin, role):
|
||||
url = reverse('api:role_detail', args=(role.id,))
|
||||
response = put(url, {'name': 'Some new name'}, alice)
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 403 or response.status_code == 405
|
||||
|
||||
|
||||
#
|
||||
@ -204,6 +267,67 @@ def test_remove_user_to_role(post, admin, role):
|
||||
post(url, {'disassociate': True, 'id': admin.id}, admin)
|
||||
assert role.members.filter(id=admin.id).count() == 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user with permissions to assign/revoke membership to a particular role can do so'
|
||||
org_admin = user('org-admin')
|
||||
joe = user('joe')
|
||||
organization.admins.add(org_admin)
|
||||
|
||||
assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, org_admin)
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user with permissions to assign/revoke membership to a particular role can do so'
|
||||
org_admin = user('org-admin')
|
||||
joe = user('joe')
|
||||
organization.admins.add(org_admin)
|
||||
check_jobtemplate.executor_role.members.add(joe)
|
||||
|
||||
assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin)
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so'
|
||||
rando = user('rando')
|
||||
joe = user('joe')
|
||||
|
||||
assert check_jobtemplate.accessible_by(rando, {'write': True}) is False
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, rando)
|
||||
assert res.status_code == 403
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so'
|
||||
rando = user('rando')
|
||||
joe = user('joe')
|
||||
check_jobtemplate.executor_role.members.add(joe)
|
||||
|
||||
assert check_jobtemplate.accessible_by(rando, {'write': True}) is False
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, rando)
|
||||
assert res.status_code == 403
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
|
||||
#
|
||||
# /roles/<id>/teams/
|
||||
#
|
||||
@ -252,22 +376,6 @@ def test_role_parents(get, team, admin, role):
|
||||
assert response.data['count'] == 1
|
||||
assert response.data['results'][0]['id'] == team.member_role.id
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_role_add_parent(post, team, admin, role):
|
||||
assert role.parents.count() == 0
|
||||
url = reverse('api:role_parents_list', args=(role.id,))
|
||||
post(url, {'id': team.member_role.id}, admin)
|
||||
assert role.parents.count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_role_remove_parent(post, team, admin, role):
|
||||
role.parents.add(team.member_role)
|
||||
assert role.parents.count() == 1
|
||||
url = reverse('api:role_parents_list', args=(role.id,))
|
||||
post(url, {'disassociate': True, 'id': team.member_role.id}, admin)
|
||||
assert role.parents.count() == 0
|
||||
|
||||
#
|
||||
# /roles/<id>/children/
|
||||
@ -282,22 +390,6 @@ def test_role_children(get, team, admin, role):
|
||||
assert response.data['count'] == 1
|
||||
assert response.data['results'][0]['id'] == role.id
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_role_add_children(post, team, admin, role):
|
||||
assert role.children.count() == 0
|
||||
url = reverse('api:role_children_list', args=(role.id,))
|
||||
post(url, {'id': team.member_role.id}, admin)
|
||||
assert role.children.count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
|
||||
def test_role_remove_children(post, team, admin, role):
|
||||
role.children.add(team.member_role)
|
||||
assert role.children.count() == 1
|
||||
url = reverse('api:role_children_list', args=(role.id,))
|
||||
post(url, {'disassociate': True, 'id': team.member_role.id}, admin)
|
||||
assert role.children.count() == 0
|
||||
|
||||
|
||||
|
||||
|
||||
@ -138,3 +138,32 @@ def test_content_object(user):
|
||||
assert org.resource.content_object.id == org.id
|
||||
assert org.admin_role.content_object.id == org.id
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_hierarchy_rebuilding():
|
||||
'Tests some subdtle cases around role hierarchy rebuilding'
|
||||
|
||||
X = Role.objects.create(name='X')
|
||||
A = Role.objects.create(name='A')
|
||||
B = Role.objects.create(name='B')
|
||||
C = Role.objects.create(name='C')
|
||||
D = Role.objects.create(name='D')
|
||||
|
||||
A.children.add(B)
|
||||
A.children.add(D)
|
||||
B.children.add(C)
|
||||
C.children.add(D)
|
||||
|
||||
assert A.is_ancestor_of(D)
|
||||
assert X.is_ancestor_of(D) is False
|
||||
|
||||
X.children.add(A)
|
||||
|
||||
assert X.is_ancestor_of(D) is True
|
||||
|
||||
X.children.remove(A)
|
||||
|
||||
# This can be the stickler, the rebuilder needs to ensure that D's role
|
||||
# hierarchy is built after both A and C are updated.
|
||||
assert X.is_ancestor_of(D) is False
|
||||
|
||||
|
||||
|
||||
@ -128,8 +128,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest):
|
||||
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||
self.assertTrue(ad_hoc_command.signal_start())
|
||||
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||
self.check_job_result(ad_hoc_command, 'failed')
|
||||
self.check_ad_hoc_command_events(ad_hoc_command, 'unreachable')
|
||||
self.check_job_result(ad_hoc_command, 'successful')
|
||||
self.check_ad_hoc_command_events(ad_hoc_command, 'skipped')
|
||||
|
||||
@mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('canceled', 0))
|
||||
def test_cancel_ad_hoc_command(self, ignore):
|
||||
|
||||
@ -1,238 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import mock
|
||||
|
||||
#Django
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
# AWX
|
||||
from awx.main.tests.base import BaseTest
|
||||
from awx.fact.tests.base import MongoDBRequired, FactScanBuilder, TEST_FACT_PACKAGES, TEST_FACT_ANSIBLE, TEST_FACT_SERVICES
|
||||
from command_base import BaseCommandMixin
|
||||
from awx.main.management.commands.cleanup_facts import Command, CleanupFacts
|
||||
from awx.fact.models.fact import * # noqa
|
||||
|
||||
__all__ = ['CommandTest','CleanupFactsUnitTest', 'CleanupFactsCommandFunctionalTest']
|
||||
|
||||
class CleanupFactsCommandFunctionalTest(BaseCommandMixin, BaseTest, MongoDBRequired):
|
||||
def setUp(self):
|
||||
super(CleanupFactsCommandFunctionalTest, self).setUp()
|
||||
self.create_test_license_file()
|
||||
self.builder = FactScanBuilder()
|
||||
self.builder.add_fact('ansible', TEST_FACT_ANSIBLE)
|
||||
|
||||
def test_invoke_zero_ok(self):
|
||||
self.builder.set_epoch(datetime(year=2015, day=2, month=1, microsecond=0))
|
||||
self.builder.build(scan_count=20, host_count=10)
|
||||
|
||||
result, stdout, stderr = self.run_command('cleanup_facts', granularity='2y', older_than='1d')
|
||||
self.assertEqual(stdout, 'Deleted %s facts.\n' % ((200 / 2)))
|
||||
|
||||
def test_invoke_zero_deleted(self):
|
||||
result, stdout, stderr = self.run_command('cleanup_facts', granularity='1w',older_than='5d')
|
||||
self.assertEqual(stdout, 'Deleted 0 facts.\n')
|
||||
|
||||
def test_invoke_all_deleted(self):
|
||||
self.builder.build(scan_count=20, host_count=10)
|
||||
|
||||
result, stdout, stderr = self.run_command('cleanup_facts', granularity='0d', older_than='0d')
|
||||
self.assertEqual(stdout, 'Deleted 200 facts.\n')
|
||||
|
||||
def test_invoke_params_required(self):
|
||||
result, stdout, stderr = self.run_command('cleanup_facts')
|
||||
self.assertIsInstance(result, CommandError)
|
||||
self.assertEqual(str(result), 'Both --granularity and --older_than are required.')
|
||||
|
||||
def test_module(self):
|
||||
self.builder.add_fact('packages', TEST_FACT_PACKAGES)
|
||||
self.builder.add_fact('services', TEST_FACT_SERVICES)
|
||||
self.builder.build(scan_count=5, host_count=5)
|
||||
|
||||
result, stdout, stderr = self.run_command('cleanup_facts', granularity='0d', older_than='0d', module='packages')
|
||||
self.assertEqual(stdout, 'Deleted 25 facts.\n')
|
||||
|
||||
class CommandTest(BaseTest):
|
||||
def setUp(self):
|
||||
super(CommandTest, self).setUp()
|
||||
self.create_test_license_file()
|
||||
|
||||
@mock.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
|
||||
def test_parameters_ok(self, run):
|
||||
|
||||
kv = {
|
||||
'older_than': '1d',
|
||||
'granularity': '1d',
|
||||
'module': None,
|
||||
}
|
||||
cmd = Command()
|
||||
cmd.handle(None, **kv)
|
||||
run.assert_called_once_with(relativedelta(days=1), relativedelta(days=1), module=None)
|
||||
|
||||
def test_string_time_to_timestamp_ok(self):
|
||||
kvs = [
|
||||
{
|
||||
'time': '2w',
|
||||
'timestamp': relativedelta(weeks=2),
|
||||
'msg': '2 weeks',
|
||||
},
|
||||
{
|
||||
'time': '23d',
|
||||
'timestamp': relativedelta(days=23),
|
||||
'msg': '23 days',
|
||||
},
|
||||
{
|
||||
'time': '11m',
|
||||
'timestamp': relativedelta(months=11),
|
||||
'msg': '11 months',
|
||||
},
|
||||
{
|
||||
'time': '14y',
|
||||
'timestamp': relativedelta(years=14),
|
||||
'msg': '14 years',
|
||||
},
|
||||
]
|
||||
for kv in kvs:
|
||||
cmd = Command()
|
||||
res = cmd.string_time_to_timestamp(kv['time'])
|
||||
self.assertEqual(kv['timestamp'], res, "%s should convert to %s" % (kv['time'], kv['msg']))
|
||||
|
||||
def test_string_time_to_timestamp_invalid(self):
|
||||
kvs = [
|
||||
{
|
||||
'time': '2weeks',
|
||||
'msg': 'weeks instead of w',
|
||||
},
|
||||
{
|
||||
'time': '2days',
|
||||
'msg': 'days instead of d',
|
||||
},
|
||||
{
|
||||
'time': '23',
|
||||
'msg': 'no unit specified',
|
||||
},
|
||||
{
|
||||
'time': None,
|
||||
'msg': 'no value specified',
|
||||
},
|
||||
{
|
||||
'time': 'zigzag',
|
||||
'msg': 'random string specified',
|
||||
},
|
||||
]
|
||||
for kv in kvs:
|
||||
cmd = Command()
|
||||
res = cmd.string_time_to_timestamp(kv['time'])
|
||||
self.assertIsNone(res, kv['msg'])
|
||||
|
||||
# Mock run() just in case, but it should never get called because an error should be thrown
|
||||
@mock.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
|
||||
def test_parameters_fail(self, run):
|
||||
kvs = [
|
||||
{
|
||||
'older_than': '1week',
|
||||
'granularity': '1d',
|
||||
'msg': 'Invalid older_than param value',
|
||||
},
|
||||
{
|
||||
'older_than': '1d',
|
||||
'granularity': '1year',
|
||||
'msg': 'Invalid granularity param value',
|
||||
}
|
||||
]
|
||||
for kv in kvs:
|
||||
cmd = Command()
|
||||
with self.assertRaises(CommandError):
|
||||
cmd.handle(None, older_than=kv['older_than'], granularity=kv['granularity'])
|
||||
|
||||
class CleanupFactsUnitTest(BaseCommandMixin, BaseTest, MongoDBRequired):
|
||||
def setUp(self):
|
||||
super(CleanupFactsUnitTest, self).setUp()
|
||||
|
||||
self.builder = FactScanBuilder()
|
||||
self.builder.add_fact('ansible', TEST_FACT_ANSIBLE)
|
||||
self.builder.add_fact('packages', TEST_FACT_PACKAGES)
|
||||
self.builder.build(scan_count=20, host_count=10)
|
||||
|
||||
'''
|
||||
Create 10 hosts with 40 facts each. After cleanup, there should be 20 facts for each host.
|
||||
Then ensure the correct facts are deleted.
|
||||
'''
|
||||
def test_cleanup_logic(self):
|
||||
cleanup_facts = CleanupFacts()
|
||||
fact_oldest = FactVersion.objects.all().order_by('timestamp').first()
|
||||
granularity = relativedelta(years=2)
|
||||
|
||||
deleted_count = cleanup_facts.cleanup(self.builder.get_timestamp(0), granularity)
|
||||
self.assertEqual(deleted_count, 2 * (self.builder.get_scan_count() * self.builder.get_host_count()) / 2)
|
||||
|
||||
# Check the number of facts per host
|
||||
for host in self.builder.get_hosts():
|
||||
count = FactVersion.objects.filter(host=host).count()
|
||||
scan_count = (2 * self.builder.get_scan_count()) / 2
|
||||
self.assertEqual(count, scan_count)
|
||||
|
||||
count = Fact.objects.filter(host=host).count()
|
||||
self.assertEqual(count, scan_count)
|
||||
|
||||
# Ensure that only 2 facts (ansible and packages) exists per granularity time
|
||||
date_pivot = self.builder.get_timestamp(0)
|
||||
for host in self.builder.get_hosts():
|
||||
while date_pivot > fact_oldest.timestamp:
|
||||
date_pivot_next = date_pivot - granularity
|
||||
kv = {
|
||||
'timestamp__lte': date_pivot,
|
||||
'timestamp__gt': date_pivot_next,
|
||||
'host': host,
|
||||
}
|
||||
count = FactVersion.objects.filter(**kv).count()
|
||||
self.assertEqual(count, 2, "should only be 2 FactVersion per the 2 year granularity")
|
||||
count = Fact.objects.filter(**kv).count()
|
||||
self.assertEqual(count, 2, "should only be 2 Fact per the 2 year granularity")
|
||||
date_pivot = date_pivot_next
|
||||
|
||||
'''
|
||||
Create 10 hosts with 40 facts each. After cleanup, there should be 30 facts for each host.
|
||||
Then ensure the correct facts are deleted.
|
||||
'''
|
||||
def test_cleanup_module(self):
|
||||
cleanup_facts = CleanupFacts()
|
||||
fact_oldest = FactVersion.objects.all().order_by('timestamp').first()
|
||||
granularity = relativedelta(years=2)
|
||||
|
||||
deleted_count = cleanup_facts.cleanup(self.builder.get_timestamp(0), granularity, module='ansible')
|
||||
self.assertEqual(deleted_count, (self.builder.get_scan_count() * self.builder.get_host_count()) / 2)
|
||||
|
||||
# Check the number of facts per host
|
||||
for host in self.builder.get_hosts():
|
||||
count = FactVersion.objects.filter(host=host).count()
|
||||
self.assertEqual(count, 30)
|
||||
|
||||
count = Fact.objects.filter(host=host).count()
|
||||
self.assertEqual(count, 30)
|
||||
|
||||
# Ensure that only 1 ansible fact exists per granularity time
|
||||
date_pivot = self.builder.get_timestamp(0)
|
||||
for host in self.builder.get_hosts():
|
||||
while date_pivot > fact_oldest.timestamp:
|
||||
date_pivot_next = date_pivot - granularity
|
||||
kv = {
|
||||
'timestamp__lte': date_pivot,
|
||||
'timestamp__gt': date_pivot_next,
|
||||
'host': host,
|
||||
'module': 'ansible',
|
||||
}
|
||||
count = FactVersion.objects.filter(**kv).count()
|
||||
self.assertEqual(count, 1)
|
||||
count = Fact.objects.filter(**kv).count()
|
||||
self.assertEqual(count, 1)
|
||||
date_pivot = date_pivot_next
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,242 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
import unittest2 as unittest
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
# AWX
|
||||
from awx.main.utils import timestamp_apiformat
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.tests.base import BaseLiveServerTest
|
||||
from awx.fact.models import * # noqa
|
||||
from awx.fact.tests.base import BaseFactTestMixin, FactScanBuilder, TEST_FACT_ANSIBLE, TEST_FACT_PACKAGES, TEST_FACT_SERVICES
|
||||
from awx.main.utils import build_url
|
||||
|
||||
__all__ = ['FactVersionApiTest', 'FactViewApiTest', 'SingleFactApiTest',]
|
||||
|
||||
class FactApiBaseTest(BaseLiveServerTest, BaseFactTestMixin):
|
||||
def setUp(self):
|
||||
super(FactApiBaseTest, self).setUp()
|
||||
self.create_test_license_file()
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.organization = self.make_organization(self.super_django_user)
|
||||
self.organization.admins.add(self.normal_django_user)
|
||||
self.inventory = self.organization.inventories.create(name='test-inventory', description='description for test-inventory')
|
||||
self.host = self.inventory.hosts.create(name='host.example.com')
|
||||
self.host2 = self.inventory.hosts.create(name='host2.example.com')
|
||||
self.host3 = self.inventory.hosts.create(name='host3.example.com')
|
||||
|
||||
def setup_facts(self, scan_count):
|
||||
self.builder = FactScanBuilder()
|
||||
self.builder.set_inventory_id(self.inventory.pk)
|
||||
self.builder.add_fact('ansible', TEST_FACT_ANSIBLE)
|
||||
self.builder.add_fact('packages', TEST_FACT_PACKAGES)
|
||||
self.builder.add_fact('services', TEST_FACT_SERVICES)
|
||||
self.builder.add_hostname('host.example.com')
|
||||
self.builder.add_hostname('host2.example.com')
|
||||
self.builder.add_hostname('host3.example.com')
|
||||
self.builder.build(scan_count=scan_count, host_count=3)
|
||||
|
||||
self.fact_host = FactHost.objects.get(hostname=self.host.name)
|
||||
|
||||
class FactVersionApiTest(FactApiBaseTest):
|
||||
def check_equal(self, fact_versions, results):
|
||||
def find(element, set1):
|
||||
for e in set1:
|
||||
if all([ e.get(field) == element.get(field) for field in element.keys()]):
|
||||
return e
|
||||
return None
|
||||
|
||||
self.assertEqual(len(results), len(fact_versions))
|
||||
for v in fact_versions:
|
||||
v_dict = {
|
||||
'timestamp': timestamp_apiformat(v.timestamp),
|
||||
'module': v.module
|
||||
}
|
||||
e = find(v_dict, results)
|
||||
self.assertIsNotNone(e, "%s not found in %s" % (v_dict, results))
|
||||
|
||||
def get_list(self, fact_versions, params=None):
|
||||
url = build_url('api:host_fact_versions_list', args=(self.host.pk,), get=params)
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
|
||||
self.check_equal(fact_versions, response['results'])
|
||||
return response
|
||||
|
||||
def test_permission_list(self):
|
||||
url = reverse('api:host_fact_versions_list', args=(self.host.pk,))
|
||||
with self.current_user('admin'):
|
||||
self.get(url, expect=200)
|
||||
with self.current_user('normal'):
|
||||
self.get(url, expect=200)
|
||||
with self.current_user('other'):
|
||||
self.get(url, expect=403)
|
||||
with self.current_user('nobody'):
|
||||
self.get(url, expect=403)
|
||||
with self.current_user(None):
|
||||
self.get(url, expect=401)
|
||||
|
||||
def test_list_empty(self):
|
||||
url = reverse('api:host_fact_versions_list', args=(self.host.pk,))
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
self.assertIn('results', response)
|
||||
self.assertIsInstance(response['results'], list)
|
||||
self.assertEqual(len(response['results']), 0)
|
||||
|
||||
def test_list_related_fact_view(self):
|
||||
self.setup_facts(2)
|
||||
url = reverse('api:host_fact_versions_list', args=(self.host.pk,))
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
for entry in response['results']:
|
||||
self.assertIn('fact_view', entry['related'])
|
||||
self.get(entry['related']['fact_view'], expect=200)
|
||||
|
||||
def test_list(self):
|
||||
self.setup_facts(2)
|
||||
self.get_list(FactVersion.objects.filter(host=self.fact_host))
|
||||
|
||||
def test_list_module(self):
|
||||
self.setup_facts(10)
|
||||
self.get_list(FactVersion.objects.filter(host=self.fact_host, module='packages'), dict(module='packages'))
|
||||
|
||||
def test_list_time_from(self):
|
||||
self.setup_facts(10)
|
||||
|
||||
params = {
|
||||
'from': timestamp_apiformat(self.builder.get_timestamp(1)),
|
||||
}
|
||||
# 'to': timestamp_apiformat(self.builder.get_timestamp(3))
|
||||
fact_versions = FactVersion.objects.filter(host=self.fact_host, timestamp__gt=params['from'])
|
||||
self.get_list(fact_versions, params)
|
||||
|
||||
def test_list_time_to(self):
|
||||
self.setup_facts(10)
|
||||
|
||||
params = {
|
||||
'to': timestamp_apiformat(self.builder.get_timestamp(3))
|
||||
}
|
||||
fact_versions = FactVersion.objects.filter(host=self.fact_host, timestamp__lte=params['to'])
|
||||
self.get_list(fact_versions, params)
|
||||
|
||||
def test_list_time_from_to(self):
|
||||
self.setup_facts(10)
|
||||
|
||||
params = {
|
||||
'from': timestamp_apiformat(self.builder.get_timestamp(1)),
|
||||
'to': timestamp_apiformat(self.builder.get_timestamp(3))
|
||||
}
|
||||
fact_versions = FactVersion.objects.filter(host=self.fact_host, timestamp__gt=params['from'], timestamp__lte=params['to'])
|
||||
self.get_list(fact_versions, params)
|
||||
|
||||
|
||||
class FactViewApiTest(FactApiBaseTest):
|
||||
def check_equal(self, fact_obj, results):
|
||||
fact_dict = {
|
||||
'timestamp': timestamp_apiformat(fact_obj.timestamp),
|
||||
'module': fact_obj.module,
|
||||
'host': {
|
||||
'hostname': fact_obj.host.hostname,
|
||||
'inventory_id': fact_obj.host.inventory_id,
|
||||
'id': str(fact_obj.host.id)
|
||||
},
|
||||
'fact': fact_obj.fact
|
||||
}
|
||||
self.assertEqual(fact_dict, results)
|
||||
|
||||
def test_permission_view(self):
|
||||
url = reverse('api:host_fact_compare_view', args=(self.host.pk,))
|
||||
with self.current_user('admin'):
|
||||
self.get(url, expect=200)
|
||||
with self.current_user('normal'):
|
||||
self.get(url, expect=200)
|
||||
with self.current_user('other'):
|
||||
self.get(url, expect=403)
|
||||
with self.current_user('nobody'):
|
||||
self.get(url, expect=403)
|
||||
with self.current_user(None):
|
||||
self.get(url, expect=401)
|
||||
|
||||
def get_fact(self, fact_obj, params=None):
|
||||
url = build_url('api:host_fact_compare_view', args=(self.host.pk,), get=params)
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
|
||||
self.check_equal(fact_obj, response)
|
||||
|
||||
def test_view(self):
|
||||
self.setup_facts(2)
|
||||
self.get_fact(Fact.objects.filter(host=self.fact_host, module='ansible').order_by('-timestamp')[0])
|
||||
|
||||
def test_view_module_filter(self):
|
||||
self.setup_facts(2)
|
||||
self.get_fact(Fact.objects.filter(host=self.fact_host, module='services').order_by('-timestamp')[0], dict(module='services'))
|
||||
|
||||
def test_view_time_filter(self):
|
||||
self.setup_facts(6)
|
||||
ts = self.builder.get_timestamp(3)
|
||||
self.get_fact(Fact.objects.filter(host=self.fact_host, module='ansible', timestamp__lte=ts).order_by('-timestamp')[0],
|
||||
dict(datetime=ts))
|
||||
|
||||
|
||||
@unittest.skip("single fact query needs to be updated to use inventory_id attribute on host document")
|
||||
class SingleFactApiTest(FactApiBaseTest):
|
||||
def setUp(self):
|
||||
super(SingleFactApiTest, self).setUp()
|
||||
|
||||
self.group = self.inventory.groups.create(name='test-group')
|
||||
self.group.hosts.add(self.host, self.host2, self.host3)
|
||||
|
||||
def test_permission_list(self):
|
||||
url = reverse('api:host_fact_versions_list', args=(self.host.pk,))
|
||||
with self.current_user('admin'):
|
||||
self.get(url, expect=200)
|
||||
with self.current_user('normal'):
|
||||
self.get(url, expect=200)
|
||||
with self.current_user('other'):
|
||||
self.get(url, expect=403)
|
||||
with self.current_user('nobody'):
|
||||
self.get(url, expect=403)
|
||||
with self.current_user(None):
|
||||
self.get(url, expect=401)
|
||||
|
||||
def _test_related(self, url):
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
self.assertTrue(len(response['results']) > 0)
|
||||
for entry in response['results']:
|
||||
self.assertIn('single_fact', entry['related'])
|
||||
# Requires fields
|
||||
self.get(entry['related']['single_fact'], expect=400)
|
||||
|
||||
def test_related_host_list(self):
|
||||
self.setup_facts(2)
|
||||
self._test_related(reverse('api:host_list'))
|
||||
|
||||
def test_related_group_list(self):
|
||||
self.setup_facts(2)
|
||||
self._test_related(reverse('api:group_list'))
|
||||
|
||||
def test_related_inventory_list(self):
|
||||
self.setup_facts(2)
|
||||
self._test_related(reverse('api:inventory_list'))
|
||||
|
||||
def test_params(self):
|
||||
self.setup_facts(2)
|
||||
params = {
|
||||
'module': 'packages',
|
||||
'fact_key': 'name',
|
||||
'fact_value': 'acpid',
|
||||
}
|
||||
url = build_url('api:inventory_single_fact_view', args=(self.inventory.pk,), get=params)
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
self.assertEqual(len(response['results']), 3)
|
||||
for entry in response['results']:
|
||||
self.assertEqual(entry['fact'][0]['name'], 'acpid')
|
||||
@ -139,12 +139,13 @@ def get_encryption_key(instance, field_name):
|
||||
h.update(field_name)
|
||||
return h.digest()[:16]
|
||||
|
||||
|
||||
def encrypt_field(instance, field_name, ask=False):
|
||||
def encrypt_field(instance, field_name, ask=False, subfield=None):
|
||||
'''
|
||||
Return content of the given instance and field name encrypted.
|
||||
'''
|
||||
value = getattr(instance, field_name)
|
||||
if isinstance(value, dict) and subfield is not None:
|
||||
value = value[subfield]
|
||||
if not value or value.startswith('$encrypted$') or (ask and value == 'ASK'):
|
||||
return value
|
||||
value = smart_str(value)
|
||||
@ -157,11 +158,13 @@ def encrypt_field(instance, field_name, ask=False):
|
||||
return '$encrypted$%s$%s' % ('AES', b64data)
|
||||
|
||||
|
||||
def decrypt_field(instance, field_name):
|
||||
def decrypt_field(instance, field_name, subfield=None):
|
||||
'''
|
||||
Return content of the given instance and field name decrypted.
|
||||
'''
|
||||
value = getattr(instance, field_name)
|
||||
if isinstance(value, dict) and subfield is not None:
|
||||
value = value[subfield]
|
||||
if not value or not value.startswith('$encrypted$'):
|
||||
return value
|
||||
algo, b64data = value[len('$encrypted$'):].split('$', 1)
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
# This file is a utility Ansible plugin that is not part of the AWX or Ansible
|
||||
# packages. It does not import any code from either package, nor does its
|
||||
# license apply to Ansible or AWX.
|
||||
#
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
@ -90,8 +90,12 @@ CENSOR_FIELD_WHITELIST=[
|
||||
'skip_reason',
|
||||
]
|
||||
|
||||
def censor(obj):
|
||||
if obj.get('_ansible_no_log', False):
|
||||
def censor(obj, no_log=False):
|
||||
if not isinstance(obj, dict):
|
||||
if no_log:
|
||||
return "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
||||
return obj
|
||||
if obj.get('_ansible_no_log', no_log):
|
||||
new_obj = {}
|
||||
for k in CENSOR_FIELD_WHITELIST:
|
||||
if k in obj:
|
||||
@ -104,8 +108,12 @@ def censor(obj):
|
||||
new_obj['censored'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
||||
obj = new_obj
|
||||
if 'results' in obj:
|
||||
for i in xrange(len(obj['results'])):
|
||||
obj['results'][i] = censor(obj['results'][i])
|
||||
if isinstance(obj['results'], list):
|
||||
for i in xrange(len(obj['results'])):
|
||||
obj['results'][i] = censor(obj['results'][i], obj.get('_ansible_no_log', no_log))
|
||||
elif obj.get('_ansible_no_log', False):
|
||||
obj['results'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
||||
|
||||
return obj
|
||||
|
||||
class TokenAuth(requests.auth.AuthBase):
|
||||
@ -460,7 +468,7 @@ class JobCallbackModule(BaseCallbackModule):
|
||||
# this from a normal task
|
||||
self._log_event('playbook_on_task_start', task=task,
|
||||
name=task.get_name())
|
||||
|
||||
|
||||
def playbook_on_vars_prompt(self, varname, private=True, prompt=None,
|
||||
encrypt=None, confirm=False, salt_size=None,
|
||||
salt=None, default=None):
|
||||
@ -529,6 +537,7 @@ class AdHocCommandCallbackModule(BaseCallbackModule):
|
||||
def __init__(self):
|
||||
self.ad_hoc_command_id = int(os.getenv('AD_HOC_COMMAND_ID', '0'))
|
||||
self.rest_api_path = '/api/v1/ad_hoc_commands/%d/events/' % self.ad_hoc_command_id
|
||||
self.skipped_hosts = set()
|
||||
super(AdHocCommandCallbackModule, self).__init__()
|
||||
|
||||
def _log_event(self, event, **event_data):
|
||||
@ -539,6 +548,18 @@ class AdHocCommandCallbackModule(BaseCallbackModule):
|
||||
def runner_on_file_diff(self, host, diff):
|
||||
pass # Ignore file diff for ad hoc commands.
|
||||
|
||||
def runner_on_ok(self, host, res):
|
||||
# When running in check mode using a module that does not support check
|
||||
# mode, Ansible v1.9 will call runner_on_skipped followed by
|
||||
# runner_on_ok for the same host; only capture the skipped event and
|
||||
# ignore the ok event.
|
||||
if host not in self.skipped_hosts:
|
||||
super(AdHocCommandCallbackModule, self).runner_on_ok(host, res)
|
||||
|
||||
def runner_on_skipped(self, host, item=None):
|
||||
super(AdHocCommandCallbackModule, self).runner_on_skipped(host, item)
|
||||
self.skipped_hosts.add(host)
|
||||
|
||||
|
||||
if os.getenv('JOB_ID', ''):
|
||||
CallbackModule = JobCallbackModule
|
||||
|
||||
@ -342,6 +342,10 @@ CELERYBEAT_SCHEDULE = {
|
||||
'task': 'awx.main.tasks.tower_periodic_scheduler',
|
||||
'schedule': timedelta(seconds=30)
|
||||
},
|
||||
'admin_checks': {
|
||||
'task': 'awx.main.tasks.run_administrative_checks',
|
||||
'schedule': timedelta(days=30)
|
||||
},
|
||||
}
|
||||
|
||||
# Social Auth configuration.
|
||||
@ -677,6 +681,10 @@ FACT_CACHE_PORT = 6564
|
||||
|
||||
ORG_ADMINS_CAN_SEE_ALL_USERS = True
|
||||
|
||||
TOWER_ADMIN_ALERTS = True
|
||||
|
||||
TOWER_URL_BASE = "https://towerhost"
|
||||
|
||||
TOWER_SETTINGS_MANIFEST = {
|
||||
"SCHEDULE_MAX_JOBS": {
|
||||
"name": "Maximum Scheduled Jobs",
|
||||
@ -804,6 +812,20 @@ TOWER_SETTINGS_MANIFEST = {
|
||||
"type": "bool",
|
||||
"category": "system",
|
||||
},
|
||||
"TOWER_ADMIN_ALERTS": {
|
||||
"name": "Enable Tower Administrator Alerts",
|
||||
"description": "Allow Tower to email Admin users for system events that may require attention",
|
||||
"default": TOWER_ADMIN_ALERTS,
|
||||
"type": "bool",
|
||||
"category": "system",
|
||||
},
|
||||
"TOWER_URL_BASE": {
|
||||
"name": "Base URL of the Tower host",
|
||||
"description": "This is used by services like Notifications to render a valid url to the Tower host",
|
||||
"default": TOWER_URL_BASE,
|
||||
"type": "string",
|
||||
"category": "system",
|
||||
},
|
||||
"LICENSE": {
|
||||
"name": "Tower License",
|
||||
"description": "Controls what features and functionality is enabled in Tower.",
|
||||
|
||||
@ -13,7 +13,6 @@ from split_settings.tools import optional, include
|
||||
# Load default settings.
|
||||
from defaults import * # NOQA
|
||||
|
||||
|
||||
MONGO_HOST = '127.0.0.1'
|
||||
MONGO_PORT = 27017
|
||||
MONGO_USERNAME = None
|
||||
@ -66,10 +65,13 @@ PASSWORD_HASHERS = (
|
||||
# Configure a default UUID for development only.
|
||||
SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
STATSD_CLIENT = 'django_statsd.clients.normal'
|
||||
STATSD_HOST = 'graphite'
|
||||
STATSD_CLIENT = 'django_statsd.clients.null'
|
||||
STATSD_HOST = None
|
||||
STATSD_PREFIX = None
|
||||
#STATSD_CLIENT = 'django_statsd.clients.normal'
|
||||
#STATSD_HOST = 'graphite'
|
||||
STATSD_PORT = 8125
|
||||
STATSD_PREFIX = 'tower'
|
||||
#STATSD_PREFIX = 'tower'
|
||||
STATSD_MAXUDPSIZE = 512
|
||||
|
||||
# If there is an `/etc/tower/settings.py`, include it.
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
|
||||
<div id="about-dialog-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-5 About-cowsay">
|
||||
<div style="width: 340px; margin: 0 auto;">
|
||||
<pre id="cowsay">
|
||||
________________
|
||||
/ Tower Version \
|
||||
\<span id='about-modal-version'></span>/
|
||||
----------------
|
||||
\ ^__^
|
||||
\ (oo)\_______
|
||||
(__)\ A)\/\
|
||||
||----w |
|
||||
|| ||
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-7 text-center">
|
||||
<img id="about-modal-titlelogo" src="/static/assets/ansible_tower_logo_minimalc.png"><br>
|
||||
<p>Copyright 2015. All rights reserved.</p>
|
||||
<p>Ansible and Ansible Tower are registered trademarks of Red Hat, Inc.</p>
|
||||
<br>
|
||||
<img class="About-redhat" src="/static/assets/redhat_ansible_lockup.png">
|
||||
<br>
|
||||
<p>Visit <a href="http://www.ansible.com" target="_blank">Ansible.com</a> for more information.</p>
|
||||
<p><span id='about-modal-subscription'></span></p>
|
||||
</div>
|
||||
</div>
|
||||
@ -99,7 +99,9 @@ a:focus {
|
||||
color: @blue-dark;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn{
|
||||
text-transform: uppercase;
|
||||
}
|
||||
/* Old style TB default button with grey background */
|
||||
.btn-grey {
|
||||
color: #333;
|
||||
@ -917,15 +919,11 @@ input[type="checkbox"].checkbox-no-label {
|
||||
|
||||
/* Display list actions next to search widget */
|
||||
.list-actions {
|
||||
text-align: right;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.fa-lg {
|
||||
vertical-align: -8%;
|
||||
}
|
||||
.fa-lg {
|
||||
vertical-align: -8%;
|
||||
}
|
||||
}
|
||||
|
||||
.jqui-accordion {
|
||||
@ -1950,11 +1948,6 @@ tr td button i {
|
||||
}
|
||||
}
|
||||
|
||||
button.dropdown-toggle,
|
||||
.input-group-btn {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#login-modal-body {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
@ -166,9 +166,6 @@
|
||||
.unreachable-hosts-color {
|
||||
color: @unreachable-hosts-color;
|
||||
}
|
||||
.missing-hosts {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.job_well {
|
||||
padding: 8px;
|
||||
@ -197,9 +194,6 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#job-detail-tables {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#job_options {
|
||||
height: 100px;
|
||||
@ -208,7 +202,6 @@
|
||||
}
|
||||
|
||||
#job_plays, #job_tasks {
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
overflow-x: none;
|
||||
}
|
||||
@ -221,10 +214,7 @@
|
||||
}
|
||||
|
||||
#job-detail-container {
|
||||
position: relative;
|
||||
padding-left: 15px;
|
||||
padding-right: 7px;
|
||||
width: 58.33333333%;
|
||||
|
||||
.well {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -292,9 +282,6 @@
|
||||
.row:first-child {
|
||||
border: none;
|
||||
}
|
||||
.active {
|
||||
background-color: @active-color;
|
||||
}
|
||||
.loading-info {
|
||||
padding-top: 5px;
|
||||
padding-left: 3px;
|
||||
@ -329,10 +316,6 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#tasks-table-detail {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
#play-section {
|
||||
.table-detail {
|
||||
height: 150px;
|
||||
|
||||
@ -32,6 +32,7 @@ table, tbody {
|
||||
background-color: @list-header-bg;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
border-bottom-width:0px!important;
|
||||
}
|
||||
|
||||
.List-tableHeader:first-of-type {
|
||||
@ -69,6 +70,7 @@ table, tbody {
|
||||
.List-tableCell {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
border-top:0px!important;
|
||||
}
|
||||
|
||||
.List-actionButtonCell {
|
||||
@ -141,7 +143,6 @@ table, tbody {
|
||||
|
||||
.List-header {
|
||||
display: flex;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -149,7 +150,7 @@ table, tbody {
|
||||
align-items: center;
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
margin-top: -2px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.List-titleBadge {
|
||||
@ -170,15 +171,22 @@ table, tbody {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.List-actions {
|
||||
.List-actionHolder {
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.List-actions {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.List-auxAction + .List-actions {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.List-auxAction {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@ -186,6 +194,10 @@ table, tbody {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.List-action:not(.ng-hide) ~ .List-action:not(.ng-hide) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.List-buttonSubmit {
|
||||
background-color: @submit-button-bg;
|
||||
color: @submit-button-text;
|
||||
@ -350,3 +362,25 @@ table, tbody {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.List-searchWidget + .List-searchWidget {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.List-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.List-actionHolder {
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1 0 auto;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.List-well {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ body {
|
||||
}
|
||||
|
||||
#content-container {
|
||||
margin-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.group-breadcrumbs {
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
#pre-container {
|
||||
overflow-x: scroll;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +1,42 @@
|
||||
/** @define About */
|
||||
.About {
|
||||
height: 309px !important;
|
||||
}
|
||||
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
||||
|
||||
.About-cowsay {
|
||||
margin-top: 30px;
|
||||
.About-cowsay--container{
|
||||
width: 340px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.About-redhat {
|
||||
max-width: 100%;
|
||||
margin-top: -61px;
|
||||
margin-bottom: -33px;
|
||||
.About-cowsay--code{
|
||||
background-color: @default-bg;
|
||||
padding-left: 30px;
|
||||
border-style: none;
|
||||
max-width: 340px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
.About .modal-header{
|
||||
border: none;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.About .modal-dialog{
|
||||
max-width: 500px;
|
||||
}
|
||||
.About .modal-body{
|
||||
padding-top: 0px;
|
||||
}
|
||||
.About-brand--redhat{
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
margin-top: -50px;
|
||||
margin-bottom: -30px;
|
||||
}
|
||||
.About-brand--ansible{
|
||||
max-width: 120px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.About-close{
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
.About p{
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
31
awx/ui/client/src/about/about.controller.js
Normal file
31
awx/ui/client/src/about/about.controller.js
Normal file
@ -0,0 +1,31 @@
|
||||
export default
|
||||
['$scope', '$state', 'CheckLicense', function($scope, $state, CheckLicense){
|
||||
var processVersion = function(version){
|
||||
// prettify version & calculate padding
|
||||
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
|
||||
var split = version.split('-')[0]
|
||||
var spaces = Math.floor((16-split.length)/2),
|
||||
paddedStr = "";
|
||||
for(var i=0; i<=spaces; i++){
|
||||
paddedStr = paddedStr +" ";
|
||||
}
|
||||
paddedStr = paddedStr + split;
|
||||
for(var j = paddedStr.length; j<16; j++){
|
||||
paddedStr = paddedStr + " ";
|
||||
}
|
||||
return paddedStr
|
||||
}
|
||||
var init = function(){
|
||||
CheckLicense.get()
|
||||
.then(function(res){
|
||||
$scope.subscription = res.data.license_info.subscription_name;
|
||||
$scope.version = processVersion(res.data.version);
|
||||
$('#about-modal').modal('show');
|
||||
});
|
||||
};
|
||||
var back = function(){
|
||||
$state.go('setup');
|
||||
}
|
||||
init();
|
||||
}
|
||||
];
|
||||
32
awx/ui/client/src/about/about.partial.html
Normal file
32
awx/ui/client/src/about/about.partial.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div class="About modal fade" id="about-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img class="About-brand--ansible img-responsive" src="/static/assets/ansible_tower_logo_minimalc.png" />
|
||||
<button type="button" class="close About-close" ng-click="back()">
|
||||
<span class="fa fa-times-circle"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="About-cowsay--container">
|
||||
<!-- Don't indent this properly, you'll break the cow -->
|
||||
<pre class="About-cowsay--code">
|
||||
________________
|
||||
/ Tower Version \\
|
||||
\\<span>{{version}}</span>/
|
||||
----------------
|
||||
\\ ^__^
|
||||
\\ (oo)\\_______
|
||||
(__)\ A)\\/\\
|
||||
||----w |
|
||||
|| ||
|
||||
</pre>
|
||||
</div>
|
||||
<img class="About-brand--redhat img-responsive" src="/static/assets/redhat_ansible_lockup.png" />
|
||||
<p class="text-center">Copyright 2016. All rights reserved.<br>
|
||||
Ansible and Ansible Tower are registered trademarks of <a href="http://www.redhat.com/" target="_blank">Red Hat, Inc</a>.<br>
|
||||
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
|
||||
{{subscription}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
12
awx/ui/client/src/about/about.route.js
Normal file
12
awx/ui/client/src/about/about.route.js
Normal file
@ -0,0 +1,12 @@
|
||||
import {templateUrl} from '../shared/template-url/template-url.factory';
|
||||
import controller from './about.controller';
|
||||
|
||||
export default {
|
||||
name: 'setup.about',
|
||||
route: '/about',
|
||||
controller: controller,
|
||||
ncyBreadcrumb: {
|
||||
label: "ABOUT"
|
||||
},
|
||||
templateUrl: templateUrl('about/about')
|
||||
};
|
||||
15
awx/ui/client/src/about/main.js
Normal file
15
awx/ui/client/src/about/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import controller from './about.controller';
|
||||
import route from './about.route';
|
||||
|
||||
export default
|
||||
angular.module('aboutTower', [])
|
||||
.controller('aboutTower', controller)
|
||||
.run(['$stateExtender', function($stateExtender){
|
||||
$stateExtender.addState(route);
|
||||
}]);
|
||||
@ -15,8 +15,8 @@ export default ['templateUrl', function(templateUrl) {
|
||||
$scope.streamTarget = ($state.params && $state.params.target) ? $state.params.target : 'dashboard';
|
||||
|
||||
$scope.options = [
|
||||
{label: 'All Activity', value: 'dashboard'},
|
||||
{label: 'Credentials', value: 'credential'},
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'Hosts', value: 'host'},
|
||||
{label: 'Inventories', value: 'inventory'},
|
||||
{label: 'Inventory Scripts', value: 'inventory_script'},
|
||||
@ -38,11 +38,11 @@ export default ['templateUrl', function(templateUrl) {
|
||||
|
||||
if($scope.streamTarget && $scope.streamTarget == 'dashboard') {
|
||||
// Just navigate to the base activity stream
|
||||
$state.go('activityStream', {}, {inherit: false, reload: true});
|
||||
$state.go('activityStream', {}, {inherit: false});
|
||||
}
|
||||
else {
|
||||
// Attach the taget to the query parameters
|
||||
$state.go('activityStream', {target: $scope.streamTarget});
|
||||
$state.go('activityStream', {target: $scope.streamTarget}, {inherit: false});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
|
||||
var urlPrefix;
|
||||
|
||||
if ($basePath) {
|
||||
@ -34,6 +32,8 @@ import managementJobs from './management-jobs/main';
|
||||
import jobDetail from './job-detail/main';
|
||||
|
||||
// modules
|
||||
import about from './about/main';
|
||||
import license from './license/main';
|
||||
import setupMenu from './setup-menu/main';
|
||||
import mainMenu from './main-menu/main';
|
||||
import breadCrumb from './bread-crumb/main';
|
||||
@ -47,7 +47,6 @@ import activityStream from './activity-stream/main';
|
||||
import standardOut from './standard-out/main';
|
||||
import lookUpHelper from './lookup/main';
|
||||
import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from './controllers/JobTemplates';
|
||||
import {LicenseController} from './controllers/License';
|
||||
import {ScheduleEditController} from './controllers/Schedules';
|
||||
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
|
||||
import {OrganizationsList, OrganizationsAdd, OrganizationsEdit} from './controllers/Organizations';
|
||||
@ -80,6 +79,8 @@ var tower = angular.module('Tower', [
|
||||
// 'ngAnimate',
|
||||
'ngSanitize',
|
||||
'ngCookies',
|
||||
about.name,
|
||||
license.name,
|
||||
RestServices.name,
|
||||
browserData.name,
|
||||
systemTracking.name,
|
||||
@ -100,7 +101,6 @@ var tower = angular.module('Tower', [
|
||||
standardOut.name,
|
||||
'templates',
|
||||
'Utilities',
|
||||
'LicenseHelper',
|
||||
'OrganizationFormDefinition',
|
||||
'UserFormDefinition',
|
||||
'FormGenerator',
|
||||
@ -172,7 +172,7 @@ var tower = angular.module('Tower', [
|
||||
'SchedulesHelper',
|
||||
'JobsListDefinition',
|
||||
'LogViewerStatusDefinition',
|
||||
'LogViewerHelper',
|
||||
'StandardOutHelper',
|
||||
'LogViewerOptionsDefinition',
|
||||
'EventViewerHelper',
|
||||
'HostEventsViewerHelper',
|
||||
@ -181,7 +181,6 @@ var tower = angular.module('Tower', [
|
||||
'lrInfiniteScroll',
|
||||
'LoadConfigHelper',
|
||||
'SocketHelper',
|
||||
'AboutAnsibleHelpModal',
|
||||
'PortalJobsListDefinition',
|
||||
'features',
|
||||
'longDateFilter',
|
||||
@ -200,9 +199,9 @@ var tower = angular.module('Tower', [
|
||||
.config(['$pendolyticsProvider', function($pendolyticsProvider) {
|
||||
$pendolyticsProvider.doNotAutoStart();
|
||||
}])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$breadcrumbProvider',
|
||||
function ($stateProvider, $urlRouterProvider, $breadcrumbProvider) {
|
||||
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$breadcrumbProvider', '$urlMatcherFactoryProvider',
|
||||
function ($stateProvider, $urlRouterProvider, $breadcrumbProvider, $urlMatcherFactoryProvider) {
|
||||
$urlMatcherFactoryProvider.strictMode(false)
|
||||
$breadcrumbProvider.setOptions({
|
||||
templateUrl: urlPrefix + 'partials/breadcrumb.html'
|
||||
});
|
||||
@ -859,21 +858,6 @@ var tower = angular.module('Tower', [
|
||||
}
|
||||
}).
|
||||
|
||||
state('license', {
|
||||
url: '/license',
|
||||
templateUrl: urlPrefix + 'partials/license.html',
|
||||
controller: LicenseController,
|
||||
ncyBreadcrumb: {
|
||||
parent: 'setup',
|
||||
label: 'LICENSE'
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
}).
|
||||
|
||||
state('sockets', {
|
||||
url: '/sockets',
|
||||
templateUrl: urlPrefix + 'partials/sockets.html',
|
||||
@ -898,12 +882,14 @@ var tower = angular.module('Tower', [
|
||||
}]);
|
||||
}])
|
||||
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
|
||||
'LoadConfig', 'Store', 'ShowSocketHelp', 'AboutAnsibleHelp', 'pendoService',
|
||||
function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService) {
|
||||
|
||||
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
|
||||
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
|
||||
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
|
||||
function (
|
||||
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
|
||||
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, pendoService)
|
||||
{
|
||||
var sock;
|
||||
|
||||
function activateTab() {
|
||||
@ -976,32 +962,28 @@ var tower = angular.module('Tower', [
|
||||
' status changed to ' + data.status +
|
||||
' send to ' + $location.$$url);
|
||||
|
||||
var urlToCheck = $location.$$url;
|
||||
if (urlToCheck.indexOf("?") !== -1) {
|
||||
urlToCheck = urlToCheck.substr(0, urlToCheck.indexOf("?"));
|
||||
}
|
||||
|
||||
// this acts as a router...it emits the proper
|
||||
// value based on what URL the user is currently
|
||||
// accessing.
|
||||
if (urlToCheck === '/jobs') {
|
||||
if ($state.is('jobs')) {
|
||||
$rootScope.$emit('JobStatusChange-jobs', data);
|
||||
} else if (/\/jobs\/(\d)+\/stdout/.test(urlToCheck) ||
|
||||
/\/ad_hoc_commands\/(\d)+/.test(urlToCheck)) {
|
||||
|
||||
// TODO: something will need to change here for stdout
|
||||
} else if ($state.is('jobDetail') ||
|
||||
$state.is('adHocJobStdout') ||
|
||||
$state.is('inventorySyncStdout') ||
|
||||
$state.is('managementJobStdout') ||
|
||||
$state.is('scmUpdateStdout')) {
|
||||
|
||||
$log.debug("sending status to standard out");
|
||||
$rootScope.$emit('JobStatusChange-jobStdout', data);
|
||||
} else if (/\/jobs\/(\d)+/.test(urlToCheck)) {
|
||||
} if ($state.is('jobDetail')) {
|
||||
$rootScope.$emit('JobStatusChange-jobDetails', data);
|
||||
} else if (urlToCheck === '/home') {
|
||||
} else if ($state.is('dashboard')) {
|
||||
$rootScope.$emit('JobStatusChange-home', data);
|
||||
} else if (urlToCheck === '/portal') {
|
||||
} else if ($state.is('portal')) {
|
||||
$rootScope.$emit('JobStatusChange-portal', data);
|
||||
} else if (urlToCheck === '/projects') {
|
||||
} else if ($state.is('projects')) {
|
||||
$rootScope.$emit('JobStatusChange-projects', data);
|
||||
} else if (/\/inventories\/(\d)+\/manage/.test(urlToCheck)) {
|
||||
} else if ($state.is('inventoryManage')) {
|
||||
$rootScope.$emit('JobStatusChange-inventory', data);
|
||||
}
|
||||
});
|
||||
@ -1043,7 +1025,6 @@ var tower = angular.module('Tower', [
|
||||
|
||||
|
||||
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
|
||||
|
||||
// this line removes the query params attached to a route
|
||||
if(prev && prev.$$route &&
|
||||
prev.$$route.name === 'systemTracking'){
|
||||
@ -1083,15 +1064,15 @@ var tower = angular.module('Tower', [
|
||||
if ($rootScope.current_user === undefined || $rootScope.current_user === null) {
|
||||
Authorization.restoreUserInfo(); //user must have hit browser refresh
|
||||
}
|
||||
if (next && next.$$route && (!/^\/(login|logout)/.test(next.$$route.originalPath))) {
|
||||
// if not headed to /login or /logout, then check the license
|
||||
CheckLicense.test();
|
||||
}
|
||||
}
|
||||
activateTab();
|
||||
});
|
||||
|
||||
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
|
||||
// catch license expiration notifications immediately after user logs in, redirect
|
||||
if (fromState.name == 'signIn'){
|
||||
CheckLicense.notify();
|
||||
}
|
||||
// broadcast event change if editing crud object
|
||||
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
|
||||
var list = $location.$$path.split("/")[3];
|
||||
@ -1140,10 +1121,6 @@ var tower = angular.module('Tower', [
|
||||
|
||||
activateTab();
|
||||
|
||||
$rootScope.viewAboutTower = function(){
|
||||
AboutAnsibleHelp();
|
||||
};
|
||||
|
||||
$rootScope.viewCurrentUser = function () {
|
||||
$location.path('/users/' + $rootScope.current_user.id);
|
||||
};
|
||||
|
||||
@ -149,7 +149,7 @@ Home.$inject = ['$scope', '$compile', '$stateParams', '$rootScope', '$location',
|
||||
* @description This controls the 'home/groups' page that is loaded from the dashboard
|
||||
*
|
||||
*/
|
||||
export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $location, $stateParams, LogViewer, HomeGroupList, GenerateList, ProcessErrors, ReturnToCaller, ClearScope,
|
||||
export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $location, $stateParams, HomeGroupList, GenerateList, ProcessErrors, ReturnToCaller, ClearScope,
|
||||
GetBasePath, SearchInit, PaginateInit, FormatDate, GetHostsStatusMsg, GetSyncStatusMsg, ViewUpdateStatus, GroupsEdit, Wait,
|
||||
Alert, Rest, Empty, InventoryUpdate, Find, GroupsCancelUpdate, Store) {
|
||||
|
||||
@ -461,58 +461,6 @@ export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $locatio
|
||||
attachElem(event, html, title);
|
||||
});
|
||||
|
||||
if (scope.removeGroupSummaryReady) {
|
||||
scope.removeGroupSummaryReady();
|
||||
}
|
||||
scope.removeGroupSummaryReady = scope.$on('GroupSummaryReady', function(e, event, inventory, data) {
|
||||
var html, title;
|
||||
|
||||
Wait('stop');
|
||||
|
||||
// Build the html for our popover
|
||||
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
|
||||
html += "<thead>\n";
|
||||
html += "<tr>";
|
||||
html += "<th>Status</th>";
|
||||
html += "<th>Last Sync</th>";
|
||||
html += "<th>Group</th>";
|
||||
html += "</tr>";
|
||||
html += "</thead>\n";
|
||||
html += "<tbody>\n";
|
||||
data.results.forEach( function(row) {
|
||||
html += "<tr>";
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\" aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) + ". Click for details\" aw-tip-placement=\"top\"><i class=\"fa icon-job-" + row.status + "\"></i></a></td>";
|
||||
html += "<td>" + ($filter('longDate')(row.last_updated)).replace(/ /,'<br />') + "</td>";
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\">" + ellipsis(row.summary_fields.group.name) + "</a></td>";
|
||||
html += "</tr>\n";
|
||||
});
|
||||
html += "</tbody>\n";
|
||||
html += "</table>\n";
|
||||
title = "Sync Status";
|
||||
attachElem(event, html, title);
|
||||
});
|
||||
|
||||
scope.showGroupSummary = function(event, id) {
|
||||
var group, status;
|
||||
if (!Empty(id)) {
|
||||
group = Find({ list: scope.home_groups, key: 'id', val: id });
|
||||
status = group.summary_fields.inventory_source.status;
|
||||
if (status === 'running' || status === 'failed' || status === 'error' || status === 'successful') {
|
||||
Wait('start');
|
||||
Rest.setUrl(group.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5');
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
scope.$emit('GroupSummaryReady', event, group, data);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors( scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + group.related.inventory_sources + ' failed. GET returned status: ' + status
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scope.showHostSummary = function(event, id) {
|
||||
var url, jobs = [];
|
||||
if (!Empty(id)) {
|
||||
@ -549,13 +497,6 @@ export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $locatio
|
||||
}
|
||||
};
|
||||
|
||||
scope.viewJob = function(url) {
|
||||
LogViewer({
|
||||
scope: modal_scope,
|
||||
url: url
|
||||
});
|
||||
};
|
||||
|
||||
scope.cancelUpdate = function(id) {
|
||||
var group = Find({ list: scope.home_groups, key: 'id', val: id });
|
||||
GroupsCancelUpdate({ scope: scope, group: group });
|
||||
@ -564,7 +505,7 @@ export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $locatio
|
||||
|
||||
}
|
||||
|
||||
HomeGroups.$inject = ['$rootScope', '$log', '$scope', '$filter', '$compile', '$location', '$stateParams', 'LogViewer', 'HomeGroupList', 'generateList', 'ProcessErrors', 'ReturnToCaller',
|
||||
HomeGroups.$inject = ['$rootScope', '$log', '$scope', '$filter', '$compile', '$location', '$stateParams', 'HomeGroupList', 'generateList', 'ProcessErrors', 'ReturnToCaller',
|
||||
'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'GetHostsStatusMsg', 'GetSyncStatusMsg', 'ViewUpdateStatus',
|
||||
'GroupsEdit', 'Wait', 'Alert', 'Rest', 'Empty', 'InventoryUpdate', 'Find', 'GroupsCancelUpdate', 'Store', 'Socket'
|
||||
];
|
||||
@ -578,7 +519,7 @@ HomeGroups.$inject = ['$rootScope', '$log', '$scope', '$filter', '$compile', '$l
|
||||
*/
|
||||
|
||||
export function HomeHosts($scope, $location, $stateParams, HomeHostList, GenerateList, ProcessErrors, ReturnToCaller, ClearScope,
|
||||
GetBasePath, SearchInit, PaginateInit, FormatDate, SetStatus, ToggleHostEnabled, HostsEdit, Find, ShowJobSummary, ViewJob) {
|
||||
GetBasePath, SearchInit, PaginateInit, FormatDate, SetStatus, ToggleHostEnabled, HostsEdit, Find, ShowJobSummary) {
|
||||
|
||||
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
|
||||
//scope.
|
||||
@ -647,10 +588,6 @@ export function HomeHosts($scope, $location, $stateParams, HomeHostList, Generat
|
||||
$scope.search(list.iterator);
|
||||
};
|
||||
|
||||
$scope.viewJob = function(id) {
|
||||
ViewJob({ scope: $scope, id: id });
|
||||
};
|
||||
|
||||
$scope.toggleHostEnabled = function (id, sources) {
|
||||
ToggleHostEnabled({
|
||||
host_id: id,
|
||||
@ -687,5 +624,5 @@ export function HomeHosts($scope, $location, $stateParams, HomeHostList, Generat
|
||||
|
||||
HomeHosts.$inject = ['$scope', '$location', '$stateParams', 'HomeHostList', 'generateList', 'ProcessErrors', 'ReturnToCaller',
|
||||
'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'SetStatus', 'ToggleHostEnabled', 'HostsEdit',
|
||||
'Find', 'ShowJobSummary', 'ViewJob'
|
||||
'Find', 'ShowJobSummary'
|
||||
];
|
||||
|
||||
@ -16,7 +16,7 @@ export function InventoriesList($scope, $rootScope, $location, $log,
|
||||
$stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList,
|
||||
generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller,
|
||||
ClearScope, ProcessErrors, GetBasePath, Wait,
|
||||
EditInventoryProperties, Find, Empty, LogViewer, $state) {
|
||||
EditInventoryProperties, Find, Empty, $state) {
|
||||
|
||||
var list = InventoryList,
|
||||
defaultUrl = GetBasePath('inventory'),
|
||||
@ -295,10 +295,12 @@ export function InventoriesList($scope, $rootScope, $location, $log,
|
||||
};
|
||||
|
||||
$scope.viewJob = function(url) {
|
||||
LogViewer({
|
||||
scope: $scope,
|
||||
url: url
|
||||
});
|
||||
|
||||
// Pull the id out of the URL
|
||||
var id = url.replace(/^\//, '').split('/')[3];
|
||||
|
||||
$state.go('inventorySyncStdout', {id: id});
|
||||
|
||||
};
|
||||
|
||||
$scope.editInventoryProperties = function (inventory_id) {
|
||||
@ -364,7 +366,7 @@ export function InventoriesList($scope, $rootScope, $location, $log,
|
||||
|
||||
InventoriesList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList', 'generateList',
|
||||
'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors',
|
||||
'GetBasePath', 'Wait', 'EditInventoryProperties', 'Find', 'Empty', 'LogViewer', '$state'
|
||||
'GetBasePath', 'Wait', 'EditInventoryProperties', 'Find', 'Empty', '$state'
|
||||
];
|
||||
|
||||
|
||||
@ -781,7 +783,7 @@ export function InventoriesManage ($log, $scope, $rootScope, $location,
|
||||
GetHostsStatusMsg, GroupsEdit, InventoryUpdate, GroupsCancelUpdate,
|
||||
ViewUpdateStatus, GroupsDelete, Store, HostsEdit, HostsDelete,
|
||||
EditInventoryProperties, ToggleHostEnabled, ShowJobSummary,
|
||||
InventoryGroupsHelp, HelpDialog, ViewJob,
|
||||
InventoryGroupsHelp, HelpDialog,
|
||||
GroupsCopy, HostsCopy, $stateParams) {
|
||||
|
||||
var PreviousSearchParams,
|
||||
@ -920,7 +922,7 @@ export function InventoriesManage ($log, $scope, $rootScope, $location,
|
||||
generateList.inject(InventoryGroups, {
|
||||
mode: 'edit',
|
||||
id: 'group-list-container',
|
||||
searchSize: 'col-lg-6 col-md-6 col-sm-6',
|
||||
searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12',
|
||||
scope: $scope
|
||||
});
|
||||
|
||||
@ -1254,12 +1256,8 @@ export function InventoriesManage ($log, $scope, $rootScope, $location,
|
||||
opts.autoShow = params.autoShow || false;
|
||||
}
|
||||
HelpDialog(opts);
|
||||
};
|
||||
|
||||
$scope.viewJob = function(id) {
|
||||
ViewJob({ scope: $scope, id: id });
|
||||
};
|
||||
|
||||
}
|
||||
;
|
||||
$scope.showHosts = function (group_id, show_failures) {
|
||||
// Clicked on group
|
||||
if (group_id !== null) {
|
||||
@ -1293,6 +1291,6 @@ InventoriesManage.$inject = ['$log', '$scope', '$rootScope', '$location',
|
||||
'GroupsEdit', 'InventoryUpdate', 'GroupsCancelUpdate', 'ViewUpdateStatus',
|
||||
'GroupsDelete', 'Store', 'HostsEdit', 'HostsDelete',
|
||||
'EditInventoryProperties', 'ToggleHostEnabled', 'ShowJobSummary',
|
||||
'InventoryGroupsHelp', 'HelpDialog', 'ViewJob', 'GroupsCopy',
|
||||
'InventoryGroupsHelp', 'HelpDialog', 'GroupsCopy',
|
||||
'HostsCopy', '$stateParams'
|
||||
];
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
/************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
*
|
||||
* Organizations.js
|
||||
*
|
||||
* Controller functions for Organization model.
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name controllers.function:Organizations
|
||||
* @description This controller's for the Organizations page
|
||||
*/
|
||||
|
||||
|
||||
export function LicenseController(ClearScope, $location, $rootScope, $compile, $filter, GenerateForm, Rest, Alert,
|
||||
GetBasePath, ProcessErrors, FormatDate, Prompt, Empty, LicenseForm, IsAdmin, CreateDialog, CheckLicense,
|
||||
TextareaResize, $scope, Wait) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
$scope.getDefaultHTML = function(license_info) {
|
||||
var fld, html,
|
||||
self = this,
|
||||
generator = GenerateForm;
|
||||
|
||||
self.form = angular.copy(LicenseForm);
|
||||
|
||||
for (fld in self.form.fields) {
|
||||
if (fld !== 'time_remaining' && fld !== 'license_status' && fld !== 'tower_version') {
|
||||
if (Empty(license_info[fld])) {
|
||||
delete self.form.fields[fld];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsAdmin()) {
|
||||
delete self.form.fields.license_key;
|
||||
}
|
||||
|
||||
if (license_info.is_aws || Empty(license_info.license_date)) {
|
||||
delete self.form.fields.license_date;
|
||||
delete self.form.fields.time_remaining;
|
||||
}
|
||||
|
||||
html = generator.buildHTML(self.form, { mode: 'edit', showButtons: false });
|
||||
return html;
|
||||
};
|
||||
|
||||
$scope.loadDefaultScope = function(license_info, version) {
|
||||
var fld, dt, days, license,
|
||||
self = this;
|
||||
|
||||
for (fld in self.form.fields) {
|
||||
if (!Empty(license_info[fld])) {
|
||||
$scope[fld] = license_info[fld];
|
||||
}
|
||||
}
|
||||
|
||||
$scope.tower_version = version;
|
||||
|
||||
if ($scope.license_date) {
|
||||
dt = new Date(parseInt($scope.license_date, 10) * 1000); // expects license_date in seconds
|
||||
$scope.license_date = FormatDate(dt);
|
||||
$scope.time_remaining = parseInt($scope.time_remaining,10) * 1000;
|
||||
if ($scope.time_remaining < 0) {
|
||||
days = 0;
|
||||
} else {
|
||||
days = Math.floor($scope.time_remaining / 86400000);
|
||||
}
|
||||
$scope.time_remaining = (days!==1) ? $filter('number')(days, 0) + ' days' : $filter('number')(days, 0) + ' day'; // '1 day' and '0 days/2 days' or more
|
||||
}
|
||||
|
||||
if (parseInt($scope.free_instances) <= 0) {
|
||||
$scope.free_instances_class = 'field-failure';
|
||||
} else {
|
||||
$scope.free_instances_class = 'field-success';
|
||||
}
|
||||
|
||||
license = license_info;
|
||||
if (license.valid_key === undefined) {
|
||||
$scope.license_status = 'Missing License Key';
|
||||
$scope.status_color = 'license-invalid';
|
||||
} else if (!license.valid_key) {
|
||||
$scope.license_status = 'Invalid License Key';
|
||||
$scope.status_color = 'license-invalid';
|
||||
} else if (license.date_expired !== undefined && license.date_expired) {
|
||||
$scope.license_status = 'License Expired';
|
||||
$scope.status_color = 'license-expired';
|
||||
} else if (license.date_warning !== undefined && license.date_warning) {
|
||||
$scope.license_status = 'License Expiring Soon';
|
||||
$scope.status_color = 'license-warning';
|
||||
} else if (license.free_instances !== undefined && parseInt(license.free_instances) <= 0) {
|
||||
$scope.license_status = 'No Available Managed Hosts';
|
||||
$scope.status_color = 'license-invalid';
|
||||
} else {
|
||||
$scope.license_status = 'Valid License';
|
||||
$scope.status_color = 'license-valid';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setLicense = function(license_info, version) {
|
||||
this.license = license_info;
|
||||
this.version = version;
|
||||
};
|
||||
|
||||
$scope.getLicense = function(){
|
||||
return this.license;
|
||||
};
|
||||
|
||||
$scope.submitLicenseKey = function() {
|
||||
CheckLicense.postLicense($scope.license_json, $scope);
|
||||
};
|
||||
|
||||
if ($scope.removeLicenseDataReady) {
|
||||
$scope.removeLicenseDataReady();
|
||||
}
|
||||
$scope.removeLicenseDataReady = $scope.$on('LicenseDataReady', function(e, data) {
|
||||
var html, version, eula, h;
|
||||
version = data.version.replace(/-.*$/,'');
|
||||
$scope.setLicense(data.license_info, version);
|
||||
html = $scope.getDefaultHTML(data.license_info);
|
||||
$scope.loadDefaultScope(data.license_info, version);
|
||||
eula = (data.eula) ? data.eula : "" ;
|
||||
|
||||
e = angular.element(document.getElementById('license-modal-dialog'));
|
||||
e.empty().html(html);
|
||||
|
||||
$scope.parseType = 'json';
|
||||
$scope.license_json = JSON.stringify($scope.license, null, ' ');
|
||||
$scope.eula = eula;
|
||||
$scope.eula_agreement = false;
|
||||
|
||||
|
||||
h = CheckLicense.getHTML($scope.getLicense(),true).body;
|
||||
$('#license-modal-dialog #license_tabs').append("<li><a id=\"update_license_link\" ng-click=\"toggleTab($event, 'update_license_link', 'license_tabs')\" href=\"#update_license\" data-toggle=\"tab\">Update License</a></li>");
|
||||
$('#license-modal-dialog .tab-content').append("<div class=\"tab-pane\" id=\"update_license\"></div>");
|
||||
$('#license-modal-dialog #update_license').html(h);
|
||||
|
||||
if ($scope.license_status === 'Invalid License Key' || $scope.license_status === 'Missing License Key') {
|
||||
$('#license_tabs li:eq(1)').hide();
|
||||
$('#license_tabs li:eq(2) a').tab('show');
|
||||
}
|
||||
|
||||
$('#license_license_json').attr('ng-required' , 'true' );
|
||||
$('#license_eula_agreement_chbox').attr('ng-required' , 'true' );
|
||||
$('#license_form_submit_btn').attr('ng-disabled' , "license_form.$invalid" );
|
||||
e = angular.element(document.getElementById('license-modal-dialog'));
|
||||
$compile(e)($scope);
|
||||
|
||||
if (IsAdmin()) {
|
||||
setTimeout(function() {
|
||||
TextareaResize({
|
||||
scope: $scope,
|
||||
textareaId: 'license_license_json',
|
||||
modalId: 'license-modal-dialog',
|
||||
formId: 'license-notification-body',
|
||||
fld: 'license_json',
|
||||
parse: true,
|
||||
bottom_margin: 90,
|
||||
onChange: function() { $scope.license_json_api_error = ''; }
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
$('#license-ok-button').focus();
|
||||
$('#update_license_link').on('shown.bs.tab', function() {
|
||||
if (IsAdmin()) {
|
||||
TextareaResize({
|
||||
scope: $scope,
|
||||
textareaId: 'license_license_json',
|
||||
modalId: 'license-modal-dialog',
|
||||
formId: 'license-notification-body',
|
||||
fld: 'license_json',
|
||||
bottom_margin: 90,
|
||||
parse: true,
|
||||
onChange: function() { $scope.license_json_api_error = ''; }
|
||||
});
|
||||
}
|
||||
});
|
||||
Wait("stop");
|
||||
});
|
||||
CheckLicense.GetLicense('LicenseDataReady', $scope);
|
||||
|
||||
}
|
||||
|
||||
LicenseController.$inject = ['ClearScope', '$location', '$rootScope', '$compile', '$filter', 'GenerateForm', 'Rest', 'Alert',
|
||||
'GetBasePath', 'ProcessErrors', 'FormatDate', 'Prompt', 'Empty', 'LicenseForm', 'IsAdmin', 'CreateDialog',
|
||||
'CheckLicense', 'TextareaResize', '$scope', "Wait"];
|
||||
@ -15,7 +15,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
Rest, Alert, ProjectList, GenerateList, Prompt, SearchInit,
|
||||
PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath,
|
||||
SelectionInit, ProjectUpdate, Refresh, Wait, GetChoices, Empty,
|
||||
Find, LogViewer, GetProjectIcon, GetProjectToolTip, $filter, $state) {
|
||||
Find, GetProjectIcon, GetProjectToolTip, $filter, $state) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -200,24 +200,19 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
$state.transitionTo('projects.edit', {id: id});
|
||||
};
|
||||
|
||||
if ($scope.removeShowLogViewer) {
|
||||
$scope.removeShowLogViewer();
|
||||
if ($scope.removeGoToJobDetails) {
|
||||
$scope.removeGoToJobDetails();
|
||||
}
|
||||
$scope.removeShowLogViewer = $scope.$on('ShowLogViewer', function(e, data) {
|
||||
if (data.related.current_update) {
|
||||
$scope.removeGoToJobDetails = $scope.$on('GoToJobDetails', function(e, data) {
|
||||
if (data.summary_fields.current_update || data.summary_fields.last_update) {
|
||||
|
||||
Wait('start');
|
||||
LogViewer({
|
||||
scope: $scope,
|
||||
url: data.related.current_update,
|
||||
getIcon: GetProjectIcon
|
||||
});
|
||||
} else if (data.related.last_update) {
|
||||
Wait('start');
|
||||
LogViewer({
|
||||
scope: $scope,
|
||||
url: data.related.last_update,
|
||||
getIcon: GetProjectIcon
|
||||
});
|
||||
|
||||
// Grab the id from summary_fields
|
||||
var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id;
|
||||
|
||||
$state.go('scmUpdateStdout', {id: id});
|
||||
|
||||
} else {
|
||||
Alert('No Updates Available', 'There is no SCM update information available for this project. An update has not yet been ' +
|
||||
' completed. If you have not already done so, start an update for this project.', 'alert-info');
|
||||
@ -235,7 +230,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
Rest.setUrl(project.url);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.$emit('ShowLogViewer', data);
|
||||
$scope.$emit('GoToJobDetails', data);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
@ -374,7 +369,7 @@ ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log',
|
||||
'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
|
||||
'ProcessErrors', 'GetBasePath', 'SelectionInit', 'ProjectUpdate',
|
||||
'Refresh', 'Wait', 'GetChoices', 'Empty', 'Find',
|
||||
'LogViewer', 'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state'
|
||||
'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state'
|
||||
];
|
||||
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
color: #848992;
|
||||
width: 100%;
|
||||
z-index: 1040;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
import './forms';
|
||||
import './lists';
|
||||
|
||||
import AboutAnsible from "./helpers/AboutAnsible";
|
||||
import Children from "./helpers/Children";
|
||||
import Credentials from "./helpers/Credentials";
|
||||
import EventViewer from "./helpers/EventViewer";
|
||||
@ -19,9 +18,7 @@ import JobDetail from "./helpers/JobDetail";
|
||||
import JobSubmission from "./helpers/JobSubmission";
|
||||
import JobTemplates from "./helpers/JobTemplates";
|
||||
import Jobs from "./helpers/Jobs";
|
||||
import License from "./helpers/License";
|
||||
import LoadConfig from "./helpers/LoadConfig";
|
||||
import LogViewer from "./helpers/LogViewer";
|
||||
import PaginationHelpers from "./helpers/PaginationHelpers";
|
||||
import Parse from "./helpers/Parse";
|
||||
import ProjectPath from "./helpers/ProjectPath";
|
||||
@ -44,8 +41,7 @@ import ApiModelHelper from "./helpers/ApiModel";
|
||||
import ActivityStreamHelper from "./helpers/ActivityStream";
|
||||
|
||||
export
|
||||
{ AboutAnsible,
|
||||
Children,
|
||||
{ Children,
|
||||
Credentials,
|
||||
EventViewer,
|
||||
Events,
|
||||
@ -56,9 +52,7 @@ export
|
||||
JobSubmission,
|
||||
JobTemplates,
|
||||
Jobs,
|
||||
License,
|
||||
LoadConfig,
|
||||
LogViewer,
|
||||
PaginationHelpers,
|
||||
Parse,
|
||||
ProjectPath,
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @name helpers
|
||||
* @description These are helpers...figure it out :)
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name helpers.function:AboutAnsible
|
||||
* @description This is the code for the About Ansible modal window that pops up with cowsay giving company/tower info and copyright information.
|
||||
*/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('AboutAnsibleHelpModal', ['RestServices', 'Utilities','ModalDialog'])
|
||||
.factory('AboutAnsibleHelp', ['$rootScope', '$compile', '$location' , 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'CreateDialog',
|
||||
function ($rootScope, $compile , $location, Rest, GetBasePath, ProcessErrors, Wait, CreateDialog) {
|
||||
return function () {
|
||||
|
||||
var scope= $rootScope.$new(),
|
||||
url;
|
||||
|
||||
url = GetBasePath('config');
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function (data){
|
||||
scope.$emit('BuildAboutDialog', data);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to get: ' + url + ' GET returned: ' + status });
|
||||
});
|
||||
|
||||
|
||||
if (scope.removeDialogReady) {
|
||||
scope.removeDialogReady();
|
||||
}
|
||||
scope.removeDialogReady = scope.$on('DialogReady', function() {
|
||||
// element = angular.element(document.getElementById('about-modal-dialog'));
|
||||
// $compile(element)(scope);
|
||||
$('#about-modal-dialog').dialog('open');
|
||||
});
|
||||
|
||||
if (scope.removeBuildAboutDialog) {
|
||||
scope.removeBuildAboutDialog();
|
||||
}
|
||||
scope.removeBuildAboutDialog = scope.$on('BuildAboutDialog', function(e, data) {
|
||||
var spaces, i, j,
|
||||
paddedStr = "",
|
||||
versionParts,
|
||||
str = data.version,
|
||||
subscription = data.license_info.subscription_name || "";
|
||||
|
||||
versionParts = str.split('-');
|
||||
spaces = Math.floor((16-versionParts[0].length)/2);
|
||||
for( i=0; i<=spaces; i++){
|
||||
paddedStr = paddedStr +" ";
|
||||
}
|
||||
paddedStr = paddedStr + versionParts[0];
|
||||
for( j = paddedStr.length; j<16; j++){
|
||||
paddedStr = paddedStr + " ";
|
||||
}
|
||||
$('#about-modal-version').html(paddedStr);
|
||||
$('#about-modal-subscription').html(subscription);
|
||||
scope.modalOK = function(){
|
||||
$('#about-modal-dialog').dialog('close');
|
||||
};
|
||||
CreateDialog({
|
||||
id: 'about-modal-dialog',
|
||||
scope: scope,
|
||||
// buttons: [],
|
||||
width: 710,
|
||||
height: 450,
|
||||
minWidth: 300,
|
||||
resizable: false,
|
||||
callback: 'DialogReady',
|
||||
onOpen: function(){
|
||||
$('#dialog-ok-button').focus();
|
||||
$('#about-modal-dialog').scrollTop(0);
|
||||
$('#about-modal-dialog').css('overflow-x', 'hidden');
|
||||
$('.ui-widget-overlay').css('width', '100%');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
]);
|
||||
@ -16,7 +16,7 @@ export default
|
||||
function () {
|
||||
return function (target) {
|
||||
|
||||
var rtnTitle = 'DASHBOARD';
|
||||
var rtnTitle = 'ALL ACTIVITY';
|
||||
|
||||
switch(target) {
|
||||
case 'project':
|
||||
@ -49,6 +49,9 @@ export default
|
||||
case 'schedule':
|
||||
rtnTitle = 'SCHEDULES';
|
||||
break;
|
||||
case 'host':
|
||||
rtnTitle = 'HOSTS';
|
||||
break;
|
||||
}
|
||||
|
||||
return rtnTitle;
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
export default
|
||||
angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFormDefinition', 'HostsHelper'])
|
||||
|
||||
.factory('EventViewer', ['$compile', 'CreateDialog', 'GetEvent', 'Wait', 'EventAddTable', 'GetBasePath', 'LookUpName', 'Empty', 'EventAddPreFormattedText',
|
||||
function($compile, CreateDialog, GetEvent, Wait, EventAddTable, GetBasePath, LookUpName, Empty, EventAddPreFormattedText) {
|
||||
.factory('EventViewer', ['$compile', 'CreateDialog', 'GetEvent', 'Wait', 'EventAddTable', 'GetBasePath', 'Empty', 'EventAddPreFormattedText',
|
||||
function($compile, CreateDialog, GetEvent, Wait, EventAddTable, GetBasePath, Empty, EventAddPreFormattedText) {
|
||||
return function(params) {
|
||||
var parent_scope = params.scope,
|
||||
url = params.url,
|
||||
|
||||
@ -18,7 +18,7 @@ export default
|
||||
angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'GroupListDefinition', 'SearchHelper',
|
||||
'PaginationHelpers', listGenerator.name, 'GroupsHelper', 'InventoryHelper', 'SelectionHelper',
|
||||
'JobSubmissionHelper', 'RefreshHelper', 'PromptDialog', 'CredentialsListDefinition', 'InventoryTree',
|
||||
'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'SourceFormDefinition', 'LogViewerHelper',
|
||||
'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'SourceFormDefinition', 'StandardOutHelper',
|
||||
'SchedulesHelper'
|
||||
])
|
||||
|
||||
@ -65,8 +65,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
* TODO: Document
|
||||
*
|
||||
*/
|
||||
.factory('ViewUpdateStatus', ['Rest', 'ProcessErrors', 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find', 'LogViewer',
|
||||
function (Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find, LogViewer) {
|
||||
.factory('ViewUpdateStatus', ['$state', 'Rest', 'ProcessErrors', 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find',
|
||||
function ($state, Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find) {
|
||||
return function (params) {
|
||||
|
||||
var scope = params.scope,
|
||||
@ -76,11 +76,13 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
if (scope.removeSourceReady) {
|
||||
scope.removeSourceReady();
|
||||
}
|
||||
scope.removeSourceReady = scope.$on('SourceReady', function(e, url) {
|
||||
LogViewer({
|
||||
scope: scope,
|
||||
url: url
|
||||
});
|
||||
scope.removeSourceReady = scope.$on('SourceReady', function(e, source) {
|
||||
|
||||
// Get the ID from the correct summary field
|
||||
var update_id = (source.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id;
|
||||
|
||||
$state.go('inventorySyncStdout', {id: update_id});
|
||||
|
||||
});
|
||||
|
||||
if (group) {
|
||||
@ -94,8 +96,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
Rest.setUrl(group.related.inventory_source);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
var url = (data.related.current_update) ? data.related.current_update : data.related.last_update;
|
||||
scope.$emit('SourceReady', url);
|
||||
scope.$emit('SourceReady', data);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
|
||||
@ -20,7 +20,7 @@ export default
|
||||
angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'HostListDefinition',
|
||||
'SearchHelper', 'PaginationHelpers', listGenerator.name, 'HostsHelper',
|
||||
'InventoryHelper', 'RelatedSearchHelper', 'InventoryFormDefinition', 'SelectionHelper',
|
||||
'HostGroupsFormDefinition', 'VariablesHelper', 'ModalDialog', 'LogViewerHelper',
|
||||
'HostGroupsFormDefinition', 'VariablesHelper', 'ModalDialog', 'StandardOutHelper',
|
||||
'GroupListDefinition'
|
||||
])
|
||||
|
||||
@ -159,17 +159,6 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name,
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('ViewJob', ['LogViewer', 'GetBasePath', function(LogViewer, GetBasePath) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
id = params.id;
|
||||
LogViewer({
|
||||
scope: scope,
|
||||
url: GetBasePath('jobs') + id + '/'
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('HostsReload', [ '$stateParams', 'Empty', 'InventoryHosts', 'GetBasePath', 'SearchInit', 'PaginateInit', 'Wait',
|
||||
'SetHostStatus', 'SetStatus', 'ApplyEllipsis',
|
||||
function($stateParams, Empty, InventoryHosts, GetBasePath, SearchInit, PaginateInit, Wait, SetHostStatus, SetStatus,
|
||||
@ -240,7 +229,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name,
|
||||
generator = GenerateList;
|
||||
|
||||
// Inject the list html
|
||||
generator.inject(InventoryHosts, { scope: host_scope, mode: 'edit', id: 'host-list-container', searchSize: 'col-lg-6 col-md-6 col-sm-6' });
|
||||
generator.inject(InventoryHosts, { scope: host_scope, mode: 'edit', id: 'host-list-container', searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12' });
|
||||
|
||||
// Load data
|
||||
HostsReload({ scope: host_scope, group_id: group_id, inventory_id: inventory_id, parent_scope: group_scope, pageSize: pageSize });
|
||||
|
||||
@ -235,7 +235,7 @@ export default
|
||||
}
|
||||
if (newActivePlay) {
|
||||
scope.activePlay = newActivePlay;
|
||||
scope.jobData.plays[scope.activePlay].playActiveClass = 'active';
|
||||
scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected';
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -265,7 +265,7 @@ export default
|
||||
}
|
||||
if (newActiveTask) {
|
||||
scope.activeTask = newActiveTask;
|
||||
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'active';
|
||||
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'JobDetail-tableRow--selected';
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -700,12 +700,12 @@ export default
|
||||
task.missingPct = task.missingPct - diff;
|
||||
}
|
||||
}
|
||||
task.successfulStyle = (task.successfulPct > 0) ? { 'display': 'inline-block', 'width': task.successfulPct + "%" } : { 'display': 'none' };
|
||||
task.changedStyle = (task.changedPct > 0) ? { 'display': 'inline-block', 'width': task.changedPct + "%" } : { 'display': 'none' };
|
||||
task.skippedStyle = (task.skippedPct > 0) ? { 'display': 'inline-block', 'width': task.skippedPct + "%" } : { 'display': 'none' };
|
||||
task.failedStyle = (task.failedPct > 0) ? { 'display': 'inline-block', 'width': task.failedPct + "%" } : { 'display': 'none' };
|
||||
task.unreachableStyle = (task.unreachablePct > 0) ? { 'display': 'inline-block', 'width': task.unreachablePct + "%" } : { 'display': 'none' };
|
||||
task.missingStyle = (task.missingPct > 0) ? { 'display': 'inline-block', 'width': task.missingPct + "%" } : { 'display': 'none' };
|
||||
task.successfulStyle = (task.successfulPct > 0) ? { 'display': 'inline-block' }: { 'display': 'none' };
|
||||
task.changedStyle = (task.changedPct > 0) ? { 'display': 'inline-block'} : { 'display': 'none' };
|
||||
task.skippedStyle = (task.skippedPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
|
||||
task.failedStyle = (task.failedPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
|
||||
task.unreachableStyle = (task.unreachablePct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
|
||||
task.missingStyle = (task.missingPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
|
||||
};
|
||||
}])
|
||||
|
||||
@ -793,7 +793,7 @@ export default
|
||||
scope.selectedPlay = id;
|
||||
scope.plays.forEach(function(play, idx) {
|
||||
if (play.id === scope.selectedPlay) {
|
||||
scope.plays[idx].playActiveClass = 'active';
|
||||
scope.plays[idx].playActiveClass = 'JobDetail-tableRow--selected';
|
||||
}
|
||||
else {
|
||||
scope.plays[idx].playActiveClass = '';
|
||||
@ -940,7 +940,7 @@ export default
|
||||
scope.selectedTask = id;
|
||||
scope.tasks.forEach(function(task, idx) {
|
||||
if (task.id === scope.selectedTask) {
|
||||
scope.tasks[idx].taskActiveClass = 'active';
|
||||
scope.tasks[idx].taskActiveClass = 'JobDetail-tableRow--selected';
|
||||
}
|
||||
else {
|
||||
scope.tasks[idx].taskActiveClass = '';
|
||||
@ -1142,8 +1142,7 @@ export default
|
||||
.factory('DrawGraph', ['DonutChart', function(DonutChart) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
resize = params.resize,
|
||||
width, height, svg_height, svg_width, svg_radius, graph_data = [];
|
||||
graph_data = [];
|
||||
|
||||
// Ready the data
|
||||
if (scope.host_summary.ok) {
|
||||
@ -1155,21 +1154,21 @@ export default
|
||||
}
|
||||
if (scope.host_summary.changed) {
|
||||
graph_data.push({
|
||||
label: 'Changed',
|
||||
label: 'CHANGED',
|
||||
value: scope.host_summary.changed,
|
||||
color: '#FF9900'
|
||||
});
|
||||
}
|
||||
if (scope.host_summary.unreachable) {
|
||||
graph_data.push({
|
||||
label: 'Unreachable',
|
||||
label: 'UNREACHABLE',
|
||||
value: scope.host_summary.unreachable,
|
||||
color: '#FF0000'
|
||||
});
|
||||
}
|
||||
if (scope.host_summary.failed) {
|
||||
graph_data.push({
|
||||
label: 'Failed',
|
||||
label: 'FAILED',
|
||||
value: scope.host_summary.failed,
|
||||
color: '#ff5850'
|
||||
});
|
||||
@ -1180,148 +1179,91 @@ export default
|
||||
total_count += graph_data[gd_obj].value;
|
||||
}
|
||||
scope.total_count_for_graph = total_count;
|
||||
// Adjust the size
|
||||
width = $('#job-summary-container .job_well').width();
|
||||
height = $('#job-summary-container .job_well').height() - $('#summary-well-top-section').height() - $('#graph-section .header').outerHeight() - 80;
|
||||
svg_radius = Math.min(width, height);
|
||||
svg_width = width;
|
||||
svg_height = height;
|
||||
if (svg_height > 0 && svg_width > 0) {
|
||||
if (!resize && $('#graph-section svg').length > 0) {
|
||||
// Donut3D.transition("completedHostsDonut", graph_data, Math.floor(svg_radius * 0.50), Math.floor(svg_radius * 0.25), 18, 0.4);
|
||||
DonutChart({
|
||||
target: '#graph-section',
|
||||
height: height,
|
||||
width: width,
|
||||
data: graph_data,
|
||||
radius: svg_radius
|
||||
});
|
||||
}
|
||||
else {
|
||||
if ($('#graph-section svg').length > 0) {
|
||||
$('#graph-section svg').remove();
|
||||
}
|
||||
// svg = d3.select("#graph-section").append("svg").attr("width", svg_width).attr("height", svg_height);
|
||||
// svg.append("g").attr("id","completedHostsDonut");
|
||||
// Donut3D.draw("completedHostsDonut", graph_data, Math.floor(svg_width / 2), Math.floor(svg_height / 2) - 35, Math.floor(svg_radius * 0.50), Math.floor(svg_radius * 0.25), 18, 0.4);
|
||||
DonutChart({
|
||||
target: '#graph-section',
|
||||
height: height,
|
||||
width: width,
|
||||
data: graph_data,
|
||||
radius: svg_radius
|
||||
});
|
||||
$('#graph-section .header .legend').show();
|
||||
}
|
||||
}
|
||||
DonutChart({
|
||||
data: graph_data
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('DonutChart', [function() {
|
||||
return function(params) {
|
||||
var target = params.target,
|
||||
height = Math.max(params.height, 250),
|
||||
width = Math.max(params.width, 250),
|
||||
dataset = params.data,
|
||||
outerRadius = Math.min(width, height) / 2,
|
||||
innerRadius = (outerRadius/3),
|
||||
svg, arc, pie, legend,
|
||||
tooltip, path,
|
||||
legendRectSize = 18,
|
||||
legendSpacing = 4;
|
||||
var dataset = params.data,
|
||||
element = $("#graph-section"),
|
||||
colors, total,job_detail_chart;
|
||||
|
||||
svg = d3.select(target)
|
||||
.append('svg')
|
||||
.data([dataset])
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + (width / 2) +
|
||||
',' + (height / 2) + ')');
|
||||
colors = _.map(dataset, function(d){
|
||||
return d.color;
|
||||
});
|
||||
total = d3.sum(dataset.map(function(d) {
|
||||
return d.value;
|
||||
}));
|
||||
job_detail_chart = nv.models.pieChart()
|
||||
.margin({bottom: 15})
|
||||
.x(function(d) {
|
||||
return d.label +': '+ Math.round((d.value/total)*100) + "%";
|
||||
})
|
||||
.y(function(d) { return d.value; })
|
||||
.showLabels(true)
|
||||
.showLegend(false)
|
||||
.growOnHover(false)
|
||||
.labelThreshold(0.01)
|
||||
.tooltipContent(function(x, y) {
|
||||
return '<p>'+x+'</p>'+ '<p>' + Math.floor(y.replace(',','')) + ' HOSTS ' + '</p>';
|
||||
})
|
||||
.color(colors);
|
||||
|
||||
arc = d3.svg.arc()
|
||||
.innerRadius(outerRadius - innerRadius)
|
||||
.outerRadius(outerRadius);
|
||||
|
||||
pie = d3.layout.pie()
|
||||
.value(function(d) { return d.value; })
|
||||
.sort(function() {return null; });
|
||||
|
||||
tooltip = d3.select(target)
|
||||
.append('div')
|
||||
.attr('class', 'donut-tooltip');
|
||||
|
||||
tooltip.append('div')
|
||||
.attr('class', 'donut-tooltip-inner');
|
||||
|
||||
path = svg.selectAll('path')
|
||||
.data(pie(dataset))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('d', arc)
|
||||
.attr('fill', function(d) {
|
||||
return d.data.color;
|
||||
d3.select(element.find('svg')[0])
|
||||
.datum(dataset)
|
||||
.transition().duration(350)
|
||||
.call(job_detail_chart)
|
||||
.style({
|
||||
"font-family": 'Open Sans',
|
||||
"font-style": "normal",
|
||||
"font-weight":400,
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)"
|
||||
});
|
||||
|
||||
path.on('mouseenter', function(d) {
|
||||
var total = d3.sum(dataset.map(function(d) {
|
||||
return d.value;
|
||||
}));
|
||||
|
||||
var label;
|
||||
if (d.data.value === 1) {
|
||||
label = " host ";
|
||||
} else {
|
||||
label = " hosts ";
|
||||
}
|
||||
var percent = Math.round(1000 * d.data.value / total) / 10;
|
||||
tooltip.select('.donut-tooltip-inner').html(d.data.value + label + " (" +
|
||||
percent + "%) " + d.data.label + ".");
|
||||
//.attr('style', 'color:white;font-family:');
|
||||
tooltip.style('display', 'block');
|
||||
});
|
||||
|
||||
path.on('mouseleave', function() {
|
||||
tooltip.style('display', 'none');
|
||||
});
|
||||
|
||||
path.on('mousemove', function() {
|
||||
// d3.mouse() gives the coordinates of hte mouse, then add
|
||||
// some offset to provide breathing room for hte tooltip
|
||||
// based on the dimensions of the donut
|
||||
tooltip.style('top', (d3.mouse(this)[1] + (height/5) + 'px'))
|
||||
.style('left', (d3.mouse(this)[0] + (width/3) + 'px'));
|
||||
});
|
||||
|
||||
legend = svg.selectAll('.legend')
|
||||
.data(pie(dataset))
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', function(d, i) {
|
||||
var height = legendRectSize + legendSpacing;
|
||||
var offset = height * dataset.length / 2;
|
||||
var horz = -2 * legendRectSize;
|
||||
var vert = i * height - offset;
|
||||
return 'translate(' + horz + ',' + vert + ')';
|
||||
});
|
||||
|
||||
legend.append('rect')
|
||||
.attr('width', legendRectSize)
|
||||
.attr('height', legendRectSize)
|
||||
.attr('fill', function(d) {
|
||||
return d.data.color;
|
||||
})
|
||||
.attr('stroke', function(d) {
|
||||
return d.data.color;
|
||||
});
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', legendRectSize + legendSpacing)
|
||||
.attr('y', legendRectSize - legendSpacing)
|
||||
.text(function(d) {
|
||||
return d.data.label;
|
||||
});
|
||||
d3.select(element.find(".nv-label text")[0])
|
||||
.attr("class", "DashboardGraphs-hostStatusLabel--successful")
|
||||
.style({
|
||||
"font-family": 'Open Sans',
|
||||
"text-anchor": "start",
|
||||
"font-size": "16px",
|
||||
"text-transform" : "uppercase",
|
||||
"fill" : colors[0],
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)"
|
||||
});
|
||||
d3.select(element.find(".nv-label text")[1])
|
||||
.attr("class", "DashboardGraphs-hostStatusLabel--failed")
|
||||
.style({
|
||||
"font-family": 'Open Sans',
|
||||
"text-anchor" : "end !imporant",
|
||||
"font-size": "16px",
|
||||
"text-transform" : "uppercase",
|
||||
"fill" : colors[1],
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)"
|
||||
});
|
||||
d3.select(element.find(".nv-label text")[2])
|
||||
.attr("class", "DashboardGraphs-hostStatusLabel--successful")
|
||||
.style({
|
||||
"font-family": 'Open Sans',
|
||||
"text-anchor" : "end !imporant",
|
||||
"font-size": "16px",
|
||||
"text-transform" : "uppercase",
|
||||
"fill" : colors[2],
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)"
|
||||
});
|
||||
d3.select(element.find(".nv-label text")[3])
|
||||
.attr("class", "DashboardGraphs-hostStatusLabel--failed")
|
||||
.style({
|
||||
"font-family": 'Open Sans',
|
||||
"text-anchor" : "end !imporant",
|
||||
"font-size": "16px",
|
||||
"text-transform" : "uppercase",
|
||||
"fill" : colors[3],
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)"
|
||||
});
|
||||
return job_detail_chart;
|
||||
};
|
||||
}])
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import listGenerator from '../shared/list-generator/main';
|
||||
|
||||
export default
|
||||
angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'JobSummaryDefinition', 'InventoryHelper', 'GeneratorHelpers',
|
||||
'JobSubmissionHelper', 'LogViewerHelper', 'SearchHelper', 'PaginationHelpers', 'AdhocHelper', listGenerator.name])
|
||||
'JobSubmissionHelper', 'StandardOutHelper', 'SearchHelper', 'PaginationHelpers', 'AdhocHelper', listGenerator.name])
|
||||
|
||||
/**
|
||||
* JobsControllerInit({ scope: $scope });
|
||||
@ -22,8 +22,8 @@ export default
|
||||
* Initialize calling scope with all the bits required to support a jobs list
|
||||
*
|
||||
*/
|
||||
.factory('JobsControllerInit', ['$state', 'Find', 'DeleteJob', 'RelaunchJob', 'LogViewer', '$window',
|
||||
function($state, Find, DeleteJob, RelaunchJob, LogViewer, $window) {
|
||||
.factory('JobsControllerInit', ['$state', 'Find', 'DeleteJob', 'RelaunchJob', '$window',
|
||||
function($state, Find, DeleteJob, RelaunchJob, $window) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
iterator = (params.iterator) ? params.iterator : scope.iterator;
|
||||
|
||||
@ -1,271 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name helpers.function:License
|
||||
* @description Routines for checking and reporting license status
|
||||
* CheckLicense.test() is called in app.js, in line 532, which is when the license is checked. The license information is
|
||||
* stored in local storage using 'Store()'.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
import '../forms';
|
||||
|
||||
export default
|
||||
angular.module('LicenseHelper', ['RestServices', 'Utilities', 'LicenseUpdateFormDefinition',
|
||||
'FormGenerator', 'ParseHelper', 'ModalDialog', 'VariablesHelper', 'LicenseFormDefinition'])
|
||||
|
||||
|
||||
.factory('CheckLicense', ['$q', '$rootScope', '$compile', 'CreateDialog', 'Store',
|
||||
'LicenseUpdateForm', 'GenerateForm', 'TextareaResize', 'ToJSON', 'GetBasePath',
|
||||
'Rest', 'ProcessErrors', 'Alert', 'IsAdmin', '$location', 'pendoService',
|
||||
'Authorization', 'Wait',
|
||||
function($q, $rootScope, $compile, CreateDialog, Store, LicenseUpdateForm, GenerateForm,
|
||||
TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location,
|
||||
pendoService, Authorization, Wait) {
|
||||
return {
|
||||
getRemainingDays: function(time_remaining) {
|
||||
// assumes time_remaining will be in seconds
|
||||
var tr = parseInt(time_remaining, 10);
|
||||
return Math.floor(tr / 86400);
|
||||
},
|
||||
|
||||
shouldNotify: function(license) {
|
||||
if (license && typeof license === 'object' && Object.keys(license).length > 0) {
|
||||
// we have a license object
|
||||
if (!license.valid_key) {
|
||||
// missing valid key
|
||||
return true;
|
||||
}
|
||||
else if (license.free_instances <= 0) {
|
||||
// host count exceeded
|
||||
return true;
|
||||
}
|
||||
else if (this.getRemainingDays(license.time_remaining) < 15) {
|
||||
// below 15 days remaining on license
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// missing license object
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
isAdmin: function() {
|
||||
return IsAdmin();
|
||||
},
|
||||
|
||||
getHTML: function(license, includeFormButton) {
|
||||
|
||||
var title, html,
|
||||
contact_us = "<a href=\"http://www.ansible.com/contact-us\" target=\"_black\">contact us <i class=\"fa fa-external-link\"></i></a>",
|
||||
renew = "<a href=\"http://www.ansible.com/renew\" target=\"_blank\">ansible.com/renew <i class=\"fa fa-external-link\"></i></a>",
|
||||
pricing = "<a href=\"http://www.ansible.com/pricing\" target=\"_blank\">ansible.com/pricing <i class=\"fa fa-external-link\"></i></a>",
|
||||
license_link = "<a href=\"http://www.ansible.com/license\" target=\"_blank\">click here</a>",
|
||||
result = {},
|
||||
license_is_valid=false;
|
||||
|
||||
if (license && typeof license === 'object' && Object.keys(license).length > 0 && license.valid_key !== undefined) {
|
||||
// we have a license
|
||||
if (!license.valid_key) {
|
||||
title = "Invalid License";
|
||||
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license is invalid.</p>";
|
||||
}
|
||||
else if (this.getRemainingDays(license.time_remaining) <= 0) {
|
||||
title = "License Expired";
|
||||
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\">\n" +
|
||||
"<p>Thank you for using Ansible Tower. The Ansible Tower license has expired</p>";
|
||||
if (parseInt(license.grace_period_remaining,10) > 86400) {
|
||||
// trial licenses don't get a grace period
|
||||
if (license.trial) {
|
||||
html += "<p>Don't worry — your existing history and content has not been affected, but playbooks will no longer run and new hosts cannot be added. " +
|
||||
"If you are ready to upgrade, " + contact_us + " or visit " + pricing + " to see all of your license options. Thanks!</p>";
|
||||
} else {
|
||||
html += "<p>Don't worry — your existing history and content has not been affected, but in " + this.getRemainingDays(license.grace_period_remaining) + " days playbooks will no longer " +
|
||||
"run and new hosts cannot be added. If you are ready to upgrade, " + contact_us + " " +
|
||||
"or visit <a href=\"http://www.ansible.com/pricing\" target=\"_blank\">ansible.com/pricing <i class=\"fa fa-external-link\"></i></a> to see all of your license options. Thanks!</p>";
|
||||
}
|
||||
} else {
|
||||
html += "<p>Don’t worry — your existing history and content has not been affected, but playbooks will no longer run and new hosts cannot be added. If you are ready to renew or upgrade, contact us " +
|
||||
"at " + renew + ". Thanks!</p>";
|
||||
}
|
||||
}
|
||||
else if (this.getRemainingDays(license.time_remaining) < 15) {
|
||||
// Warning: license expiring in less than 15 days
|
||||
title = "License Warning";
|
||||
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Thank you for using Ansible Tower. The Ansible Tower license " +
|
||||
"has " + this.getRemainingDays(license.time_remaining) + " days remaining.</p>";
|
||||
// trial licenses don't get a grace period
|
||||
if (license.trial) {
|
||||
html += "<p>After this license expires, playbooks will no longer run and hosts cannot be added. If you are ready to upgrade, " + contact_us + " or visit " + pricing + " to see all of your license options. Thanks!</p>";
|
||||
} else {
|
||||
html += "<p>After this license expires, playbooks will no longer run and hosts cannot be added. If you are ready to renew or upgrade, contact us at " + renew + ". Thanks!</p>";
|
||||
}
|
||||
|
||||
// If there is exactly one day remaining, change "days remaining"
|
||||
// to "day remaining".
|
||||
html = html.replace('has 1 days remaining', 'has 1 day remaining');
|
||||
}
|
||||
else if (license.free_instances <= 0) {
|
||||
title = "Host Count Exceeded";
|
||||
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license has reached capacity for the number of managed hosts allowed. No new hosts can be added. Existing " +
|
||||
"playbooks can still be run against hosts already in inventory.</p>" +
|
||||
"<p>If you are ready to upgrade, contact us at " + renew + ". Thanks!</p>";
|
||||
|
||||
} else {
|
||||
// license is valid. the following text is displayed in the license viewer
|
||||
title = "Update License";
|
||||
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license is valid.</p>" +
|
||||
"<p>If you are ready to upgrade, contact us at " + renew + ". Thanks!</p>";
|
||||
license_is_valid = true;
|
||||
}
|
||||
} else {
|
||||
// No license
|
||||
title = "Add Your License";
|
||||
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Now that you’ve successfully installed or upgraded Ansible Tower, the next step is to add a license file. " +
|
||||
"If you don’t have a license file yet, " + license_link + " to see all of our free and paid license options.</p>" +
|
||||
"<p style=\"margin-top:15px; margin-bottom 15px; text-align:center;\"><a href=\"http://ansible.com/license\" target=\"_blank\" class=\"btn btn-danger free-button\">Get a Free Tower Trial License</a></p>";
|
||||
}
|
||||
|
||||
if (IsAdmin()) {
|
||||
html += "<p>Copy and paste the contents of your license in the field below, agree to the End User License Agreement, and click Submit.</p>";
|
||||
} else {
|
||||
html += "<p>A system administrator can install the new license by choosing View License on the Account Menu and clicking on the Update License tab.</p>";
|
||||
}
|
||||
|
||||
html += "</div>";
|
||||
|
||||
if (IsAdmin()) {
|
||||
html += GenerateForm.buildHTML(LicenseUpdateForm, { mode: 'edit', showButtons:((includeFormButton) ? true : false) });
|
||||
}
|
||||
|
||||
html += "</div>";
|
||||
|
||||
result.body = html;
|
||||
result.title = title;
|
||||
return result;
|
||||
},
|
||||
|
||||
postLicense: function(license_key, in_scope) {
|
||||
var url = GetBasePath('config'),
|
||||
self = this,
|
||||
json_data, scope;
|
||||
|
||||
scope = (in_scope) ? in_scope : self.scope;
|
||||
|
||||
json_data = ToJSON('json', license_key);
|
||||
json_data.eula_accepted = scope.eula_agreement;
|
||||
if (typeof json_data === 'object' && Object.keys(json_data).length > 0) {
|
||||
Rest.setUrl(url);
|
||||
Rest.post(json_data)
|
||||
.success(function (response) {
|
||||
response.license_info = response;
|
||||
Alert('License Accepted', 'The Ansible Tower license was updated. To review or update the license, choose View License from the Setup menu.','alert-info');
|
||||
$rootScope.features = undefined;
|
||||
|
||||
Authorization.getLicense()
|
||||
.success(function (data) {
|
||||
Authorization.setLicense(data);
|
||||
pendoService.issuePendoIdentity();
|
||||
Wait("stop");
|
||||
$location.path('/home');
|
||||
})
|
||||
.error(function () {
|
||||
Wait('stop');
|
||||
Alert('Error', 'Failed to access license information. GET returned status: ' + status, 'alert-danger',
|
||||
$location.path('/logout'));
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
.catch(function (response) {
|
||||
scope.license_json_api_error = "A valid license key in JSON format is required";
|
||||
ProcessErrors(scope, response.data, response.status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to update license. POST returned: ' + response.status
|
||||
});
|
||||
});
|
||||
} else {
|
||||
scope.license_json_api_error = "A valid license key in JSON format is required";
|
||||
}
|
||||
},
|
||||
|
||||
test: function() {
|
||||
var license = Store('license'),
|
||||
self = this,
|
||||
scope;
|
||||
|
||||
var getLicense = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
if (license === null) {
|
||||
Rest.setUrl(GetBasePath('config'));
|
||||
return Rest.get()
|
||||
.then(function (data) {
|
||||
license = data.data.license_info;
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
}, function () {
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
});
|
||||
} else {
|
||||
deferred.resolve(license);
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
||||
|
||||
var promise = getLicense();
|
||||
promise.then(function() {
|
||||
self.scope = $rootScope.$new();
|
||||
scope = self.scope;
|
||||
|
||||
if (license && typeof license === 'object' && Object.keys(license).length > 0) {
|
||||
if (license.tested) {
|
||||
return true;
|
||||
}
|
||||
license.tested = true;
|
||||
Store('license',license); //update with tested flag
|
||||
}
|
||||
|
||||
// Don't do anything when the license is valid
|
||||
if (!self.shouldNotify(license)) {
|
||||
return true; // if the license is valid it would exit 'test' here, otherwise it moves on to making the modal for the license
|
||||
}
|
||||
|
||||
$location.path('/license');
|
||||
});
|
||||
},
|
||||
|
||||
GetLicense: function(callback, inScope) {
|
||||
// Retrieve license detail
|
||||
var self = this,
|
||||
scope = (inScope) ? inScope : self.scope,
|
||||
url = GetBasePath('config');
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if (scope && callback) {
|
||||
scope.$emit(callback, data);
|
||||
}
|
||||
else if (scope) {
|
||||
scope.$emit('CheckLicenseReady', data);
|
||||
}
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve license. GET status: ' + status
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@ -1,390 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name helpers.function:LogViewer
|
||||
* @description logviewer
|
||||
*/
|
||||
|
||||
export default
|
||||
angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator', 'VariablesHelper'])
|
||||
|
||||
.factory('LogViewer', ['$location', '$compile', 'CreateDialog', 'GetJob', 'Wait', 'GenerateForm', 'LogViewerStatusForm', 'AddTable', 'AddTextarea',
|
||||
'LogViewerOptionsForm', 'EnvTable', 'GetBasePath', 'LookUpName', 'Empty', 'AddPreFormattedText', 'ParseVariableString', 'GetChoices',
|
||||
function($location, $compile, CreateDialog, GetJob, Wait, GenerateForm, LogViewerStatusForm, AddTable, AddTextarea, LogViewerOptionsForm, EnvTable,
|
||||
GetBasePath, LookUpName, Empty, AddPreFormattedText, ParseVariableString, GetChoices) {
|
||||
return function(params) {
|
||||
var parent_scope = params.scope,
|
||||
url = params.url,
|
||||
getIcon = params.getIcon,
|
||||
scope = parent_scope.$new(true),
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
pieces;
|
||||
|
||||
if (scope.removeModalReady) {
|
||||
scope.removeModalReady();
|
||||
}
|
||||
scope.removeModalReady = scope.$on('ModalReady', function() {
|
||||
Wait('stop');
|
||||
$('#logviewer-modal-dialog').dialog('open');
|
||||
});
|
||||
|
||||
if (scope.removeJobReady) {
|
||||
scope.removeJobReady();
|
||||
}
|
||||
scope.removeJobReady = scope.$on('JobReady', function(e, data) {
|
||||
var key, resizeText, elem;
|
||||
$('#status-form-container').empty();
|
||||
$('#options-form-container').empty();
|
||||
$('#stdout-form-container').empty();
|
||||
$('#traceback-form-container').empty();
|
||||
$('#variables-container').empty();
|
||||
$('#source-container').empty();
|
||||
$('#logview-tabs li:eq(1)').hide();
|
||||
$('#logview-tabs li:eq(2)').hide();
|
||||
$('#logview-tabs li:eq(4)').hide();
|
||||
$('#logview-tabs li:eq(5)').hide();
|
||||
|
||||
// Make sure subsequenct scope references don't bubble up to the parent
|
||||
for (key in LogViewerStatusForm.fields) {
|
||||
scope[key] = '';
|
||||
}
|
||||
for (key in LogViewerOptionsForm.fields) {
|
||||
scope[key] = '';
|
||||
}
|
||||
|
||||
for (key in data) {
|
||||
scope[key] = data[key];
|
||||
}
|
||||
scope.created_by = '';
|
||||
scope.job_template = '';
|
||||
|
||||
if (data.related.created_by) {
|
||||
pieces = data.related.created_by.replace(/^\//,'').replace(/\/$/,'').split('/');
|
||||
scope.created_by = parseInt(pieces[pieces.length - 1],10);
|
||||
LookUpName({
|
||||
scope: scope,
|
||||
scope_var: 'created_by',
|
||||
url: GetBasePath('users') + scope.created_by + '/'
|
||||
});
|
||||
}
|
||||
|
||||
// For jobs link the name to the job parent
|
||||
if (base === 'jobs') {
|
||||
if (data.type === 'job') {
|
||||
scope.name_link = "job_template";
|
||||
scope.job_template = data.unified_job_template;
|
||||
scope.job_template_name = (data.summary_fields.job_template) ? data.summary_fields.job_template.name : data.name;
|
||||
scope.name_id = data.unified_job_template;
|
||||
}
|
||||
if (data.type === 'project_update') {
|
||||
scope.name_link = "project";
|
||||
scope.name_id = data.unified_job_template;
|
||||
}
|
||||
if (data.type === 'inventory_update') {
|
||||
scope.name_link = "inventory_source";
|
||||
scope.name_id = scope.group;
|
||||
}
|
||||
}
|
||||
|
||||
AddTable({ scope: scope, form: LogViewerStatusForm, id: 'status-form-container', getIcon: getIcon });
|
||||
AddTable({ scope: scope, form: LogViewerOptionsForm, id: 'options-form-container', getIcon: getIcon });
|
||||
|
||||
if (data.result_stdout) {
|
||||
$('#logview-tabs li:eq(1)').show();
|
||||
var showStandardOut = (data.type !== "system_job") ? true : false;
|
||||
AddPreFormattedText({
|
||||
id: 'stdout-form-container',
|
||||
val: data.result_stdout,
|
||||
standardOut: showStandardOut,
|
||||
jobUrl: data.url
|
||||
});
|
||||
}
|
||||
|
||||
if (data.result_traceback) {
|
||||
$('#logview-tabs li:eq(2)').show();
|
||||
AddPreFormattedText({
|
||||
id: 'traceback-form-container',
|
||||
val: data.result_traceback
|
||||
});
|
||||
}
|
||||
|
||||
/*if (data.job_env) {
|
||||
EnvTable({
|
||||
id: 'env-form-container',
|
||||
vars: data.job_env
|
||||
});
|
||||
}*/
|
||||
|
||||
if (data.extra_vars) {
|
||||
$('#logview-tabs li:eq(4)').show();
|
||||
AddTextarea({
|
||||
container_id: 'variables-container',
|
||||
fld_id: 'variables',
|
||||
val: ParseVariableString(data.extra_vars)
|
||||
});
|
||||
}
|
||||
|
||||
if (data.source_vars) {
|
||||
$('#logview-tabs li:eq(5)').show();
|
||||
AddTextarea({
|
||||
container_id: 'source-container',
|
||||
fld_id: 'source-variables',
|
||||
val: ParseVariableString(data.source_vars)
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(scope.source)) {
|
||||
if (scope.removeChoicesReady) {
|
||||
scope.removeChoicesReady();
|
||||
}
|
||||
scope.removeChoicesReady = scope.$on('ChoicesReady', function() {
|
||||
scope.source_choices.every(function(e) {
|
||||
if (e.value === scope.source) {
|
||||
scope.source = e.label;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
GetChoices({
|
||||
scope: scope,
|
||||
url: GetBasePath('inventory_sources'),
|
||||
field: 'source',
|
||||
variable: 'source_choices',
|
||||
choice_name: 'choices',
|
||||
callback: 'ChoicesReady'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(scope.credential)) {
|
||||
LookUpName({
|
||||
scope: scope,
|
||||
scope_var: 'credential',
|
||||
url: GetBasePath('credentials') + scope.credential + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(scope.inventory)) {
|
||||
LookUpName({
|
||||
scope: scope,
|
||||
scope_var: 'inventory',
|
||||
url: GetBasePath('inventory') + scope.inventory + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(scope.project)) {
|
||||
LookUpName({
|
||||
scope: scope,
|
||||
scope_var: 'project',
|
||||
url: GetBasePath('projects') + scope.project + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(scope.cloud_credential)) {
|
||||
LookUpName({
|
||||
scope: scope,
|
||||
scope_var: 'cloud_credential',
|
||||
url: GetBasePath('credentials') + scope.cloud_credential + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(scope.inventory_source)) {
|
||||
LookUpName({
|
||||
scope: scope,
|
||||
scope_var: 'inventory_source',
|
||||
url: GetBasePath('inventory_sources') + scope.inventory_source + '/'
|
||||
});
|
||||
}
|
||||
|
||||
resizeText = function() {
|
||||
var u = $('#logview-tabs').outerHeight() + 25,
|
||||
h = $('#logviewer-modal-dialog').innerHeight(),
|
||||
rows = Math.floor((h - u) / 20);
|
||||
rows -= 3;
|
||||
rows = (rows < 6) ? 6 : rows;
|
||||
$('#logviewer-modal-dialog #variables').attr({ rows: rows });
|
||||
$('#logviewer-modal-dialog #source-variables').attr({ rows: rows });
|
||||
};
|
||||
|
||||
elem = angular.element(document.getElementById('logviewer-modal-dialog'));
|
||||
$compile(elem)(scope);
|
||||
|
||||
CreateDialog({
|
||||
scope: scope,
|
||||
width: 600,
|
||||
height: 550,
|
||||
minWidth: 450,
|
||||
callback: 'ModalReady',
|
||||
id: 'logviewer-modal-dialog',
|
||||
onResizeStop: resizeText,
|
||||
title: 'Job Results',
|
||||
onOpen: function() {
|
||||
$('#logview-tabs a:first').tab('show');
|
||||
$('#dialog-ok-button').focus();
|
||||
resizeText();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
GetJob({
|
||||
url: url,
|
||||
scope: scope
|
||||
});
|
||||
|
||||
scope.modalOK = function() {
|
||||
$('#logviewer-modal-dialog').dialog('close');
|
||||
scope.$destroy();
|
||||
};
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('GetJob', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
|
||||
return function(params) {
|
||||
var url = params.url,
|
||||
scope = params.scope;
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function(data){
|
||||
scope.$emit('JobReady', data);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('LookUpName', ['Rest', 'ProcessErrors', 'Empty', function(Rest, ProcessErrors, Empty) {
|
||||
return function(params) {
|
||||
var url = params.url,
|
||||
scope_var = params.scope_var,
|
||||
scope = params.scope;
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
if (scope_var === 'inventory_source') {
|
||||
scope[scope_var + '_name'] = data.summary_fields.group.name;
|
||||
}
|
||||
else if (!Empty(data.name)) {
|
||||
scope[scope_var + '_name'] = data.name;
|
||||
}
|
||||
if (!Empty(data.group)) {
|
||||
// Used for inventory_source
|
||||
scope.group = data.group;
|
||||
}
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('AddTable', ['$compile', 'Empty', 'Find', function($compile, Empty, Find) {
|
||||
return function(params) {
|
||||
var form = params.form,
|
||||
id = params.id,
|
||||
scope = params.scope,
|
||||
getIcon = params.getIcon,
|
||||
fld, html, url, e,
|
||||
urls = [
|
||||
{ "variable": "credential", "url": "/#/credentials/" },
|
||||
{ "variable": "project", "url": "/#/projects/" },
|
||||
{ "variable": "inventory", "url": "/#/inventories/" },
|
||||
{ "variable": "cloud_credential", "url": "/#/credentials/" },
|
||||
{ "variable": "inventory_source", "url": "/#/home/groups/?id={{ group }}" },
|
||||
{ "variable": "job_template", "url": "/#/job_templates/" },
|
||||
{ "variable": "created_by", "url": "/#/users/" }
|
||||
];
|
||||
html = "<table class=\"table logviewer-status\">\n";
|
||||
for (fld in form.fields) {
|
||||
if (!Empty(scope[fld])) {
|
||||
html += "<tr><td class=\"fld-label col-md-3 col-sm-3 col-xs-3\">" + form.fields[fld].label + "</td>" +
|
||||
"<td>";
|
||||
url = Find({ list: urls, key: "variable", val: fld });
|
||||
if (url) {
|
||||
html += "<a href=\"" + url.url;
|
||||
html += (fld === "inventory_source") ? "" : scope[fld];
|
||||
html += "\" ng-click=\"modalOK()\">{{ " + fld + '_name' + " }}</a>";
|
||||
}
|
||||
else if (fld === 'name' && scope.name_link) {
|
||||
url = Find({ list: urls, key: "variable", val: scope.name_link });
|
||||
html += "<a href=\"" + url.url + ( (scope.name_link === 'inventory_source') ? '' : scope.name_id ) + "\" ng-click=\"modalOK()\">{{ " +
|
||||
( (scope.name_link === 'inventory_source') ? 'inventory_source_name' : fld ) + " }}</a>";
|
||||
}
|
||||
else if (fld === 'elapsed') {
|
||||
html += scope[fld] + " <span class=\"small-text\">seconds</span>";
|
||||
}
|
||||
else if (fld === 'status') {
|
||||
if (getIcon) {
|
||||
html += "<i class=\"fa icon-job-" + getIcon(scope[fld]) + "\"></i> " + scope[fld];
|
||||
}
|
||||
else {
|
||||
html += "<i class=\"fa icon-job-" + scope[fld] + "\"></i> " + scope[fld];
|
||||
}
|
||||
if (scope.job_explanation) {
|
||||
html += "<p style=\"padding-top: 12px\">" + scope.job_explanation + "</p>";
|
||||
}
|
||||
}
|
||||
else {
|
||||
html += "{{ " + fld ;
|
||||
html += (form.fields[fld].filter) ? " | " + form.fields[fld].filter : "" ;
|
||||
html += " }}";
|
||||
}
|
||||
html += "</td></tr>\n";
|
||||
}
|
||||
}
|
||||
html += "</table>\n";
|
||||
e = angular.element(document.getElementById(id));
|
||||
e.empty().html(html);
|
||||
$compile(e)(scope);
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('AddTextarea', [ function() {
|
||||
return function(params) {
|
||||
var container_id = params.container_id,
|
||||
val = params.val,
|
||||
fld_id = params.fld_id,
|
||||
html;
|
||||
html = "<div class=\"form-group\">\n" +
|
||||
"<textarea id=\"" + fld_id + "\" ng-non-bindable class=\"form-control mono-space\" rows=\"12\" readonly>" + val + "</textarea>" +
|
||||
"</div>\n";
|
||||
$('#' + container_id).empty().html(html);
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('AddPreFormattedText', ['$rootScope', function($rootScope) {
|
||||
return function(params) {
|
||||
var id = params.id,
|
||||
val = params.val,
|
||||
html = "";
|
||||
if (params.standardOut) {
|
||||
html += '<a ng-href="' + params.jobUrl + 'stdout?format=txt_download&token=' + $rootScope.token + '" class="btn btn-primary btn-xs DownloadStandardOut DownloadStandardOut--onModal" id="download-stdout-button" type="button" aw-tool-tip="Download standard out as a .txt file" data-placement="top" ng-show="status === \'cancelled\' || status === \'failed\' || status === \'error\' || status === \'successful\'"><i class="fa fa-download DownloadStandardOut-icon DownloadStandardOut-icon--withText"></i>Download</a>';
|
||||
html += "<pre class='DownloadStandardOut-pre' ng-non-bindable>" + val + "</pre>\n";
|
||||
} else {
|
||||
html += "<pre ng-non-bindable>" + val + "</pre>\n";
|
||||
}
|
||||
$('#' + id).empty().html(html);
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('EnvTable', [ function() {
|
||||
return function(params) {
|
||||
var id = params.id,
|
||||
vars = params.vars,
|
||||
key, html;
|
||||
html = "<table class=\"table logviewer-status\">\n";
|
||||
for (key in vars) {
|
||||
html += "<tr><td class=\"fld-label col-md-4 col-sm-3 col-xs-3 break\">" + key + "</td>" +
|
||||
"<td ng-non-bindable class=\"break\">" + vars[key] + "</td></tr>\n";
|
||||
}
|
||||
html += "</table>\n";
|
||||
$('#' + id).empty().html(html);
|
||||
};
|
||||
}]);
|
||||
@ -32,14 +32,14 @@ export default
|
||||
// Which page are we on?
|
||||
if (Empty(next) && previous) {
|
||||
// no next page, but there is a previous page
|
||||
scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/, '')) + 1;
|
||||
scope[iterator + '_page'] = scope[iterator + '_num_pages'];
|
||||
} else if (next && Empty(previous)) {
|
||||
// next page available, but no previous page
|
||||
scope[iterator + '_page'] = 1;
|
||||
$('#'+iterator+'-pagination #pagination-links li:eq(1)').attr('class', 'disabled');
|
||||
} else if (next && previous) {
|
||||
// we're in between next and previous
|
||||
scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/, '')) + 1;
|
||||
scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2;
|
||||
}
|
||||
|
||||
// Calc the range of up to 10 pages to show
|
||||
|
||||
@ -3,9 +3,32 @@
|
||||
@import '../shared/branding/colors.less';
|
||||
@import '../shared/branding/colors.default.less';
|
||||
|
||||
.JobDetail-panelHeader{
|
||||
height: 50px;
|
||||
.JobDetail{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.JobDetail-leftSide{
|
||||
flex: 1 0 auto;
|
||||
width: 50%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.JobDetail-rightSide{
|
||||
flex: 1 0 auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.JobDetail-panelHeader{
|
||||
display: flex;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.JobDetail-expandContainer{
|
||||
flex: 1;
|
||||
margin: 0px;
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.JobDetail-panelHeaderText{
|
||||
@ -38,6 +61,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.JobDetail-resultRow{
|
||||
@ -45,6 +69,10 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.JobDetail-resultRowLabel{
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.JobDetail-resultRow label{
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
@ -52,7 +80,103 @@
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.JobDetail-resultRow--variables{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left:15px;
|
||||
}
|
||||
|
||||
.JobDetail-extraVars{
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.JobDetail-extraVarsLabel{
|
||||
margin-left:-15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.JobDetail-resultRowText{
|
||||
width: 40%;
|
||||
flex: 1 0 auto;
|
||||
padding:0px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.JobDetail-searchHeaderRow{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
height: 50px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.JobDetail-searchContainer{
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.JobDetail-tableToggleContainer{
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.JobDetail-tableToggle{
|
||||
padding-left:10px;
|
||||
padding-right: 10px;
|
||||
border: 1px solid @default-second-border;
|
||||
}
|
||||
|
||||
.JobDetail-tableToggle.active{
|
||||
background-color: @default-link;
|
||||
border: 1px solid @default-link;
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.JobDetail-tableToggle--left{
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.JobDetail-tableToggle--right{
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.JobDetail-searchInput{
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
.JobDetail-tableHeader:last-of-type{
|
||||
text-align:justify;
|
||||
}
|
||||
|
||||
.JobDetail-statusIcon{
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.JobDetail-tableRow--selected,
|
||||
.JobDetail-tableRow--selected > :first-child{
|
||||
border-left: 5px solid @list-row-select-bord;
|
||||
}
|
||||
|
||||
.JobDetail-tableRow--selected > :first-child > .JobDetail-statusIcon{
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.JobDetail-statusIcon--results{
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.JobDetail-graphSection{
|
||||
height: 320px;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.JobDetail-stdoutActionButton--active{
|
||||
flex:none;
|
||||
width:0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ export default
|
||||
'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer',
|
||||
'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit',
|
||||
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
|
||||
'EditSchedule',
|
||||
'EditSchedule', 'ParseTypeChange',
|
||||
function(
|
||||
$location, $rootScope, $filter, $scope, $compile, $stateParams,
|
||||
$log, ClearScope, GetBasePath, Wait, Rest, ProcessErrors,
|
||||
@ -28,7 +28,7 @@ export default
|
||||
SetTaskStyles, DigestEvent, UpdateDOM, EventViewer, DeleteJob,
|
||||
PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts,
|
||||
HostsEdit, ParseVariableString, GetChoices, fieldChoices,
|
||||
fieldLabels, EditSchedule
|
||||
fieldLabels, EditSchedule, ParseTypeChange
|
||||
) {
|
||||
ClearScope();
|
||||
|
||||
@ -41,8 +41,9 @@ export default
|
||||
job_type_options;
|
||||
|
||||
scope.plays = [];
|
||||
|
||||
scope.parseType = 'yaml';
|
||||
scope.previousTaskFailed = false;
|
||||
$scope.stdoutFullScreen = false;
|
||||
|
||||
scope.$watch('job_status', function(job_status) {
|
||||
if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") {
|
||||
@ -201,6 +202,8 @@ export default
|
||||
scope.haltEventQueue = false;
|
||||
scope.processing = false;
|
||||
scope.lessStatus = false;
|
||||
scope.lessDetail = false;
|
||||
scope.lessEvents = true;
|
||||
|
||||
scope.host_summary = {};
|
||||
scope.host_summary.ok = 0;
|
||||
@ -555,7 +558,7 @@ export default
|
||||
});
|
||||
});
|
||||
if (scope.activeTask && scope.jobData.plays[scope.activePlay] && scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) {
|
||||
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'active';
|
||||
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'JobDetail-tableRow--selected';
|
||||
}
|
||||
scope.$emit('LoadHosts');
|
||||
})
|
||||
@ -675,7 +678,7 @@ export default
|
||||
scope.host_summary.failed;
|
||||
});
|
||||
if (scope.activePlay && scope.jobData.plays[scope.activePlay]) {
|
||||
scope.jobData.plays[scope.activePlay].playActiveClass = 'active';
|
||||
scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected';
|
||||
}
|
||||
scope.$emit('LoadTasks', events_url);
|
||||
})
|
||||
@ -804,6 +807,7 @@ export default
|
||||
return true;
|
||||
});
|
||||
//scope.setSearchAll('host');
|
||||
ParseTypeChange({ scope: scope, field_id: 'pre-formatted-variables' });
|
||||
scope.$emit('LoadPlays', data.related.job_events);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
@ -839,7 +843,6 @@ export default
|
||||
$('.overlay').hide();
|
||||
$('#summary-button').hide();
|
||||
$('#hide-summary-button').hide();
|
||||
$('#job-detail-container').css({ "width": "58.33333333%", "padding-right": "7px" });
|
||||
$('#job-summary-container .job_well').css({
|
||||
'box-shadow': 'none',
|
||||
'height': 'auto'
|
||||
@ -859,12 +862,12 @@ export default
|
||||
// Detail table height adjusting. First, put page height back to 'normal'.
|
||||
$('#plays-table-detail').height(80);
|
||||
//$('#plays-table-detail').mCustomScrollbar("update");
|
||||
$('#tasks-table-detail').height(120);
|
||||
// $('#tasks-table-detail').height(120);
|
||||
//$('#tasks-table-detail').mCustomScrollbar("update");
|
||||
$('#hosts-table-detail').height(150);
|
||||
//$('#hosts-table-detail').mCustomScrollbar("update");
|
||||
height = $(window).height() - $('#main-menu-container .navbar').outerHeight() -
|
||||
$('#job-detail-container').outerHeight() - $('#job-detail-footer').outerHeight() - 20;
|
||||
$('#job-detail-container').outerHeight() - 20;
|
||||
if (height > 15) {
|
||||
// there's a bunch of white space at the bottom, let's use it
|
||||
$('#plays-table-detail').height(80 + (height * 0.10));
|
||||
@ -872,10 +875,9 @@ export default
|
||||
$('#hosts-table-detail').height(150 + (height * 0.70));
|
||||
}
|
||||
// Summary table height adjusting.
|
||||
height = ($('#job-detail-container').height() / 2) - $('#hosts-summary-section .header').outerHeight() -
|
||||
$('#hosts-summary-section .table-header').outerHeight() -
|
||||
$('#summary-search-section').outerHeight() - 20;
|
||||
$('#hosts-summary-table').height(height);
|
||||
height = ($('#job-detail-container').height() / 2) - $('#hosts-summary-section .JobDetail-searchHeaderRow').outerHeight() -
|
||||
$('#hosts-summary-section .table-header').outerHeight() - 20;
|
||||
// $('#hosts-summary-table').height(height);
|
||||
//$('#hosts-summary-table').mCustomScrollbar("update");
|
||||
scope.$emit('RefreshCompleted');
|
||||
};
|
||||
@ -980,15 +982,38 @@ export default
|
||||
|
||||
scope.toggleLessStatus = function() {
|
||||
if (!scope.lessStatus) {
|
||||
$('#job-status-form .toggle-show').slideUp(200);
|
||||
$('#job-status-form').slideUp(200);
|
||||
scope.lessStatus = true;
|
||||
}
|
||||
else {
|
||||
$('#job-status-form .toggle-show').slideDown(200);
|
||||
$('#job-status-form').slideDown(200);
|
||||
scope.lessStatus = false;
|
||||
}
|
||||
};
|
||||
|
||||
scope.toggleLessDetail = function() {
|
||||
if (!scope.lessDetail) {
|
||||
$('#job-detail-details').slideUp(200);
|
||||
scope.lessDetail = true;
|
||||
}
|
||||
else {
|
||||
$('#job-detail-details').slideDown(200);
|
||||
scope.lessDetail = false;
|
||||
}
|
||||
};
|
||||
|
||||
scope.toggleLessEvents = function() {
|
||||
if (!scope.lessEvents) {
|
||||
$('#events-summary').slideUp(200);
|
||||
scope.lessEvents = true;
|
||||
}
|
||||
else {
|
||||
$('#events-summary').slideDown(200);
|
||||
scope.lessEvents = false;
|
||||
DrawGraph({scope:scope});
|
||||
}
|
||||
};
|
||||
|
||||
scope.filterPlayStatus = function() {
|
||||
scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all';
|
||||
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
|
||||
@ -1409,16 +1434,10 @@ export default
|
||||
$scope.$emit('LoadJob');
|
||||
};
|
||||
|
||||
scope.editHost = function(id) {
|
||||
HostsEdit({
|
||||
host_scope: scope,
|
||||
group_scope: null,
|
||||
host_id: id,
|
||||
inventory_id: scope.job.inventory,
|
||||
mode: 'edit', // 'add' or 'edit'
|
||||
selected_group_id: null
|
||||
});
|
||||
};
|
||||
// Click binding for the expand/collapse button on the standard out log
|
||||
$scope.toggleStdoutFullscreen = function() {
|
||||
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
|
||||
}
|
||||
|
||||
scope.editSchedule = function() {
|
||||
// We need to get the schedule's ID out of the related links
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user