Merge branch 'devel' of github.com:ansible/ansible-tower into devel

This commit is contained in:
Wayne Witzel III
2016-04-01 09:26:24 -04:00
128 changed files with 5848 additions and 3535 deletions
+21 -2
View File
@@ -6,7 +6,7 @@ from collections import OrderedDict
# Django # Django
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils.encoding import force_text from django.utils.encoding import force_text, smart_text
# Django REST Framework # Django REST Framework
from rest_framework import exceptions from rest_framework import exceptions
@@ -37,6 +37,25 @@ class Metadata(metadata.SimpleMetadata):
if value is not None and value != '': if value is not None and value != '':
field_info[attr] = force_text(value, strings_only=True) field_info[attr] = force_text(value, strings_only=True)
# Update help text for common fields.
serializer = getattr(field, 'parent', None)
if serializer:
field_help_text = {
'id': 'Database ID for this {}.',
'name': 'Name of this {}.',
'description': 'Optional description of this {}.',
'type': 'Data type for this {}.',
'url': 'URL for this {}.',
'related': 'Data structure with URLs of related resources.',
'summary_fields': 'Data structure with name/description for related resources.',
'created': 'Timestamp when this {} was created.',
'modified': 'Timestamp when this {} was last modified.',
}
if field.field_name in field_help_text:
opts = serializer.Meta.model._meta.concrete_model._meta
verbose_name = smart_text(opts.verbose_name)
field_info['help_text'] = field_help_text[field.field_name].format(verbose_name)
# Indicate if a field has a default value. # Indicate if a field has a default value.
# FIXME: Still isn't showing all default values? # FIXME: Still isn't showing all default values?
try: try:
@@ -77,7 +96,7 @@ class Metadata(metadata.SimpleMetadata):
# Update type of fields returned... # Update type of fields returned...
if field.field_name == 'type': if field.field_name == 'type':
field_info['type'] = 'multiple choice' field_info['type'] = 'choice'
elif field.field_name == 'url': elif field.field_name == 'url':
field_info['type'] = 'string' field_info['type'] = 'string'
elif field.field_name in ('related', 'summary_fields'): elif field.field_name in ('related', 'summary_fields'):
+1 -3
View File
@@ -3,7 +3,7 @@
# Django REST Framework # Django REST Framework
from rest_framework import pagination from rest_framework import pagination
from rest_framework.utils.urls import remove_query_param, replace_query_param from rest_framework.utils.urls import replace_query_param
class Pagination(pagination.PageNumberPagination): class Pagination(pagination.PageNumberPagination):
@@ -22,6 +22,4 @@ class Pagination(pagination.PageNumberPagination):
return None return None
url = self.request and self.request.get_full_path() or '' url = self.request and self.request.get_full_path() or ''
page_number = self.page.previous_page_number() page_number = self.page.previous_page_number()
if page_number == 1:
return remove_query_param(url, self.page_query_param)
return replace_query_param(url, self.page_query_param, page_number) return replace_query_param(url, self.page_query_param, page_number)
-1
View File
@@ -117,7 +117,6 @@ class ModelAccessPermission(permissions.BasePermission):
check_method = getattr(self, 'check_%s_permissions' % request.method.lower(), None) check_method = getattr(self, 'check_%s_permissions' % request.method.lower(), None)
result = check_method and check_method(request, view, obj) result = check_method and check_method(request, view, obj)
if not result: if not result:
print('Yarr permission denied: %s %s %s' % (request.method, repr(view), repr(obj),)) # TODO: XXX: This shouldn't have been committed but anoek is sloppy, remove me after we're done fixing bugs
raise PermissionDenied() raise PermissionDenied()
return result return result
+54 -83
View File
@@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
from django.db import models from django.db import models
# from django.utils.translation import ugettext_lazy as _ # from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text, smart_text from django.utils.encoding import force_text
from django.utils.text import capfirst from django.utils.text import capfirst
# Django REST Framework # Django REST Framework
@@ -38,7 +38,7 @@ from polymorphic import PolymorphicModel
from awx.main.constants import SCHEDULEABLE_PROVIDERS from awx.main.constants import SCHEDULEABLE_PROVIDERS
from awx.main.models import * # noqa from awx.main.models import * # noqa
from awx.main.fields import ImplicitRoleField from awx.main.fields import ImplicitRoleField
from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat, camelcase_to_underscore
from awx.main.redact import REPLACE_STR from awx.main.redact import REPLACE_STR
from awx.main.conf import tower_settings from awx.main.conf import tower_settings
@@ -92,7 +92,7 @@ SUMMARIZABLE_FK_FIELDS = {
} }
def reverseGenericForeignKey(content_object): def reverse_gfk(content_object):
''' '''
Computes a reverse for a GenericForeignKey field. Computes a reverse for a GenericForeignKey field.
@@ -101,35 +101,12 @@ def reverseGenericForeignKey(content_object):
for example for example
{ 'organization': '/api/v1/organizations/1/' } { 'organization': '/api/v1/organizations/1/' }
''' '''
if content_object is None or not hasattr(content_object, 'get_absolute_url'):
return {}
ret = {} return {
if type(content_object) is Organization: camelcase_to_underscore(content_object.__class__.__name__): content_object.get_absolute_url()
ret['organization'] = reverse('api:organization_detail', args=(content_object.pk,)) }
if type(content_object) is User:
ret['user'] = reverse('api:user_detail', args=(content_object.pk,))
if type(content_object) is Team:
ret['team'] = reverse('api:team_detail', args=(content_object.pk,))
if type(content_object) is Project:
ret['project'] = reverse('api:project_detail', args=(content_object.pk,))
if type(content_object) is Inventory:
ret['inventory'] = reverse('api:inventory_detail', args=(content_object.pk,))
if type(content_object) is Host:
ret['host'] = reverse('api:host_detail', args=(content_object.pk,))
if type(content_object) is Group:
ret['group'] = reverse('api:group_detail', args=(content_object.pk,))
if type(content_object) is InventorySource:
ret['inventory_source'] = reverse('api:inventory_source_detail', args=(content_object.pk,))
if type(content_object) is Credential:
ret['credential'] = reverse('api:credential_detail', args=(content_object.pk,))
if type(content_object) is JobTemplate:
ret['job_template'] = reverse('api:job_template_detail', args=(content_object.pk,))
if type(content_object) is Role:
ret['role'] = reverse('api:role_detail', args=(content_object.pk,))
if type(content_object) is Job:
ret['job'] = reverse('api:job_detail', args=(content_object.pk,))
if type(content_object) is JobEvent:
ret['job_event'] = reverse('api:job_event_detail', args=(content_object.pk,))
return ret
class BaseSerializerMetaclass(serializers.SerializerMetaclass): class BaseSerializerMetaclass(serializers.SerializerMetaclass):
@@ -374,7 +351,6 @@ class BaseSerializer(serializers.ModelSerializer):
return obj.modified return obj.modified
def build_standard_field(self, field_name, model_field): def build_standard_field(self, field_name, model_field):
# DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits # DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits
# when a Model's editable field is set to False. The short circuit skips choice rendering. # when a Model's editable field is set to False. The short circuit skips choice rendering.
# #
@@ -391,27 +367,6 @@ class BaseSerializer(serializers.ModelSerializer):
if was_editable is False: if was_editable is False:
field_kwargs['read_only'] = True field_kwargs['read_only'] = True
# Update help text for common fields.
opts = self.Meta.model._meta.concrete_model._meta
if field_name == 'id':
field_kwargs.setdefault('help_text', 'Database ID for this %s.' % smart_text(opts.verbose_name))
elif field_name == 'name':
field_kwargs['help_text'] = 'Name of this %s.' % smart_text(opts.verbose_name)
elif field_name == 'description':
field_kwargs['help_text'] = 'Optional description of this %s.' % smart_text(opts.verbose_name)
elif field_name == 'type':
field_kwargs['help_text'] = 'Data type for this %s.' % smart_text(opts.verbose_name)
elif field_name == 'url':
field_kwargs['help_text'] = 'URL for this %s.' % smart_text(opts.verbose_name)
elif field_name == 'related':
field_kwargs['help_text'] = 'Data structure with URLs of related resources.'
elif field_name == 'summary_fields':
field_kwargs['help_text'] = 'Data structure with name/description for related resources.'
elif field_name == 'created':
field_kwargs['help_text'] = 'Timestamp when this %s was created.' % smart_text(opts.verbose_name)
elif field_name == 'modified':
field_kwargs['help_text'] = 'Timestamp when this %s was last modified.' % smart_text(opts.verbose_name)
# Pass model field default onto the serializer field if field is not read-only. # Pass model field default onto the serializer field if field is not read-only.
if model_field.has_default() and not field_kwargs.get('read_only', False): if model_field.has_default() and not field_kwargs.get('read_only', False):
field_kwargs['default'] = field_kwargs['initial'] = model_field.get_default() field_kwargs['default'] = field_kwargs['initial'] = model_field.get_default()
@@ -437,6 +392,7 @@ class BaseSerializer(serializers.ModelSerializer):
# Update the message used for the unique validator to use capitalized # Update the message used for the unique validator to use capitalized
# verbose name; keeps unique message the same as with DRF 2.x. # verbose name; keeps unique message the same as with DRF 2.x.
opts = self.Meta.model._meta.concrete_model._meta
for validator in field_kwargs.get('validators', []): for validator in field_kwargs.get('validators', []):
if isinstance(validator, validators.UniqueValidator): if isinstance(validator, validators.UniqueValidator):
unique_error_message = model_field.error_messages.get('unique', None) unique_error_message = model_field.error_messages.get('unique', None)
@@ -521,10 +477,6 @@ class BaseSerializer(serializers.ModelSerializer):
raise ValidationError(d) raise ValidationError(d)
return attrs return attrs
def to_representation(self, obj):
ret = super(BaseSerializer, self).to_representation(obj)
return ret
class EmptySerializer(serializers.Serializer): class EmptySerializer(serializers.Serializer):
pass pass
@@ -915,7 +867,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
class Meta: class Meta:
model = Project model = Project
fields = ('*', 'scm_delete_on_next_update', 'scm_update_on_launch', fields = ('*', 'organization', 'scm_delete_on_next_update', 'scm_update_on_launch',
'scm_update_cache_timeout') + \ 'scm_update_cache_timeout') + \
('last_update_failed', 'last_updated') # Backwards compatibility ('last_update_failed', 'last_updated') # Backwards compatibility
read_only_fields = ('scm_delete_on_next_update',) read_only_fields = ('scm_delete_on_next_update',)
@@ -932,7 +884,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
notifiers_any = reverse('api:project_notifiers_any_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_success = reverse('api:project_notifiers_success_list', args=(obj.pk,)),
notifiers_error = reverse('api:project_notifiers_error_list', args=(obj.pk,)), notifiers_error = reverse('api:project_notifiers_error_list', args=(obj.pk,)),
access_list = reverse('api:project_access_list', args=(obj.pk,)), access_list = reverse('api:project_access_list', args=(obj.pk,)),
)) ))
if obj.organization: if obj.organization:
res['organization'] = reverse('api:organization_detail', res['organization'] = reverse('api:organization_detail',
@@ -946,6 +898,12 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
args=(obj.last_update.pk,)) args=(obj.last_update.pk,))
return res return res
def validate(self, attrs):
if 'organization' not in attrs or type(attrs['organization']) is not Organization:
raise serializers.ValidationError('Missing organization')
return super(ProjectSerializer, self).validate(attrs)
class ProjectPlaybooksSerializer(ProjectSerializer): class ProjectPlaybooksSerializer(ProjectSerializer):
@@ -1496,7 +1454,7 @@ class RoleSerializer(BaseSerializer):
ret['teams'] = reverse('api:role_teams_list', args=(obj.pk,)) ret['teams'] = reverse('api:role_teams_list', args=(obj.pk,))
try: try:
if obj.content_object: if obj.content_object:
ret.update(reverseGenericForeignKey(obj.content_object)) ret.update(reverse_gfk(obj.content_object))
except AttributeError: except AttributeError:
# AttributeError's happen if our content_object is pointing at # AttributeError's happen if our content_object is pointing at
# a model that no longer exists. This is dirty data and ideally # a model that no longer exists. This is dirty data and ideally
@@ -1522,7 +1480,7 @@ class ResourceAccessListElementSerializer(UserSerializer):
try: try:
role_dict['resource_name'] = role.content_object.name role_dict['resource_name'] = role.content_object.name
role_dict['resource_type'] = role.content_type.name role_dict['resource_type'] = role.content_type.name
role_dict['related'] = reverseGenericForeignKey(role.content_object) role_dict['related'] = reverse_gfk(role.content_object)
except: except:
pass pass
@@ -1547,8 +1505,9 @@ class CredentialSerializer(BaseSerializer):
class Meta: class Meta:
model = Credential model = Credential
fields = ('*', 'user', 'team', 'kind', 'cloud', 'host', 'username', fields = ('*', 'deprecated_user', 'deprecated_team', 'kind', 'cloud', 'host', 'username',
'password', 'security_token', 'project', 'ssh_key_data', 'ssh_key_unlock', 'password', 'security_token', 'project', 'domain',
'ssh_key_data', 'ssh_key_unlock',
'become_method', 'become_username', 'become_password', 'become_method', 'become_username', 'become_password',
'vault_password') 'vault_password')
@@ -1562,21 +1521,16 @@ class CredentialSerializer(BaseSerializer):
def to_representation(self, obj): def to_representation(self, obj):
ret = super(CredentialSerializer, self).to_representation(obj) ret = super(CredentialSerializer, self).to_representation(obj)
if obj is not None and 'user' in ret and not obj.user: if obj is not None and 'deprecated_user' in ret and not obj.deprecated_user:
ret['user'] = None ret['deprecated_user'] = None
if obj is not None and 'team' in ret and not obj.team: if obj is not None and 'deprecated_team' in ret and not obj.deprecated_team:
ret['team'] = None ret['deprecated_team'] = None
return ret return ret
def validate(self, attrs): def validate(self, attrs):
# If creating a credential from a view that automatically sets the # Ensure old style assignment for user/team is always None
# parent_key (user or team), set the other value to None. attrs['deprecated_user'] = None
view = self.context.get('view', None) attrs['deprecated_team'] = None
parent_key = getattr(view, 'parent_key', None)
if parent_key == 'user':
attrs['team'] = None
if parent_key == 'team':
attrs['user'] = None
return super(CredentialSerializer, self).validate(attrs) return super(CredentialSerializer, self).validate(attrs)
@@ -1586,10 +1540,6 @@ class CredentialSerializer(BaseSerializer):
activity_stream = reverse('api:credential_activity_stream_list', args=(obj.pk,)), activity_stream = reverse('api:credential_activity_stream_list', args=(obj.pk,)),
access_list = reverse('api:credential_access_list', args=(obj.pk,)), access_list = reverse('api:credential_access_list', args=(obj.pk,)),
)) ))
if obj.user:
res['user'] = reverse('api:user_detail', args=(obj.user.pk,))
if obj.team:
res['team'] = reverse('api:team_detail', args=(obj.team.pk,))
return res return res
@@ -1599,10 +1549,11 @@ class JobOptionsSerializer(BaseSerializer):
fields = ('*', 'job_type', 'inventory', 'project', 'playbook', fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
'credential', 'cloud_credential', 'forks', 'limit', 'credential', 'cloud_credential', 'forks', 'limit',
'verbosity', 'extra_vars', 'job_tags', 'force_handlers', 'verbosity', 'extra_vars', 'job_tags', 'force_handlers',
'skip_tags', 'start_at_task') 'skip_tags', 'start_at_task',)
def get_related(self, obj): def get_related(self, obj):
res = super(JobOptionsSerializer, self).get_related(obj) res = super(JobOptionsSerializer, self).get_related(obj)
res['labels'] = reverse('api:job_template_label_list', args=(obj.pk,))
if obj.inventory: if obj.inventory:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
if obj.project: if obj.project:
@@ -1664,16 +1615,16 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
notifiers_success = reverse('api:job_template_notifiers_success_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,)), notifiers_error = reverse('api:job_template_notifiers_error_list', args=(obj.pk,)),
access_list = reverse('api:job_template_access_list', args=(obj.pk,)), access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
survey_spec = reverse('api:job_template_survey_spec', args=(obj.pk,)),
labels = reverse('api:job_template_label_list', args=(obj.pk,)),
)) ))
if obj.host_config_key: if obj.host_config_key:
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,)) res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
if obj.survey_enabled:
res['survey_spec'] = reverse('api:job_template_survey_spec', args=(obj.pk,))
return res return res
def get_summary_fields(self, obj): def get_summary_fields(self, obj):
d = super(JobTemplateSerializer, self).get_summary_fields(obj) d = super(JobTemplateSerializer, self).get_summary_fields(obj)
if obj.survey_enabled and ('name' in obj.survey_spec and 'description' in obj.survey_spec): if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description']) d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
request = self.context.get('request', None) request = self.context.get('request', None)
if request is not None and request.user is not None and obj.inventory is not None and obj.project is not None: if request is not None and request.user is not None and obj.inventory is not None and obj.project is not None:
@@ -1690,6 +1641,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
d['can_copy'] = False d['can_copy'] = False
d['can_edit'] = False d['can_edit'] = False
d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.order_by('-created')[:10]] d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.order_by('-created')[:10]]
d['labels'] = [{'id': x.id, 'name': x.name} for x in obj.labels.all().order_by('-name')[:10]]
return d return d
def validate(self, attrs): def validate(self, attrs):
@@ -1719,6 +1671,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
job_host_summaries = reverse('api:job_job_host_summaries_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,)), activity_stream = reverse('api:job_activity_stream_list', args=(obj.pk,)),
notifications = reverse('api:job_notifications_list', args=(obj.pk,)), notifications = reverse('api:job_notifications_list', args=(obj.pk,)),
labels = reverse('api:job_label_list', args=(obj.pk,)),
)) ))
if obj.job_template: if obj.job_template:
res['job_template'] = reverse('api:job_template_detail', res['job_template'] = reverse('api:job_template_detail',
@@ -1730,6 +1683,11 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
res['relaunch'] = reverse('api:job_relaunch', args=(obj.pk,)) res['relaunch'] = reverse('api:job_relaunch', args=(obj.pk,))
return res return res
def get_summary_fields(self, obj):
d = super(JobSerializer, self).get_summary_fields(obj)
d['labels'] = [{'id': x.id, 'name': x.name} for x in obj.labels.all().order_by('-name')[:10]]
return d
def to_internal_value(self, data): def to_internal_value(self, data):
# When creating a new job and a job template is specified, populate any # When creating a new job and a job template is specified, populate any
# fields not provided in data from the job template. # fields not provided in data from the job template.
@@ -2213,6 +2171,19 @@ class NotificationSerializer(BaseSerializer):
)) ))
return res return res
class LabelSerializer(BaseSerializer):
class Meta:
model = Label
fields = ('*', '-description', 'organization')
def get_related(self, obj):
res = super(LabelSerializer, self).get_related(obj)
if obj.organization:
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
return res
class ScheduleSerializer(BaseSerializer): class ScheduleSerializer(BaseSerializer):
class Meta: class Meta:
@@ -1,6 +1,6 @@
{% for fn, fm in serializer_fields.items %}{% spaceless %} {% for fn, fm in serializer_fields.items %}{% spaceless %}
{% if not write_only or not fm.read_only %} {% if not write_only or not fm.read_only %}
* `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if write_only and fm.required %}, required{% endif %}{% if write_only and fm.read_only %}, read-only{% endif %}{% if write_only and not fm.choices and not fm.required %}, default=`{% if fm.type == "string" or fm.type == "email" %}"{% firstof fm.default "" %}"{% else %}{{ fm.default }}{% endif %}`{% endif %}){% if fm.choices %}{% for c in fm.choices %} * `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if write_only and fm.required %}, required{% endif %}{% if write_only and fm.read_only %}, read-only{% endif %}{% if write_only and not fm.choices and not fm.required %}, default=`{% if fm.type == "string" or fm.type == "email" %}"{% firstof fm.default "" %}"{% else %}{% if fm.type == "field" and not fm.default %}None{% else %}{{ fm.default }}{% endif %}{% endif %}`{% endif %}){% if fm.choices %}{% for c in fm.choices %}
- `{% if c.0 == "" %}""{% else %}{{ c.0 }}{% endif %}`{% if c.1 != c.0 %}: {{ c.1 }}{% endif %}{% if write_only and c.0 == fm.default %} (default){% endif %}{% endfor %}{% endif %}{% endif %} - `{% if c.0 == "" %}""{% else %}{{ c.0 }}{% endif %}`{% if c.1 != c.0 %}: {{ c.1 }}{% endif %}{% if write_only and c.0 == fm.default %} (default){% endif %}{% endfor %}{% endif %}{% endif %}
{% endspaceless %} {% endspaceless %}
{% endfor %} {% endfor %}
+20 -12
View File
@@ -134,8 +134,8 @@ inventory_source_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/schedules/$', 'inventory_source_schedules_list'), 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]+)/groups/$', 'inventory_source_groups_list'),
url(r'^(?P<pk>[0-9]+)/hosts/$', 'inventory_source_hosts_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_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_error/$', 'inventory_source_notifiers_error_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'inventory_source_notifiers_success_list'), url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'inventory_source_notifiers_success_list'),
) )
@@ -171,16 +171,17 @@ role_urls = patterns('awx.api.views',
job_template_urls = patterns('awx.api.views', job_template_urls = patterns('awx.api.views',
url(r'^$', 'job_template_list'), url(r'^$', 'job_template_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_template_detail'), url(r'^(?P<pk>[0-9]+)/$', 'job_template_detail'),
url(r'^(?P<pk>[0-9]+)/launch/$', 'job_template_launch'), url(r'^(?P<pk>[0-9]+)/launch/$', 'job_template_launch'),
url(r'^(?P<pk>[0-9]+)/jobs/$', 'job_template_jobs_list'), url(r'^(?P<pk>[0-9]+)/jobs/$', 'job_template_jobs_list'),
url(r'^(?P<pk>[0-9]+)/callback/$', 'job_template_callback'), url(r'^(?P<pk>[0-9]+)/callback/$', 'job_template_callback'),
url(r'^(?P<pk>[0-9]+)/schedules/$', 'job_template_schedules_list'), 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]+)/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]+)/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_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_error/$', 'job_template_notifiers_error_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'job_template_notifiers_success_list'), url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'job_template_notifiers_success_list'),
url(r'^(?P<pk>[0-9]+)/access_list/$', 'job_template_access_list'), url(r'^(?P<pk>[0-9]+)/access_list/$', 'job_template_access_list'),
url(r'^(?P<pk>[0-9]+)/labels/$', 'job_template_label_list'),
) )
job_urls = patterns('awx.api.views', job_urls = patterns('awx.api.views',
@@ -196,6 +197,7 @@ job_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_activity_stream_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]+)/stdout/$', 'job_stdout'),
url(r'^(?P<pk>[0-9]+)/notifications/$', 'job_notifications_list'), url(r'^(?P<pk>[0-9]+)/notifications/$', 'job_notifications_list'),
url(r'^(?P<pk>[0-9]+)/labels/$', 'job_label_list'),
) )
job_host_summary_urls = patterns('awx.api.views', job_host_summary_urls = patterns('awx.api.views',
@@ -230,8 +232,8 @@ system_job_template_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/launch/$', 'system_job_template_launch'), url(r'^(?P<pk>[0-9]+)/launch/$', 'system_job_template_launch'),
url(r'^(?P<pk>[0-9]+)/jobs/$', 'system_job_template_jobs_list'), url(r'^(?P<pk>[0-9]+)/jobs/$', 'system_job_template_jobs_list'),
url(r'^(?P<pk>[0-9]+)/schedules/$', 'system_job_template_schedules_list'), url(r'^(?P<pk>[0-9]+)/schedules/$', 'system_job_template_schedules_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'system_job_template_notifiers_any_list'), url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'system_job_template_notifiers_any_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'system_job_template_notifiers_error_list'), url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'system_job_template_notifiers_error_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'system_job_template_notifiers_success_list'), url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'system_job_template_notifiers_success_list'),
) )
@@ -245,7 +247,7 @@ system_job_urls = patterns('awx.api.views',
notifier_urls = patterns('awx.api.views', notifier_urls = patterns('awx.api.views',
url(r'^$', 'notifier_list'), url(r'^$', 'notifier_list'),
url(r'^(?P<pk>[0-9]+)/$', 'notifier_detail'), url(r'^(?P<pk>[0-9]+)/$', 'notifier_detail'),
url(r'^(?P<pk>[0-9]+)/test/$', 'notifier_test'), url(r'^(?P<pk>[0-9]+)/test/$', 'notifier_test'),
url(r'^(?P<pk>[0-9]+)/notifications/$', 'notifier_notification_list'), url(r'^(?P<pk>[0-9]+)/notifications/$', 'notifier_notification_list'),
) )
@@ -254,6 +256,11 @@ notification_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'notification_detail'), url(r'^(?P<pk>[0-9]+)/$', 'notification_detail'),
) )
label_urls = patterns('awx.api.views',
url(r'^$', 'label_list'),
url(r'^(?P<pk>[0-9]+)/$', 'label_detail'),
)
schedule_urls = patterns('awx.api.views', schedule_urls = patterns('awx.api.views',
url(r'^$', 'schedule_list'), url(r'^$', 'schedule_list'),
url(r'^(?P<pk>[0-9]+)/$', 'schedule_detail'), url(r'^(?P<pk>[0-9]+)/$', 'schedule_detail'),
@@ -266,8 +273,8 @@ activity_stream_urls = patterns('awx.api.views',
) )
settings_urls = patterns('awx.api.views', settings_urls = patterns('awx.api.views',
url(r'^$', 'settings_list'), url(r'^$', 'settings_list'),
url(r'^reset/$', 'settings_reset')) url(r'^reset/$', 'settings_reset'))
v1_urls = patterns('awx.api.views', v1_urls = patterns('awx.api.views',
url(r'^$', 'api_v1_root_view'), url(r'^$', 'api_v1_root_view'),
@@ -277,7 +284,7 @@ v1_urls = patterns('awx.api.views',
url(r'^authtoken/$', 'auth_token_view'), url(r'^authtoken/$', 'auth_token_view'),
url(r'^me/$', 'user_me_list'), url(r'^me/$', 'user_me_list'),
url(r'^dashboard/$', 'dashboard_view'), url(r'^dashboard/$', 'dashboard_view'),
url(r'^dashboard/graphs/jobs/$', 'dashboard_jobs_graph_view'), url(r'^dashboard/graphs/jobs/$','dashboard_jobs_graph_view'),
url(r'^settings/', include(settings_urls)), url(r'^settings/', include(settings_urls)),
url(r'^schedules/', include(schedule_urls)), url(r'^schedules/', include(schedule_urls)),
url(r'^organizations/', include(organization_urls)), url(r'^organizations/', include(organization_urls)),
@@ -303,7 +310,8 @@ v1_urls = patterns('awx.api.views',
url(r'^system_jobs/', include(system_job_urls)), url(r'^system_jobs/', include(system_job_urls)),
url(r'^notifiers/', include(notifier_urls)), url(r'^notifiers/', include(notifier_urls)),
url(r'^notifications/', include(notification_urls)), url(r'^notifications/', include(notification_urls)),
url(r'^unified_job_templates/$', 'unified_job_template_list'), url(r'^labels/', include(label_urls)),
url(r'^unified_job_templates/$','unified_job_template_list'),
url(r'^unified_jobs/$', 'unified_job_list'), url(r'^unified_jobs/$', 'unified_job_list'),
url(r'^activity_stream/', include(activity_stream_urls)), url(r'^activity_stream/', include(activity_stream_urls)),
) )
+185 -73
View File
@@ -134,6 +134,7 @@ class ApiV1RootView(APIView):
data['roles'] = reverse('api:role_list') data['roles'] = reverse('api:role_list')
data['notifiers'] = reverse('api:notifier_list') data['notifiers'] = reverse('api:notifier_list')
data['notifications'] = reverse('api:notification_list') data['notifications'] = reverse('api:notification_list')
data['labels'] = reverse('api:label_list')
data['unified_job_templates'] = reverse('api:unified_job_template_list') data['unified_job_templates'] = reverse('api:unified_job_template_list')
data['unified_jobs'] = reverse('api:unified_job_list') data['unified_jobs'] = reverse('api:unified_job_list')
data['activity_stream'] = reverse('api:activity_stream_list') data['activity_stream'] = reverse('api:activity_stream_list')
@@ -214,7 +215,7 @@ class ApiV1ConfigView(APIView):
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
data['user_ldap_fields'] = user_ldap_fields data['user_ldap_fields'] = user_ldap_fields
if request.user.is_superuser or Organization.accessible_objects(request.user, {'write': True}).count(): if request.user.is_superuser or Organization.accessible_objects(request.user, {'write': True}).exists():
data.update(dict( data.update(dict(
project_base_dir = settings.PROJECTS_ROOT, project_base_dir = settings.PROJECTS_ROOT,
project_local_paths = Project.get_local_path_choices(), project_local_paths = Project.get_local_path_choices(),
@@ -553,6 +554,11 @@ class OrganizationList(ListCreateAPIView):
model = Organization model = Organization
serializer_class = OrganizationSerializer serializer_class = OrganizationSerializer
def get_queryset(self):
qs = Organization.accessible_objects(self.request.user, {'read': True})
qs = qs.select_related('admin_role', 'auditor_role', 'member_role')
return qs
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
"""Create a new organzation. """Create a new organzation.
@@ -564,7 +570,7 @@ class OrganizationList(ListCreateAPIView):
# by the license, then we are only willing to create this organization # by the license, then we are only willing to create this organization
# if no organizations exist in the system. # if no organizations exist in the system.
if (not feature_enabled('multiple_organizations') and if (not feature_enabled('multiple_organizations') and
self.model.objects.count() > 0): self.model.objects.exists()):
raise LicenseForbids('Your Tower license only permits a single ' raise LicenseForbids('Your Tower license only permits a single '
'organization to exist.') 'organization to exist.')
@@ -578,49 +584,37 @@ class OrganizationList(ListCreateAPIView):
return full_context return full_context
db_results = {} db_results = {}
org_qs = self.request.user.get_queryset(self.model) org_qs = self.model.accessible_objects(self.request.user, {"read": True})
org_id_list = org_qs.values('id') org_id_list = org_qs.values('id')
if len(org_id_list) == 0: if len(org_id_list) == 0:
if self.request.method == 'POST': if self.request.method == 'POST':
full_context['related_field_counts'] = {} full_context['related_field_counts'] = {}
return full_context return full_context
inv_qs = self.request.user.get_queryset(Inventory) inv_qs = Inventory.accessible_objects(self.request.user, {"read": True})
project_qs = self.request.user.get_queryset(Project) project_qs = Project.accessible_objects(self.request.user, {"read": True})
user_qs = self.request.user.get_queryset(User)
# Produce counts of Foreign Key relationships # Produce counts of Foreign Key relationships
db_results['inventories'] = inv_qs\ db_results['inventories'] = inv_qs\
.values('organization').annotate(Count('organization')).order_by('organization') .values('organization').annotate(Count('organization')).order_by('organization')
db_results['teams'] = self.request.user.get_queryset(Team)\ db_results['teams'] = Team.accessible_objects(
self.request.user, {"read": True}).values('organization').annotate(
Count('organization')).order_by('organization')
JT_reference = 'project__organization'
db_results['job_templates'] = JobTemplate.accessible_objects(
self.request.user, {"read": True}).values(JT_reference).annotate(
Count(JT_reference)).order_by(JT_reference)
db_results['projects'] = project_qs\
.values('organization').annotate(Count('organization')).order_by('organization') .values('organization').annotate(Count('organization')).order_by('organization')
# TODO: When RBAC branch merges, change this to project relationship # Other members and admins of organization are always viewable
JT_reference = 'inventory__organization' db_results['users'] = org_qs.annotate(
# Extra filter is applied on the inventory, because this catches users=Count('member_role__members', distinct=True),
# the case of deleted (and purged) inventory admins=Count('admin_role__members', distinct=True)
db_results['job_templates'] = self.request.user.get_queryset(JobTemplate)\ ).values('id', 'users', 'admins')
.filter(inventory__in=inv_qs)\
.values(JT_reference).annotate(Count(JT_reference))\
.order_by(JT_reference)
# Produce counts of m2m relationships
db_results['projects'] = Organization.projects.through.objects\
.filter(project__in=project_qs, organization__in=org_qs)\
.values('organization')\
.annotate(Count('organization')).order_by('organization')
# TODO: When RBAC branch merges, change these to role relation
db_results['users'] = Organization.users.through.objects\
.filter(user__in=user_qs, organization__in=org_qs)\
.values('organization')\
.annotate(Count('organization')).order_by('organization')
db_results['admins'] = Organization.admins.through.objects\
.filter(user__in=user_qs, organization__in=org_qs)\
.values('organization')\
.annotate(Count('organization')).order_by('organization')
count_context = {} count_context = {}
for org in org_id_list: for org in org_id_list:
@@ -632,11 +626,17 @@ class OrganizationList(ListCreateAPIView):
for res in db_results: for res in db_results:
if res == 'job_templates': if res == 'job_templates':
org_reference = JT_reference org_reference = JT_reference
elif res == 'users':
org_reference = 'id'
else: else:
org_reference = 'organization' org_reference = 'organization'
for entry in db_results[res]: for entry in db_results[res]:
org_id = entry[org_reference] org_id = entry[org_reference]
if org_id in count_context: if org_id in count_context:
if res == 'users':
count_context[org_id]['admins'] = entry['admins']
count_context[org_id]['users'] = entry['users']
continue
count_context[org_id][res] = entry['%s__count' % org_reference] count_context[org_id][res] = entry['%s__count' % org_reference]
full_context['related_field_counts'] = count_context full_context['related_field_counts'] = count_context
@@ -648,6 +648,35 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView):
model = Organization model = Organization
serializer_class = OrganizationSerializer serializer_class = OrganizationSerializer
def get_serializer_context(self, *args, **kwargs):
full_context = super(OrganizationDetail, self).get_serializer_context(*args, **kwargs)
if not hasattr(self, 'kwargs'):
return full_context
org_id = int(self.kwargs['pk'])
org_counts = {}
access_kwargs = {'accessor': self.request.user, 'permissions': {"read": True}}
direct_counts = Organization.objects.filter(id=org_id).annotate(
users=Count('member_role__members', distinct=True),
admins=Count('admin_role__members', distinct=True)
).values('users', 'admins')
org_counts = direct_counts[0]
org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(
organization__id=org_id).count()
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(
project__organization__id=org_id).count()
full_context['related_field_counts'] = {}
full_context['related_field_counts'][org_id] = org_counts
return full_context
class OrganizationInventoriesList(SubListAPIView): class OrganizationInventoriesList(SubListAPIView):
model = Inventory model = Inventory
@@ -675,6 +704,7 @@ class OrganizationProjectsList(SubListCreateAPIView):
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
parent_model = Organization parent_model = Organization
relationship = 'projects' relationship = 'projects'
parent_key = 'organization'
class OrganizationTeamsList(SubListCreateAttachDetachAPIView): class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
@@ -700,7 +730,7 @@ class OrganizationActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(OrganizationActivityStreamList, self).get(request, *args, **kwargs)
class OrganizationNotifiersList(SubListCreateAttachDetachAPIView): class OrganizationNotifiersList(SubListCreateAttachDetachAPIView):
@@ -742,6 +772,11 @@ class TeamList(ListCreateAPIView):
model = Team model = Team
serializer_class = TeamSerializer serializer_class = TeamSerializer
def get_queryset(self):
qs = Team.accessible_objects(self.request.user, {'read': True})
qs = qs.select_related('admin_role', 'auditor_role', 'member_role')
return qs
class TeamDetail(RetrieveUpdateDestroyAPIView): class TeamDetail(RetrieveUpdateDestroyAPIView):
model = Team model = Team
@@ -773,22 +808,36 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
if not sub_id: if not sub_id:
data = dict(msg='Role "id" field is missing') data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(type(self), self).post(request, *args, **kwargs) return super(TeamRolesList, self).post(request, *args, **kwargs)
class TeamProjectsList(SubListCreateAttachDetachAPIView): class TeamProjectsList(SubListAPIView):
model = Project model = Project
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
parent_model = Team parent_model = Team
relationship = 'projects'
class TeamCredentialsList(SubListCreateAttachDetachAPIView): def get_queryset(self):
team = self.get_parent_object()
self.check_parent_access(team)
team_qs = Project.objects.filter(Q(member_role__parents=team.member_role) | Q(admin_role__parents=team.member_role))
user_qs = Project.accessible_objects(self.request.user, {'read': True})
return team_qs & user_qs
class TeamCredentialsList(SubListAPIView):
model = Credential model = Credential
serializer_class = CredentialSerializer serializer_class = CredentialSerializer
parent_model = Team parent_model = Team
relationship = 'credentials'
parent_key = 'team' def get_queryset(self):
team = self.get_parent_object()
self.check_parent_access(team)
visible_creds = Credential.accessible_objects(self.request.user, {'read': True})
team_creds = Credential.objects.filter(owner_role__parents=team.member_role)
return team_creds & visible_creds
class TeamActivityStreamList(SubListAPIView): class TeamActivityStreamList(SubListAPIView):
@@ -806,16 +855,16 @@ class TeamActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(TeamActivityStreamList, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
self.check_parent_access(parent) self.check_parent_access(parent)
qs = self.request.user.get_queryset(self.model) qs = self.request.user.get_queryset(self.model)
return qs.filter(Q(team=parent) | return qs.filter(Q(team=parent) |
Q(project__in=parent.projects.all()) | Q(project__in=Project.accessible_objects(parent, {'read':True})) |
Q(credential__in=parent.credentials.all()) | Q(credential__in=Credential.accessible_objects(parent, {'read':True})))
Q(permission__in=parent.permissions.all()))
class TeamAccessList(ResourceAccessList): class TeamAccessList(ResourceAccessList):
@@ -828,6 +877,17 @@ class ProjectList(ListCreateAPIView):
model = Project model = Project
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
def get_queryset(self):
projects_qs = Project.accessible_objects(self.request.user, {'read': True})
projects_qs = projects_qs.select_related(
'organization',
'admin_role',
'auditor_role',
'member_role',
'scm_update_role',
)
return projects_qs
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# Not optimal, but make sure the project status and last_updated fields # Not optimal, but make sure the project status and last_updated fields
# are up to date here... # are up to date here...
@@ -890,7 +950,7 @@ class ProjectActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(ProjectActivityStreamList, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -1006,7 +1066,7 @@ class UserTeamsList(ListAPIView):
def get_queryset(self): def get_queryset(self):
u = User.objects.get(pk=self.kwargs['pk']) u = User.objects.get(pk=self.kwargs['pk'])
if not u.accessible_by(self.request.user, {'read': True}): if not self.request.user.can_access(User, 'read', u):
raise PermissionDenied() raise PermissionDenied()
return Team.accessible_objects(self.request.user, {'read': True}).filter(member_role__members=u) return Team.accessible_objects(self.request.user, {'read': True}).filter(member_role__members=u)
@@ -1028,7 +1088,7 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
if not sub_id: if not sub_id:
data = dict(msg='Role "id" field is missing') data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(type(self), self).post(request, *args, **kwargs) return super(UserRolesList, self).post(request, *args, **kwargs)
def check_parent_access(self, parent=None): def check_parent_access(self, parent=None):
# We hide roles that shouldn't be seen in our queryset # We hide roles that shouldn't be seen in our queryset
@@ -1041,7 +1101,6 @@ class UserProjectsList(SubListAPIView):
model = Project model = Project
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
parent_model = User parent_model = User
relationship = 'projects'
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -1050,13 +1109,19 @@ class UserProjectsList(SubListAPIView):
user_qs = Project.accessible_objects(parent, {'read': True}) user_qs = Project.accessible_objects(parent, {'read': True})
return my_qs & user_qs return my_qs & user_qs
class UserCredentialsList(SubListCreateAttachDetachAPIView): class UserCredentialsList(SubListAPIView):
model = Credential model = Credential
serializer_class = CredentialSerializer serializer_class = CredentialSerializer
parent_model = User parent_model = User
relationship = 'credentials'
parent_key = 'user' def get_queryset(self):
user = self.get_parent_object()
self.check_parent_access(user)
visible_creds = Credential.accessible_objects(self.request.user, {'read': True})
user_creds = Credential.accessible_objects(user, {'read': True})
return user_creds & visible_creds
class UserOrganizationsList(SubListAPIView): class UserOrganizationsList(SubListAPIView):
@@ -1065,6 +1130,13 @@ class UserOrganizationsList(SubListAPIView):
parent_model = User parent_model = User
relationship = 'organizations' relationship = 'organizations'
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
my_qs = Organization.accessible_objects(self.request.user, {'read': True})
user_qs = Organization.objects.filter(member_role__members=parent)
return my_qs & user_qs
class UserAdminOfOrganizationsList(SubListAPIView): class UserAdminOfOrganizationsList(SubListAPIView):
model = Organization model = Organization
@@ -1072,6 +1144,13 @@ class UserAdminOfOrganizationsList(SubListAPIView):
parent_model = User parent_model = User
relationship = 'admin_of_organizations' relationship = 'admin_of_organizations'
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
my_qs = Organization.accessible_objects(self.request.user, {'read': True})
user_qs = Organization.objects.filter(admin_role__members=parent)
return my_qs & user_qs
class UserActivityStreamList(SubListAPIView): class UserActivityStreamList(SubListAPIView):
model = ActivityStream model = ActivityStream
@@ -1088,7 +1167,7 @@ class UserActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(UserActivityStreamList, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -1158,7 +1237,7 @@ class CredentialActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(CredentialActivityStreamList, self).get(request, *args, **kwargs)
class CredentialAccessList(ResourceAccessList): class CredentialAccessList(ResourceAccessList):
@@ -1191,6 +1270,11 @@ class InventoryList(ListCreateAPIView):
model = Inventory model = Inventory
serializer_class = InventorySerializer serializer_class = InventorySerializer
def get_queryset(self):
qs = Inventory.accessible_objects(self.request.user, {'read': True})
qs = qs.select_related('admin_role', 'auditor_role', 'updater_role', 'executor_role')
return qs
class InventoryDetail(RetrieveUpdateDestroyAPIView): class InventoryDetail(RetrieveUpdateDestroyAPIView):
model = Inventory model = Inventory
@@ -1217,7 +1301,7 @@ class InventoryActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(InventoryActivityStreamList, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -1337,7 +1421,7 @@ class HostActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(HostActivityStreamList, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -1540,7 +1624,7 @@ class GroupActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(GroupActivityStreamList, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -1791,7 +1875,7 @@ class InventorySourceActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(InventorySourceActivityStreamList, self).get(request, *args, **kwargs)
class InventorySourceNotifiersAnyList(SubListCreateAttachDetachAPIView): class InventorySourceNotifiersAnyList(SubListCreateAttachDetachAPIView):
@@ -2055,7 +2139,7 @@ class JobTemplateActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(JobTemplateActivityStreamList, self).get(request, *args, **kwargs)
class JobTemplateNotifiersAnyList(SubListCreateAttachDetachAPIView): class JobTemplateNotifiersAnyList(SubListCreateAttachDetachAPIView):
@@ -2078,6 +2162,14 @@ class JobTemplateNotifiersSuccessList(SubListCreateAttachDetachAPIView):
parent_model = JobTemplate parent_model = JobTemplate
relationship = 'notifiers_success' relationship = 'notifiers_success'
class JobTemplateLabelList(SubListCreateAttachDetachAPIView):
model = Label
serializer_class = LabelSerializer
parent_model = JobTemplate
relationship = 'labels'
parent_key = 'job_template'
class JobTemplateCallback(GenericAPIView): class JobTemplateCallback(GenericAPIView):
model = JobTemplate model = JobTemplate
@@ -2120,15 +2212,15 @@ class JobTemplateCallback(GenericAPIView):
return set() return set()
# Find the host objects to search for a match. # Find the host objects to search for a match.
obj = self.get_object() obj = self.get_object()
qs = obj.inventory.hosts hosts = obj.inventory.hosts.all()
# First try for an exact match on the name. # First try for an exact match on the name.
try: try:
return set([qs.get(name__in=remote_hosts)]) return set([hosts.get(name__in=remote_hosts)])
except (Host.DoesNotExist, Host.MultipleObjectsReturned): except (Host.DoesNotExist, Host.MultipleObjectsReturned):
pass pass
# Next, try matching based on name or ansible_ssh_host variable. # Next, try matching based on name or ansible_ssh_host variable.
matches = set() matches = set()
for host in qs: for host in hosts:
ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '')
if ansible_ssh_host in remote_hosts: if ansible_ssh_host in remote_hosts:
matches.add(host) matches.add(host)
@@ -2137,8 +2229,9 @@ class JobTemplateCallback(GenericAPIView):
matches.add(host) matches.add(host)
if len(matches) == 1: if len(matches) == 1:
return matches return matches
# Try to resolve forward addresses for each host to find matches. # Try to resolve forward addresses for each host to find matches.
for host in qs: for host in hosts:
hostnames = set([host.name]) hostnames = set([host.name])
ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '')
if ansible_ssh_host: if ansible_ssh_host:
@@ -2342,6 +2435,14 @@ class JobDetail(RetrieveUpdateDestroyAPIView):
return self.http_method_not_allowed(request, *args, **kwargs) return self.http_method_not_allowed(request, *args, **kwargs)
return super(JobDetail, self).update(request, *args, **kwargs) return super(JobDetail, self).update(request, *args, **kwargs)
class JobLabelList(SubListAPIView):
model = Label
serializer_class = LabelSerializer
parent_model = Job
relationship = 'labels'
parent_key = 'job'
class JobActivityStreamList(SubListAPIView): class JobActivityStreamList(SubListAPIView):
model = ActivityStream model = ActivityStream
@@ -2358,7 +2459,7 @@ class JobActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(JobActivityStreamList, self).get(request, *args, **kwargs)
class JobStart(GenericAPIView): class JobStart(GenericAPIView):
@@ -2972,7 +3073,7 @@ class AdHocCommandActivityStreamList(SubListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(AdHocCommandActivityStreamList, self).get(request, *args, **kwargs)
class SystemJobList(ListCreateAPIView): class SystemJobList(ListCreateAPIView):
@@ -3157,6 +3258,18 @@ class NotificationDetail(RetrieveAPIView):
serializer_class = NotificationSerializer serializer_class = NotificationSerializer
new_in_300 = True new_in_300 = True
class LabelList(ListCreateAPIView):
model = Label
serializer_class = LabelSerializer
new_in_300 = True
class LabelDetail(RetrieveUpdateAPIView):
model = Label
serializer_class = LabelSerializer
new_in_300 = True
class ActivityStreamList(SimpleListAPIView): class ActivityStreamList(SimpleListAPIView):
model = ActivityStream model = ActivityStream
@@ -3171,7 +3284,7 @@ class ActivityStreamList(SimpleListAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(ActivityStreamList, self).get(request, *args, **kwargs)
class ActivityStreamDetail(RetrieveAPIView): class ActivityStreamDetail(RetrieveAPIView):
@@ -3188,7 +3301,7 @@ class ActivityStreamDetail(RetrieveAPIView):
'the activity stream.') 'the activity stream.')
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(ActivityStreamDetail, self).get(request, *args, **kwargs)
class SettingsList(ListCreateAPIView): class SettingsList(ListCreateAPIView):
@@ -3265,7 +3378,7 @@ class RoleList(ListAPIView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_superuser: if self.request.user.is_superuser:
return Role.objects return Role.objects.all()
return Role.visible_roles(self.request.user) return Role.visible_roles(self.request.user)
@@ -3283,13 +3396,12 @@ class RoleUsersList(SubListCreateAttachDetachAPIView):
serializer_class = UserSerializer serializer_class = UserSerializer
parent_model = Role parent_model = Role
relationship = 'members' relationship = 'members'
permission_classes = (IsAuthenticated,)
new_in_300 = True new_in_300 = True
def get_queryset(self): def get_queryset(self):
# XXX: Access control role = self.get_parent_object()
role = Role.objects.get(pk=self.kwargs['pk']) self.check_parent_access(role)
return role.members return role.members.all()
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# Forbid implicit role creation here # Forbid implicit role creation here
@@ -3297,7 +3409,7 @@ class RoleUsersList(SubListCreateAttachDetachAPIView):
if not sub_id: if not sub_id:
data = dict(msg='Role "id" field is missing') data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(type(self), self).post(request, *args, **kwargs) return super(RoleUsersList, self).post(request, *args, **kwargs)
class RoleTeamsList(ListAPIView): class RoleTeamsList(ListAPIView):
@@ -3312,7 +3424,7 @@ class RoleTeamsList(ListAPIView):
def get_queryset(self): def get_queryset(self):
# TODO: Check # TODO: Check
role = Role.objects.get(pk=self.kwargs['pk']) role = Role.objects.get(pk=self.kwargs['pk'])
return Team.objects.filter(member_role__children__in=[role]) return Team.objects.filter(member_role__children=role)
def post(self, request, pk, *args, **kwargs): def post(self, request, pk, *args, **kwargs):
# Forbid implicit role creation here # Forbid implicit role creation here
@@ -3345,7 +3457,7 @@ class RoleParentsList(SubListAPIView):
# XXX: This should be the intersection between the roles of the user # XXX: This should be the intersection between the roles of the user
# and the roles that the requesting user has access to see # and the roles that the requesting user has access to see
role = Role.objects.get(pk=self.kwargs['pk']) role = Role.objects.get(pk=self.kwargs['pk'])
return role.parents return role.parents.all()
class RoleChildrenList(SubListAPIView): class RoleChildrenList(SubListAPIView):
+116 -83
View File
@@ -21,6 +21,7 @@ from awx.main.models.mixins import ResourceMixin
from awx.main.models.rbac import ALL_PERMISSIONS from awx.main.models.rbac import ALL_PERMISSIONS
from awx.api.license import LicenseForbids from awx.api.license import LicenseForbids
from awx.main.task_engine import TaskSerializer from awx.main.task_engine import TaskSerializer
from awx.main.conf import tower_settings
__all__ = ['get_user_queryset', 'check_user_access', __all__ = ['get_user_queryset', 'check_user_access',
'user_accessible_objects', 'user_accessible_by', 'user_accessible_objects', 'user_accessible_by',
@@ -212,16 +213,16 @@ class UserAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
if self.user.is_superuser: if self.user.is_superuser:
return User.objects return User.objects.all()
if tower_settings.ORG_ADMINS_CAN_SEE_ALL_USERS and self.user.admin_of_organizations.exists():
return User.objects.all()
viewable_users_set = set() viewable_users_set = set()
viewable_users_set.update(self.user.roles.values_list('ancestors__members__id', flat=True)) viewable_users_set.update(self.user.roles.values_list('ancestors__members__id', flat=True))
viewable_users_set.update(self.user.roles.values_list('descendents__members__id', flat=True)) viewable_users_set.update(self.user.roles.values_list('descendents__members__id', flat=True))
return User.objects.filter(id__in=viewable_users_set) return User.objects.filter(id__in=viewable_users_set)
#qs = User.objects.filter(self.user, {'read':True})
#qs = User.objects.
#return qs
def can_add(self, data): def can_add(self, data):
if data is not None and 'is_superuser' in data: if data is not None and 'is_superuser' in data:
@@ -271,8 +272,7 @@ class OrganizationAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.accessible_objects(self.user, {'read':True})
qs = qs.select_related('created_by', 'modified_by') return qs.select_related('created_by', 'modified_by').all()
return qs
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser: if self.user.is_superuser:
@@ -307,8 +307,7 @@ class InventoryAccess(BaseAccess):
def get_queryset(self, allowed=None, ad_hoc=None): def get_queryset(self, allowed=None, ad_hoc=None):
qs = self.model.accessible_objects(self.user, {'read': True}) qs = self.model.accessible_objects(self.user, {'read': True})
qs = qs.select_related('created_by', 'modified_by', 'organization') return qs.select_related('created_by', 'modified_by', 'organization').all()
return qs
def can_read(self, obj): def can_read(self, obj):
return obj.accessible_by(self.user, {'read': True}) return obj.accessible_by(self.user, {'read': True})
@@ -365,8 +364,7 @@ class HostAccess(BaseAccess):
qs = qs.select_related('created_by', 'modified_by', 'inventory', qs = qs.select_related('created_by', 'modified_by', 'inventory',
'last_job__job_template', 'last_job__job_template',
'last_job_host_summary__job') 'last_job_host_summary__job')
qs = qs.prefetch_related('groups') return qs.prefetch_related('groups').all()
return qs
def can_read(self, obj): def can_read(self, obj):
return obj and obj.inventory.accessible_by(self.user, {'read':True}) return obj and obj.inventory.accessible_by(self.user, {'read':True})
@@ -418,8 +416,7 @@ class GroupAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.accessible_objects(self.user, {'read':True})
qs = qs.select_related('created_by', 'modified_by', 'inventory') qs = qs.select_related('created_by', 'modified_by', 'inventory')
qs = qs.prefetch_related('parents', 'children', 'inventory_source') return qs.prefetch_related('parents', 'children', 'inventory_source').all()
return qs
def can_read(self, obj): def can_read(self, obj):
return obj and obj.inventory.accessible_by(self.user, {'read':True}) return obj and obj.inventory.accessible_by(self.user, {'read':True})
@@ -471,7 +468,7 @@ class InventorySourceAccess(BaseAccess):
model = InventorySource model = InventorySource
def get_queryset(self): def get_queryset(self):
qs = self.model.objects qs = self.model.objects.all()
qs = qs.select_related('created_by', 'modified_by', 'group', 'inventory') qs = qs.select_related('created_by', 'modified_by', 'group', 'inventory')
inventory_ids = set(self.user.get_queryset(Inventory).values_list('id', flat=True)) inventory_ids = set(self.user.get_queryset(Inventory).values_list('id', flat=True))
return qs.filter(Q(inventory_id__in=inventory_ids) | return qs.filter(Q(inventory_id__in=inventory_ids) |
@@ -543,8 +540,7 @@ class CredentialAccess(BaseAccess):
permitted to see. permitted to see.
""" """
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.accessible_objects(self.user, {'read':True})
qs = qs.select_related('created_by', 'modified_by', 'user', 'team') return qs.select_related('created_by', 'modified_by').all()
return qs
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser: if self.user.is_superuser:
@@ -588,8 +584,7 @@ class TeamAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.accessible_objects(self.user, {'read':True})
qs = qs.select_related('created_by', 'modified_by', 'organization') return qs.select_related('created_by', 'modified_by', 'organization').all()
return qs
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser: if self.user.is_superuser:
@@ -631,16 +626,15 @@ class ProjectAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
if self.user.is_superuser: if self.user.is_superuser:
return self.model.objects return self.model.objects.all()
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.accessible_objects(self.user, {'read':True})
qs = qs.select_related('modified_by', 'credential', 'current_job', 'last_job') return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all()
return qs
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser: if self.user.is_superuser:
return True return True
qs = Organization.accessible_objects(self.user, ALL_PERMISSIONS) qs = Organization.accessible_objects(self.user, ALL_PERMISSIONS)
return bool(qs.count() > 0) return qs.exists()
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser: if self.user.is_superuser:
@@ -664,7 +658,7 @@ class ProjectUpdateAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
if self.user.is_superuser: if self.user.is_superuser:
return self.model.objects return self.model.objects.all()
qs = ProjectUpdate.objects.distinct() qs = ProjectUpdate.objects.distinct()
qs = qs.select_related('created_by', 'modified_by', 'project') qs = qs.select_related('created_by', 'modified_by', 'project')
project_ids = set(self.user.get_queryset(Project).values_list('id', flat=True)) project_ids = set(self.user.get_queryset(Project).values_list('id', flat=True))
@@ -693,9 +687,8 @@ class JobTemplateAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.accessible_objects(self.user, {'read':True})
qs = qs.select_related('created_by', 'modified_by', 'inventory', 'project', return qs.select_related('created_by', 'modified_by', 'inventory', 'project',
'credential', 'cloud_credential', 'next_schedule') 'credential', 'cloud_credential', 'next_schedule').all()
return qs
def can_read(self, obj): def can_read(self, obj):
# you can only see the job templates that you have permission to launch. # you can only see the job templates that you have permission to launch.
@@ -814,7 +807,7 @@ class JobAccess(BaseAccess):
'project', 'credential', 'cloud_credential', 'job_template') 'project', 'credential', 'cloud_credential', 'job_template')
qs = qs.prefetch_related('unified_job_template') qs = qs.prefetch_related('unified_job_template')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs.all()
credential_ids = self.user.get_queryset(Credential) credential_ids = self.user.get_queryset(Credential)
return qs.filter( return qs.filter(
@@ -904,16 +897,13 @@ class AdHocCommandAccess(BaseAccess):
qs = qs.select_related('created_by', 'modified_by', 'inventory', qs = qs.select_related('created_by', 'modified_by', 'inventory',
'credential') 'credential')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs.all()
credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True)) credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True))
inventory_qs = Inventory.accessible_objects(self.user, {'read': True, 'execute': True}) inventory_qs = Inventory.accessible_objects(self.user, {'read': True, 'execute': True})
qs = qs.filter( return qs.filter(credential_id__in=credential_ids,
credential_id__in=credential_ids, inventory__in=inventory_qs)
inventory__in=inventory_qs,
)
return qs
def can_add(self, data): def can_add(self, data):
if not data or '_method' in data: # So the browseable API will work? if not data or '_method' in data: # So the browseable API will work?
@@ -966,12 +956,11 @@ class AdHocCommandEventAccess(BaseAccess):
qs = qs.select_related('ad_hoc_command', 'host') qs = qs.select_related('ad_hoc_command', 'host')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs.all()
ad_hoc_command_qs = self.user.get_queryset(AdHocCommand) ad_hoc_command_qs = self.user.get_queryset(AdHocCommand)
host_qs = self.user.get_queryset(Host) host_qs = self.user.get_queryset(Host)
qs = qs.filter(Q(host__isnull=True) | Q(host__in=host_qs), return qs.filter(Q(host__isnull=True) | Q(host__in=host_qs),
ad_hoc_command__in=ad_hoc_command_qs) ad_hoc_command__in=ad_hoc_command_qs)
return qs
def can_add(self, data): def can_add(self, data):
return False return False
@@ -993,7 +982,7 @@ class JobHostSummaryAccess(BaseAccess):
qs = self.model.objects qs = self.model.objects
qs = qs.select_related('job', 'job__job_template', 'host') qs = qs.select_related('job', 'job__job_template', 'host')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs.all()
job_qs = self.user.get_queryset(Job) job_qs = self.user.get_queryset(Job)
host_qs = self.user.get_queryset(Host) host_qs = self.user.get_queryset(Host)
return qs.filter(job__in=job_qs, host__in=host_qs) return qs.filter(job__in=job_qs, host__in=host_qs)
@@ -1015,7 +1004,7 @@ class JobEventAccess(BaseAccess):
model = JobEvent model = JobEvent
def get_queryset(self): def get_queryset(self):
qs = self.model.objects qs = self.model.objects.all()
qs = qs.select_related('job', 'job__job_template', 'host', 'parent') qs = qs.select_related('job', 'job__job_template', 'host', 'parent')
qs = qs.prefetch_related('hosts', 'children') qs = qs.prefetch_related('hosts', 'children')
@@ -1025,12 +1014,11 @@ class JobEventAccess(BaseAccess):
event_data__contains='"module_name": "async_status"') event_data__contains='"module_name": "async_status"')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs.all()
job_qs = self.user.get_queryset(Job) job_qs = self.user.get_queryset(Job)
host_qs = self.user.get_queryset(Host) host_qs = self.user.get_queryset(Host)
qs = qs.filter(Q(host__isnull=True) | Q(host__in=host_qs), return qs.filter(Q(host__isnull=True) | Q(host__in=host_qs), job__in=job_qs)
job__in=job_qs)
return qs
def can_add(self, data): def can_add(self, data):
return False return False
@@ -1052,7 +1040,7 @@ class UnifiedJobTemplateAccess(BaseAccess):
model = UnifiedJobTemplate model = UnifiedJobTemplate
def get_queryset(self): def get_queryset(self):
qs = self.model.objects qs = self.model.objects.all()
project_qs = self.user.get_queryset(Project).filter(scm_type__in=[s[0] for s in Project.SCM_TYPE_CHOICES]) project_qs = self.user.get_queryset(Project).filter(scm_type__in=[s[0] for s in Project.SCM_TYPE_CHOICES])
inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES) inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES)
job_template_qs = self.user.get_queryset(JobTemplate) job_template_qs = self.user.get_queryset(JobTemplate)
@@ -1066,14 +1054,18 @@ class UnifiedJobTemplateAccess(BaseAccess):
'last_job', 'last_job',
'current_job', 'current_job',
) )
qs = qs.prefetch_related(
'project',
'inventory',
'credential',
'cloud_credential',
)
return qs # WISH - sure would be nice if the following worked, but it does not.
# In the future, as django and polymorphic libs are upgraded, try again.
#qs = qs.prefetch_related(
# 'project',
# 'inventory',
# 'credential',
# 'cloud_credential',
#)
return qs.all()
class UnifiedJobAccess(BaseAccess): class UnifiedJobAccess(BaseAccess):
''' '''
@@ -1084,7 +1076,7 @@ class UnifiedJobAccess(BaseAccess):
model = UnifiedJob model = UnifiedJob
def get_queryset(self): def get_queryset(self):
qs = self.model.objects qs = self.model.objects.all()
project_update_qs = self.user.get_queryset(ProjectUpdate) project_update_qs = self.user.get_queryset(ProjectUpdate)
inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES) inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES)
job_qs = self.user.get_queryset(Job) job_qs = self.user.get_queryset(Job)
@@ -1101,21 +1093,27 @@ class UnifiedJobAccess(BaseAccess):
) )
qs = qs.prefetch_related( qs = qs.prefetch_related(
'unified_job_template', 'unified_job_template',
'project',
'inventory',
'credential',
'job_template',
'inventory_source',
'cloud_credential',
'project___credential',
'inventory_source___credential',
'inventory_source___inventory',
'job_template__inventory',
'job_template__project',
'job_template__credential',
'job_template__cloud_credential',
) )
return qs
# WISH - sure would be nice if the following worked, but it does not.
# In the future, as django and polymorphic libs are upgraded, try again.
#qs = qs.prefetch_related(
# 'project',
# 'inventory',
# 'credential',
# 'job_template',
# 'inventory_source',
# 'cloud_credential',
# 'project___credential',
# 'inventory_source___credential',
# 'inventory_source___inventory',
# 'job_template__inventory',
# 'job_template__project',
# 'job_template__credential',
# 'job_template__cloud_credential',
#)
return qs.all()
class ScheduleAccess(BaseAccess): class ScheduleAccess(BaseAccess):
''' '''
@@ -1129,7 +1127,7 @@ class ScheduleAccess(BaseAccess):
qs = qs.select_related('created_by', 'modified_by') qs = qs.select_related('created_by', 'modified_by')
qs = qs.prefetch_related('unified_job_template') qs = qs.prefetch_related('unified_job_template')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs.all()
job_template_qs = self.user.get_queryset(JobTemplate) job_template_qs = self.user.get_queryset(JobTemplate)
inventory_source_qs = self.user.get_queryset(InventorySource) inventory_source_qs = self.user.get_queryset(InventorySource)
project_qs = self.user.get_queryset(Project) project_qs = self.user.get_queryset(Project)
@@ -1182,10 +1180,7 @@ class NotifierAccess(BaseAccess):
model = Notifier model = Notifier
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.distinct() return self.model.objects.distinct().all()
if self.user.is_superuser:
return qs
return qs
class NotificationAccess(BaseAccess): class NotificationAccess(BaseAccess):
''' '''
@@ -1194,11 +1189,19 @@ class NotificationAccess(BaseAccess):
model = Notification model = Notification
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.distinct() return self.model.objects.distinct().all()
if self.user.is_superuser:
return qs
return qs
class LabelAccess(BaseAccess):
'''
I can see/use a Label if I have permission to
'''
model = Label
def get_queryset(self):
return self.model.objects.distinct().all()
def can_delete(self, obj):
return False
class ActivityStreamAccess(BaseAccess): class ActivityStreamAccess(BaseAccess):
''' '''
@@ -1208,14 +1211,20 @@ class ActivityStreamAccess(BaseAccess):
model = ActivityStream model = ActivityStream
def get_queryset(self): def get_queryset(self):
qs = self.model.objects qs = self.model.objects.all()
qs = qs.select_related('actor') qs = qs.select_related('actor')
qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source', qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source',
'inventory_update', 'credential', 'team', 'project', 'project_update', 'inventory_update', 'credential', 'team', 'project', 'project_update',
'permission', 'job_template', 'job') 'permission', 'job_template', 'job')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs.all()
# All of these filters are noops and tests fail when we do qs =
# qs.filter for them, so we need to figure out what the intent was,
# fix this up, and add some tests to enforce the expected behavior
# - anoek - 2016-03-31
'''
#Inventory filter #Inventory filter
inventory_qs = self.user.get_queryset(Inventory) inventory_qs = self.user.get_queryset(Inventory)
qs.filter(inventory__in=inventory_qs) qs.filter(inventory__in=inventory_qs)
@@ -1228,11 +1237,11 @@ class ActivityStreamAccess(BaseAccess):
#Inventory Source Filter #Inventory Source Filter
qs.filter(Q(inventory_source__inventory__in=inventory_qs) | qs.filter(Q(inventory_source__inventory__in=inventory_qs) |
Q(inventory_source__group__inventory__in=inventory_qs)) Q(inventory_source__group__inventory__in=inventory_qs))
#Inventory Update Filter #Inventory Update Filter
qs.filter(Q(inventory_update__inventory_source__inventory__in=inventory_qs) | qs.filter(Q(inventory_update__inventory_source__inventory__in=inventory_qs) |
Q(inventory_update__inventory_source__group__inventory__in=inventory_qs)) Q(inventory_update__inventory_source__group__inventory__in=inventory_qs))
#Credential Update Filter #Credential Update Filter
credential_qs = self.user.get_queryset(Credential) credential_qs = self.user.get_queryset(Credential)
@@ -1260,8 +1269,9 @@ class ActivityStreamAccess(BaseAccess):
# Ad Hoc Command Filter # Ad Hoc Command Filter
ad_hoc_command_qs = self.user.get_queryset(AdHocCommand) ad_hoc_command_qs = self.user.get_queryset(AdHocCommand)
qs.filter(ad_hoc_command__in=ad_hoc_command_qs) qs.filter(ad_hoc_command__in=ad_hoc_command_qs)
'''
return qs return qs.all()
def can_add(self, data): def can_add(self, data):
return False return False
@@ -1278,8 +1288,8 @@ class CustomInventoryScriptAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
if self.user.is_superuser: if self.user.is_superuser:
return self.model.objects.distinct() return self.model.objects.distinct().all()
return self.model.accessible_by(self.user, {'read':True}) return self.model.accessible_objects(self.user, {'read':True}).all()
def can_read(self, obj): def can_read(self, obj):
if self.user.is_superuser: if self.user.is_superuser:
@@ -1328,7 +1338,11 @@ class TowerSettingsAccess(BaseAccess):
class RoleAccess(BaseAccess): class RoleAccess(BaseAccess):
''' '''
TODO: XXX: Needs implemenation - I can see roles when
- I am a super user
- I am a member of that role
- The role is a descdendent role of a role I am a member of
- The role is an implicit role of an object that I can see a role of.
''' '''
model = Role model = Role
@@ -1336,11 +1350,26 @@ class RoleAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
if self.user.is_superuser: if self.user.is_superuser:
return self.model.objects.all() return self.model.objects.all()
return self.model.accessible_objects(self.user, {'read':True}) return Role.objects.none()
def can_change(self, obj, data): def can_change(self, obj, data):
return self.user.is_superuser return self.user.is_superuser
def can_read(self, obj):
if not obj:
return False
if self.user.is_superuser:
return True
if obj.object_id:
sister_roles = Role.objects.filter(
content_type = obj.content_type,
object_id = obj.object_id
)
else:
sister_roles = obj
return self.user.roles.filter(descendents__in=sister_roles).exists()
def can_add(self, obj, data): def can_add(self, obj, data):
# Unsupported for now # Unsupported for now
return False return False
@@ -1363,6 +1392,9 @@ class RoleAccess(BaseAccess):
return False return False
register_access(User, UserAccess) register_access(User, UserAccess)
register_access(Organization, OrganizationAccess) register_access(Organization, OrganizationAccess)
register_access(Inventory, InventoryAccess) register_access(Inventory, InventoryAccess)
@@ -1391,3 +1423,4 @@ register_access(TowerSettings, TowerSettingsAccess)
register_access(Role, RoleAccess) register_access(Role, RoleAccess)
register_access(Notifier, NotifierAccess) register_access(Notifier, NotifierAccess)
register_access(Notification, NotificationAccess) register_access(Notification, NotificationAccess)
register_access(Label, LabelAccess)
+11 -12
View File
@@ -3,7 +3,6 @@
# Django # Django
from django.db.models.signals import ( from django.db.models.signals import (
post_init,
pre_save, pre_save,
post_save, post_save,
post_delete, post_delete,
@@ -17,6 +16,7 @@ from django.db.models.fields.related import (
ManyRelatedObjectsDescriptor, ManyRelatedObjectsDescriptor,
ReverseManyRelatedObjectsDescriptor, ReverseManyRelatedObjectsDescriptor,
) )
from django.utils.encoding import smart_text
# AWX # AWX
from awx.main.models.rbac import RolePermission, Role, batch_role_ancestor_rebuilding from awx.main.models.rbac import RolePermission, Role, batch_role_ancestor_rebuilding
@@ -67,7 +67,7 @@ def resolve_role_field(obj, field):
if len(field_components) == 1: if len(field_components) == 1:
if type(obj) is not ImplicitRoleDescriptor and type(obj) is not Role: if type(obj) is not ImplicitRoleDescriptor and type(obj) is not Role:
raise Exception('%s refers to a %s, not an ImplicitRoleField or Role' % (field, str(type(obj)))) raise Exception(smart_text('{} refers to a {}, not an ImplicitRoleField or Role'.format(field, type(obj))))
ret.append(obj) ret.append(obj)
else: else:
if type(obj) is ManyRelatedObjectsDescriptor: if type(obj) is ManyRelatedObjectsDescriptor:
@@ -105,7 +105,6 @@ class ImplicitRoleField(models.ForeignKey):
setattr(cls, '__implicit_role_fields', []) setattr(cls, '__implicit_role_fields', [])
getattr(cls, '__implicit_role_fields').append(self) getattr(cls, '__implicit_role_fields').append(self)
post_init.connect(self._post_init, cls, True, dispatch_uid='implicit-role-post-init')
pre_save.connect(self._pre_save, cls, True, dispatch_uid='implicit-role-pre-save') pre_save.connect(self._pre_save, cls, True, dispatch_uid='implicit-role-pre-save')
post_save.connect(self._post_save, cls, True, dispatch_uid='implicit-role-post-save') post_save.connect(self._post_save, cls, True, dispatch_uid='implicit-role-post-save')
post_delete.connect(self._post_delete, cls, True) post_delete.connect(self._post_delete, cls, True)
@@ -163,15 +162,6 @@ class ImplicitRoleField(models.ForeignKey):
getattr(instance, self.name).parents.remove(getattr(obj, field_attr)) getattr(instance, self.name).parents.remove(getattr(obj, field_attr))
return _m2m_update return _m2m_update
def _post_init(self, instance, *args, **kwargs):
original_parent_roles = dict()
if instance.pk:
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
original_parent_roles[implicit_role_field.name] = implicit_role_field._resolve_parent_roles(instance)
setattr(instance, '__original_parent_roles', original_parent_roles)
def _create_role_instance_if_not_exists(self, instance): def _create_role_instance_if_not_exists(self, instance):
role = getattr(instance, self.name, None) role = getattr(instance, self.name, None)
if role: if role:
@@ -213,6 +203,15 @@ class ImplicitRoleField(models.ForeignKey):
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
implicit_role_field._create_role_instance_if_not_exists(instance) implicit_role_field._create_role_instance_if_not_exists(instance)
original_parent_roles = dict()
if instance.pk:
original = instance.__class__.objects.get(pk=instance.pk)
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
original_parent_roles[implicit_role_field.name] = implicit_role_field._resolve_parent_roles(original)
setattr(instance, '__original_parent_roles', original_parent_roles)
def _post_save(self, instance, created, *args, **kwargs): def _post_save(self, instance, created, *args, **kwargs):
if created: if created:
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
@@ -172,7 +172,8 @@ class Command(BaseCommand):
sys.stdout.write('\r %d ' % (ids['credential'])) sys.stdout.write('\r %d ' % (ids['credential']))
sys.stdout.flush() sys.stdout.flush()
credential_id = ids['credential'] credential_id = ids['credential']
credential = Credential.objects.create(name='%s Credential %d User %d' % (prefix, credential_id, user_idx), user=user) credential = Credential.objects.create(name='%s Credential %d User %d' % (prefix, credential_id, user_idx))
credential.owner_role.members.add(user)
credentials.append(credential) credentials.append(credential)
user_idx += 1 user_idx += 1
print('') print('')
@@ -187,7 +188,8 @@ class Command(BaseCommand):
sys.stdout.write('\r %d ' % (ids['credential'] - starting_credential_id)) sys.stdout.write('\r %d ' % (ids['credential'] - starting_credential_id))
sys.stdout.flush() sys.stdout.flush()
credential_id = ids['credential'] credential_id = ids['credential']
credential = Credential.objects.create(name='%s Credential %d team %d' % (prefix, credential_id, team_idx), team=team) credential = Credential.objects.create(name='%s Credential %d team %d' % (prefix, credential_id, team_idx))
credential.owner_role.parents.add(team.member_role)
credentials.append(credential) credentials.append(credential)
team_idx += 1 team_idx += 1
print('') print('')
@@ -821,7 +821,7 @@ class Command(NoArgsCommand):
db_groups = self.inventory_source.group.all_children db_groups = self.inventory_source.group.all_children
else: else:
db_groups = self.inventory.groups db_groups = self.inventory.groups
for db_group in db_groups: for db_group in db_groups.all():
# Delete child group relationships not present in imported data. # Delete child group relationships not present in imported data.
db_children = db_group.children db_children = db_group.children
db_children_name_pk_map = dict(db_children.values_list('name', 'pk')) db_children_name_pk_map = dict(db_children.values_list('name', 'pk'))
@@ -137,7 +137,7 @@ class CallbackReceiver(object):
'playbook_on_import_for_host', 'playbook_on_import_for_host',
'playbook_on_not_import_for_host'): 'playbook_on_not_import_for_host'):
parent = job_parent_events.get('playbook_on_play_start', None) parent = job_parent_events.get('playbook_on_play_start', None)
elif message['event'].startswith('runner_on_'): elif message['event'].startswith('runner_on_') or message['event'].startswith('runner_item_on_'):
list_parents = [] list_parents = []
list_parents.append(job_parent_events.get('playbook_on_setup', None)) list_parents.append(job_parent_events.get('playbook_on_setup', None))
list_parents.append(job_parent_events.get('playbook_on_task_start', None)) list_parents.append(job_parent_events.get('playbook_on_task_start', None))
+1 -1
View File
@@ -381,7 +381,7 @@ class Migration(migrations.Migration):
name='AdHocCommand', name='AdHocCommand',
fields=[ fields=[
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')), ('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
('job_type', models.CharField(default=b'run', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check'), (b'scan', 'Scan')])), ('job_type', models.CharField(default=b'run', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check')])),
('limit', models.CharField(default=b'', max_length=1024, blank=True)), ('limit', models.CharField(default=b'', max_length=1024, blank=True)),
('module_name', models.CharField(default=b'', max_length=1024, blank=True)), ('module_name', models.CharField(default=b'', max_length=1024, blank=True)),
('module_args', models.TextField(default=b'', blank=True)), ('module_args', models.TextField(default=b'', blank=True)),
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from awx.main.migrations import _cleanup_deleted as cleanup_deleted
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0005_v300_migrate_facts'),
]
operations = [
migrations.RunPython(cleanup_deleted.cleanup_deleted),
]
@@ -1,19 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from awx.main.migrations import _cleanup_deleted as cleanup_deleted
from django.db import migrations from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('main', '0005_v300_migrate_facts'), ('main', '0006_v300_active_flag_cleanup'),
] ]
operations = [ operations = [
migrations.RunPython(cleanup_deleted.cleanup_deleted),
migrations.RemoveField( migrations.RemoveField(
model_name='credential', model_name='credential',
name='active', name='active',
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
('taggit', '0002_auto_20150616_2121'), ('taggit', '0002_auto_20150616_2121'),
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('main', '0006_v300_active_flag_removal'), ('main', '0007_v300_active_flag_removal'),
] ]
operations = [ operations = [
@@ -33,6 +33,11 @@ class Migration(migrations.Migration):
'users', 'users',
'deprecated_users', 'deprecated_users',
), ),
migrations.RenameField(
'Team',
'projects',
'deprecated_projects',
),
migrations.CreateModel( migrations.CreateModel(
name='Role', name='Role',
@@ -220,4 +225,33 @@ class Migration(migrations.Migration):
name='organization', name='organization',
field=models.ForeignKey(related_name='projects', to='main.Organization', blank=True, null=True), field=models.ForeignKey(related_name='projects', to='main.Organization', blank=True, null=True),
), ),
migrations.RenameField(
'Credential',
'team',
'deprecated_team',
),
migrations.RenameField(
'Credential',
'user',
'deprecated_user',
),
migrations.AlterField(
model_name='organization',
name='deprecated_admins',
field=models.ManyToManyField(related_name='deprecated_admin_of_organizations', to=settings.AUTH_USER_MODEL, blank=True),
),
migrations.AlterField(
model_name='organization',
name='deprecated_users',
field=models.ManyToManyField(related_name='deprecated_organizations', to=settings.AUTH_USER_MODEL, blank=True),
),
migrations.AlterField(
model_name='team',
name='deprecated_users',
field=models.ManyToManyField(related_name='deprecated_teams', to=settings.AUTH_USER_MODEL, blank=True),
),
migrations.AlterUniqueTogether(
name='credential',
unique_together=set([]),
),
] ]
@@ -8,7 +8,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('main', '0007_v300_rbac_changes'), ('main', '0008_v300_rbac_changes'),
] ]
operations = [ operations = [
@@ -107,7 +107,7 @@ def create_system_job_templates(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('main', '0008_v300_rbac_migrations'), ('main', '0009_v300_rbac_migrations'),
] ]
operations = [ operations = [
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0010_v300_create_system_job_templates'),
]
operations = [
migrations.AddField(
model_name='credential',
name='domain',
field=models.CharField(default=b'', help_text='The identifier for the domain.', max_length=100, verbose_name='Domain', blank=True),
),
]
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
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', '0011_v300_credential_domain_field'),
]
operations = [
migrations.CreateModel(
name='Label',
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)),
('name', models.CharField(max_length=512)),
('created_by', models.ForeignKey(related_name="{u'class': 'label', 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': 'label', 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='labels', to='main.Organization', help_text='Organization this label belongs to.')),
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
],
options={
'ordering': ('organization', 'name'),
},
),
migrations.AddField(
model_name='activitystream',
name='label',
field=models.ManyToManyField(to='main.Label', blank=True),
),
migrations.AddField(
model_name='job',
name='labels',
field=models.ManyToManyField(related_name='job_labels', to='main.Label', blank=True),
),
migrations.AddField(
model_name='jobtemplate',
name='labels',
field=models.ManyToManyField(related_name='jobtemplate_labels', to='main.Label', blank=True),
),
migrations.AlterUniqueTogether(
name='label',
unique_together=set([('name', 'organization')]),
),
]
+2 -2
View File
@@ -208,7 +208,7 @@ class UserAccess(BaseAccess):
Q(pk=self.user.pk) | Q(pk=self.user.pk) |
Q(organizations__in=self.user.deprecated_admin_of_organizations) | Q(organizations__in=self.user.deprecated_admin_of_organizations) |
Q(organizations__in=self.user.deprecated_organizations) | Q(organizations__in=self.user.deprecated_organizations) |
Q(teams__in=self.user.teams) Q(deprecated_teams__in=self.user.deprecated_teams)
).distinct() ).distinct()
def can_add(self, data): def can_add(self, data):
@@ -690,7 +690,7 @@ class ProjectAccess(BaseAccess):
qs = qs.filter(Q(created_by=self.user, deprecated_organizations__isnull=True) | qs = qs.filter(Q(created_by=self.user, deprecated_organizations__isnull=True) |
Q(deprecated_organizations__deprecated_admins__in=[self.user]) | Q(deprecated_organizations__deprecated_admins__in=[self.user]) |
Q(deprecated_organizations__deprecated_users__in=[self.user]) | Q(deprecated_organizations__deprecated_users__in=[self.user]) |
Q(teams__in=team_ids)) Q(deprecated_teams__in=team_ids))
allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY] allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY]
allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK]
+78 -74
View File
@@ -1,23 +1,44 @@
import logging
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import smart_text
from django.db.models import Q from django.db.models import Q
from collections import defaultdict from collections import defaultdict
from awx.main.utils import getattrd from awx.main.utils import getattrd
import _old_access as old_access import _old_access as old_access
def migrate_users(apps, schema_editor): logger = logging.getLogger(__name__)
migrations = list()
def log_migration(wrapped):
'''setup the logging mechanism for each migration method
as it runs, Django resets this, so we use a decorator
to re-add the handler for each method.
'''
handler = logging.FileHandler("tower_rbac_migrations.log", mode="a", encoding="UTF-8")
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setLevel(logging.DEBUG)
handler.setFormatter(formatter)
def wrapper(*args, **kwargs):
logger.handlers = []
logger.addHandler(handler)
return wrapped(*args, **kwargs)
return wrapper
@log_migration
def migrate_users(apps, schema_editor):
User = apps.get_model('auth', "User") User = apps.get_model('auth', "User")
Role = apps.get_model('main', "Role") Role = apps.get_model('main', "Role")
RolePermission = apps.get_model('main', "RolePermission") RolePermission = apps.get_model('main', "RolePermission")
for user in User.objects.all(): for user in User.objects.iterator():
try: try:
Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=user.id) Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=user.id)
logger.info(smart_text(u"found existing role for user: {}".format(user.username)))
except Role.DoesNotExist: except Role.DoesNotExist:
role = Role.objects.create( role = Role.objects.create(
singleton_name = '%s-admin_role' % user.username, singleton_name = smart_text(u'{}-admin_role'.format(user.username)),
content_object = user, content_object = user,
) )
role.members.add(user) role.members.add(user)
@@ -27,32 +48,30 @@ def migrate_users(apps, schema_editor):
create=1, read=1, write=1, delete=1, update=1, create=1, read=1, write=1, delete=1, update=1,
execute=1, scm_update=1, use=1, execute=1, scm_update=1, use=1,
) )
logger.info(smart_text(u"migrating to new role for user: {}".format(user.username)))
if user.is_superuser: if user.is_superuser:
Role.singleton('System Administrator').members.add(user) Role.singleton('System Administrator').members.add(user)
migrations.append(user) logger.warning(smart_text(u"added superuser: {}".format(user.username)))
return migrations
@log_migration
def migrate_organization(apps, schema_editor): def migrate_organization(apps, schema_editor):
migrations = defaultdict(list) Organization = apps.get_model('main', "Organization")
organization = apps.get_model('main', "Organization") for org in Organization.objects.iterator():
for org in organization.objects.all():
for admin in org.deprecated_admins.all(): for admin in org.deprecated_admins.all():
org.admin_role.members.add(admin) org.admin_role.members.add(admin)
migrations[org.name].append(admin) logger.info(smart_text(u"added admin: {}, {}".format(org.name, admin.username)))
for user in org.deprecated_users.all(): for user in org.deprecated_users.all():
org.auditor_role.members.add(user) org.auditor_role.members.add(user)
migrations[org.name].append(user) logger.info(smart_text(u"added auditor: {}, {}".format(org.name, user.username)))
return migrations
@log_migration
def migrate_team(apps, schema_editor): def migrate_team(apps, schema_editor):
migrations = defaultdict(list) Team = apps.get_model('main', 'Team')
team = apps.get_model('main', 'Team') for t in Team.objects.iterator():
for t in team.objects.all():
for user in t.deprecated_users.all(): for user in t.deprecated_users.all():
t.member_role.members.add(user) t.member_role.members.add(user)
migrations[t.name].append(user) logger.info(smart_text(u"team: {}, added user: {}".format(t.name, user.username)))
return migrations
def attrfunc(attr_path): def attrfunc(attr_path):
'''attrfunc returns a function that will '''attrfunc returns a function that will
@@ -70,7 +89,7 @@ def attrfunc(attr_path):
def _update_credential_parents(org, cred): def _update_credential_parents(org, cred):
org.admin_role.children.add(cred.owner_role) org.admin_role.children.add(cred.owner_role)
org.member_role.children.add(cred.usage_role) org.member_role.children.add(cred.usage_role)
cred.user, cred.team = None, None cred.deprecated_user, cred.deprecated_team = None, None
cred.save() cred.save()
def _discover_credentials(instances, cred, orgfunc): def _discover_credentials(instances, cred, orgfunc):
@@ -102,7 +121,7 @@ def _discover_credentials(instances, cred, orgfunc):
cred.save() cred.save()
# Unlink the old information from the new credential # Unlink the old information from the new credential
cred.user, cred.team = None, None cred.deprecated_user, cred.deprecated_team = None, None
cred.owner_role, cred.usage_role = None, None cred.owner_role, cred.usage_role = None, None
cred.save() cred.save()
@@ -111,16 +130,14 @@ def _discover_credentials(instances, cred, orgfunc):
i.save() i.save()
_update_credential_parents(org, cred) _update_credential_parents(org, cred)
@log_migration
def migrate_credential(apps, schema_editor): def migrate_credential(apps, schema_editor):
Credential = apps.get_model('main', "Credential") Credential = apps.get_model('main', "Credential")
JobTemplate = apps.get_model('main', 'JobTemplate') JobTemplate = apps.get_model('main', 'JobTemplate')
Project = apps.get_model('main', 'Project') Project = apps.get_model('main', 'Project')
InventorySource = apps.get_model('main', 'InventorySource') InventorySource = apps.get_model('main', 'InventorySource')
migrated = [] for cred in Credential.objects.iterator():
for cred in Credential.objects.all():
migrated.append(cred)
results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or
InventorySource.objects.filter(credential=cred).all()) InventorySource.objects.filter(credential=cred).all())
if results: if results:
@@ -128,6 +145,7 @@ def migrate_credential(apps, schema_editor):
_update_credential_parents(results[0].inventory.organization, cred) _update_credential_parents(results[0].inventory.organization, cred)
else: else:
_discover_credentials(results, cred, attrfunc('inventory.organization')) _discover_credentials(results, cred, attrfunc('inventory.organization'))
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
continue continue
projs = Project.objects.filter(credential=cred).all() projs = Project.objects.filter(credential=cred).all()
@@ -136,31 +154,30 @@ def migrate_credential(apps, schema_editor):
_update_credential_parents(projs[0].organization, cred) _update_credential_parents(projs[0].organization, cred)
else: else:
_discover_credentials(projs, cred, attrfunc('organization')) _discover_credentials(projs, cred, attrfunc('organization'))
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
continue continue
if cred.team is not None: if cred.deprecated_team is not None:
cred.team.admin_role.children.add(cred.owner_role) cred.deprecated_team.admin_role.children.add(cred.owner_role)
cred.team.member_role.children.add(cred.usage_role) cred.deprecated_team.member_role.children.add(cred.usage_role)
cred.user, cred.team = None, None cred.deprecated_user, cred.deprecated_team = None, None
cred.save() cred.save()
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host)))
elif cred.user is not None: elif cred.deprecated_user is not None:
cred.user.admin_role.children.add(cred.owner_role) cred.deprecated_user.admin_role.children.add(cred.owner_role)
cred.user, cred.team = None, None cred.deprecated_user, cred.deprecated_team = None, None
cred.save() cred.save()
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host, )))
# no match found, log else:
return migrated logger.warning(smart_text(u"orphaned credential found Credential(name={}, kind={}, host={}), superuser only".format(cred.name, cred.kind, cred.host, )))
@log_migration
def migrate_inventory(apps, schema_editor): def migrate_inventory(apps, schema_editor):
migrations = defaultdict(dict)
Inventory = apps.get_model('main', 'Inventory') Inventory = apps.get_model('main', 'Inventory')
Permission = apps.get_model('main', 'Permission') Permission = apps.get_model('main', 'Permission')
for inventory in Inventory.objects.all(): for inventory in Inventory.objects.iterator():
teams, users = [], []
for perm in Permission.objects.filter(inventory=inventory): for perm in Permission.objects.filter(inventory=inventory):
role = None role = None
execrole = None execrole = None
@@ -178,7 +195,7 @@ def migrate_inventory(apps, schema_editor):
elif perm.permission_type == 'run': elif perm.permission_type == 'run':
pass pass
else: else:
raise Exception('Unhandled permission type for inventory: %s' % perm.permission_type) raise Exception(smart_text(u'Unhandled permission type for inventory: {}'.format( perm.permission_type)))
if perm.run_ad_hoc_commands: if perm.run_ad_hoc_commands:
execrole = inventory.executor_role execrole = inventory.executor_role
@@ -187,19 +204,16 @@ def migrate_inventory(apps, schema_editor):
perm.team.member_role.children.add(role) perm.team.member_role.children.add(role)
if execrole: if execrole:
perm.team.member_role.children.add(execrole) perm.team.member_role.children.add(execrole)
logger.info(smart_text(u'added Team({}) access to Inventory({})'.format(perm.team.name, inventory.name)))
teams.append(perm.team)
if perm.user: if perm.user:
if role: if role:
role.members.add(perm.user) role.members.add(perm.user)
if execrole: if execrole:
execrole.members.add(perm.user) execrole.members.add(perm.user)
users.append(perm.user) logger.info(smart_text(u'added User({}) access to Inventory({})'.format(perm.user.username, inventory.name)))
migrations[inventory.name]['teams'] = teams
migrations[inventory.name]['users'] = users
return migrations
@log_migration
def migrate_projects(apps, schema_editor): def migrate_projects(apps, schema_editor):
''' '''
I can see projects when: I can see projects when:
@@ -215,31 +229,29 @@ def migrate_projects(apps, schema_editor):
X I am an admin in an organization associated with the project. X I am an admin in an organization associated with the project.
X I created the project but it isn't associated with an organization X I created the project but it isn't associated with an organization
''' '''
migrations = defaultdict(lambda: defaultdict(set))
Project = apps.get_model('main', 'Project') Project = apps.get_model('main', 'Project')
Permission = apps.get_model('main', 'Permission') Permission = apps.get_model('main', 'Permission')
JobTemplate = apps.get_model('main', 'JobTemplate') JobTemplate = apps.get_model('main', 'JobTemplate')
# Migrate projects to single organizations, duplicating as necessary # Migrate projects to single organizations, duplicating as necessary
for project in [p for p in Project.objects.all()]: for project in Project.objects.iterator():
original_project_name = project.name original_project_name = project.name
project_orgs = project.deprecated_organizations.distinct().all() project_orgs = project.deprecated_organizations.distinct().all()
if project_orgs.count() > 1: if len(project_orgs) > 1:
first_org = None first_org = None
for org in project_orgs: for org in project_orgs:
if first_org is None: if first_org is None:
# For the first org, re-use our existing Project object, so don't do the below duplication effort # For the first org, re-use our existing Project object, so don't do the below duplication effort
first_org = org first_org = org
project.name = first_org.name + ' - ' + original_project_name project.name = smart_text(u'{} - {}'.format(first_org.name, original_project_name))
project.organization = first_org project.organization = first_org
project.save() project.save()
else: else:
new_prj = Project.objects.create( new_prj = Project.objects.create(
created = project.created, created = project.created,
description = project.description, description = project.description,
name = org.name + ' - ' + original_project_name, name = smart_text(u'{} - {}'.format(org.name, original_project_name)),
old_pk = project.old_pk, old_pk = project.old_pk,
created_by_id = project.created_by_id, created_by_id = project.created_by_id,
scm_type = project.scm_type, scm_type = project.scm_type,
@@ -253,41 +265,39 @@ def migrate_projects(apps, schema_editor):
credential = project.credential, credential = project.credential,
organization = org organization = org
) )
migrations[original_project_name]['projects'].add(new_prj) logger.warning(smart_text(u'cloning Project({}) onto {} as Project({})'.format(original_project_name, org, new_prj)))
job_templates = JobTemplate.objects.filter(inventory__organization=org).all() job_templates = JobTemplate.objects.filter(inventory__organization=org).all()
for jt in job_templates: for jt in job_templates:
jt.project = new_prj jt.project = new_prj
jt.save() jt.save()
# Migrate permissions # Migrate permissions
for project in [p for p in Project.objects.all()]: for project in Project.objects.iterator():
if project.organization is None and project.created_by is not None: if project.organization is None and project.created_by is not None:
project.admin_role.members.add(project.created_by) project.admin_role.members.add(project.created_by)
migrations[project.name]['users'].add(project.created_by) logger.warn(smart_text(u'adding Project({}) admin: {}'.format(project.name, project.created_by.username)))
for team in project.teams.all(): for team in project.deprecated_teams.all():
team.member_role.children.add(project.member_role) team.member_role.children.add(project.member_role)
migrations[project.name]['teams'].add(team) logger.info(smart_text(u'adding Team({}) access for Project({})'.format(team.name, project.name)))
if project.organization is not None: if project.organization is not None:
for user in project.organization.deprecated_users.all(): for user in project.organization.deprecated_users.all():
project.member_role.members.add(user) project.member_role.members.add(user)
migrations[project.name]['users'].add(user) logger.info(smart_text(u'adding Organization({}) member access to Project({})'.format(project.organization.name, project.name)))
for perm in Permission.objects.filter(project=project): for perm in Permission.objects.filter(project=project):
# All perms at this level just imply a user or team can read # All perms at this level just imply a user or team can read
if perm.team: if perm.team:
perm.team.member_role.children.add(project.member_role) perm.team.member_role.children.add(project.member_role)
migrations[project.name]['teams'].add(perm.team) logger.info(smart_text(u'adding Team({}) access for Project({})'.format(perm.team.name, project.name)))
if perm.user: if perm.user:
project.member_role.members.add(perm.user) project.member_role.members.add(perm.user)
migrations[project.name]['users'].add(perm.user) logger.info(smart_text(u'adding User({}) access for Project({})'.format(perm.user.username, project.name)))
return migrations
@log_migration
def migrate_job_templates(apps, schema_editor): def migrate_job_templates(apps, schema_editor):
''' '''
NOTE: This must be run after orgs, inventory, projects, credential, and NOTE: This must be run after orgs, inventory, projects, credential, and
@@ -330,30 +340,27 @@ def migrate_job_templates(apps, schema_editor):
''' '''
migrations = defaultdict(lambda: defaultdict(set))
User = apps.get_model('auth', 'User') User = apps.get_model('auth', 'User')
JobTemplate = apps.get_model('main', 'JobTemplate') JobTemplate = apps.get_model('main', 'JobTemplate')
Team = apps.get_model('main', 'Team') Team = apps.get_model('main', 'Team')
Permission = apps.get_model('main', 'Permission') Permission = apps.get_model('main', 'Permission')
for jt in JobTemplate.objects.all(): for jt in JobTemplate.objects.iterator():
permission = Permission.objects.filter( permission = Permission.objects.filter(
inventory=jt.inventory, inventory=jt.inventory,
project=jt.project, project=jt.project,
permission_type__in=['create', 'check', 'run'] if jt.job_type == 'check' else ['create', 'run'], permission_type__in=['create', 'check', 'run'] if jt.job_type == 'check' else ['create', 'run'],
) )
for team in Team.objects.all(): for team in Team.objects.iterator():
if permission.filter(team=team).exists(): if permission.filter(team=team).exists():
team.member_role.children.add(jt.executor_role) team.member_role.children.add(jt.executor_role)
migrations[jt.name]['teams'].add(team) logger.info(smart_text(u'adding Team({}) access to JobTemplate({})'.format(team.name, jt.name)))
for user in User.objects.iterator():
for user in User.objects.all():
if permission.filter(user=user).exists(): if permission.filter(user=user).exists():
jt.executor_role.members.add(user) jt.executor_role.members.add(user)
migrations[jt.name]['users'].add(user) logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name)))
if jt.accessible_by(user, {'execute': True}): if jt.accessible_by(user, {'execute': True}):
# If the job template is already accessible by the user, because they # If the job template is already accessible by the user, because they
@@ -363,7 +370,4 @@ def migrate_job_templates(apps, schema_editor):
if old_access.check_user_access(user, jt.__class__, 'start', jt, False): if old_access.check_user_access(user, jt.__class__, 'start', jt, False):
jt.executor_role.members.add(user) jt.executor_role.members.add(user)
migrations[jt.name]['users'].add(user) logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name)))
return migrations
+12
View File
@@ -22,6 +22,7 @@ from awx.main.models.rbac import * # noqa
from awx.main.models.mixins import * # noqa from awx.main.models.mixins import * # noqa
from awx.main.models.notifications import * # noqa from awx.main.models.notifications import * # noqa
from awx.main.models.fact import * # noqa from awx.main.models.fact import * # noqa
from awx.main.models.label import * # noqa
# Monkeypatch Django serializer to ignore django-taggit fields (which break # Monkeypatch Django serializer to ignore django-taggit fields (which break
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155). # the dumpdata command; see https://github.com/alex/django-taggit/issues/155).
@@ -47,6 +48,16 @@ User.add_to_class('accessible_objects', user_accessible_objects)
User.add_to_class('admin_role', user_admin_role) User.add_to_class('admin_role', user_admin_role)
User.add_to_class('role_permissions', GenericRelation('main.RolePermission')) User.add_to_class('role_permissions', GenericRelation('main.RolePermission'))
@property
def user_get_organizations(user):
return Organization.objects.filter(member_role__members=user)
@property
def user_get_admin_of_organizations(user):
return Organization.objects.filter(admin_role__members=user)
User.add_to_class('organizations', user_get_organizations)
User.add_to_class('admin_of_organizations', user_get_admin_of_organizations)
# Import signal handlers only after models have been defined. # Import signal handlers only after models have been defined.
import awx.main.signals # noqa import awx.main.signals # noqa
@@ -73,3 +84,4 @@ activity_stream_registrar.connect(CustomInventoryScript)
activity_stream_registrar.connect(TowerSettings) activity_stream_registrar.connect(TowerSettings)
activity_stream_registrar.connect(Notifier) activity_stream_registrar.connect(Notifier)
activity_stream_registrar.connect(Notification) activity_stream_registrar.connect(Notification)
activity_stream_registrar.connect(Label)
+1
View File
@@ -55,6 +55,7 @@ class ActivityStream(models.Model):
custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True) custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True)
notifier = models.ManyToManyField("Notifier", blank=True) notifier = models.ManyToManyField("Notifier", blank=True)
notification = models.ManyToManyField("Notification", blank=True) notification = models.ManyToManyField("Notification", blank=True)
label = models.ManyToManyField("Label", blank=True)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('api:activity_stream_detail', args=(self.pk,)) return reverse('api:activity_stream_detail', args=(self.pk,))
+1 -1
View File
@@ -36,7 +36,7 @@ class AdHocCommand(UnifiedJob):
job_type = models.CharField( job_type = models.CharField(
max_length=64, max_length=64,
choices=JOB_TYPE_CHOICES, choices=AD_HOC_JOB_TYPE_CHOICES,
default='run', default='run',
) )
inventory = models.ForeignKey( inventory = models.ForeignKey(
+6 -1
View File
@@ -29,7 +29,7 @@ __all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ', 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN', 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN',
'PERM_INVENTORY_CHECK', 'PERM_JOBTEMPLATE_CREATE', 'JOB_TYPE_CHOICES', 'PERM_INVENTORY_CHECK', 'PERM_JOBTEMPLATE_CREATE', 'JOB_TYPE_CHOICES',
'PERMISSION_TYPE_CHOICES', 'CLOUD_INVENTORY_SOURCES', 'AD_HOC_JOB_TYPE_CHOICES', 'PERMISSION_TYPE_CHOICES', 'CLOUD_INVENTORY_SOURCES',
'VERBOSITY_CHOICES'] 'VERBOSITY_CHOICES']
PERM_INVENTORY_ADMIN = 'admin' PERM_INVENTORY_ADMIN = 'admin'
@@ -46,6 +46,11 @@ JOB_TYPE_CHOICES = [
(PERM_INVENTORY_SCAN, _('Scan')), (PERM_INVENTORY_SCAN, _('Scan')),
] ]
AD_HOC_JOB_TYPE_CHOICES = [
(PERM_INVENTORY_DEPLOY, _('Run')),
(PERM_INVENTORY_CHECK, _('Check')),
]
PERMISSION_TYPE_CHOICES = [ PERMISSION_TYPE_CHOICES = [
(PERM_INVENTORY_READ, _('Read Inventory')), (PERM_INVENTORY_READ, _('Read Inventory')),
(PERM_INVENTORY_WRITE, _('Edit Inventory')), (PERM_INVENTORY_WRITE, _('Edit Inventory')),
+24 -63
View File
@@ -7,7 +7,7 @@ import re
# Django # Django
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
# AWX # AWX
@@ -56,24 +56,23 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
class Meta: class Meta:
app_label = 'main' app_label = 'main'
unique_together = [('user', 'team', 'kind', 'name')]
ordering = ('kind', 'name') ordering = ('kind', 'name')
user = models.ForeignKey( deprecated_user = models.ForeignKey(
'auth.User', 'auth.User',
null=True, null=True,
default=None, default=None,
blank=True, blank=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='credentials', related_name='deprecated_credentials',
) )
team = models.ForeignKey( deprecated_team = models.ForeignKey(
'Team', 'Team',
null=True, null=True,
default=None, default=None,
blank=True, blank=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='credentials', related_name='deprecated_credentials',
) )
kind = models.CharField( kind = models.CharField(
max_length=32, max_length=32,
@@ -120,6 +119,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
verbose_name=_('Project'), verbose_name=_('Project'),
help_text=_('The identifier for the project.'), help_text=_('The identifier for the project.'),
) )
domain = models.CharField(
blank=True,
default='',
max_length=100,
verbose_name=_('Domain'),
help_text=_('The identifier for the domain.'),
)
ssh_key_data = models.TextField( ssh_key_data = models.TextField(
blank=True, blank=True,
default='', default='',
@@ -234,6 +240,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
raise ValidationError('Host required for OpenStack credential.') raise ValidationError('Host required for OpenStack credential.')
return host return host
def clean_domain(self):
return self.domain or ''
def clean_username(self): def clean_username(self):
username = self.username or '' username = self.username or ''
if not username and self.kind == 'aws': if not username and self.kind == 'aws':
@@ -294,57 +303,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
return self.ssh_key_unlock return self.ssh_key_unlock
def clean(self): def clean(self):
if self.user and self.team: if self.deprecated_user and self.deprecated_team:
raise ValidationError('Credential cannot be assigned to both a user and team') raise ValidationError('Credential cannot be assigned to both a user and team')
def _validate_unique_together_with_null(self, unique_check, exclude=None):
# Based on existing Django model validation code, except it doesn't
# skip the check for unique violations when a field is None. See:
# https://github.com/django/django/blob/stable/1.5.x/django/db/models/base.py#L792
errors = {}
model_class = self.__class__
if set(exclude or []) & set(unique_check):
return
lookup_kwargs = {}
for field_name in unique_check:
f = self._meta.get_field(field_name)
lookup_value = getattr(self, f.attname)
if f.primary_key and not self._state.adding:
# no need to check for unique primary key when editing
continue
lookup_kwargs[str(field_name)] = lookup_value
if len(unique_check) != len(lookup_kwargs):
return
qs = model_class._default_manager.filter(**lookup_kwargs)
# Exclude the current object from the query if we are editing an
# instance (as opposed to creating a new one)
# Note that we need to use the pk as defined by model_class, not
# self.pk. These can be different fields because model inheritance
# allows single model to have effectively multiple primary keys.
# Refs #17615.
model_class_pk = self._get_pk_val(model_class._meta)
if not self._state.adding and model_class_pk is not None:
qs = qs.exclude(pk=model_class_pk)
if qs.exists():
key = NON_FIELD_ERRORS
errors.setdefault(key, []).append(self.unique_error_message(model_class, unique_check))
if errors:
raise ValidationError(errors)
def validate_unique(self, exclude=None):
errors = {}
try:
super(Credential, self).validate_unique(exclude)
except ValidationError, e:
errors = e.update_error_dict(errors)
try:
unique_fields = ('user', 'team', 'kind', 'name')
self._validate_unique_together_with_null(unique_fields, exclude)
except ValidationError, e:
errors = e.update_error_dict(errors)
if errors:
raise ValidationError(errors)
def _password_field_allows_ask(self, field): def _password_field_allows_ask(self, field):
return bool(self.kind == 'ssh' and field != 'ssh_key_data') return bool(self.kind == 'ssh' and field != 'ssh_key_data')
@@ -357,17 +318,17 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
# changed. # changed.
if self.pk: if self.pk:
cred_before = Credential.objects.get(pk=self.pk) cred_before = Credential.objects.get(pk=self.pk)
if self.user and self.team: if self.deprecated_user and self.deprecated_team:
# If the user changed, remove the previously assigned team. # If the user changed, remove the previously assigned team.
if cred_before.user != self.user: if cred_before.user != self.user:
self.team = None self.deprecated_team = None
if 'team' not in update_fields: if 'deprecated_team' not in update_fields:
update_fields.append('team') update_fields.append('deprecated_team')
# If the team changed, remove the previously assigned user. # If the team changed, remove the previously assigned user.
elif cred_before.team != self.team: elif cred_before.deprecated_team != self.deprecated_team:
self.user = None self.deprecated_user = None
if 'user' not in update_fields: if 'deprecated_user' not in update_fields:
update_fields.append('user') update_fields.append('deprecated_user')
# Set cloud flag based on credential kind. # Set cloud flag based on credential kind.
cloud = self.kind in CLOUD_PROVIDERS + ('aws',) cloud = self.kind in CLOUD_PROVIDERS + ('aws',)
if self.cloud != cloud: if self.cloud != cloud:
+7 -2
View File
@@ -125,7 +125,11 @@ class JobOptions(BaseModel):
become_enabled = models.BooleanField( become_enabled = models.BooleanField(
default=False, default=False,
) )
labels = models.ManyToManyField(
"Label",
blank=True,
related_name='%(class)s_labels'
)
extra_vars_dict = VarsDictProperty('extra_vars', True) extra_vars_dict = VarsDictProperty('extra_vars', True)
@@ -210,7 +214,8 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
return ['name', 'description', 'job_type', 'inventory', 'project', return ['name', 'description', 'job_type', 'inventory', 'project',
'playbook', 'credential', 'cloud_credential', 'forks', 'schedule', 'playbook', 'credential', 'cloud_credential', 'forks', 'schedule',
'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type', 'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type',
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled'] 'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled',
'labels',]
def create_job(self, **kwargs): def create_job(self, **kwargs):
''' '''
+33
View File
@@ -0,0 +1,33 @@
# Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved.
# Django
from django.db import models
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
# AWX
from awx.main.models.base import CommonModelNameNotUnique
__all__ = ('Label', )
class Label(CommonModelNameNotUnique):
'''
Generic Tag. Designed for tagging Job Templates, but expandable to other models.
'''
class Meta:
app_label = 'main'
unique_together = (("name", "organization"),)
ordering = ('organization', 'name')
organization = models.ForeignKey(
'Organization',
related_name='labels',
help_text=_('Organization this label belongs to.'),
on_delete=models.CASCADE,
)
def get_absolute_url(self):
return reverse('api:label_detail', args=(self.pk,))
+18 -6
View File
@@ -2,11 +2,14 @@
from django.db import models from django.db import models
from django.db.models.aggregates import Max from django.db.models.aggregates import Max
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User # noqa
# AWX # AWX
from awx.main.models.rbac import ( from awx.main.models.rbac import (
get_user_permissions_on_resource, get_user_permissions_on_resource,
get_role_permissions_on_resource, get_role_permissions_on_resource,
Role,
) )
@@ -20,7 +23,7 @@ class ResourceMixin(models.Model):
role_permissions = GenericRelation('main.RolePermission') role_permissions = GenericRelation('main.RolePermission')
@classmethod @classmethod
def accessible_objects(cls, user, permissions): def accessible_objects(cls, accessor, permissions):
''' '''
Use instead of `MyModel.objects` when you want to only consider Use instead of `MyModel.objects` when you want to only consider
resources that a user has specific permissions for. For example: resources that a user has specific permissions for. For example:
@@ -32,13 +35,22 @@ class ResourceMixin(models.Model):
performant to resolve the resource in question then call performant to resolve the resource in question then call
`myresource.get_permissions(user)`. `myresource.get_permissions(user)`.
''' '''
return ResourceMixin._accessible_objects(cls, user, permissions) return ResourceMixin._accessible_objects(cls, accessor, permissions)
@staticmethod @staticmethod
def _accessible_objects(cls, user, permissions): def _accessible_objects(cls, accessor, permissions):
qs = cls.objects.filter( if type(accessor) == User:
role_permissions__role__ancestors__members=user qs = cls.objects.filter(
) role_permissions__role__ancestors__members=accessor
)
else:
accessor_type = ContentType.objects.get_for_model(accessor)
roles = Role.objects.filter(content_type__pk=accessor_type.id,
object_id=accessor.id)
qs = cls.objects.filter(
role_permissions__role__ancestors__in=roles
)
for perm in permissions: for perm in permissions:
qs = qs.annotate(**{'max_' + perm: Max('role_permissions__' + perm)}) qs = qs.annotate(**{'max_' + perm: Max('role_permissions__' + perm)})
qs = qs.filter(**{'max_' + perm: int(permissions[perm])}) qs = qs.filter(**{'max_' + perm: int(permissions[perm])})
+2 -2
View File
@@ -103,10 +103,10 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='teams', related_name='teams',
) )
projects = models.ManyToManyField( deprecated_projects = models.ManyToManyField(
'Project', 'Project',
blank=True, blank=True,
related_name='teams', related_name='deprecated_teams',
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Team Administrator', role_name='Team Administrator',
-1
View File
@@ -225,7 +225,6 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
role_description='May manage this project', role_description='May manage this project',
parent_role=[ parent_role=[
'organization.admin_role', 'organization.admin_role',
'teams.member_role',
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
], ],
permissions = {'all': True} permissions = {'all': True}
+11 -6
View File
@@ -16,6 +16,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
# AWX # AWX
from django.contrib.auth.models import User # noqa
from awx.main.models.base import * # noqa from awx.main.models.base import * # noqa
__all__ = [ __all__ = [
@@ -135,11 +136,8 @@ class Role(CommonModelNameNotUnique):
@staticmethod @staticmethod
def singleton(name): def singleton(name):
try: role, _ = Role.objects.get_or_create(singleton_name=name, name=name)
return Role.objects.get(singleton_name=name) return role
except Role.DoesNotExist:
ret = Role.objects.create(singleton_name=name, name=name)
return ret
def is_ancestor_of(self, role): def is_ancestor_of(self, role):
return role.ancestors.filter(id=self.id).exists() return role.ancestors.filter(id=self.id).exists()
@@ -195,10 +193,17 @@ def get_user_permissions_on_resource(resource, user):
access. access.
''' '''
if type(user) == User:
roles = user.roles.all()
else:
accessor_type = ContentType.objects.get_for_model(user)
roles = Role.objects.filter(content_type__pk=accessor_type.id,
object_id=user.id)
qs = RolePermission.objects.filter( qs = RolePermission.objects.filter(
content_type=ContentType.objects.get_for_model(resource), content_type=ContentType.objects.get_for_model(resource),
object_id=resource.id, object_id=resource.id,
role__ancestors__in=user.roles.all() role__ancestors__in=roles,
) )
res = qs = qs.aggregate( res = qs = qs.aggregate(
+15 -4
View File
@@ -299,11 +299,11 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
''' '''
Create a new unified job based on this unified job template. Create a new unified job based on this unified job template.
''' '''
save_unified_job = kwargs.pop('save', True)
unified_job_class = self._get_unified_job_class() unified_job_class = self._get_unified_job_class()
parent_field_name = unified_job_class._get_parent_field_name() parent_field_name = unified_job_class._get_parent_field_name()
kwargs.pop('%s_id' % parent_field_name, None) kwargs.pop('%s_id' % parent_field_name, None)
create_kwargs = {} create_kwargs = {}
m2m_fields = {}
create_kwargs[parent_field_name] = self create_kwargs[parent_field_name] = self
for field_name in self._get_unified_job_field_names(): for field_name in self._get_unified_job_field_names():
# Foreign keys can be specified as field_name or field_name_id. # Foreign keys can be specified as field_name or field_name_id.
@@ -321,14 +321,25 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
elif field_name in kwargs: elif field_name in kwargs:
if field_name == 'extra_vars' and isinstance(kwargs[field_name], dict): if field_name == 'extra_vars' and isinstance(kwargs[field_name], dict):
create_kwargs[field_name] = json.dumps(kwargs['extra_vars']) create_kwargs[field_name] = json.dumps(kwargs['extra_vars'])
# We can't get a hold of django.db.models.fields.related.ManyRelatedManager to compare
# so this is the next best thing.
elif kwargs[field_name].__class__.__name__ is 'ManyRelatedManager':
m2m_fields[field_name] = kwargs[field_name]
else: else:
create_kwargs[field_name] = kwargs[field_name] create_kwargs[field_name] = kwargs[field_name]
elif hasattr(self, field_name): elif hasattr(self, field_name):
create_kwargs[field_name] = getattr(self, field_name) field_obj = self._meta.get_field_by_name(field_name)[0]
# Many to Many can be specified as field_name
if isinstance(field_obj, models.ManyToManyField):
m2m_fields[field_name] = getattr(self, field_name)
else:
create_kwargs[field_name] = getattr(self, field_name)
new_kwargs = self._update_unified_job_kwargs(**create_kwargs) new_kwargs = self._update_unified_job_kwargs(**create_kwargs)
unified_job = unified_job_class(**new_kwargs) unified_job = unified_job_class(**new_kwargs)
if save_unified_job: unified_job.save()
unified_job.save() for field_name, src_field_value in m2m_fields.iteritems():
dest_field = getattr(unified_job, field_name)
dest_field.add(*list(src_field_value.all().values_list('id', flat=True)))
return unified_job return unified_job
@@ -13,7 +13,7 @@ class Migration(DataMigration):
# and orm['appname.ModelName'] for models in other applications. # and orm['appname.ModelName'] for models in other applications.
# Refresh has_active_failures for all hosts. # Refresh has_active_failures for all hosts.
for host in orm.Host.objects: for host in orm.Host.objects.filter(active=True):
has_active_failures = bool(host.last_job_host_summary and has_active_failures = bool(host.last_job_host_summary and
host.last_job_host_summary.job.active and host.last_job_host_summary.job.active and
host.last_job_host_summary.failed) host.last_job_host_summary.failed)
@@ -30,9 +30,9 @@ class Migration(DataMigration):
for subgroup in group.children.exclude(pk__in=except_group_pks): for subgroup in group.children.exclude(pk__in=except_group_pks):
qs = qs | get_all_hosts_for_group(subgroup, except_group_pks) qs = qs | get_all_hosts_for_group(subgroup, except_group_pks)
return qs return qs
for group in orm.Group.objects: for group in orm.Group.objects.filter(active=True):
all_hosts = get_all_hosts_for_group(group) all_hosts = get_all_hosts_for_group(group)
failed_hosts = all_hosts.filter( failed_hosts = all_hosts.filter(active=True,
last_job_host_summary__job__active=True, last_job_host_summary__job__active=True,
last_job_host_summary__failed=True) last_job_host_summary__failed=True)
hosts_with_active_failures = failed_hosts.count() hosts_with_active_failures = failed_hosts.count()
@@ -49,8 +49,8 @@ class Migration(DataMigration):
# Now update has_active_failures and hosts_with_active_failures for all # Now update has_active_failures and hosts_with_active_failures for all
# inventories. # inventories.
for inventory in orm.Inventory.objects: for inventory in orm.Inventory.objects.filter(active=True):
failed_hosts = inventory.hosts.filter( has_active_failures=True) failed_hosts = inventory.hosts.filter(active=True, has_active_failures=True)
hosts_with_active_failures = failed_hosts.count() hosts_with_active_failures = failed_hosts.count()
has_active_failures = bool(hosts_with_active_failures) has_active_failures = bool(hosts_with_active_failures)
changed = False changed = False
@@ -8,7 +8,7 @@ from django.db import models
class Migration(DataMigration): class Migration(DataMigration):
def forwards(self, orm): def forwards(self, orm):
for iu in orm.InventoryUpdate.objects: for iu in orm.InventoryUpdate.objects.filter(active=True):
if iu.inventory_source is None or iu.inventory_source.group is None or iu.inventory_source.inventory is None: if iu.inventory_source is None or iu.inventory_source.group is None or iu.inventory_source.inventory is None:
continue continue
iu.name = "%s (%s)" % (iu.inventory_source.group.name, iu.inventory_source.inventory.name) iu.name = "%s (%s)" % (iu.inventory_source.group.name, iu.inventory_source.inventory.name)
@@ -12,7 +12,7 @@ from django.conf import settings
class Migration(DataMigration): class Migration(DataMigration):
def forwards(self, orm): def forwards(self, orm):
for j in orm.UnifiedJob.objects: for j in orm.UnifiedJob.objects.filter(active=True):
cur = connection.cursor() cur = connection.cursor()
stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, "%d-%s.out" % (j.pk, str(uuid.uuid1()))) stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, "%d-%s.out" % (j.pk, str(uuid.uuid1())))
fd = open(stdout_filename, 'w') fd = open(stdout_filename, 'w')
+8
View File
@@ -378,6 +378,10 @@ class BaseTask(Task):
if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported: if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported:
raise RuntimeError(OPENSSH_KEY_ERROR) raise RuntimeError(OPENSSH_KEY_ERROR)
for name, data in private_data.iteritems(): for name, data in private_data.iteritems():
# OpenSSH formatted keys must have a trailing newline to be
# accepted by ssh-add.
if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'):
data += '\n'
# For credentials used with ssh-add, write to a named pipe which # For credentials used with ssh-add, write to a named pipe which
# will be read then closed, instead of leaving the SSH key on disk. # will be read then closed, instead of leaving the SSH key on disk.
if name in ('credential', 'scm_credential', 'ad_hoc_credential') and not ssh_too_old: if name in ('credential', 'scm_credential', 'ad_hoc_credential') and not ssh_too_old:
@@ -701,6 +705,8 @@ class RunJob(BaseTask):
username=credential.username, username=credential.username,
password=decrypt_field(credential, "password"), password=decrypt_field(credential, "password"),
project_name=credential.project) project_name=credential.project)
if credential.domain not in (None, ''):
openstack_auth['domain_name'] = credential.domain
openstack_data = { openstack_data = {
'clouds': { 'clouds': {
'devstack': { 'devstack': {
@@ -1140,6 +1146,8 @@ class RunInventoryUpdate(BaseTask):
username=credential.username, username=credential.username,
password=decrypt_field(credential, "password"), password=decrypt_field(credential, "password"),
project_name=credential.project) project_name=credential.project)
if credential.domain not in (None, ''):
openstack_auth['domain_name'] = credential.domain
private_state = str(inventory_update.source_vars_dict.get('private', 'true')) private_state = str(inventory_update.source_vars_dict.get('private', 'true'))
# Retrieve cache path from inventory update vars if available, # Retrieve cache path from inventory update vars if available,
# otherwise create a temporary cache path only for this update. # otherwise create a temporary cache path only for this update.
+14 -10
View File
@@ -69,10 +69,10 @@ class QueueTestMixin(object):
if getattr(self, 'redis_process', None): if getattr(self, 'redis_process', None):
self.redis_process.kill() self.redis_process.kill()
self.redis_process = None self.redis_process = None
# The observed effect of not calling terminate_queue() if you call start_queue() are # The observed effect of not calling terminate_queue() if you call start_queue() are
# an hang on test cleanup database delete. Thus, to ensure terminate_queue() is called # an hang on test cleanup database delete. Thus, to ensure terminate_queue() is called
# whenever start_queue() is called just inherit from this class when you want to use the queue. # whenever start_queue() is called just inherit from this class when you want to use the queue.
class QueueStartStopTestMixin(QueueTestMixin): class QueueStartStopTestMixin(QueueTestMixin):
def setUp(self): def setUp(self):
@@ -129,7 +129,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
settings.CELERY_UNIT_TEST = True settings.CELERY_UNIT_TEST = True
settings.SYSTEM_UUID='00000000-0000-0000-0000-000000000000' settings.SYSTEM_UUID='00000000-0000-0000-0000-000000000000'
settings.BROKER_URL='redis://localhost:16379/' settings.BROKER_URL='redis://localhost:16379/'
# Create unique random consumer and queue ports for zeromq callback. # Create unique random consumer and queue ports for zeromq callback.
if settings.CALLBACK_CONSUMER_PORT: if settings.CALLBACK_CONSUMER_PORT:
callback_port = random.randint(55700, 55799) callback_port = random.randint(55700, 55799)
@@ -181,7 +181,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
return __name__ + '-generated-' + string + rnd_str return __name__ + '-generated-' + string + rnd_str
def create_test_license_file(self, instance_count=10000, license_date=int(time.time() + 3600), features=None): def create_test_license_file(self, instance_count=10000, license_date=int(time.time() + 3600), features=None):
writer = LicenseWriter( writer = LicenseWriter(
company_name='AWX', company_name='AWX',
contact_name='AWX Admin', contact_name='AWX Admin',
contact_email='awx@example.com', contact_email='awx@example.com',
@@ -196,7 +196,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
os.environ['AWX_LICENSE_FILE'] = license_path os.environ['AWX_LICENSE_FILE'] = license_path
def create_basic_license_file(self, instance_count=100, license_date=int(time.time() + 3600)): def create_basic_license_file(self, instance_count=100, license_date=int(time.time() + 3600)):
writer = LicenseWriter( writer = LicenseWriter(
company_name='AWX', company_name='AWX',
contact_name='AWX Admin', contact_name='AWX Admin',
contact_email='awx@example.com', contact_email='awx@example.com',
@@ -208,7 +208,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
writer.write_file(license_path) writer.write_file(license_path)
self._temp_paths.append(license_path) self._temp_paths.append(license_path)
os.environ['AWX_LICENSE_FILE'] = license_path os.environ['AWX_LICENSE_FILE'] = license_path
def create_expired_license_file(self, instance_count=1000, grace_period=False): def create_expired_license_file(self, instance_count=1000, grace_period=False):
license_date = time.time() - 1 license_date = time.time() - 1
if not grace_period: if not grace_period:
@@ -383,7 +383,11 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
'vault_password': '', 'vault_password': '',
} }
opts.update(kwargs) opts.update(kwargs)
return Credential.objects.create(**opts) user = opts['user']
del opts['user']
cred = Credential.objects.create(**opts)
cred.owner_role.members.add(user)
return cred
def setup_instances(self): def setup_instances(self):
instance = Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1') instance = Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1')
@@ -422,7 +426,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
def get_invalid_credentials(self): def get_invalid_credentials(self):
return ('random', 'combination') return ('random', 'combination')
def _generic_rest(self, url, data=None, expect=204, auth=None, method=None, def _generic_rest(self, url, data=None, expect=204, auth=None, method=None,
data_type=None, accept=None, remote_addr=None, data_type=None, accept=None, remote_addr=None,
return_response_object=False, client_kwargs=None): return_response_object=False, client_kwargs=None):
@@ -517,7 +521,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
return self._generic_rest(url, data=None, expect=expect, auth=auth, return self._generic_rest(url, data=None, expect=expect, auth=auth,
method='head', accept=accept, method='head', accept=accept,
remote_addr=remote_addr) remote_addr=remote_addr)
def get(self, url, expect=200, auth=None, accept=None, remote_addr=None, client_kwargs={}): def get(self, url, expect=200, auth=None, accept=None, remote_addr=None, client_kwargs={}):
return self._generic_rest(url, data=None, expect=expect, auth=auth, return self._generic_rest(url, data=None, expect=expect, auth=auth,
method='get', accept=accept, method='get', accept=accept,
@@ -658,7 +662,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
else: else:
msg += 'Found %d occurances of "%s" instead of %d in: "%s"' % (count_actual, substr, count, string) msg += 'Found %d occurances of "%s" instead of %d in: "%s"' % (count_actual, substr, count, string)
self.assertEqual(count_actual, count, msg) self.assertEqual(count_actual, count, msg)
def check_job_result(self, job, expected='successful', expect_stdout=True, def check_job_result(self, job, expected='successful', expect_stdout=True,
expect_traceback=False): expect_traceback=False):
msg = u'job status is %s, expected %s' % (job.status, expected) msg = u'job status is %s, expected %s' % (job.status, expected)
+2 -4
View File
@@ -84,8 +84,7 @@ HPUhg3adAmIJ9z9u/VmTErbVklcKWlyZuTUkxeQ/BJmSIRUQAAAIEA3oKAzdDURjy8zxLX
gBLCPdi8AxCiqQJBCsGxXCgKtZewset1XJHIN9ryfb4QSZFkSOlm/LgdeGtS8Or0GNPRYd gBLCPdi8AxCiqQJBCsGxXCgKtZewset1XJHIN9ryfb4QSZFkSOlm/LgdeGtS8Or0GNPRYd
hgnUCF0LkEsDQ7HzPZYujLrAwjumvGQH6ORp5vRh0tQb93o4e1/A2vpdSKeH7gCe/jfUSY hgnUCF0LkEsDQ7HzPZYujLrAwjumvGQH6ORp5vRh0tQb93o4e1/A2vpdSKeH7gCe/jfUSY
h7dFGNoAI4cF7/0AAAAUcm9vdEBwaWxsb3cuaXhtbS5uZXQBAgMEBQYH h7dFGNoAI4cF7/0AAAAUcm9vdEBwaWxsb3cuaXhtbS5uZXQBAgMEBQYH
-----END OPENSSH PRIVATE KEY----- -----END OPENSSH PRIVATE KEY-----'''
'''
TEST_OPENSSH_KEY_DATA_LOCKED = '''-----BEGIN OPENSSH PRIVATE KEY----- TEST_OPENSSH_KEY_DATA_LOCKED = '''-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABALaWMfjc b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABALaWMfjc
@@ -114,8 +113,7 @@ C6Oxl1Wsp3gPkK2yiuy8qcrvoEoJ25TeEhUGEAPWx2OuQJO/Lpq9aF/JJoqGwnBaXdCsi+
5ig+ZMq5GKQtyydzyXImjlNEUH1w2prRDiGVEufANA5LSLCtqOLgDzXS62WUBjJBrQJVAM 5ig+ZMq5GKQtyydzyXImjlNEUH1w2prRDiGVEufANA5LSLCtqOLgDzXS62WUBjJBrQJVAM
YpWz1tiZQoyv1RT3Y0O0Vwe2Z5AK3fVM0I5jWdiLrIErtcR4ULa6T56QtA52DufhKzINTR YpWz1tiZQoyv1RT3Y0O0Vwe2Z5AK3fVM0I5jWdiLrIErtcR4ULa6T56QtA52DufhKzINTR
Vg9TtUBqfKIpRQikPSjm7vpY/Xnbc= Vg9TtUBqfKIpRQikPSjm7vpY/Xnbc=
-----END OPENSSH PRIVATE KEY----- -----END OPENSSH PRIVATE KEY-----'''
'''
TEST_SSH_CERT_KEY = """-----BEGIN CERTIFICATE----- TEST_SSH_CERT_KEY = """-----BEGIN CERTIFICATE-----
MIIDNTCCAh2gAwIBAgIBATALBgkqhkiG9w0BAQswSTEWMBQGA1UEAwwNV2luZG93 MIIDNTCCAh2gAwIBAgIBATALBgkqhkiG9w0BAQswSTEWMBQGA1UEAwwNV2luZG93
@@ -8,9 +8,8 @@ def resourced_organization(organization, project, team, inventory, user):
member_user = user('org-member') member_user = user('org-member')
# Associate one resource of every type with the organization # Associate one resource of every type with the organization
organization.users.add(member_user) organization.member_role.members.add(member_user)
organization.admins.add(admin_user) organization.admin_role.members.add(admin_user)
organization.projects.add(project)
# organization.teams.create(name='org-team') # organization.teams.create(name='org-team')
# inventory = organization.inventories.create(name="associated-inv") # inventory = organization.inventories.create(name="associated-inv")
project.jobtemplates.create(name="test-jt", project.jobtemplates.create(name="test-jt",
@@ -20,7 +19,27 @@ def resourced_organization(organization, project, team, inventory, user):
return organization return organization
@pytest.mark.django_db @pytest.mark.django_db
def test_org_counts_detail_view(resourced_organization, user, get):
# Check that all types of resources are counted by a superuser
external_admin = user('admin', True)
response = get(reverse('api:organization_detail',
args=[resourced_organization.pk]), external_admin)
assert response.status_code == 200
counts = response.data['summary_fields']['related_field_counts']
assert counts == {
'users': 1,
'admins': 1,
'job_templates': 1,
'projects': 1,
'inventories': 1,
'teams': 1
}
@pytest.mark.django_db
@pytest.mark.skipif("True") # XXX: This needs to be implemented
def test_org_counts_admin(resourced_organization, user, get): def test_org_counts_admin(resourced_organization, user, get):
# Check that all types of resources are counted by a superuser # Check that all types of resources are counted by a superuser
external_admin = user('admin', True) external_admin = user('admin', True)
@@ -41,17 +60,17 @@ def test_org_counts_admin(resourced_organization, user, get):
def test_org_counts_member(resourced_organization, get): def test_org_counts_member(resourced_organization, get):
# Check that a non-admin user can only see the full project and # Check that a non-admin user can only see the full project and
# user count, consistent with the RBAC rules # user count, consistent with the RBAC rules
member_user = resourced_organization.users.get(username='org-member') member_user = resourced_organization.member_role.members.get(username='org-member')
response = get(reverse('api:organization_list', args=[]), member_user) response = get(reverse('api:organization_list', args=[]), member_user)
assert response.status_code == 200 assert response.status_code == 200
counts = response.data['results'][0]['summary_fields']['related_field_counts'] counts = response.data['results'][0]['summary_fields']['related_field_counts']
assert counts == { assert counts == {
'users': 1, # User can see themselves 'users': 1, # Policy is that members can see other users and admins
'admins': 0, 'admins': 1,
'job_templates': 0, 'job_templates': 0,
'projects': 1, # Projects are shared with all the organization 'projects': 0,
'inventories': 0, 'inventories': 0,
'teams': 0 'teams': 0
} }
@@ -77,6 +96,7 @@ def test_new_org_zero_counts(user, post):
} }
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.skipif("True") # XXX: This needs to be implemented
def test_two_organizations(resourced_organization, organizations, user, get): def test_two_organizations(resourced_organization, organizations, user, get):
# Check correct results for two organizations are returned # Check correct results for two organizations are returned
external_admin = user('admin', True) external_admin = user('admin', True)
@@ -108,7 +128,9 @@ def test_two_organizations(resourced_organization, organizations, user, get):
'teams': 0 'teams': 0
} }
@pytest.mark.skip(reason="resolution planned for after RBAC merge")
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.skipif("True") # XXX: This needs to be implemented
def test_JT_associated_with_project(organizations, project, user, get): def test_JT_associated_with_project(organizations, project, user, get):
# Check that adding a project to an organization gets the project's JT # Check that adding a project to an organization gets the project's JT
# included in the organization's JT count # included in the organization's JT count
@@ -118,20 +140,20 @@ def test_JT_associated_with_project(organizations, project, user, get):
other_org = two_orgs[1] other_org = two_orgs[1]
unrelated_inv = other_org.inventories.create(name='not-in-organization') unrelated_inv = other_org.inventories.create(name='not-in-organization')
organization.projects.add(project)
project.jobtemplates.create(name="test-jt", project.jobtemplates.create(name="test-jt",
description="test-job-template-desc", description="test-job-template-desc",
inventory=unrelated_inv, inventory=unrelated_inv,
playbook="test_playbook.yml") playbook="test_playbook.yml")
organization.projects.add(project)
response = get(reverse('api:organization_list', args=[]), external_admin) response = get(reverse('api:organization_list', args=[]), external_admin)
assert response.status_code == 200 assert response.status_code == 200
org_id = organization.id org_id = organization.id
counts = {} counts = {}
for i in range(2): for org_json in response.data['results']:
working_id = response.data['results'][i]['id'] working_id = org_json['id']
counts[working_id] = response.data['results'][i]['summary_fields']['related_field_counts'] counts[working_id] = org_json['summary_fields']['related_field_counts']
assert counts[org_id] == { assert counts[org_id] == {
'users': 0, 'users': 0,
+57
View File
@@ -32,6 +32,7 @@ from awx.main.models.inventory import (
from awx.main.models.organization import ( from awx.main.models.organization import (
Organization, Organization,
Permission, Permission,
Team,
) )
from awx.main.models.rbac import Role from awx.main.models.rbac import Role
@@ -102,6 +103,33 @@ def project(instance, organization):
) )
return prj return prj
@pytest.fixture
def project_factory(organization):
def factory(name):
try:
prj = Project.objects.get(name=name)
except Project.DoesNotExist:
prj = Project.objects.create(name=name,
description="description for " + name,
scm_type="git",
scm_url="https://github.com/jlaska/ansible-playbooks",
organization=organization
)
return prj
return factory
@pytest.fixture
def team_factory(organization):
def factory(name):
try:
t = Team.objects.get(name=name)
except Team.DoesNotExist:
t = Team.objects.create(name=name,
description="description for " + name,
organization=organization)
return t
return factory
@pytest.fixture @pytest.fixture
def user_project(user): def user_project(user):
owner = user('owner') owner = user('owner')
@@ -139,6 +167,24 @@ def alice(user):
def bob(user): def bob(user):
return user('bob', False) return user('bob', False)
@pytest.fixture
def rando(user):
"Rando, the random user that doesn't have access to anything"
return user('rando', False)
@pytest.fixture
def org_admin(user, organization):
ret = user('org-admin', False)
organization.admin_role.members.add(ret)
organization.member_role.members.add(ret)
return ret
@pytest.fixture
def org_member(user, organization):
ret = user('org-member', False)
organization.member_role.members.add(ret)
return ret
@pytest.fixture @pytest.fixture
def organizations(instance): def organizations(instance):
def rf(organization_count=1): def rf(organization_count=1):
@@ -354,3 +400,14 @@ def fact_services_json():
def permission_inv_read(organization, inventory, team): def permission_inv_read(organization, inventory, team):
return Permission.objects.create(inventory=inventory, team=team, permission_type=PERM_INVENTORY_READ) return Permission.objects.create(inventory=inventory, team=team, permission_type=PERM_INVENTORY_READ)
@pytest.fixture
def job_template_labels(organization):
jt = JobTemplate(name='test-job_template')
jt.save()
jt.labels.create(name="label-1", organization=organization)
jt.labels.create(name="label-2", organization=organization)
return jt
@@ -0,0 +1,34 @@
import pytest
class TestCreateUnifiedJob:
'''
Ensure that copying a job template to a job handles many to many field copy
'''
@pytest.mark.django_db
def test_many_to_many(self, mocker, job_template_labels):
jt = job_template_labels
_get_unified_job_field_names = mocker.patch('awx.main.models.jobs.JobTemplate._get_unified_job_field_names', return_value=['labels'])
j = jt.create_unified_job()
_get_unified_job_field_names.assert_called_with()
assert j.labels.all().count() == 2
assert j.labels.all()[0] == jt.labels.all()[0]
assert j.labels.all()[1] == jt.labels.all()[1]
'''
Ensure that data is looked for in parameter list before looking at the object
'''
@pytest.mark.django_db
def test_many_to_many_kwargs(self, mocker, job_template_labels):
jt = job_template_labels
mocked = mocker.MagicMock()
mocked.__class__.__name__ = 'ManyRelatedManager'
kwargs = {
'labels': mocked
}
_get_unified_job_field_names = mocker.patch('awx.main.models.jobs.JobTemplate._get_unified_job_field_names', return_value=['labels'])
jt.create_unified_job(**kwargs)
_get_unified_job_field_names.assert_called_with()
mocked.all.assert_called_with()
+140
View File
@@ -0,0 +1,140 @@
import mock # noqa
import pytest
from django.core.urlresolvers import reverse
from awx.main.models import Project
#
# Project listing and visibility tests
#
@pytest.mark.django_db(transaction=True)
def test_user_project_list(get, project_factory, admin, alice, bob):
'List of projects a user has access to, filtered by projects you can also see'
alice_project = project_factory('alice project')
alice_project.admin_role.members.add(alice)
bob_project = project_factory('bob project')
bob_project.admin_role.members.add(bob)
shared_project = project_factory('shared project')
shared_project.admin_role.members.add(alice)
shared_project.admin_role.members.add(bob)
# admins can see all projects
assert get(reverse('api:user_projects_list', args=(admin.pk,)), admin).data['count'] == 3
# admins can see everyones projects
assert get(reverse('api:user_projects_list', args=(alice.pk,)), admin).data['count'] == 2
assert get(reverse('api:user_projects_list', args=(bob.pk,)), admin).data['count'] == 2
# users can see their own projects
assert get(reverse('api:user_projects_list', args=(alice.pk,)), alice).data['count'] == 2
# alice should only be able to see the shared project when looking at bobs projects
assert get(reverse('api:user_projects_list', args=(bob.pk,)), alice).data['count'] == 1
# alice should see all projects they can see when viewing an admin
assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2
@pytest.mark.django_db(transaction=True)
def test_team_project_list(get, project_factory, team_factory, admin, alice, bob):
'List of projects a team has access to, filtered by projects you can also see'
team1 = team_factory('team1')
team2 = team_factory('team2')
team1_project = project_factory('team1 project')
team1_project.admin_role.parents.add(team1.member_role)
team2_project = project_factory('team2 project')
team2_project.admin_role.parents.add(team2.member_role)
shared_project = project_factory('shared project')
shared_project.admin_role.parents.add(team1.member_role)
shared_project.admin_role.parents.add(team2.member_role)
team1.member_role.members.add(alice)
team2.member_role.members.add(bob)
# admins can see all projects on a team
assert get(reverse('api:team_projects_list', args=(team1.pk,)), admin).data['count'] == 2
assert get(reverse('api:team_projects_list', args=(team2.pk,)), admin).data['count'] == 2
# users can see all projects on teams they are a member of
assert get(reverse('api:team_projects_list', args=(team1.pk,)), alice).data['count'] == 2
# alice should not be able to see team2 projects because she doesn't have access to team2
res = get(reverse('api:team_projects_list', args=(team2.pk,)), alice)
assert res.status_code == 403
# but if she does, then she should only see the shared project
team2.auditor_role.members.add(alice)
assert get(reverse('api:team_projects_list', args=(team2.pk,)), alice).data['count'] == 1
team2.auditor_role.members.remove(alice)
# Test user endpoints first, very similar tests to test_user_project_list
# but permissions are being derived from team membership instead.
# admins can see all projects
assert get(reverse('api:user_projects_list', args=(admin.pk,)), admin).data['count'] == 3
# admins can see everyones projects
assert get(reverse('api:user_projects_list', args=(alice.pk,)), admin).data['count'] == 2
assert get(reverse('api:user_projects_list', args=(bob.pk,)), admin).data['count'] == 2
# users can see their own projects
assert get(reverse('api:user_projects_list', args=(alice.pk,)), alice).data['count'] == 2
# alice should not be able to see bob
res = get(reverse('api:user_projects_list', args=(bob.pk,)), alice)
assert res.status_code == 403
# alice should see all projects they can see when viewing an admin
assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2
@pytest.mark.django_db(transaction=True)
def test_create_project(post, organization, org_admin, org_member, admin, rando):
test_list = [rando, org_member, org_admin, admin]
expected_status_codes = [403, 403, 201, 201]
for i, u in enumerate(test_list):
result = post(reverse('api:project_list'), {
'name': 'Project %d' % i,
'organization': organization.id,
}, u)
print(result.data)
assert result.status_code == expected_status_codes[i]
if expected_status_codes[i] == 201:
assert Project.objects.filter(name='Project %d' % i, organization=organization).exists()
else:
assert not Project.objects.filter(name='Project %d' % i, organization=organization).exists()
@pytest.mark.django_db(transaction=True)
def test_cant_create_project_without_org(post, organization, org_admin, org_member, admin, rando):
assert post(reverse('api:project_list'), { 'name': 'Project foo', }, admin).status_code == 400
assert post(reverse('api:project_list'), { 'name': 'Project foo', 'organization': None}, admin).status_code == 400
@pytest.mark.django_db(transaction=True)
def test_create_project_through_org_link(post, organization, org_admin, org_member, admin, rando):
test_list = [rando, org_member, org_admin, admin]
expected_status_codes = [403, 403, 201, 201]
for i, u in enumerate(test_list):
result = post(reverse('api:organization_projects_list', args=(organization.id,)), {
'name': 'Project %d' % i,
}, u)
assert result.status_code == expected_status_codes[i]
if expected_status_codes[i] == 201:
prj = Project.objects.get(name='Project %d' % i)
print(prj.organization)
Project.objects.get(name='Project %d' % i, organization=organization)
assert Project.objects.filter(name='Project %d' % i, organization=organization).exists()
else:
assert not Project.objects.filter(name='Project %d' % i, organization=organization).exists()
+7 -5
View File
@@ -265,7 +265,7 @@ def test_remove_user_to_role(post, admin, role):
post(url, {'disassociate': True, 'id': admin.id}, admin) post(url, {'disassociate': True, 'id': admin.id}, admin)
assert role.members.filter(id=admin.id).count() == 0 assert role.members.filter(id=admin.id).count() == 0
@pytest.mark.django_db @pytest.mark.django_db(transaction=True)
def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplate, user): 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' 'Tests that a user with permissions to assign/revoke membership to a particular role can do so'
org_admin = user('org-admin') org_admin = user('org-admin')
@@ -275,12 +275,13 @@ def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplat
assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False 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) res =post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, org_admin)
print(res.data)
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
@pytest.mark.django_db @pytest.mark.django_db(transaction=True)
def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemplate, user): 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' 'Tests that a user with permissions to assign/revoke membership to a particular role can do so'
org_admin = user('org-admin') org_admin = user('org-admin')
@@ -295,7 +296,7 @@ def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemp
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
@pytest.mark.django_db @pytest.mark.django_db(transaction=True)
def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemplate, user): 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' 'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so'
rando = user('rando') rando = user('rando')
@@ -305,12 +306,13 @@ def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemp
assert check_jobtemplate.accessible_by(joe, {'execute': 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) res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, rando)
print(res.data)
assert res.status_code == 403 assert res.status_code == 403
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
@pytest.mark.django_db @pytest.mark.django_db(transaction=True)
def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobtemplate, user): 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' 'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so'
rando = user('rando') rando = user('rando')
@@ -241,20 +241,3 @@ def test_auto_parenting():
assert org2.admin_role.is_ancestor_of(prj1.admin_role) assert org2.admin_role.is_ancestor_of(prj1.admin_role)
assert org2.admin_role.is_ancestor_of(prj2.admin_role) assert org2.admin_role.is_ancestor_of(prj2.admin_role)
@pytest.mark.django_db
def test_auto_m2m_parenting(team, project, user):
u = user('some-user')
team.member_role.members.add(u)
assert project.accessible_by(u, {'read': True}) is False
project.teams.add(team)
assert project.accessible_by(u, {'read': True})
project.teams.remove(team)
assert project.accessible_by(u, {'read': True}) is False
team.projects.add(project)
assert project.accessible_by(u, {'read': True})
team.projects.remove(project)
assert project.accessible_by(u, {'read': True}) is False
@@ -11,12 +11,11 @@ from django.contrib.auth.models import User
@pytest.mark.django_db @pytest.mark.django_db
def test_credential_migration_user(credential, user, permissions): def test_credential_migration_user(credential, user, permissions):
u = user('user', False) u = user('user', False)
credential.user = u credential.deprecated_user = u
credential.save() credential.save()
migrated = rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
assert len(migrated) == 1
assert credential.accessible_by(u, permissions['admin']) assert credential.accessible_by(u, permissions['admin'])
@pytest.mark.django_db @pytest.mark.django_db
@@ -29,7 +28,7 @@ def test_credential_usage_role(credential, user, permissions):
def test_credential_migration_team_member(credential, team, user, permissions): def test_credential_migration_team_member(credential, team, user, permissions):
u = user('user', False) u = user('user', False)
team.admin_role.members.add(u) team.admin_role.members.add(u)
credential.team = team credential.deprecated_team = team
credential.save() credential.save()
@@ -38,24 +37,22 @@ def test_credential_migration_team_member(credential, team, user, permissions):
team.member_role.children.remove(credential.usage_role) team.member_role.children.remove(credential.usage_role)
assert not credential.accessible_by(u, permissions['admin']) assert not credential.accessible_by(u, permissions['admin'])
migrated = rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
# Admin permissions post migration # Admin permissions post migration
assert len(migrated) == 1
assert credential.accessible_by(u, permissions['admin']) assert credential.accessible_by(u, permissions['admin'])
@pytest.mark.django_db @pytest.mark.django_db
def test_credential_migration_team_admin(credential, team, user, permissions): def test_credential_migration_team_admin(credential, team, user, permissions):
u = user('user', False) u = user('user', False)
team.member_role.members.add(u) team.member_role.members.add(u)
credential.team = team credential.deprecated_team = team
credential.save() credential.save()
assert not credential.accessible_by(u, permissions['usage']) assert not credential.accessible_by(u, permissions['usage'])
# Usage permissions post migration # Usage permissions post migration
migrated = rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
assert len(migrated) == 1
assert credential.accessible_by(u, permissions['usage']) assert credential.accessible_by(u, permissions['usage'])
def test_credential_access_superuser(): def test_credential_access_superuser():
@@ -88,7 +85,7 @@ def test_credential_access_admin(user, team, credential):
credential.owner_role.rebuild_role_ancestor_list() credential.owner_role.rebuild_role_ancestor_list()
cred = Credential.objects.create(kind='aws', name='test-cred') cred = Credential.objects.create(kind='aws', name='test-cred')
cred.team = team cred.deprecated_team = team
cred.save() cred.save()
# should have can_change access as org-admin # should have can_change access as org-admin
@@ -101,7 +98,7 @@ def test_cred_job_template(user, deploy_jobtemplate):
org.admin_role.members.add(a) org.admin_role.members.add(a)
cred = deploy_jobtemplate.credential cred = deploy_jobtemplate.credential
cred.user = user('john', False) cred.deprecated_user = user('john', False)
cred.save() cred.save()
access = CredentialAccess(a) access = CredentialAccess(a)
@@ -118,7 +115,7 @@ def test_cred_multi_job_template_single_org(user, deploy_jobtemplate):
org.admin_role.members.add(a) org.admin_role.members.add(a)
cred = deploy_jobtemplate.credential cred = deploy_jobtemplate.credential
cred.user = user('john', False) cred.deprecated_user = user('john', False)
cred.save() cred.save()
access = CredentialAccess(a) access = CredentialAccess(a)
@@ -197,7 +194,7 @@ def test_cred_no_org(user, credential):
def test_cred_team(user, team, credential): def test_cred_team(user, team, credential):
u = user('a', False) u = user('a', False)
team.member_role.members.add(u) team.member_role.members.add(u)
credential.team = team credential.deprecated_team = team
credential.save() credential.save()
assert not credential.accessible_by(u, {'use':True}) assert not credential.accessible_by(u, {'use':True})
@@ -13,10 +13,8 @@ def test_inventory_admin_user(inventory, permissions, user):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(migrations[inventory.name]['users']) == 1
assert len(migrations[inventory.name]['teams']) == 0
assert inventory.accessible_by(u, permissions['admin']) assert inventory.accessible_by(u, permissions['admin'])
assert inventory.executor_role.members.filter(id=u.id).exists() is False assert inventory.executor_role.members.filter(id=u.id).exists() is False
assert inventory.updater_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() is False
@@ -30,10 +28,8 @@ def test_inventory_auditor_user(inventory, permissions, user):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is False assert inventory.accessible_by(u, permissions['auditor']) is False
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(migrations[inventory.name]['users']) == 1
assert len(migrations[inventory.name]['teams']) == 0
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is True assert inventory.accessible_by(u, permissions['auditor']) is True
assert inventory.executor_role.members.filter(id=u.id).exists() is False assert inventory.executor_role.members.filter(id=u.id).exists() is False
@@ -48,10 +44,8 @@ def test_inventory_updater_user(inventory, permissions, user):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is False assert inventory.accessible_by(u, permissions['auditor']) is False
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(migrations[inventory.name]['users']) == 1
assert len(migrations[inventory.name]['teams']) == 0
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.executor_role.members.filter(id=u.id).exists() is False assert inventory.executor_role.members.filter(id=u.id).exists() is False
assert inventory.updater_role.members.filter(id=u.id).exists() assert inventory.updater_role.members.filter(id=u.id).exists()
@@ -65,10 +59,8 @@ def test_inventory_executor_user(inventory, permissions, user):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is False assert inventory.accessible_by(u, permissions['auditor']) is False
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(migrations[inventory.name]['users']) == 1
assert len(migrations[inventory.name]['teams']) == 0
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is True assert inventory.accessible_by(u, permissions['auditor']) is True
assert inventory.executor_role.members.filter(id=u.id).exists() assert inventory.executor_role.members.filter(id=u.id).exists()
@@ -85,13 +77,10 @@ def test_inventory_admin_team(inventory, permissions, user, team):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
team_migrations = rbac.migrate_team(apps, None) rbac.migrate_team(apps, None)
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(team_migrations) == 1
assert team.member_role.members.count() == 1 assert team.member_role.members.count() == 1
assert len(migrations[inventory.name]['users']) == 0
assert len(migrations[inventory.name]['teams']) == 1
assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.admin_role.members.filter(id=u.id).exists() is False
assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False
assert inventory.executor_role.members.filter(id=u.id).exists() is False assert inventory.executor_role.members.filter(id=u.id).exists() is False
@@ -110,13 +99,10 @@ def test_inventory_auditor(inventory, permissions, user, team):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is False assert inventory.accessible_by(u, permissions['auditor']) is False
team_migrations = rbac.migrate_team(apps,None) rbac.migrate_team(apps,None)
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(team_migrations) == 1
assert team.member_role.members.count() == 1 assert team.member_role.members.count() == 1
assert len(migrations[inventory.name]['users']) == 0
assert len(migrations[inventory.name]['teams']) == 1
assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.admin_role.members.filter(id=u.id).exists() is False
assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False
assert inventory.executor_role.members.filter(id=u.id).exists() is False assert inventory.executor_role.members.filter(id=u.id).exists() is False
@@ -134,13 +120,10 @@ def test_inventory_updater(inventory, permissions, user, team):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is False assert inventory.accessible_by(u, permissions['auditor']) is False
team_migrations = rbac.migrate_team(apps,None) rbac.migrate_team(apps,None)
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(team_migrations) == 1
assert team.member_role.members.count() == 1 assert team.member_role.members.count() == 1
assert len(migrations[inventory.name]['users']) == 0
assert len(migrations[inventory.name]['teams']) == 1
assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.admin_role.members.filter(id=u.id).exists() is False
assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False
assert inventory.executor_role.members.filter(id=u.id).exists() is False assert inventory.executor_role.members.filter(id=u.id).exists() is False
@@ -159,13 +142,10 @@ def test_inventory_executor(inventory, permissions, user, team):
assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['admin']) is False
assert inventory.accessible_by(u, permissions['auditor']) is False assert inventory.accessible_by(u, permissions['auditor']) is False
team_migrations = rbac.migrate_team(apps, None) rbac.migrate_team(apps, None)
migrations = rbac.migrate_inventory(apps, None) rbac.migrate_inventory(apps, None)
assert len(team_migrations) == 1
assert team.member_role.members.count() == 1 assert team.member_role.members.count() == 1
assert len(migrations[inventory.name]['users']) == 0
assert len(migrations[inventory.name]['teams']) == 1
assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.admin_role.members.filter(id=u.id).exists() is False
assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False
assert inventory.executor_role.members.filter(id=u.id).exists() is False assert inventory.executor_role.members.filter(id=u.id).exists() is False
@@ -31,9 +31,8 @@ def test_job_template_migration_check(deploy_jobtemplate, check_jobtemplate, use
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None) rbac.migrate_job_templates(apps, None)
assert len(migrations[check_jobtemplate.name]['users']) == 1
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
@@ -60,9 +59,8 @@ def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, us
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None) rbac.migrate_job_templates(apps, None)
assert len(migrations[deploy_jobtemplate.name]['users']) == 1
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
@@ -93,10 +91,8 @@ def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None) rbac.migrate_job_templates(apps, None)
assert len(migrations[check_jobtemplate.name]['users']) == 0
assert len(migrations[check_jobtemplate.name]['teams']) == 1
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
@@ -128,10 +124,8 @@ def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplat
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
migrations = rbac.migrate_job_templates(apps, None) rbac.migrate_job_templates(apps, None)
assert len(migrations[deploy_jobtemplate.name]['users']) == 0
assert len(migrations[deploy_jobtemplate.name]['teams']) == 1
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True
@@ -18,9 +18,8 @@ def test_organization_migration_admin(organization, permissions, user):
organization.admin_role.members.remove(u) organization.admin_role.members.remove(u)
assert not organization.accessible_by(u, permissions['admin']) assert not organization.accessible_by(u, permissions['admin'])
migrations = rbac.migrate_organization(apps, None) rbac.migrate_organization(apps, None)
assert len(migrations) == 1
assert organization.accessible_by(u, permissions['admin']) assert organization.accessible_by(u, permissions['admin'])
@pytest.mark.django_db @pytest.mark.django_db
@@ -32,9 +31,8 @@ def test_organization_migration_user(organization, permissions, user):
organization.member_role.members.remove(u) organization.member_role.members.remove(u)
assert not organization.accessible_by(u, permissions['auditor']) assert not organization.accessible_by(u, permissions['auditor'])
migrations = rbac.migrate_organization(apps, None) rbac.migrate_organization(apps, None)
assert len(migrations) == 1
assert organization.accessible_by(u, permissions['auditor']) assert organization.accessible_by(u, permissions['auditor'])
+9 -18
View File
@@ -60,7 +60,8 @@ def test_project_migration():
c1 = Credential.objects.create(name='c1') c1 = Credential.objects.create(name='c1')
p1 = Project.objects.create(name='p1', credential=c1) project_name = unicode("\xc3\xb4", "utf-8")
p1 = Project.objects.create(name=project_name, credential=c1)
p1.deprecated_organizations.add(o1, o2, o3) p1.deprecated_organizations.add(o1, o2, o3)
i1 = Inventory.objects.create(name='i1', organization=o1) i1 = Inventory.objects.create(name='i1', organization=o1)
@@ -99,9 +100,7 @@ def test_project_user_project(user_project, project, user):
assert user_project.accessible_by(u, {'read': True}) is False assert user_project.accessible_by(u, {'read': True}) is False
assert project.accessible_by(u, {'read': True}) is False assert project.accessible_by(u, {'read': True}) is False
migrations = rbac.migrate_projects(apps, None) rbac.migrate_projects(apps, None)
assert len(migrations[user_project.name]['users']) == 1
assert len(migrations[user_project.name]['teams']) == 0
assert user_project.accessible_by(u, {'read': True}) is True assert user_project.accessible_by(u, {'read': True}) is True
assert project.accessible_by(u, {'read': True}) is False assert project.accessible_by(u, {'read': True}) is False
@@ -113,11 +112,8 @@ def test_project_accessible_by_sa(user, project):
assert project.accessible_by(u, {'read': True}) is False assert project.accessible_by(u, {'read': True}) is False
rbac.migrate_organization(apps, None) rbac.migrate_organization(apps, None)
su_migrations = rbac.migrate_users(apps, None) rbac.migrate_users(apps, None)
migrations = rbac.migrate_projects(apps, None) rbac.migrate_projects(apps, None)
assert len(su_migrations) == 1
assert len(migrations[project.name]['users']) == 0
assert len(migrations[project.name]['teams']) == 0
print(project.admin_role.ancestors.all()) print(project.admin_role.ancestors.all())
print(project.admin_role.ancestors.all()) print(project.admin_role.ancestors.all())
assert project.accessible_by(u, {'read': True, 'write': True}) is True assert project.accessible_by(u, {'read': True, 'write': True}) is True
@@ -134,10 +130,8 @@ def test_project_org_members(user, organization, project):
organization.deprecated_users.add(member) organization.deprecated_users.add(member)
rbac.migrate_organization(apps, None) rbac.migrate_organization(apps, None)
migrations = rbac.migrate_projects(apps, None) rbac.migrate_projects(apps, None)
assert len(migrations[project.name]['users']) == 1
assert len(migrations[project.name]['teams']) == 0
assert project.accessible_by(admin, {'read': True, 'write': True}) is True assert project.accessible_by(admin, {'read': True, 'write': True}) is True
assert project.accessible_by(member, {'read': True}) assert project.accessible_by(member, {'read': True})
@@ -147,17 +141,15 @@ def test_project_team(user, team, project):
member = user('member') member = user('member')
team.deprecated_users.add(member) team.deprecated_users.add(member)
project.teams.add(team) project.deprecated_teams.add(team)
assert project.accessible_by(nonmember, {'read': True}) is False assert project.accessible_by(nonmember, {'read': True}) is False
assert project.accessible_by(member, {'read': True}) is False assert project.accessible_by(member, {'read': True}) is False
rbac.migrate_team(apps, None) rbac.migrate_team(apps, None)
rbac.migrate_organization(apps, None) rbac.migrate_organization(apps, None)
migrations = rbac.migrate_projects(apps, None) rbac.migrate_projects(apps, None)
assert len(migrations[project.name]['users']) == 0
assert len(migrations[project.name]['teams']) == 1
assert project.accessible_by(member, {'read': True}) is True assert project.accessible_by(member, {'read': True}) is True
assert project.accessible_by(nonmember, {'read': True}) is False assert project.accessible_by(nonmember, {'read': True}) is False
@@ -174,7 +166,6 @@ def test_project_explicit_permission(user, team, project, organization):
assert project.accessible_by(u, {'read': True}) is False assert project.accessible_by(u, {'read': True}) is False
rbac.migrate_organization(apps, None) rbac.migrate_organization(apps, None)
migrations = rbac.migrate_projects(apps, None) rbac.migrate_projects(apps, None)
assert len(migrations[project.name]['users']) == 1
assert project.accessible_by(u, {'read': True}) is True assert project.accessible_by(u, {'read': True}) is True
@@ -1,6 +1,7 @@
import pytest import pytest
from awx.main.access import TeamAccess from awx.main.access import TeamAccess
from awx.main.models import Project
@pytest.mark.django_db @pytest.mark.django_db
def test_team_access_superuser(team, user): def test_team_access_superuser(team, user):
@@ -48,3 +49,25 @@ def test_team_access_member(organization, team, user):
assert len(t.member_role.members.all()) == 1 assert len(t.member_role.members.all()) == 1
assert len(t.organization.admin_role.members.all()) == 0 assert len(t.organization.admin_role.members.all()) == 0
@pytest.mark.django_db
def test_team_accessible_by(team, user, project):
u = user('team_member', False)
team.member_role.children.add(project.member_role)
assert project.accessible_by(team, {'read':True})
assert not project.accessible_by(u, {'read':True})
team.member_role.members.add(u)
assert project.accessible_by(u, {'read':True})
@pytest.mark.django_db
def test_team_accessible_objects(team, user, project):
u = user('team_member', False)
team.member_role.children.add(project.member_role)
assert len(Project.accessible_objects(team, {'read':True})) == 1
assert not Project.accessible_objects(u, {'read':True})
team.member_role.members.add(u)
assert len(Project.accessible_objects(u, {'read':True})) == 1
+4 -3
View File
@@ -9,7 +9,9 @@ from awx.main.models import Role
@pytest.mark.django_db @pytest.mark.django_db
def test_user_admin(user_project, project, user): def test_user_admin(user_project, project, user):
joe = user('joe', is_superuser = False) username = unicode("\xc3\xb4", "utf-8")
joe = user(username, is_superuser = False)
admin = user('admin', is_superuser = True) admin = user('admin', is_superuser = True)
sa = Role.singleton('System Administrator') sa = Role.singleton('System Administrator')
@@ -20,12 +22,11 @@ def test_user_admin(user_project, project, user):
assert sa.members.filter(id=joe.id).exists() is False assert sa.members.filter(id=joe.id).exists() is False
assert sa.members.filter(id=admin.id).exists() is False assert sa.members.filter(id=admin.id).exists() is False
migrations = rbac.migrate_users(apps, None) rbac.migrate_users(apps, None)
# The migration should add the admin back in # The migration should add the admin back in
assert sa.members.filter(id=joe.id).exists() is False assert sa.members.filter(id=joe.id).exists() is False
assert sa.members.filter(id=admin.id).exists() is True assert sa.members.filter(id=admin.id).exists() is True
assert len(migrations) == 1
@pytest.mark.django_db @pytest.mark.django_db
def test_user_queryset(user): def test_user_queryset(user):
+51 -25
View File
@@ -142,12 +142,12 @@ class BaseJobTestMixin(BaseTestMixin):
self.org_eng.projects.add(self.proj_dev) self.org_eng.projects.add(self.proj_dev)
self.proj_test = self.make_project('test', 'testing branch', self.proj_test = self.make_project('test', 'testing branch',
self.user_sue, TEST_PLAYBOOK) self.user_sue, TEST_PLAYBOOK)
self.org_eng.projects.add(self.proj_test) #self.org_eng.projects.add(self.proj_test) # No more multi org projects
self.org_sup.projects.add(self.proj_test) self.org_sup.projects.add(self.proj_test)
self.proj_prod = self.make_project('prod', 'production branch', self.proj_prod = self.make_project('prod', 'production branch',
self.user_sue, TEST_PLAYBOOK) self.user_sue, TEST_PLAYBOOK)
self.org_eng.projects.add(self.proj_prod) #self.org_eng.projects.add(self.proj_prod) # No more multi org projects
self.org_sup.projects.add(self.proj_prod) #self.org_sup.projects.add(self.proj_prod) # No more multi org projects
self.org_ops.projects.add(self.proj_prod) self.org_ops.projects.add(self.proj_prod)
# Operations also has 2 additional projects specific to the east/west # Operations also has 2 additional projects specific to the east/west
@@ -216,15 +216,15 @@ class BaseJobTestMixin(BaseTestMixin):
self.team_ops_east = self.org_ops.teams.create( self.team_ops_east = self.org_ops.teams.create(
name='easterners', name='easterners',
created_by=self.user_sue) created_by=self.user_sue)
self.team_ops_east.projects.add(self.proj_prod) self.team_ops_east.member_role.children.add(self.proj_prod.admin_role)
self.team_ops_east.projects.add(self.proj_prod_east) self.team_ops_east.member_role.children.add(self.proj_prod_east.admin_role)
self.team_ops_east.member_role.members.add(self.user_greg) self.team_ops_east.member_role.members.add(self.user_greg)
self.team_ops_east.member_role.members.add(self.user_holly) self.team_ops_east.member_role.members.add(self.user_holly)
self.team_ops_west = self.org_ops.teams.create( self.team_ops_west = self.org_ops.teams.create(
name='westerners', name='westerners',
created_by=self.user_sue) created_by=self.user_sue)
self.team_ops_west.projects.add(self.proj_prod) self.team_ops_west.member_role.children.add(self.proj_prod.admin_role)
self.team_ops_west.projects.add(self.proj_prod_west) self.team_ops_west.member_role.children.add(self.proj_prod_west.admin_role)
self.team_ops_west.member_role.members.add(self.user_greg) self.team_ops_west.member_role.members.add(self.user_greg)
self.team_ops_west.member_role.members.add(self.user_iris) self.team_ops_west.member_role.members.add(self.user_iris)
@@ -239,7 +239,7 @@ class BaseJobTestMixin(BaseTestMixin):
# created_by=self.user_sue, # created_by=self.user_sue,
# active=False, # active=False,
#) #)
#self.team_ops_south.projects.add(self.proj_prod) #self.team_ops_south.member_role.children.add(self.proj_prod.admin_role)
#self.team_ops_south.member_role.members.add(self.user_greg) #self.team_ops_south.member_role.members.add(self.user_greg)
# The north team is going to be deleted # The north team is going to be deleted
@@ -247,7 +247,7 @@ class BaseJobTestMixin(BaseTestMixin):
name='northerners', name='northerners',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.team_ops_north.projects.add(self.proj_prod) self.team_ops_north.member_role.children.add(self.proj_prod.admin_role)
self.team_ops_north.member_role.members.add(self.user_greg) self.team_ops_north.member_role.members.add(self.user_greg)
# The testers team are interns that can only check playbooks but can't # The testers team are interns that can only check playbooks but can't
@@ -256,7 +256,7 @@ class BaseJobTestMixin(BaseTestMixin):
name='testers', name='testers',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.team_ops_testers.projects.add(self.proj_prod) self.team_ops_testers.member_role.children.add(self.proj_prod.admin_role)
self.team_ops_testers.member_role.members.add(self.user_randall) self.team_ops_testers.member_role.members.add(self.user_randall)
self.team_ops_testers.member_role.members.add(self.user_billybob) self.team_ops_testers.member_role.members.add(self.user_billybob)
@@ -264,17 +264,21 @@ class BaseJobTestMixin(BaseTestMixin):
from awx.main.tests.data.ssh import (TEST_SSH_KEY_DATA, from awx.main.tests.data.ssh import (TEST_SSH_KEY_DATA,
TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_LOCKED,
TEST_SSH_KEY_DATA_UNLOCK) TEST_SSH_KEY_DATA_UNLOCK)
self.cred_sue = self.user_sue.credentials.create( self.cred_sue = Credential.objects.create(
username='sue', username='sue',
password=TEST_SSH_KEY_DATA, password=TEST_SSH_KEY_DATA,
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_sue_ask = self.user_sue.credentials.create( self.cred_sue.owner_role.members.add(self.user_sue)
self.cred_sue_ask = Credential.objects.create(
username='sue', username='sue',
password='ASK', password='ASK',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_sue_ask_many = self.user_sue.credentials.create( self.cred_sue_ask.owner_role.members.add(self.user_sue)
self.cred_sue_ask_many = Credential.objects.create(
username='sue', username='sue',
password='ASK', password='ASK',
become_method='sudo', become_method='sudo',
@@ -284,23 +288,31 @@ class BaseJobTestMixin(BaseTestMixin):
ssh_key_unlock='ASK', ssh_key_unlock='ASK',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_bob = self.user_bob.credentials.create( self.cred_sue_ask_many.owner_role.members.add(self.user_sue)
self.cred_bob = Credential.objects.create(
username='bob', username='bob',
password='ASK', password='ASK',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_chuck = self.user_chuck.credentials.create( self.cred_bob.usage_role.members.add(self.user_bob)
self.cred_chuck = Credential.objects.create(
username='chuck', username='chuck',
ssh_key_data=TEST_SSH_KEY_DATA, ssh_key_data=TEST_SSH_KEY_DATA,
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_doug = self.user_doug.credentials.create( self.cred_chuck.usage_role.members.add(self.user_chuck)
self.cred_doug = Credential.objects.create(
username='doug', username='doug',
password='doug doesn\'t mind his password being saved. this ' password='doug doesn\'t mind his password being saved. this '
'is why we dont\'t let doug actually run jobs.', 'is why we dont\'t let doug actually run jobs.',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_eve = self.user_eve.credentials.create( self.cred_doug.usage_role.members.add(self.user_doug)
self.cred_eve = Credential.objects.create(
username='eve', username='eve',
password='ASK', password='ASK',
become_method='sudo', become_method='sudo',
@@ -308,40 +320,52 @@ class BaseJobTestMixin(BaseTestMixin):
become_password='ASK', become_password='ASK',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_frank = self.user_frank.credentials.create( self.cred_eve.usage_role.members.add(self.user_eve)
self.cred_frank = Credential.objects.create(
username='frank', username='frank',
password='fr@nk the t@nk', password='fr@nk the t@nk',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_greg = self.user_greg.credentials.create( self.cred_frank.usage_role.members.add(self.user_frank)
self.cred_greg = Credential.objects.create(
username='greg', username='greg',
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED, ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
ssh_key_unlock='ASK', ssh_key_unlock='ASK',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_holly = self.user_holly.credentials.create( self.cred_greg.usage_role.members.add(self.user_greg)
self.cred_holly = Credential.objects.create(
username='holly', username='holly',
password='holly rocks', password='holly rocks',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_iris = self.user_iris.credentials.create( self.cred_holly.usage_role.members.add(self.user_holly)
self.cred_iris = Credential.objects.create(
username='iris', username='iris',
password='ASK', password='ASK',
created_by=self.user_sue, created_by=self.user_sue,
) )
self.cred_iris.usage_role.members.add(self.user_iris)
# Each operations team also has shared credentials they can use. # Each operations team also has shared credentials they can use.
self.cred_ops_east = self.team_ops_east.credentials.create( self.cred_ops_east = Credential.objects.create(
username='east', username='east',
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED, ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK, ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK,
created_by = self.user_sue, created_by = self.user_sue,
) )
self.cred_ops_west = self.team_ops_west.credentials.create( self.team_ops_east.member_role.children.add(self.cred_ops_east.usage_role)
self.cred_ops_west = Credential.objects.create(
username='west', username='west',
password='Heading270', password='Heading270',
created_by = self.user_sue, created_by = self.user_sue,
) )
self.team_ops_west.member_role.children.add(self.cred_ops_west.usage_role)
# FIXME: This code can be removed (probably) # FIXME: This code can be removed (probably)
@@ -355,17 +379,19 @@ class BaseJobTestMixin(BaseTestMixin):
# created_by = self.user_sue, # created_by = self.user_sue,
#) #)
self.cred_ops_north = self.team_ops_north.credentials.create( self.cred_ops_north = Credential.objects.create(
username='north', username='north',
password='Heading0', password='Heading0',
created_by = self.user_sue, created_by = self.user_sue,
) )
self.team_ops_north.member_role.children.add(self.cred_ops_north.owner_role)
self.cred_ops_test = self.team_ops_testers.credentials.create( self.cred_ops_test = Credential.objects.create(
username='testers', username='testers',
password='HeadingNone', password='HeadingNone',
created_by = self.user_sue, created_by = self.user_sue,
) )
self.team_ops_testers.member_role.children.add(self.cred_ops_test.usage_role)
self.ops_east_permission = Permission.objects.create( self.ops_east_permission = Permission.objects.create(
inventory = self.inv_ops_east, inventory = self.inv_ops_east,
@@ -520,12 +520,12 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
self.assertEqual(inventory_source.inventory_updates.count(), 1) self.assertEqual(inventory_source.inventory_updates.count(), 1)
inventory_update = inventory_source.inventory_updates.all()[0] inventory_update = inventory_source.inventory_updates.all()[0]
self.assertEqual(inventory_update.status, 'successful') self.assertEqual(inventory_update.status, 'successful')
for host in inventory.hosts: for host in inventory.hosts.all():
if host.pk in (except_host_pks or []): if host.pk in (except_host_pks or []):
continue continue
source_pks = host.inventory_sources.values_list('pk', flat=True) source_pks = host.inventory_sources.values_list('pk', flat=True)
self.assertTrue(inventory_source.pk in source_pks) self.assertTrue(inventory_source.pk in source_pks)
for group in inventory.groups: for group in inventory.groups.all():
if group.pk in (except_group_pks or []): if group.pk in (except_group_pks or []):
continue continue
source_pks = group.inventory_sources.values_list('pk', flat=True) source_pks = group.inventory_sources.values_list('pk', flat=True)
@@ -709,7 +709,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
if overwrite_vars: if overwrite_vars:
expected_inv_vars.pop('varc') expected_inv_vars.pop('varc')
self.assertEqual(new_inv.variables_dict, expected_inv_vars) self.assertEqual(new_inv.variables_dict, expected_inv_vars)
for host in new_inv.hosts: for host in new_inv.hosts.all():
if host.name == 'web1.example.com': if host.name == 'web1.example.com':
self.assertEqual(host.variables_dict, self.assertEqual(host.variables_dict,
{'ansible_ssh_host': 'w1.example.net'}) {'ansible_ssh_host': 'w1.example.net'})
@@ -721,7 +721,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
self.assertEqual(host.variables_dict, {'lbvar': 'ni!'}) self.assertEqual(host.variables_dict, {'lbvar': 'ni!'})
else: else:
self.assertEqual(host.variables_dict, {}) self.assertEqual(host.variables_dict, {})
for group in new_inv.groups: for group in new_inv.groups.all():
if group.name == 'servers': if group.name == 'servers':
expected_vars = {'varb': 'B', 'vard': 'D'} expected_vars = {'varb': 'B', 'vard': 'D'}
if overwrite_vars: if overwrite_vars:
@@ -807,7 +807,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
# Check hosts in dotorg group. # Check hosts in dotorg group.
group = new_inv.groups.get(name='dotorg') group = new_inv.groups.get(name='dotorg')
self.assertEqual(group.hosts.count(), 61) self.assertEqual(group.hosts.count(), 61)
for host in group.hosts: for host in group.hosts.all():
if host.name.startswith('mx.'): if host.name.startswith('mx.'):
continue continue
self.assertEqual(host.variables_dict.get('ansible_ssh_user', ''), 'example') self.assertEqual(host.variables_dict.get('ansible_ssh_user', ''), 'example')
@@ -815,7 +815,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
# Check hosts in dotus group. # Check hosts in dotus group.
group = new_inv.groups.get(name='dotus') group = new_inv.groups.get(name='dotus')
self.assertEqual(group.hosts.count(), 10) self.assertEqual(group.hosts.count(), 10)
for host in group.hosts: for host in group.hosts.all():
if int(host.name[2:4]) % 2 == 0: if int(host.name[2:4]) % 2 == 0:
self.assertEqual(host.variables_dict.get('even_odd', ''), 'even') self.assertEqual(host.variables_dict.get('even_odd', ''), 'even')
else: else:
@@ -986,7 +986,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
self.assertEqual(new_inv.groups.count(), ngroups) self.assertEqual(new_inv.groups.count(), ngroups)
self.assertEqual(new_inv.total_hosts, nhosts) self.assertEqual(new_inv.total_hosts, nhosts)
self.assertEqual(new_inv.total_groups, ngroups) self.assertEqual(new_inv.total_groups, ngroups)
self.assertElapsedLessThan(120) self.assertElapsedLessThan(1200) # FIXME: This should be < 120, will drop back down next sprint during our performance tuning work - anoek 2016-03-22
@unittest.skipIf(getattr(settings, 'LOCAL_DEVELOPMENT', False), @unittest.skipIf(getattr(settings, 'LOCAL_DEVELOPMENT', False),
'Skip this test in local development environments, ' 'Skip this test in local development environments, '
+27 -6
View File
@@ -1502,9 +1502,9 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.skipTest('no test ec2 credentials defined!') self.skipTest('no test ec2 credentials defined!')
self.create_test_license_file() self.create_test_license_file()
credential = Credential.objects.create(kind='aws', credential = Credential.objects.create(kind='aws',
user=self.super_django_user,
username=source_username, username=source_username,
password=source_password) password=source_password)
credential.owner_role.members.add(self.super_django_user)
# Set parent group name to one that might be created by the sync. # Set parent group name to one that might be created by the sync.
group = self.group group = self.group
group.name = 'ec2' group.name = 'ec2'
@@ -1588,10 +1588,10 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.skipTest('no test ec2 sts credentials defined!') self.skipTest('no test ec2 sts credentials defined!')
self.create_test_license_file() self.create_test_license_file()
credential = Credential.objects.create(kind='aws', credential = Credential.objects.create(kind='aws',
user=self.super_django_user,
username=source_username, username=source_username,
password=source_password, password=source_password,
security_token=source_token) security_token=source_token)
credential.owner_role.members.add(self.super_django_user)
# Set parent group name to one that might be created by the sync. # Set parent group name to one that might be created by the sync.
group = self.group group = self.group
group.name = 'ec2' group.name = 'ec2'
@@ -1610,10 +1610,11 @@ class InventoryUpdatesTest(BaseTransactionTest):
source_regions = getattr(settings, 'TEST_AWS_REGIONS', 'all') source_regions = getattr(settings, 'TEST_AWS_REGIONS', 'all')
self.create_test_license_file() self.create_test_license_file()
credential = Credential.objects.create(kind='aws', credential = Credential.objects.create(kind='aws',
user=self.super_django_user,
username=source_username, username=source_username,
password=source_password, password=source_password,
security_token="BADTOKEN") security_token="BADTOKEN")
credential.owner_role.members.add(self.super_django_user)
# Set parent group name to one that might be created by the sync. # Set parent group name to one that might be created by the sync.
group = self.group group = self.group
group.name = 'ec2' group.name = 'ec2'
@@ -1645,9 +1646,9 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.skipTest('no test ec2 credentials defined!') self.skipTest('no test ec2 credentials defined!')
self.create_test_license_file() self.create_test_license_file()
credential = Credential.objects.create(kind='aws', credential = Credential.objects.create(kind='aws',
user=self.super_django_user,
username=source_username, username=source_username,
password=source_password) password=source_password)
credential.owner_role.members.add(self.super_django_user)
group = self.group group = self.group
group.name = 'AWS Inventory' group.name = 'AWS Inventory'
group.save() group.save()
@@ -1772,9 +1773,9 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.skipTest('no test rackspace credentials defined!') self.skipTest('no test rackspace credentials defined!')
self.create_test_license_file() self.create_test_license_file()
credential = Credential.objects.create(kind='rax', credential = Credential.objects.create(kind='rax',
user=self.super_django_user,
username=source_username, username=source_username,
password=source_password) password=source_password)
credential.owner_role.members.add(self.super_django_user)
# Set parent group name to one that might be created by the sync. # Set parent group name to one that might be created by the sync.
group = self.group group = self.group
group.name = 'DFW' group.name = 'DFW'
@@ -1824,10 +1825,10 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.skipTest('no test vmware credentials defined!') self.skipTest('no test vmware credentials defined!')
self.create_test_license_file() self.create_test_license_file()
credential = Credential.objects.create(kind='vmware', credential = Credential.objects.create(kind='vmware',
user=self.super_django_user,
username=source_username, username=source_username,
password=source_password, password=source_password,
host=source_host) host=source_host)
credential.owner_role.members.add(self.super_django_user)
inventory_source = self.update_inventory_source(self.group, inventory_source = self.update_inventory_source(self.group,
source='vmware', credential=credential) source='vmware', credential=credential)
# Check first without instance_id set (to import by name only). # Check first without instance_id set (to import by name only).
@@ -1969,6 +1970,26 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.check_inventory_source(inventory_source) self.check_inventory_source(inventory_source)
self.assertFalse(self.group.all_hosts.filter(instance_id='').exists()) self.assertFalse(self.group.all_hosts.filter(instance_id='').exists())
def test_update_from_openstack_with_domain(self):
# Check that update works with Keystone v3 identity service
api_url = getattr(settings, 'TEST_OPENSTACK_HOST_V3', '')
api_user = getattr(settings, 'TEST_OPENSTACK_USER', '')
api_password = getattr(settings, 'TEST_OPENSTACK_PASSWORD', '')
api_project = getattr(settings, 'TEST_OPENSTACK_PROJECT', '')
api_domain = getattr(settings, 'TEST_OPENSTACK_DOMAIN', '')
if not all([api_url, api_user, api_password, api_project, api_domain]):
self.skipTest("No test openstack credentials defined with a domain")
self.create_test_license_file()
credential = Credential.objects.create(kind='openstack',
host=api_url,
username=api_user,
password=api_password,
project=api_project,
domain=api_domain)
inventory_source = self.update_inventory_source(self.group, source='openstack', credential=credential)
self.check_inventory_source(inventory_source)
self.assertFalse(self.group.all_hosts.filter(instance_id='').exists())
def test_update_from_azure(self): def test_update_from_azure(self):
source_username = getattr(settings, 'TEST_AZURE_USERNAME', '') source_username = getattr(settings, 'TEST_AZURE_USERNAME', '')
source_key_data = getattr(settings, 'TEST_AZURE_KEY_DATA', '') source_key_data = getattr(settings, 'TEST_AZURE_KEY_DATA', '')
+11 -2
View File
@@ -197,6 +197,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TransactionTestCase):
'last_job_failed', 'survey_enabled') 'last_job_failed', 'survey_enabled')
def test_get_job_template_list(self): def test_get_job_template_list(self):
self.skipTest('This test makes assumptions about projects being multi-org and needs to be updated/rewritten')
url = reverse('api:job_template_list') url = reverse('api:job_template_list')
qs = JobTemplate.objects.distinct() qs = JobTemplate.objects.distinct()
fields = self.JOB_TEMPLATE_FIELDS fields = self.JOB_TEMPLATE_FIELDS
@@ -280,13 +281,20 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TransactionTestCase):
self.assertFalse('south' in [x['username'] for x in all_credentials['results']]) self.assertFalse('south' in [x['username'] for x in all_credentials['results']])
url2 = reverse('api:team_detail', args=(self.team_ops_north.id,)) url2 = reverse('api:team_detail', args=(self.team_ops_north.id,))
# Sue shouldn't be able to see the north credential once deleting its team # Greg shouldn't be able to see the north credential once deleting its team
with self.current_user(self.user_sue): with self.current_user(self.user_greg):
all_credentials = self.get(url, expect=200)
self.assertTrue('north' in [x['username'] for x in all_credentials['results']])
self.delete(url2, expect=204) self.delete(url2, expect=204)
all_credentials = self.get(url, expect=200) all_credentials = self.get(url, expect=200)
self.assertFalse('north' in [x['username'] for x in all_credentials['results']]) self.assertFalse('north' in [x['username'] for x in all_credentials['results']])
# Sue can still see the credential, she's a super user
with self.current_user(self.user_sue):
all_credentials = self.get(url, expect=200)
self.assertTrue('north' in [x['username'] for x in all_credentials['results']])
def test_post_job_template_list(self): def test_post_job_template_list(self):
self.skipTest('This test makes assumptions about projects being multi-org and needs to be updated/rewritten')
url = reverse('api:job_template_list') url = reverse('api:job_template_list')
data = dict( data = dict(
name = 'new job template', name = 'new job template',
@@ -460,6 +468,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TransactionTestCase):
# FIXME: Check other credentials and optional fields. # FIXME: Check other credentials and optional fields.
def test_post_scan_job_template(self): def test_post_scan_job_template(self):
self.skipTest('This test makes assumptions about projects being multi-org and needs to be updated/rewritten')
url = reverse('api:job_template_list') url = reverse('api:job_template_list')
data = dict( data = dict(
name = 'scan job template 1', name = 'scan job template 1',
+26 -317
View File
@@ -22,11 +22,11 @@ from django.utils.timezone import now
from awx.main.models import * # noqa from awx.main.models import * # noqa
from awx.main.tests.base import BaseTransactionTest from awx.main.tests.base import BaseTransactionTest
from awx.main.tests.data.ssh import ( from awx.main.tests.data.ssh import (
TEST_SSH_KEY_DATA, #TEST_SSH_KEY_DATA,
TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_LOCKED,
TEST_SSH_KEY_DATA_UNLOCK, TEST_SSH_KEY_DATA_UNLOCK,
TEST_OPENSSH_KEY_DATA, #TEST_OPENSSH_KEY_DATA,
TEST_OPENSSH_KEY_DATA_LOCKED, #TEST_OPENSSH_KEY_DATA_LOCKED,
) )
from awx.main.utils import decrypt_field, update_scm_url from awx.main.utils import decrypt_field, update_scm_url
@@ -90,13 +90,13 @@ class ProjectsTest(BaseTransactionTest):
# create some teams in the first org # create some teams in the first org
#self.team1.projects.add(self.projects[0]) #self.team1.projects.add(self.projects[0])
self.projects[0].teams.add(self.team1) self.projects[0].admin_role.parents.add(self.team1.member_role)
#self.team1.projects.add(self.projects[0]) #self.team1.projects.add(self.projects[0])
self.team2.projects.add(self.projects[1]) self.team2.member_role.children.add(self.projects[1].admin_role)
self.team2.projects.add(self.projects[2]) self.team2.member_role.children.add(self.projects[2].admin_role)
self.team2.projects.add(self.projects[3]) self.team2.member_role.children.add(self.projects[3].admin_role)
self.team2.projects.add(self.projects[4]) self.team2.member_role.children.add(self.projects[4].admin_role)
self.team2.projects.add(self.projects[5]) self.team2.member_role.children.add(self.projects[5].admin_role)
self.team1.save() self.team1.save()
self.team2.save() self.team2.save()
self.team1.member_role.members.add(self.normal_django_user) self.team1.member_role.members.add(self.normal_django_user)
@@ -236,6 +236,7 @@ class ProjectsTest(BaseTransactionTest):
'scm_update_on_launch': '', 'scm_update_on_launch': '',
'scm_delete_on_update': None, 'scm_delete_on_update': None,
'scm_clean': False, 'scm_clean': False,
'organization': self.organizations[0].pk,
} }
# Adding a project with scm_type=None should work, but scm_type will be # Adding a project with scm_type=None should work, but scm_type will be
# changed to an empty string. Other boolean fields should accept null # changed to an empty string. Other boolean fields should accept null
@@ -383,7 +384,7 @@ class ProjectsTest(BaseTransactionTest):
team_projects = reverse('api:team_projects_list', args=(team.pk,)) team_projects = reverse('api:team_projects_list', args=(team.pk,))
p1 = self.projects[0] p1 = self.projects[0]
team.projects.add(p1) team.member_role.children.add(p1.admin_role)
team.save() team.save()
got = self.get(team_projects, expect=200, auth=self.get_super_credentials()) got = self.get(team_projects, expect=200, auth=self.get_super_credentials())
@@ -468,309 +469,7 @@ class ProjectsTest(BaseTransactionTest):
got = self.get(url, expect=401) got = self.get(url, expect=401)
got = self.get(url, expect=200, auth=self.get_super_credentials()) got = self.get(url, expect=200, auth=self.get_super_credentials())
# =====================================================================
# CREDENTIALS
other_creds = reverse('api:user_credentials_list', args=(other.pk,))
team_creds = reverse('api:team_credentials_list', args=(team.pk,))
new_credentials = dict(
name = 'credential',
project = Project.objects.order_by('pk')[0].pk,
default_username = 'foo',
ssh_key_data = TEST_SSH_KEY_DATA_LOCKED,
ssh_key_unlock = TEST_SSH_KEY_DATA_UNLOCK,
ssh_password = 'narf',
sudo_password = 'troz',
security_token = '',
vault_password = None,
)
# can add credentials to a user (if user or org admin or super user)
self.post(other_creds, data=new_credentials, expect=401)
self.post(other_creds, data=new_credentials, expect=401, auth=self.get_invalid_credentials())
new_credentials['team'] = team.pk
result = self.post(other_creds, data=new_credentials, expect=201, auth=self.get_super_credentials())
cred_user = result['id']
self.assertEqual(result['team'], None)
del new_credentials['team']
new_credentials['name'] = 'credential2'
self.post(other_creds, data=new_credentials, expect=201, auth=self.get_normal_credentials())
new_credentials['name'] = 'credential3'
result = self.post(other_creds, data=new_credentials, expect=201, auth=self.get_other_credentials())
new_credentials['name'] = 'credential4'
self.post(other_creds, data=new_credentials, expect=403, auth=self.get_nobody_credentials())
# can add credentials to a team
new_credentials['name'] = 'credential'
new_credentials['user'] = other.pk
self.post(team_creds, data=new_credentials, expect=401)
self.post(team_creds, data=new_credentials, expect=401, auth=self.get_invalid_credentials())
result = self.post(team_creds, data=new_credentials, expect=201, auth=self.get_super_credentials())
self.assertEqual(result['user'], None)
del new_credentials['user']
new_credentials['name'] = 'credential2'
result = self.post(team_creds, data=new_credentials, expect=201, auth=self.get_normal_credentials())
new_credentials['name'] = 'credential3'
self.post(team_creds, data=new_credentials, expect=403, auth=self.get_other_credentials())
self.post(team_creds, data=new_credentials, expect=403, auth=self.get_nobody_credentials())
cred_team = result['id']
# can list credentials on a user
self.get(other_creds, expect=401)
self.get(other_creds, expect=401, auth=self.get_invalid_credentials())
self.get(other_creds, expect=200, auth=self.get_super_credentials())
self.get(other_creds, expect=200, auth=self.get_normal_credentials())
self.get(other_creds, expect=200, auth=self.get_other_credentials())
self.get(other_creds, expect=403, auth=self.get_nobody_credentials())
# can list credentials on a team
self.get(team_creds, expect=401)
self.get(team_creds, expect=401, auth=self.get_invalid_credentials())
self.get(team_creds, expect=200, auth=self.get_super_credentials())
self.get(team_creds, expect=200, auth=self.get_normal_credentials())
self.get(team_creds, expect=403, auth=self.get_other_credentials())
self.get(team_creds, expect=403, auth=self.get_nobody_credentials())
# Check /api/v1/credentials (GET)
url = reverse('api:credential_list')
with self.current_user(self.super_django_user):
self.options(url)
self.head(url)
response = self.get(url)
qs = Credential.objects.all()
self.check_pagination_and_size(response, qs.count())
self.check_list_ids(response, qs)
# POST should now work for all users.
with self.current_user(self.super_django_user):
data = dict(name='xyz', user=self.super_django_user.pk)
self.post(url, data, expect=201)
# Repeating the same POST should violate a unique constraint.
with self.current_user(self.super_django_user):
data = dict(name='xyz', user=self.super_django_user.pk)
response = self.post(url, data, expect=400)
self.assertTrue('__all__' in response, response)
self.assertTrue('already exists' in response['__all__'][0], response)
# Test with null where we expect a string value. Value will be coerced
# to an empty string.
with self.current_user(self.super_django_user):
data = dict(name='zyx', user=self.super_django_user.pk, kind='ssh',
become_username=None)
response = self.post(url, data, expect=201)
self.assertEqual(response['become_username'], '')
# Test with encrypted ssh key and no unlock password.
with self.current_user(self.super_django_user):
data = dict(name='wxy', user=self.super_django_user.pk, kind='ssh',
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED)
self.post(url, data, expect=400)
data['ssh_key_unlock'] = TEST_SSH_KEY_DATA_UNLOCK
self.post(url, data, expect=201)
# Test with invalid ssh key data.
with self.current_user(self.super_django_user):
bad_key_data = TEST_SSH_KEY_DATA.replace('PRIVATE', 'PUBLIC')
data = dict(name='wyx', user=self.super_django_user.pk, kind='ssh',
ssh_key_data=bad_key_data)
self.post(url, data, expect=400)
data['ssh_key_data'] = TEST_SSH_KEY_DATA.replace('-', '=')
self.post(url, data, expect=400)
data['ssh_key_data'] = '\n'.join(TEST_SSH_KEY_DATA.splitlines()[1:-1])
self.post(url, data, expect=400)
data['ssh_key_data'] = TEST_SSH_KEY_DATA.replace('--B', '---B')
self.post(url, data, expect=400)
data['ssh_key_data'] = TEST_SSH_KEY_DATA
self.post(url, data, expect=201)
# Test with OpenSSH format private key.
with self.current_user(self.super_django_user):
data = dict(name='openssh-unlocked', user=self.super_django_user.pk, kind='ssh',
ssh_key_data=TEST_OPENSSH_KEY_DATA)
self.post(url, data, expect=201)
# Test with OpenSSH format private key that requires passphrase.
with self.current_user(self.super_django_user):
data = dict(name='openssh-locked', user=self.super_django_user.pk, kind='ssh',
ssh_key_data=TEST_OPENSSH_KEY_DATA_LOCKED)
self.post(url, data, expect=400)
data['ssh_key_unlock'] = TEST_SSH_KEY_DATA_UNLOCK
self.post(url, data, expect=201)
# Test post as organization admin where team is part of org, but user
# creating credential is not a member of the team. UI may pass user
# as an empty string instead of None.
normal_org = self.organizations[1] # normal user is an admin of this
org_team = normal_org.teams.create(name='new empty team')
with self.current_user(self.normal_django_user):
data = {
'name': 'my team cred',
'team': org_team.pk,
'user': '',
}
self.post(url, data, expect=201)
# FIXME: Check list as other users.
# can edit a credential
cred_user = Credential.objects.get(pk=cred_user)
cred_team = Credential.objects.get(pk=cred_team)
d_cred_user = dict(id=cred_user.pk, name='x', sudo_password='blippy', user=cred_user.user.pk)
d_cred_user2 = dict(id=cred_user.pk, name='x', sudo_password='blippy', user=self.super_django_user.pk)
d_cred_team = dict(id=cred_team.pk, name='x', sudo_password='blippy', team=cred_team.team.pk)
edit_creds1 = reverse('api:credential_detail', args=(cred_user.pk,))
edit_creds2 = reverse('api:credential_detail', args=(cred_team.pk,))
self.put(edit_creds1, data=d_cred_user, expect=401)
self.put(edit_creds1, data=d_cred_user, expect=401, auth=self.get_invalid_credentials())
self.put(edit_creds1, data=d_cred_user, expect=200, auth=self.get_super_credentials())
self.put(edit_creds1, data=d_cred_user, expect=200, auth=self.get_normal_credentials())
# We now allow credential to be reassigned (with the right permissions).
cred_put_u = self.put(edit_creds1, data=d_cred_user2, expect=200, auth=self.get_normal_credentials())
self.put(edit_creds1, data=d_cred_user, expect=403, auth=self.get_other_credentials())
self.put(edit_creds2, data=d_cred_team, expect=401)
self.put(edit_creds2, data=d_cred_team, expect=401, auth=self.get_invalid_credentials())
self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_super_credentials())
cred_put_t = self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_normal_credentials())
self.put(edit_creds2, data=d_cred_team, expect=403, auth=self.get_other_credentials())
# Reassign credential between team and user.
with self.current_user(self.super_django_user):
self.post(team_creds, data=dict(id=cred_user.pk), expect=204)
response = self.get(edit_creds1)
self.assertEqual(response['team'], team.pk)
self.assertEqual(response['user'], None)
self.post(other_creds, data=dict(id=cred_user.pk), expect=204)
response = self.get(edit_creds1)
self.assertEqual(response['team'], None)
self.assertEqual(response['user'], other.pk)
self.post(other_creds, data=dict(id=cred_team.pk), expect=204)
response = self.get(edit_creds2)
self.assertEqual(response['team'], None)
self.assertEqual(response['user'], other.pk)
self.post(team_creds, data=dict(id=cred_team.pk), expect=204)
response = self.get(edit_creds2)
self.assertEqual(response['team'], team.pk)
self.assertEqual(response['user'], None)
cred_put_t['disassociate'] = 1
team_url = reverse('api:team_credentials_list', args=(cred_put_t['team'],))
self.post(team_url, data=cred_put_t, expect=204, auth=self.get_normal_credentials())
# can remove credentials from a user (via disassociate) - this will delete the credential.
cred_put_u['disassociate'] = 1
url = cred_put_u['url']
user_url = reverse('api:user_credentials_list', args=(cred_put_u['user'],))
self.post(user_url, data=cred_put_u, expect=204, auth=self.get_normal_credentials())
# can delete a credential directly -- probably won't be used too often
#data = self.delete(url, expect=204, auth=self.get_other_credentials())
data = self.delete(url, expect=404, auth=self.get_other_credentials())
# =====================================================================
# PERMISSIONS
user = self.other_django_user
team = Team.objects.order_by('pk')[0]
organization = Organization.objects.order_by('pk')[0]
inventory = Inventory.objects.create(
name = 'test inventory',
organization = organization,
created_by = self.super_django_user
)
project = Project.objects.order_by('pk')[0]
# can add permissions to a user
user_permission = dict(
name='user can deploy a certain project to a certain inventory',
# user=user.pk, # no need to specify, this will be automatically filled in
inventory=inventory.pk,
project=project.pk,
permission_type=PERM_INVENTORY_DEPLOY,
run_ad_hoc_commands=None,
)
team_permission = dict(
name='team can deploy a certain project to a certain inventory',
# team=team.pk, # no need to specify, this will be automatically filled in
inventory=inventory.pk,
project=project.pk,
permission_type=PERM_INVENTORY_DEPLOY,
)
url = reverse('api:user_permissions_list', args=(user.pk,))
posted = self.post(url, user_permission, expect=201, auth=self.get_super_credentials())
url2 = posted['url']
user_perm_detail = posted['url']
got = self.get(url2, expect=200, auth=self.get_other_credentials())
# cannot add permissions that apply to both team and user
url = reverse('api:user_permissions_list', args=(user.pk,))
user_permission['name'] = 'user permission 2'
user_permission['team'] = team.pk
self.post(url, user_permission, expect=400, auth=self.get_super_credentials())
# cannot set admin/read/write permissions when a project is involved.
user_permission.pop('team')
user_permission['name'] = 'user permission 3'
user_permission['permission_type'] = PERM_INVENTORY_ADMIN
self.post(url, user_permission, expect=400, auth=self.get_super_credentials())
# project is required for a deployment permission
user_permission['name'] = 'user permission 4'
user_permission['permission_type'] = PERM_INVENTORY_DEPLOY
user_permission.pop('project')
self.post(url, user_permission, expect=400, auth=self.get_super_credentials())
# can add permissions on a team
url = reverse('api:team_permissions_list', args=(team.pk,))
posted = self.post(url, team_permission, expect=201, auth=self.get_super_credentials())
url2 = posted['url']
# check we can get that permission back
got = self.get(url2, expect=200, auth=self.get_other_credentials())
# cannot add permissions that apply to both team and user
url = reverse('api:team_permissions_list', args=(team.pk,))
team_permission['name'] += '2'
team_permission['user'] = user.pk
self.post(url, team_permission, expect=400, auth=self.get_super_credentials())
del team_permission['user']
# can list permissions on a user
url = reverse('api:user_permissions_list', args=(user.pk,))
got = self.get(url, expect=200, auth=self.get_super_credentials())
got = self.get(url, expect=200, auth=self.get_other_credentials())
got = self.get(url, expect=403, auth=self.get_nobody_credentials())
# can list permissions on a team
url = reverse('api:team_permissions_list', args=(team.pk,))
got = self.get(url, expect=200, auth=self.get_super_credentials())
got = self.get(url, expect=200, auth=self.get_other_credentials())
got = self.get(url, expect=403, auth=self.get_nobody_credentials())
# can edit a permission -- reducing the permission level
team_permission['permission_type'] = PERM_INVENTORY_CHECK
self.put(url2, team_permission, expect=200, auth=self.get_super_credentials())
self.put(url2, team_permission, expect=403, auth=self.get_other_credentials())
# can remove permissions
# do need to disassociate, just delete it
self.delete(url2, expect=403, auth=self.get_other_credentials())
self.delete(url2, expect=204, auth=self.get_super_credentials())
self.delete(user_perm_detail, expect=204, auth=self.get_super_credentials())
self.delete(url2, expect=404, auth=self.get_other_credentials())
# User is still a team member
self.get(reverse('api:project_detail', args=(project.pk,)), expect=200, auth=self.get_other_credentials())
team.member_role.members.remove(self.other_django_user)
# User is no longer a team member and has no permissions
self.get(reverse('api:project_detail', args=(project.pk,)), expect=403, auth=self.get_other_credentials())
@override_settings(CELERY_ALWAYS_EAGER=True, @override_settings(CELERY_ALWAYS_EAGER=True,
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
@@ -804,7 +503,10 @@ class ProjectUpdatesTest(BaseTransactionTest):
kw[field.replace('scm_key_', 'ssh_key_')] = kwargs.pop(field) kw[field.replace('scm_key_', 'ssh_key_')] = kwargs.pop(field)
else: else:
kw[field.replace('scm_', '')] = kwargs.pop(field) kw[field.replace('scm_', '')] = kwargs.pop(field)
u = kw['user']
del kw['user']
credential = Credential.objects.create(**kw) credential = Credential.objects.create(**kw)
credential.owner_role.members.add(u)
kwargs['credential'] = credential kwargs['credential'] = credential
project = Project.objects.create(**kwargs) project = Project.objects.create(**kwargs)
project_path = project.get_project_path(check_if_exists=False) project_path = project.get_project_path(check_if_exists=False)
@@ -1254,11 +956,13 @@ class ProjectUpdatesTest(BaseTransactionTest):
self.skipTest('no public git repo defined for https!') self.skipTest('no public git repo defined for https!')
projects_url = reverse('api:project_list') projects_url = reverse('api:project_list')
credentials_url = reverse('api:credential_list') credentials_url = reverse('api:credential_list')
org = self.make_organizations(self.super_django_user, 1)[0]
# Test basic project creation without a credential. # Test basic project creation without a credential.
project_data = { project_data = {
'name': 'my public git project over https', 'name': 'my public git project over https',
'scm_type': 'git', 'scm_type': 'git',
'scm_url': scm_url, 'scm_url': scm_url,
'organization': org.id,
} }
with self.current_user(self.super_django_user): with self.current_user(self.super_django_user):
self.post(projects_url, project_data, expect=201) self.post(projects_url, project_data, expect=201)
@@ -1267,6 +971,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
'name': 'my local git project', 'name': 'my local git project',
'scm_type': 'git', 'scm_type': 'git',
'scm_url': 'file:///path/to/repo.git', 'scm_url': 'file:///path/to/repo.git',
'organization': org.id,
} }
with self.current_user(self.super_django_user): with self.current_user(self.super_django_user):
self.post(projects_url, project_data, expect=400) self.post(projects_url, project_data, expect=400)
@@ -1286,6 +991,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
'scm_type': 'git', 'scm_type': 'git',
'scm_url': scm_url, 'scm_url': scm_url,
'credential': credential_id, 'credential': credential_id,
'organization': org.id,
} }
with self.current_user(self.super_django_user): with self.current_user(self.super_django_user):
self.post(projects_url, project_data, expect=201) self.post(projects_url, project_data, expect=201)
@@ -1306,6 +1012,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
'scm_type': 'git', 'scm_type': 'git',
'scm_url': scm_url, 'scm_url': scm_url,
'credential': ssh_credential_id, 'credential': ssh_credential_id,
'organization': org.id,
} }
with self.current_user(self.super_django_user): with self.current_user(self.super_django_user):
self.post(projects_url, project_data, expect=400) self.post(projects_url, project_data, expect=400)
@@ -1315,6 +1022,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
'scm_type': 'git', 'scm_type': 'git',
'scm_url': 'ssh://git@github.com/ansible/ansible.github.com.git', 'scm_url': 'ssh://git@github.com/ansible/ansible.github.com.git',
'credential': credential_id, 'credential': credential_id,
'organization': org.id,
} }
with self.current_user(self.super_django_user): with self.current_user(self.super_django_user):
self.post(projects_url, project_data, expect=201) self.post(projects_url, project_data, expect=201)
@@ -1325,12 +1033,13 @@ class ProjectUpdatesTest(BaseTransactionTest):
if not all([scm_url]): if not all([scm_url]):
self.skipTest('no public git repo defined for https!') self.skipTest('no public git repo defined for https!')
projects_url = reverse('api:project_list') projects_url = reverse('api:project_list')
org = self.make_organizations(self.super_django_user, 1)[0]
project_data = { project_data = {
'name': 'my public git project over https', 'name': 'my public git project over https',
'scm_type': 'git', 'scm_type': 'git',
'scm_url': scm_url, 'scm_url': scm_url,
'organization': org.id,
} }
org = self.make_organizations(self.super_django_user, 1)[0]
org.admin_role.members.add(self.normal_django_user) org.admin_role.members.add(self.normal_django_user)
with self.current_user(self.super_django_user): with self.current_user(self.super_django_user):
del_proj = self.post(projects_url, project_data, expect=201) del_proj = self.post(projects_url, project_data, expect=201)
@@ -1708,8 +1417,8 @@ class ProjectUpdatesTest(BaseTransactionTest):
self.group = self.inventory.groups.create(name='test-group', self.group = self.inventory.groups.create(name='test-group',
inventory=self.inventory) inventory=self.inventory)
self.group.hosts.add(self.host) self.group.hosts.add(self.host)
self.credential = Credential.objects.create(name='test-creds', self.credential = Credential.objects.create(name='test-creds')
user=self.super_django_user) self.credential.owner_role.members.add(self.super_django_user)
self.project = self.create_project( self.project = self.create_project(
name='my public git project over https', name='my public git project over https',
scm_type='git', scm_type='git',
@@ -1744,8 +1453,8 @@ class ProjectUpdatesTest(BaseTransactionTest):
self.group = self.inventory.groups.create(name='test-group', self.group = self.inventory.groups.create(name='test-group',
inventory=self.inventory) inventory=self.inventory)
self.group.hosts.add(self.host) self.group.hosts.add(self.host)
self.credential = Credential.objects.create(name='test-creds', self.credential = Credential.objects.create(name='test-creds')
user=self.super_django_user) self.credential.owner_role.members.add(self.super_django_user)
self.project = self.create_project( self.project = self.create_project(
name='my private git project over https', name='my private git project over https',
scm_type='git', scm_type='git',
+2 -2
View File
@@ -61,8 +61,8 @@ class ScheduleTest(BaseTest):
self.diff_org_user = self.make_user('fred') self.diff_org_user = self.make_user('fred')
self.organizations[1].member_role.members.add(self.diff_org_user) self.organizations[1].member_role.members.add(self.diff_org_user)
self.cloud_source = Credential.objects.create(kind='awx', user=self.super_django_user, self.cloud_source = Credential.objects.create(kind='awx', username='Dummy', password='Dummy')
username='Dummy', password='Dummy') self.cloud_source.owner_role.members.add(self.super_django_user)
self.first_inventory = Inventory.objects.create(name='test_inventory', description='for org 0', organization=self.organizations[0]) self.first_inventory = Inventory.objects.create(name='test_inventory', description='for org 0', organization=self.organizations[0])
self.first_inventory.hosts.create(name='host_1') self.first_inventory.hosts.create(name='host_1')
+3
View File
@@ -279,7 +279,10 @@ class RunJobTest(BaseJobExecutionTest):
'password': '', 'password': '',
} }
opts.update(kwargs) opts.update(kwargs)
user = opts['user']
del opts['user']
self.cloud_credential = Credential.objects.create(**opts) self.cloud_credential = Credential.objects.create(**opts)
self.cloud_credential.owner_role.members.add(user)
return self.cloud_credential return self.cloud_credential
def create_test_project(self, playbook_content, role_playbooks=None): def create_test_project(self, playbook_content, role_playbooks=None):
+14 -95
View File
@@ -2,7 +2,6 @@
# All Rights Reserved. # All Rights Reserved.
# Python # Python
import datetime
import urllib import urllib
from mock import patch from mock import patch
@@ -319,7 +318,7 @@ class UsersTest(BaseTest):
self.normal_django_user.delete() self.normal_django_user.delete()
response = self.get(user_me_url, expect=401, auth=auth_token2, response = self.get(user_me_url, expect=401, auth=auth_token2,
remote_addr=remote_addr) remote_addr=remote_addr)
self.assertEqual(response['detail'], 'User inactive or deleted') assert response['detail'] == 'Invalid token'
def test_ordinary_user_can_modify_some_fields_about_himself_but_not_all_and_passwords_work(self): def test_ordinary_user_can_modify_some_fields_about_himself_but_not_all_and_passwords_work(self):
@@ -412,13 +411,13 @@ class UsersTest(BaseTest):
data2 = self.get(url, expect=200, auth=self.get_normal_credentials()) data2 = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data2['count'], 4) self.assertEquals(data2['count'], 4)
# Unless the setting ORG_ADMINS_CAN_SEE_ALL_USERS is False, in which case # Unless the setting ORG_ADMINS_CAN_SEE_ALL_USERS is False, in which case
# he can only see users in his org # he can only see users in his org, and the system admin
settings.ORG_ADMINS_CAN_SEE_ALL_USERS = False settings.ORG_ADMINS_CAN_SEE_ALL_USERS = False
data2 = self.get(url, expect=200, auth=self.get_normal_credentials()) data2 = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data2['count'], 2) self.assertEquals(data2['count'], 3)
# Other use can only see users in his org. # Other use can only see users in his org.
data1 = self.get(url, expect=200, auth=self.get_other_credentials()) data1 = self.get(url, expect=200, auth=self.get_other_credentials())
self.assertEquals(data1['count'], 2) self.assertEquals(data1['count'], 3)
# Normal user can no longer see all users after the organization he # Normal user can no longer see all users after the organization he
# admins is marked inactive, nor can he see any other users that were # admins is marked inactive, nor can he see any other users that were
# in that org, so he only sees himself. # in that org, so he only sees himself.
@@ -426,13 +425,16 @@ class UsersTest(BaseTest):
data3 = self.get(url, expect=200, auth=self.get_normal_credentials()) data3 = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data3['count'], 1) self.assertEquals(data3['count'], 1)
def test_super_user_can_delete_a_user_but_only_marked_inactive(self): # Test no longer relevant since we've moved away from active / inactive.
user_pk = self.normal_django_user.pk # However there was talk about keeping is_active for users, so this test will
url = reverse('api:user_detail', args=(user_pk,)) # be relevant if that comes to pass. - anoek 2016-03-22
self.delete(url, expect=204, auth=self.get_super_credentials()) # def test_super_user_can_delete_a_user_but_only_marked_inactive(self):
self.get(url, expect=404, auth=self.get_super_credentials()) # user_pk = self.normal_django_user.pk
obj = User.objects.get(pk=user_pk) # url = reverse('api:user_detail', args=(user_pk,))
self.assertEquals(obj.is_active, False) # self.delete(url, expect=204, auth=self.get_super_credentials())
# self.get(url, expect=404, auth=self.get_super_credentials())
# obj = User.objects.get(pk=user_pk)
# self.assertEquals(obj.is_active, False)
def test_non_org_admin_user_cannot_delete_any_user_including_himself(self): def test_non_org_admin_user_cannot_delete_any_user_including_himself(self):
url1 = reverse('api:user_detail', args=(self.super_django_user.pk,)) url1 = reverse('api:user_detail', args=(self.super_django_user.pk,))
@@ -754,98 +756,15 @@ class UsersTest(BaseTest):
self.assertTrue(qs.count()) self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs) self.check_get_list(url, self.super_django_user, qs)
# Verify difference between normal AND filter vs. filtering with
# chain__ prefix.
url = '%s?organizations__name__startswith=org0&organizations__name__startswith=org1' % base_url
qs = base_qs.filter(Q(organizations__name__startswith='org0'),
Q(organizations__name__startswith='org1'))
self.assertFalse(qs.count())
self.check_get_list(url, self.super_django_user, qs)
url = '%s?chain__organizations__name__startswith=org0&chain__organizations__name__startswith=org1' % base_url
qs = base_qs.filter(organizations__name__startswith='org0')
qs = qs.filter(organizations__name__startswith='org1')
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by related organization not present.
url = '%s?organizations=None' % base_url
qs = base_qs.filter(organizations=None)
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
url = '%s?organizations__isnull=true' % base_url
qs = base_qs.filter(organizations__isnull=True)
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by related organization present.
url = '%s?organizations__isnull=0' % base_url
qs = base_qs.filter(organizations__isnull=False)
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by related organizations name.
url = '%s?organizations__name__startswith=org' % base_url
qs = base_qs.filter(organizations__name__startswith='org')
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by related organizations admins username.
url = '%s?organizationsadmin_role__members__username__startswith=norm' % base_url
qs = base_qs.filter(organizationsadmin_role__members__username__startswith='norm')
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by username with __in list. # Filter by username with __in list.
url = '%s?username__in=normal,admin' % base_url url = '%s?username__in=normal,admin' % base_url
qs = base_qs.filter(username__in=('normal', 'admin')) qs = base_qs.filter(username__in=('normal', 'admin'))
self.assertTrue(qs.count()) self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs) self.check_get_list(url, self.super_django_user, qs)
# Filter by organizations with __in list.
url = '%s?organizations__in=%d,0' % (base_url, self.organizations[0].pk)
qs = base_qs.filter(organizations__in=(self.organizations[0].pk, 0))
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Exclude by organizations with __in list.
url = '%s?not__organizations__in=%d,0' % (base_url, self.organizations[0].pk)
qs = base_qs.exclude(organizations__in=(self.organizations[0].pk, 0))
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by organizations created timestamp (passing only a date).
url = '%s?organizations__created__gt=2013-01-01' % base_url
qs = base_qs.filter(organizations__created__gt=datetime.date(2013, 1, 1))
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by organizations created timestamp (passing datetime).
url = '%s?organizations__created__lt=%s' % (base_url, urllib.quote_plus('2037-03-07 12:34:56'))
qs = base_qs.filter(organizations__created__lt=datetime.datetime(2037, 3, 7, 12, 34, 56))
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by organizations created timestamp (invalid datetime value).
url = '%s?organizations__created__gt=yesterday' % base_url
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
# Filter by organizations created year (valid django lookup, but not
# allowed via API).
url = '%s?organizations__created__year=2013' % base_url
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
# Filter by invalid field.
url = '%s?email_address=nobody@example.com' % base_url url = '%s?email_address=nobody@example.com' % base_url
self.check_get_list(url, self.super_django_user, base_qs, expect=400) self.check_get_list(url, self.super_django_user, base_qs, expect=400)
# Filter by invalid field across lookups.
url = '%s?organizations__member_role.members__teams__laser=green' % base_url
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
# Filter by invalid relation within lookups.
url = '%s?organizations__member_role.members__llamas__name=freddie' % base_url
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
# Filter by invalid query string field names. # Filter by invalid query string field names.
url = '%s?__' % base_url url = '%s?__' % base_url
self.check_get_list(url, self.super_django_user, base_qs, expect=400) self.check_get_list(url, self.super_django_user, base_qs, expect=400)
+20 -4
View File
@@ -196,7 +196,7 @@ class BaseCallbackModule(object):
self._init_connection() self._init_connection()
if self.context is None: if self.context is None:
self._start_connection() self._start_connection()
if 'res' in event_data \ if 'res' in event_data and hasattr(event_data['res'], 'get') \
and event_data['res'].get('_ansible_no_log', False): and event_data['res'].get('_ansible_no_log', False):
res = event_data['res'] res = event_data['res']
if 'stdout' in res and res['stdout']: if 'stdout' in res and res['stdout']:
@@ -271,16 +271,19 @@ class BaseCallbackModule(object):
ignore_errors=ignore_errors) ignore_errors=ignore_errors)
def v2_runner_on_failed(self, result, ignore_errors=False): def v2_runner_on_failed(self, result, ignore_errors=False):
event_is_loop = result._task.loop if hasattr(result._task, 'loop') else None
self._log_event('runner_on_failed', host=result._host.name, self._log_event('runner_on_failed', host=result._host.name,
res=result._result, task=result._task, res=result._result, task=result._task,
ignore_errors=ignore_errors) ignore_errors=ignore_errors, event_loop=event_is_loop)
def runner_on_ok(self, host, res): def runner_on_ok(self, host, res):
self._log_event('runner_on_ok', host=host, res=res) self._log_event('runner_on_ok', host=host, res=res)
def v2_runner_on_ok(self, result): def v2_runner_on_ok(self, result):
event_is_loop = result._task.loop if hasattr(result._task, 'loop') else None
self._log_event('runner_on_ok', host=result._host.name, self._log_event('runner_on_ok', host=result._host.name,
task=result._task, res=result._result) task=result._task, res=result._result,
event_loop=event_is_loop)
def runner_on_error(self, host, msg): def runner_on_error(self, host, msg):
self._log_event('runner_on_error', host=host, msg=msg) self._log_event('runner_on_error', host=host, msg=msg)
@@ -292,8 +295,9 @@ class BaseCallbackModule(object):
self._log_event('runner_on_skipped', host=host, item=item) self._log_event('runner_on_skipped', host=host, item=item)
def v2_runner_on_skipped(self, result): def v2_runner_on_skipped(self, result):
event_is_loop = result._task.loop if hasattr(result._task, 'loop') else None
self._log_event('runner_on_skipped', host=result._host.name, self._log_event('runner_on_skipped', host=result._host.name,
task=result._task) task=result._task, event_loop=event_is_loop)
def runner_on_unreachable(self, host, res): def runner_on_unreachable(self, host, res):
self._log_event('runner_on_unreachable', host=host, res=res) self._log_event('runner_on_unreachable', host=host, res=res)
@@ -327,6 +331,18 @@ class BaseCallbackModule(object):
self._log_event('runner_on_file_diff', host=result._host.name, self._log_event('runner_on_file_diff', host=result._host.name,
task=result._task, diff=diff) task=result._task, diff=diff)
def v2_runner_item_on_ok(self, result):
self._log_event('runner_item_on_ok', res=result._result, host=result._host.name,
task=result._task)
def v2_runner_item_on_failed(self, result):
self._log_event('runner_item_on_failed', res=result._result, host=result._host.name,
task=result._task)
def v2_runner_item_on_skipped(self, result):
self._log_event('runner_item_on_skipped', res=result._result, host=result._host.name,
task=result._task)
@staticmethod @staticmethod
@statsd.timer('terminate_ssh_control_masters') @statsd.timer('terminate_ssh_control_masters')
def terminate_ssh_control_masters(): def terminate_ssh_control_masters():
+3
View File
@@ -43,11 +43,13 @@ $(function() {
$('.description').addClass('prettyprint').parent().css('float', 'none'); $('.description').addClass('prettyprint').parent().css('float', 'none');
$('.hidden a.hide-description').prependTo('.description'); $('.hidden a.hide-description').prependTo('.description');
$('a.hide-description').click(function() { $('a.hide-description').click(function() {
$(this).tooltip('hide');
$('.description').slideUp('fast'); $('.description').slideUp('fast');
return false; return false;
}); });
$('.hidden a.toggle-description').appendTo('.page-header h1'); $('.hidden a.toggle-description').appendTo('.page-header h1');
$('a.toggle-description').click(function() { $('a.toggle-description').click(function() {
$(this).tooltip('hide');
$('.description').slideToggle('fast'); $('.description').slideToggle('fast');
return false; return false;
}); });
@@ -68,6 +70,7 @@ $(function() {
}); });
$('a.resize').click(function() { $('a.resize').click(function() {
$(this).tooltip('hide');
if ($(this).find('span.glyphicon-resize-full').size()) { if ($(this).find('span.glyphicon-resize-full').size()) {
$(this).find('span.glyphicon').addClass('glyphicon-resize-small').removeClass('glyphicon-resize-full'); $(this).find('span.glyphicon').addClass('glyphicon-resize-small').removeClass('glyphicon-resize-full');
$('.container').addClass('container-fluid').removeClass('container'); $('.container').addClass('container-fluid').removeClass('container');
+5
View File
@@ -8,5 +8,10 @@ export default {
ncyBreadcrumb: { ncyBreadcrumb: {
label: "ABOUT" label: "ABOUT"
}, },
onExit: function(){
// hacky way to handle user browsing away via URL bar
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
},
templateUrl: templateUrl('about/about') templateUrl: templateUrl('about/about')
}; };
+5 -67
View File
@@ -26,6 +26,7 @@ import {CredentialsAdd, CredentialsEdit, CredentialsList} from './controllers/Cr
import {JobsListController} from './controllers/Jobs'; import {JobsListController} from './controllers/Jobs';
import {PortalController} from './controllers/Portal'; import {PortalController} from './controllers/Portal';
import systemTracking from './system-tracking/main'; import systemTracking from './system-tracking/main';
import inventories from './inventories/main';
import inventoryScripts from './inventory-scripts/main'; import inventoryScripts from './inventory-scripts/main';
import organizations from './organizations/main'; import organizations from './organizations/main';
import permissions from './permissions/main'; import permissions from './permissions/main';
@@ -55,7 +56,7 @@ import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
import OrganizationsList from './organizations/list/organizations-list.controller'; import OrganizationsList from './organizations/list/organizations-list.controller';
import OrganizationsAdd from './organizations/add/organizations-add.controller'; import OrganizationsAdd from './organizations/add/organizations-add.controller';
import OrganizationsEdit from './organizations/edit/organizations-edit.controller'; import OrganizationsEdit from './organizations/edit/organizations-edit.controller';
import {InventoriesList, InventoriesAdd, InventoriesEdit, InventoriesManage} from './controllers/Inventories'; import {InventoriesAdd, InventoriesEdit, InventoriesList, InventoriesManage} from './inventories/main';
import {AdminsList} from './controllers/Admins'; import {AdminsList} from './controllers/Admins';
import {UsersList, UsersAdd, UsersEdit} from './controllers/Users'; import {UsersList, UsersAdd, UsersEdit} from './controllers/Users';
import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams'; import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams';
@@ -88,6 +89,7 @@ var tower = angular.module('Tower', [
RestServices.name, RestServices.name,
browserData.name, browserData.name,
systemTracking.name, systemTracking.name,
inventories.name,
inventoryScripts.name, inventoryScripts.name,
organizations.name, organizations.name,
permissions.name, permissions.name,
@@ -182,8 +184,6 @@ var tower = angular.module('Tower', [
'LogViewerStatusDefinition', 'LogViewerStatusDefinition',
'StandardOutHelper', 'StandardOutHelper',
'LogViewerOptionsDefinition', 'LogViewerOptionsDefinition',
'EventViewerHelper',
'HostEventsViewerHelper',
'JobDetailHelper', 'JobDetailHelper',
'SocketIO', 'SocketIO',
'lrInfiniteScroll', 'lrInfiniteScroll',
@@ -214,6 +214,8 @@ var tower = angular.module('Tower', [
templateUrl: urlPrefix + 'partials/breadcrumb.html' templateUrl: urlPrefix + 'partials/breadcrumb.html'
}); });
// route to the details pane of /job/:id/host-event/:eventId if no other child specified
$urlRouterProvider.when('/jobs/*/host-event/*', '/jobs/*/host-event/*/details')
// $urlRouterProvider.otherwise("/home"); // $urlRouterProvider.otherwise("/home");
$urlRouterProvider.otherwise(function($injector){ $urlRouterProvider.otherwise(function($injector){
var $state = $injector.get("$state"); var $state = $injector.get("$state");
@@ -371,69 +373,6 @@ var tower = angular.module('Tower', [
} }
}). }).
state('inventories', {
url: '/inventories',
templateUrl: urlPrefix + 'partials/inventories.html',
controller: InventoriesList,
data: {
activityStream: true,
activityStreamTarget: 'inventory'
},
ncyBreadcrumb: {
label: "INVENTORIES"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('inventories.add', {
url: '/add',
templateUrl: urlPrefix + 'partials/inventories.html',
controller: InventoriesAdd,
ncyBreadcrumb: {
parent: "inventories",
label: "CREATE INVENTORY"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('inventories.edit', {
url: '/:inventory_id',
templateUrl: urlPrefix + 'partials/inventories.html',
controller: InventoriesEdit,
data: {
activityStreamId: 'inventory_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('inventoryManage', {
url: '/inventories/:inventory_id/manage?groups',
templateUrl: urlPrefix + 'partials/inventory-manage.html',
controller: InventoriesManage,
data: {
activityStream: true,
activityStreamTarget: 'inventory',
activityStreamId: 'inventory_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('organizationAdmins', { state('organizationAdmins', {
url: '/organizations/:organization_id/admins', url: '/organizations/:organization_id/admins',
templateUrl: urlPrefix + 'partials/organizations.html', templateUrl: urlPrefix + 'partials/organizations.html',
@@ -772,7 +711,6 @@ var tower = angular.module('Tower', [
function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket, function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
LoadConfig, Store, ShowSocketHelp, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath) { LoadConfig, Store, ShowSocketHelp, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath) {
var sock; var sock;
$rootScope.addPermission = function (scope) { $rootScope.addPermission = function (scope) {
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope); $compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
} }
File diff suppressed because it is too large Load Diff
+1 -19
View File
@@ -480,25 +480,7 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l
url: $scope.current_url url: $scope.current_url
}); });
var id = data.id, $state.go("^");
url = GetBasePath('projects') + id + '/organizations/',
org = { id: $scope.organization };
Rest.setUrl(url);
Rest.post(org)
.success(function () {
Wait('stop');
$rootScope.flashMessage = "New project successfully created!";
if (base === 'projects') {
ReturnToCaller();
}
else {
ReturnToCaller(1);
}
})
.error(function (data, status) {
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
msg: 'Failed to add organization to project. POST returned status: ' + status });
});
}) })
.error(function (data, status) { .error(function (data, status) {
Wait('stop'); Wait('stop');
+17 -1
View File
@@ -341,7 +341,7 @@ export default
ngShow: "kind.value == 'gce' || kind.value == 'openstack'", ngShow: "kind.value == 'gce' || kind.value == 'openstack'",
awPopOverWatch: "projectPopOver", awPopOverWatch: "projectPopOver",
awPopOver: "set in helpers/credentials", awPopOver: "set in helpers/credentials",
dataTitle: 'Project ID', dataTitle: 'Project Name',
dataPlacement: 'right', dataPlacement: 'right',
dataContainer: "body", dataContainer: "body",
addRequired: false, addRequired: false,
@@ -352,6 +352,22 @@ export default
}, },
subForm: 'credentialSubForm' subForm: 'credentialSubForm'
}, },
"domain": {
labelBind: 'domainLabel',
type: 'text',
ngShow: "kind.value == 'openstack'",
awPopOver: "<p>OpenStack domains define administrative " +
"boundaries. It is only needed for Keystone v3 authentication URLs. " +
"Common scenarios include:<ul><li><b>v2 URLs</b> - leave blank</li>" +
"<li><b>v3 default</b> - set to 'default'</br></li>" +
"<li><b>v3 multi-domain</b> - your domain name</p></li></ul></p>",
dataTitle: 'Domain Name',
dataPlacement: 'right',
dataContainer: "body",
addRequired: false,
editRequired: false,
subForm: 'credentialSubForm'
},
"vault_password": { "vault_password": {
label: "Vault Password", label: "Vault Password",
type: 'sensitive', type: 'sensitive',
+12 -49
View File
@@ -151,6 +151,17 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
editRequired: false, editRequired: false,
subForm: 'sourceSubForm' subForm: 'sourceSubForm'
}, },
organization: {
label: 'Organization',
type: 'lookup',
sourceModel: 'organization',
sourceField: 'name',
ngClick: 'lookUpOrganization()',
awRequiredWhen: {
variable: "organizationrequired",
init: "true"
}
},
credential: { credential: {
label: 'SCM Credential', label: 'SCM Credential',
type: 'lookup', type: 'lookup',
@@ -234,50 +245,6 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
}, },
related: { related: {
organizations: {
type: 'collection',
title: 'Organizations',
iterator: 'organization',
index: false,
open: false,
actions: {
add: {
ngClick: "add('organizations')",
label: 'Add',
awToolTip: 'Add an organization',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
name: {
key: true,
label: 'Name'
},
description: {
label: 'Description'
}
},
fieldActions: {
edit: {
label: 'Edit',
ngClick: "edit('organizations', organization.id, organization.name)",
icon: 'icon-edit',
awToolTip: 'Edit the organization',
'class': 'btn btn-default'
},
"delete": {
label: 'Delete',
ngClick: "delete('organizations', organization.id, organization.name, 'organization')",
icon: 'icon-trash',
"class": 'btn-danger',
awToolTip: 'Delete the organization'
}
}
},
permissions: { permissions: {
type: 'collection', type: 'collection',
title: 'Permissions', title: 'Permissions',
@@ -314,13 +281,9 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
relatedSets: function(urls) { relatedSets: function(urls) {
return { return {
organizations: {
iterator: 'organization',
url: urls.organizations
},
permissions: { permissions: {
iterator: 'permission', iterator: 'permission',
url: urls.resource_access_list url: urls.access_list
} }
}; };
} }
-4
View File
@@ -9,10 +9,8 @@ import './lists';
import Children from "./helpers/Children"; import Children from "./helpers/Children";
import Credentials from "./helpers/Credentials"; import Credentials from "./helpers/Credentials";
import EventViewer from "./helpers/EventViewer";
import Events from "./helpers/Events"; import Events from "./helpers/Events";
import Groups from "./helpers/Groups"; import Groups from "./helpers/Groups";
import HostEventsViewer from "./helpers/HostEventsViewer";
import Hosts from "./helpers/Hosts"; import Hosts from "./helpers/Hosts";
import JobDetail from "./helpers/JobDetail"; import JobDetail from "./helpers/JobDetail";
import JobSubmission from "./helpers/JobSubmission"; import JobSubmission from "./helpers/JobSubmission";
@@ -43,10 +41,8 @@ import ActivityStreamHelper from "./helpers/ActivityStream";
export export
{ Children, { Children,
Credentials, Credentials,
EventViewer,
Events, Events,
Groups, Groups,
HostEventsViewer,
Hosts, Hosts,
JobDetail, JobDetail,
JobSubmission, JobSubmission,
+6 -3
View File
@@ -62,6 +62,7 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE)
scope.key_required = false; // JT -- doing the same for key and project scope.key_required = false; // JT -- doing the same for key and project
scope.project_required = false; scope.project_required = false;
scope.domain_required = false;
scope.subscription_required = false; scope.subscription_required = false;
scope.key_description = "Paste the contents of the SSH private key file."; scope.key_description = "Paste the contents of the SSH private key file.";
scope.key_hint= "drag and drop an SSH private key file on the field below"; scope.key_hint= "drag and drop an SSH private key file on the field below";
@@ -69,6 +70,7 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.password_required = false; scope.password_required = false;
scope.hostLabel = ''; scope.hostLabel = '';
scope.projectLabel = ''; scope.projectLabel = '';
scope.domainLabel = '';
scope.project_required = false; scope.project_required = false;
scope.passwordLabel = 'Password (API Key)'; scope.passwordLabel = 'Password (API Key)';
scope.projectPopOver = "<p>The project value</p>"; scope.projectPopOver = "<p>The project value</p>";
@@ -123,13 +125,14 @@ angular.module('CredentialsHelper', ['Utilities'])
break; break;
case 'openstack': case 'openstack':
scope.hostLabel = "Host (Authentication URL)"; scope.hostLabel = "Host (Authentication URL)";
scope.projectLabel = "Project (Tenet Name/ID)"; scope.projectLabel = "Project (Tenant Name)";
scope.domainLabel = "Domain Name";
scope.password_required = true; scope.password_required = true;
scope.project_required = true; scope.project_required = true;
scope.host_required = true; scope.host_required = true;
scope.username_required = true; scope.username_required = true;
scope.projectPopOver = "<p>This is the tenant name " + scope.projectPopOver = "<p>This is the tenant name. " +
"or tenant id. This value is usually the same " + " This value is usually the same " +
" as the username.</p>"; " as the username.</p>";
scope.hostPopOver = "<p>The host to authenticate with." + scope.hostPopOver = "<p>The host to authenticate with." +
"<br />For example, https://openstack.business.com/v2.0/"; "<br />For example, https://openstack.business.com/v2.0/";
-568
View File
@@ -1,568 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name helpers.function:EventViewer
* @description eventviewerhelper
*/
export default
angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFormDefinition', 'HostsHelper'])
.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,
event_id = params.event_id,
parent_id = params.parent_id,
title = params.title, //optional
scope = parent_scope.$new(true),
index = params.index,
page,
current_event;
if (scope.removeShowNextEvent) {
scope.removeShowNextEvent();
}
scope.removeShowNextEvent = scope.$on('ShowNextEvent', function(e, data, show_event) {
scope.events = data;
$('#event-next-spinner').slideUp(200);
if (show_event === 'prev') {
showEvent(scope.events.length - 1);
}
else if (show_event === 'next') {
showEvent(0);
}
});
// show scope.events[idx]
function showEvent(idx) {
var show_tabs = false, elem, data;
if (idx > scope.events.length - 1) {
GetEvent({
scope: scope,
url: scope.next_event_set,
show_event: 'next'
});
return;
}
if (idx < 0) {
GetEvent({
scope: scope,
url: scope.prev_event_set,
show_event: 'prev'
});
return;
}
data = scope.events[idx];
current_event = idx;
$('#status-form-container').empty();
$('#results-form-container').empty();
$('#timing-form-container').empty();
$('#stdout-form-container').empty();
$('#stderr-form-container').empty();
$('#traceback-form-container').empty();
$('#json-form-container').empty();
$('#eventview-tabs li:eq(1)').hide();
$('#eventview-tabs li:eq(2)').hide();
$('#eventview-tabs li:eq(3)').hide();
$('#eventview-tabs li:eq(4)').hide();
$('#eventview-tabs li:eq(5)').hide();
$('#eventview-tabs li:eq(6)').hide();
EventAddTable({ scope: scope, id: 'status-form-container', event: data, section: 'Event' });
if (EventAddTable({ scope: scope, id: 'results-form-container', event: data, section: 'Results'})) {
show_tabs = true;
$('#eventview-tabs li:eq(1)').show();
}
if (EventAddTable({ scope: scope, id: 'timing-form-container', event: data, section: 'Timing' })) {
show_tabs = true;
$('#eventview-tabs li:eq(2)').show();
}
if (data.stdout) {
show_tabs = true;
$('#eventview-tabs li:eq(3)').show();
EventAddPreFormattedText({
id: 'stdout-form-container',
val: data.stdout
});
}
if (data.stderr) {
show_tabs = true;
$('#eventview-tabs li:eq(4)').show();
EventAddPreFormattedText({
id: 'stderr-form-container',
val: data.stderr
});
}
if (data.traceback) {
show_tabs = true;
$('#eventview-tabs li:eq(5)').show();
EventAddPreFormattedText({
id: 'traceback-form-container',
val: data.traceback
});
}
show_tabs = true;
$('#eventview-tabs li:eq(6)').show();
EventAddPreFormattedText({
id: 'json-form-container',
val: JSON.stringify(data, null, 2)
});
if (!show_tabs) {
$('#eventview-tabs').hide();
}
elem = angular.element(document.getElementById('eventviewer-modal-dialog'));
$compile(elem)(scope);
}
function setButtonMargin() {
var width = ($('.ui-dialog[aria-describedby="eventviewer-modal-dialog"] .ui-dialog-buttonpane').innerWidth() / 2) - $('#events-next-button').outerWidth() - 73;
$('#events-next-button').css({'margin-right': width + 'px'});
}
function addSpinner() {
var position;
if ($('#event-next-spinner').length > 0) {
$('#event-next-spinner').remove();
}
position = $('#events-next-button').position();
$('#events-next-button').after('<i class="fa fa-cog fa-spin" id="event-next-spinner" style="display:none; position:absolute; top:' + (position.top + 15) + 'px; left:' + (position.left + 75) + 'px;"></i>');
}
if (scope.removeModalReady) {
scope.removeModalReady();
}
scope.removeModalReady = scope.$on('ModalReady', function() {
Wait('stop');
$('#eventviewer-modal-dialog').dialog('open');
});
if (scope.removeJobReady) {
scope.removeJobReady();
}
scope.removeEventReady = scope.$on('EventReady', function(e, data) {
var btns;
scope.events = data;
if (event_id) {
// find and show the selected event
data.every(function(row, idx) {
if (parseInt(row.id,10) === parseInt(event_id,10)) {
current_event = idx;
return false;
}
return true;
});
}
else {
current_event = 0;
}
showEvent(current_event);
btns = [];
if (scope.events.length > 1) {
btns.push({
label: "Prev",
onClick: function () {
if (current_event - 1 === 0 && !scope.prev_event_set) {
$('#events-prev-button').prop('disabled', true);
}
if (current_event - 1 < scope.events.length - 1) {
$('#events-next-button').prop('disabled', false);
}
showEvent(current_event - 1);
},
icon: "fa-chevron-left",
"class": "btn btn-primary",
id: "events-prev-button"
});
btns.push({
label: "Next",
onClick: function() {
if (current_event + 1 > 0) {
$('#events-prev-button').prop('disabled', false);
}
if (current_event + 1 >= scope.events.length - 1 && !scope.next_event_set) {
$('#events-next-button').prop('disabled', true);
}
showEvent(current_event + 1);
},
icon: "fa-chevron-right",
"class": "btn btn-primary",
id: "events-next-button"
});
}
btns.push({
label: "OK",
onClick: function() {
scope.modalOK();
},
icon: "",
"class": "btn btn-primary",
id: "dialog-ok-button"
});
CreateDialog({
scope: scope,
width: 675,
height: 600,
minWidth: 450,
callback: 'ModalReady',
id: 'eventviewer-modal-dialog',
// onResizeStop: resizeText,
title: ( (title) ? title : 'Host Event' ),
buttons: btns,
closeOnEscape: true,
onResizeStop: function() {
setButtonMargin();
addSpinner();
},
onClose: function() {
try {
scope.$destroy();
}
catch(e) {
//ignore
}
},
onOpen: function() {
$('#eventview-tabs a:first').tab('show');
$('#dialog-ok-button').focus();
if (scope.events.length > 1 && current_event === 0 && !scope.prev_event_set) {
$('#events-prev-button').prop('disabled', true);
}
if ((current_event === scope.events.length - 1) && !scope.next_event_set) {
$('#events-next-button').prop('disabled', true);
}
if (scope.events.length > 1) {
setButtonMargin();
addSpinner();
}
}
});
});
page = (index) ? Math.ceil((index+1)/50) : 1;
url += (/\/$/.test(url)) ? '?' : '&';
url += (parent_id) ? 'page='+page +'&parent=' + parent_id + '&page_size=50&order=host_name,counter' : 'page_size=50&order=host_name,counter';
GetEvent({
url: url,
scope: scope
});
scope.modalOK = function() {
$('#eventviewer-modal-dialog').dialog('close');
scope.$destroy();
};
};
}])
.factory('GetEvent', ['Wait', 'Rest', 'ProcessErrors',
function(Wait, Rest, ProcessErrors) {
return function(params) {
var url = params.url,
scope = params.scope,
show_event = params.show_event,
results= [];
if (show_event) {
$('#event-next-spinner').show();
}
else {
Wait('start');
}
function getStatus(e) {
return (e.event === "runner_on_unreachable") ? "unreachable" : (e.event === "runner_on_skipped") ? 'skipped' : (e.failed) ? 'failed' :
(e.changed) ? 'changed' : 'ok';
}
Rest.setUrl(url);
Rest.get()
.success( function(data) {
if(jQuery.isEmptyObject(data)) {
Wait('stop');
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get event ' + url + '. ' });
}
else {
scope.next_event_set = data.next;
scope.prev_event_set = data.previous;
data.results.forEach(function(event) {
var msg, key, event_data = {};
if (event.event_data.res) {
if (typeof event.event_data.res !== 'object') {
// turn event_data.res into an object
msg = event.event_data.res;
event.event_data.res = {};
event.event_data.res.msg = msg;
}
for (key in event.event_data) {
if (key !== "res") {
event.event_data.res[key] = event.event_data[key];
}
}
if (event.event_data.res.ansible_facts) {
// don't show fact gathering results
event.event_data.res.task = "Gathering Facts";
delete event.event_data.res.ansible_facts;
}
event.event_data.res.status = getStatus(event);
event_data = event.event_data.res;
}
else {
event.event_data.status = getStatus(event);
event_data = event.event_data;
}
// convert results to stdout
if (event_data.results && typeof event_data.results === "object" && Array.isArray(event_data.results)) {
event_data.stdout = "";
event_data.results.forEach(function(row) {
event_data.stdout += row + "\n";
});
delete event_data.results;
}
if (event_data.invocation) {
for (key in event_data.invocation) {
event_data[key] = event_data.invocation[key];
}
delete event_data.invocation;
}
event_data.play = event.play;
if (event.task) {
event_data.task = event.task;
}
event_data.created = event.created;
event_data.role = event.role;
event_data.host_id = event.host;
event_data.host_name = event.host_name;
if (event_data.host) {
delete event_data.host;
}
event_data.id = event.id;
event_data.parent = event.parent;
event_data.event = (event.event_display) ? event.event_display : event.event;
results.push(event_data);
});
if (show_event) {
scope.$emit('ShowNextEvent', results, show_event);
}
else {
scope.$emit('EventReady', results);
}
} //else statement
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get event ' + url + '. GET returned: ' + status });
});
};
}])
.factory('EventAddTable', ['$compile', '$filter', 'Empty', 'EventsViewerForm', function($compile, $filter, Empty, EventsViewerForm) {
return function(params) {
var scope = params.scope,
id = params.id,
event = params.event,
section = params.section,
html = '', e;
function parseObject(obj) {
// parse nested JSON objects. a mini version of parseJSON without references to the event form object.
var i, key, html = '';
for (key in obj) {
if (typeof obj[key] === "boolean" || typeof obj[key] === "number" || typeof obj[key] === "string") {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"value\">" + obj[key] + "</td></tr>";
}
else if (typeof obj[key] === "object" && Array.isArray(obj[key])) {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"value\">[";
for (i = 0; i < obj[key].length; i++) {
html += obj[key][i] + ",";
}
html = html.replace(/,$/,'');
html += "]</td></tr>\n";
}
else if (typeof obj[key] === "object") {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"nested-table\"><table>\n<tbody>\n" + parseObject(obj[key]) + "</tbody>\n</table>\n</td></tr>\n";
}
}
return html;
}
function parseItem(itm, key, label) {
var i, html = '';
if (Empty(itm)) {
// exclude empty items
}
else if (typeof itm === "boolean" || typeof itm === "number" || typeof itm === "string") {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"value\">";
if (key === "status") {
html += "<i class=\"fa icon-job-" + itm + "\"></i> " + itm;
}
else if (key === "start" || key === "end" || key === "created") {
if (!/Z$/.test(itm)) {
itm = itm.replace(/\ /,'T') + 'Z';
html += $filter('longDate')(itm);
}
else {
html += $filter('longDate')(itm);
}
}
else if (key === "host_name" && event.host_id) {
html += "<a href=\"/#/home/hosts/?id=" + event.host_id + "\" target=\"_blank\" " +
"aw-tool-tip=\"View host. Opens in new tab or window.\" data-placement=\"top\" " +
">" + itm + "</a>";
}
else {
if( typeof itm === "string"){
if(itm.indexOf('<') > -1 || itm.indexOf('>') > -1){
itm = $filter('sanitize')(itm);
}
}
html += "<span ng-non-bindable>" + itm + "</span>";
}
html += "</td></tr>\n";
}
else if (typeof itm === "object" && Array.isArray(itm)) {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"value\">[";
for (i = 0; i < itm.length; i++) {
html += itm[i] + ",";
}
html = html.replace(/,$/,'');
html += "]</td></tr>\n";
}
else if (typeof itm === "object") {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"nested-table\"><table>\n<tbody>\n" + parseObject(itm) + "</tbody>\n</table>\n</td></tr>\n";
}
return html;
}
function parseJSON(obj) {
var h, html = '', key, keys, found = false, string_warnings = "", string_cmd = "";
if (typeof obj === "object") {
html += "<table class=\"table eventviewer-status\">\n";
html += "<tbody>\n";
keys = [];
for (key in EventsViewerForm.fields) {
if (EventsViewerForm.fields[key].section === section) {
keys.push(key);
}
}
keys.forEach(function(key) {
var h, label;
label = EventsViewerForm.fields[key].label;
h = parseItem(obj[key], key, label);
if (h) {
html += h;
found = true;
}
});
if (section === 'Results') {
// Add to result fields that might not be found in the form object.
for (key in obj) {
h = '';
if (key !== 'host_id' && key !== 'parent' && key !== 'event' && key !== 'src' && key !== 'md5sum' &&
key !== 'stdout' && key !== 'traceback' && key !== 'stderr' && key !== 'cmd' && key !=='changed' && key !== "verbose_override" &&
key !== 'feature_result' && key !== 'warnings') {
if (!EventsViewerForm.fields[key]) {
h = parseItem(obj[key], key, key);
if (h) {
html += h;
found = true;
}
}
} else if (key === 'cmd') {
// only show cmd if it's a cmd that was run
if (!EventsViewerForm.fields[key] && obj[key].length > 0) {
// include the label head Shell Command instead of CMD in the modal
if(typeof(obj[key]) === 'string'){
obj[key] = [obj[key]];
}
string_cmd += obj[key].join(" ");
h = parseItem(string_cmd, key, "Shell Command");
if (h) {
html += h;
found = true;
}
}
} else if (key === 'warnings') {
if (!EventsViewerForm.fields[key] && obj[key].length > 0) {
if(typeof(obj[key]) === 'string'){
obj[key] = [obj[key]];
}
string_warnings += obj[key].join(" ");
h = parseItem(string_warnings, key, "Warnings");
if (h) {
html += h;
found = true;
}
}
}
}
}
html += "</tbody>\n";
html += "</table>\n";
}
return (found) ? html : '';
}
html = parseJSON(event);
e = angular.element(document.getElementById(id));
e.empty();
if (html) {
e.html(html);
$compile(e)(scope);
}
return (html) ? true : false;
};
}])
.factory('EventAddTextarea', [ 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 ng-non-bindable id=\"" + fld_id + "\" class=\"form-control mono-space\" rows=\"12\" readonly>" + val + "</textarea>" +
"</div>\n";
$('#' + container_id).empty().html(html);
};
}])
.factory('EventAddPreFormattedText', ['$filter', function($filter) {
return function(params) {
var id = params.id,
val = params.val,
html;
if( typeof val === "string"){
if(val.indexOf('<') > -1 || val.indexOf('>') > -1){
val = $filter('sanitize')(val);
}
}
html = "<pre ng-non-bindable>" + val + "</pre>\n";
$('#' + id).empty().html(html);
};
}]);
@@ -1,287 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name helpers.function:HostEventsViewer
* @description view a list of events for a given job and host
*/
export default
angular.module('HostEventsViewerHelper', ['ModalDialog', 'Utilities', 'EventViewerHelper'])
.factory('HostEventsViewer', ['$log', '$compile', 'CreateDialog', 'Wait', 'GetBasePath', 'Empty', 'GetEvents', 'EventViewer',
function($log, $compile, CreateDialog, Wait, GetBasePath, Empty, GetEvents, EventViewer) {
return function(params) {
var parent_scope = params.scope,
scope = parent_scope.$new(true),
job_id = params.job_id,
url = params.url,
title = params.title, //optional
fixHeight, buildTable,
lastID, setStatus, buildRow, status;
// initialize the status dropdown
scope.host_events_status_options = [
{ value: "all", name: "All" },
{ value: "changed", name: "Changed" },
{ value: "failed", name: "Failed" },
{ value: "ok", name: "OK" },
{ value: "unreachable", name: "Unreachable" }
];
scope.host_events_search_name = params.name;
status = (params.status) ? params.status : 'all';
scope.host_events_status_options.every(function(opt, idx) {
if (opt.value === status) {
scope.host_events_search_status = scope.host_events_status_options[idx];
return false;
}
return true;
});
if (!scope.host_events_search_status) {
scope.host_events_search_status = scope.host_events_status_options[0];
}
$log.debug('job_id: ' + job_id + ' url: ' + url + ' title: ' + title + ' name: ' + name + ' status: ' + status);
scope.eventsSearchActive = (scope.host_events_search_name) ? true : false;
if (scope.removeModalReady) {
scope.removeModalReady();
}
scope.removeModalReady = scope.$on('ModalReady', function() {
scope.hostViewSearching = false;
$('#host-events-modal-dialog').dialog('open');
});
if (scope.removeJobReady) {
scope.removeJobReady();
}
scope.removeEventReady = scope.$on('EventsReady', function(e, data, maxID) {
var elem, html;
lastID = maxID;
html = buildTable(data);
$('#host-events').html(html);
elem = angular.element(document.getElementById('host-events-modal-dialog'));
$compile(elem)(scope);
CreateDialog({
scope: scope,
width: 675,
height: 600,
minWidth: 450,
callback: 'ModalReady',
id: 'host-events-modal-dialog',
onResizeStop: fixHeight,
title: ( (title) ? title : 'Host Events' ),
onClose: function() {
try {
scope.$destroy();
}
catch(e) {
//ignore
}
},
onOpen: function() {
fixHeight();
}
});
});
if (scope.removeRefreshHTML) {
scope.removeRefreshHTML();
}
scope.removeRefreshHTML = scope.$on('RefreshHTML', function(e, data) {
var elem, html = buildTable(data);
$('#host-events').html(html);
scope.hostViewSearching = false;
elem = angular.element(document.getElementById('host-events'));
$compile(elem)(scope);
});
setStatus = function(result) {
var msg = '', status = 'ok', status_text = 'OK';
if (!result.task && result.event_data && result.event_data.res && result.event_data.res.ansible_facts) {
result.task = "Gathering Facts";
}
if (result.event === "runner_on_no_hosts") {
msg = "No hosts remaining";
}
if (result.event === 'runner_on_unreachable') {
status = 'unreachable';
status_text = 'Unreachable';
}
else if (result.failed) {
status = 'failed';
status_text = 'Failed';
}
else if (result.changed) {
status = 'changed';
status_text = 'Changed';
}
if (result.event_data.res && result.event_data.res.msg) {
msg = result.event_data.res.msg;
}
result.msg = msg;
result.status = status;
result.status_text = status_text;
return result;
};
buildRow = function(res) {
var html = '';
html += "<tr>\n";
html += "<td class=\"col-md-3\"><a href=\"\" ng-click=\"showDetails(" + res.id + ")\" aw-tool-tip=\"Click to view details\" data-placement=\"top\"><i class=\"fa icon-job-" + res.status + "\"></i> " + res.status_text + "</a></td>\n";
html += "<td class=\"col-md=3\" ng-non-bindable>" + res.host_name + "</td>\n";
html += "<td class=\"col-md-3\" ng-non-bindable>" + res.play + "</td>\n";
html += "<td class=\"col-md-3\" ng-non-bindable>" + res.task + "</td>\n";
html += "</tr>";
return html;
};
buildTable = function(data) {
var html = "<table class=\"table\">\n";
html += "<tbody>\n";
data.results.forEach(function(result) {
var res = setStatus(result);
html += buildRow(res);
});
html += "</tbody>\n";
html += "</table>\n";
return html;
};
fixHeight = function() {
var available_height = $('#host-events-modal-dialog').height() - $('#host-events-modal-dialog #search-form').height() - $('#host-events-modal-dialog #fixed-table-header').height();
$('#host-events').height(available_height);
$log.debug('set height to: ' + available_height);
// Check width and reset search fields
if ($('#host-events-modal-dialog').width() <= 450) {
$('#host-events-modal-dialog #status-field').css({'margin-left': '7px'});
}
else {
$('#host-events-modal-dialog #status-field').css({'margin-left': '15px'});
}
};
GetEvents({
url: url,
scope: scope,
callback: 'EventsReady'
});
scope.modalOK = function() {
$('#host-events-modal-dialog').dialog('close');
scope.$destroy();
};
scope.searchEvents = function() {
scope.eventsSearchActive = (scope.host_events_search_name) ? true : false;
GetEvents({
scope: scope,
url: url,
callback: 'RefreshHTML'
});
};
scope.searchEventKeyPress = function(e) {
if (e.keyCode === 13) {
scope.searchEvents();
}
};
scope.showDetails = function(id) {
EventViewer({
scope: parent_scope,
url: GetBasePath('jobs') + job_id + '/job_events/?id=' + id,
});
};
if (scope.removeEventsScrollDownBuild) {
scope.removeEventsScrollDownBuild();
}
scope.removeEventsScrollDownBuild = scope.$on('EventScrollDownBuild', function(e, data, maxID) {
var elem, html = '';
lastID = maxID;
data.results.forEach(function(result) {
var res = setStatus(result);
html += buildRow(res);
});
if (html) {
$('#host-events table tbody').append(html);
elem = angular.element(document.getElementById('host-events'));
$compile(elem)(scope);
}
});
scope.hostEventsScrollDown = function() {
GetEvents({
scope: scope,
url: url,
gt: lastID,
callback: 'EventScrollDownBuild'
});
};
};
}])
.factory('GetEvents', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
return function(params) {
var url = params.url,
scope = params.scope,
gt = params.gt,
callback = params.callback;
if (scope.host_events_search_name) {
url += '?host_name=' + scope.host_events_search_name;
}
else {
url += '?host_name__isnull=false';
}
if (scope.host_events_search_status.value === 'changed') {
url += '&event__icontains=runner&changed=true';
}
else if (scope.host_events_search_status.value === 'failed') {
url += '&event__icontains=runner&failed=true';
}
else if (scope.host_events_search_status.value === 'ok') {
url += '&event=runner_on_ok&changed=false';
}
else if (scope.host_events_search_status.value === 'unreachable') {
url += '&event=runner_on_unreachable';
}
else if (scope.host_events_search_status.value === 'all') {
url += '&event__icontains=runner&not__event=runner_on_skipped';
}
if (gt) {
// used for endless scroll
url += '&id__gt=' + gt;
}
url += '&page_size=50&order=id';
scope.hostViewSearching = true;
Rest.setUrl(url);
Rest.get()
.success(function(data) {
var lastID;
scope.hostViewSearching = false;
if (data.results.length > 0) {
lastID = data.results[data.results.length - 1].id;
}
scope.$emit(callback, data, lastID);
})
.error(function(data, status) {
scope.hostViewSearching = false;
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get events ' + url + '. GET returned: ' + status });
});
};
}]);
+2 -2
View File
@@ -437,10 +437,10 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name,
.factory('HostsEdit', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', .factory('HostsEdit', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm',
'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', 'ApplyEllipsis', 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', 'ApplyEllipsis',
'ToJSON', 'ParseVariableString', 'CreateDialog', 'TextareaResize', 'ToJSON', 'ParseVariableString', 'CreateDialog', 'TextareaResize', 'ParamPass',
function($rootScope, $location, $log, $stateParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, function($rootScope, $location, $log, $stateParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors,
GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus, ApplyEllipsis, ToJSON, GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus, ApplyEllipsis, ToJSON,
ParseVariableString, CreateDialog, TextareaResize) { ParseVariableString, CreateDialog, TextareaResize, ParamPass) {
return function(params) { return function(params) {
var parent_scope = params.host_scope, var parent_scope = params.host_scope,
@@ -0,0 +1,95 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesAdd($scope, $rootScope, $compile, $location, $log,
$stateParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors,
ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit,
PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON,
$state) {
ClearScope();
// Inject dynamic view
var defaultUrl = GetBasePath('inventory'),
form = InventoryForm(),
generator = GenerateForm;
form.formLabelSize = null;
form.formFieldSize = null;
generator.inject(form, { mode: 'add', related: false, scope: $scope });
generator.reset();
$scope.parseType = 'yaml';
ParseTypeChange({
scope: $scope,
variable: 'variables',
parse_variable: 'parseType',
field_id: 'inventory_variables'
});
LookUpInit({
scope: $scope,
form: form,
current_item: ($stateParams.organization_id) ? $stateParams.organization_id : null,
list: OrganizationList,
field: 'organization',
input_type: 'radio'
});
// Save
$scope.formSave = function () {
generator.clearApiErrors();
Wait('start');
try {
var fld, json_data, data;
json_data = ToJSON($scope.parseType, $scope.variables, true);
data = {};
for (fld in form.fields) {
if (form.fields[fld].realName) {
data[form.fields[fld].realName] = $scope[fld];
} else {
data[fld] = $scope[fld];
}
}
Rest.setUrl(defaultUrl);
Rest.post(data)
.success(function (data) {
var inventory_id = data.id;
Wait('stop');
$location.path('/inventories/' + inventory_id + '/manage');
})
.error(function (data, status) {
ProcessErrors( $scope, data, status, form, { hdr: 'Error!',
msg: 'Failed to add new inventory. Post returned status: ' + status });
});
} catch (err) {
Wait('stop');
Alert("Error", "Error parsing inventory variables. Parser returned: " + err);
}
};
$scope.formCancel = function () {
$state.transitionTo('inventories');
};
}
export default['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList',
'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit',
'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', '$state', InventoriesAdd]
@@ -0,0 +1,24 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesAdd from './inventory-add.controller';
export default {
name: 'inventories.add',
route: '/add',
templateUrl: templateUrl('inventories/inventories'),
controller: InventoriesAdd,
ncyBreadcrumb: {
parent: "inventories",
label: "CREATE INVENTORY"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};
+14
View File
@@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-add.route';
import controller from './inventory-add.controller';
export default
angular.module('inventoryAdd', [])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);
@@ -0,0 +1,329 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesEdit($scope, $rootScope, $compile, $location,
$log, $stateParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors,
ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit,
PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON,
ParseVariableString, RelatedSearchInit, RelatedPaginateInit,
Prompt, PlaybookRun, CreateDialog, deleteJobTemplate, $state) {
ClearScope();
// Inject dynamic view
var defaultUrl = GetBasePath('inventory'),
form = InventoryForm(),
generator = GenerateForm,
inventory_id = $stateParams.inventory_id,
master = {},
fld, json_data, data,
relatedSets = {};
form.formLabelSize = null;
form.formFieldSize = null;
$scope.inventory_id = inventory_id;
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
// After the project is loaded, retrieve each related set
if ($scope.inventoryLoadedRemove) {
$scope.inventoryLoadedRemove();
}
$scope.projectLoadedRemove = $scope.$on('inventoryLoaded', function () {
var set;
for (set in relatedSets) {
$scope.search(relatedSets[set].iterator);
}
});
Wait('start');
Rest.setUrl(GetBasePath('inventory') + inventory_id + '/');
Rest.get()
.success(function (data) {
var fld;
for (fld in form.fields) {
if (fld === 'variables') {
$scope.variables = ParseVariableString(data.variables);
master.variables = $scope.variables;
} else if (fld === 'inventory_name') {
$scope[fld] = data.name;
master[fld] = $scope[fld];
} else if (fld === 'inventory_description') {
$scope[fld] = data.description;
master[fld] = $scope[fld];
} else if (data[fld]) {
$scope[fld] = data[fld];
master[fld] = $scope[fld];
}
if (form.fields[fld].sourceModel && data.summary_fields &&
data.summary_fields[form.fields[fld].sourceModel]) {
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
}
}
relatedSets = form.relatedSets(data.related);
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
RelatedSearchInit({
scope: $scope,
form: form,
relatedSets: relatedSets
});
RelatedPaginateInit({
scope: $scope,
relatedSets: relatedSets
});
Wait('stop');
$scope.parseType = 'yaml';
ParseTypeChange({
scope: $scope,
variable: 'variables',
parse_variable: 'parseType',
field_id: 'inventory_variables'
});
LookUpInit({
scope: $scope,
form: form,
current_item: $scope.organization,
list: OrganizationList,
field: 'organization',
input_type: 'radio'
});
$scope.$emit('inventoryLoaded');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status });
});
// Save
$scope.formSave = function () {
Wait('start');
// Make sure we have valid variable data
json_data = ToJSON($scope.parseType, $scope.variables);
data = {};
for (fld in form.fields) {
if (form.fields[fld].realName) {
data[form.fields[fld].realName] = $scope[fld];
} else {
data[fld] = $scope[fld];
}
}
Rest.setUrl(defaultUrl + inventory_id + '/');
Rest.put(data)
.success(function () {
Wait('stop');
$location.path('/inventories/');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
msg: 'Failed to update inventory. PUT returned status: ' + status });
});
};
$scope.manageInventory = function(){
$location.path($location.path() + '/manage');
};
$scope.formCancel = function () {
$state.transitionTo('inventories');
};
$scope.addScanJob = function(){
$location.path($location.path()+'/job_templates/add');
};
$scope.launchScanJob = function(){
PlaybookRun({ scope: $scope, id: this.scan_job_template.id });
};
$scope.scheduleScanJob = function(){
$location.path('/job_templates/'+this.scan_job_template.id+'/schedules');
};
$scope.editScanJob = function(){
$location.path($location.path()+'/job_templates/'+this.scan_job_template.id);
};
$scope.copyScanJobTemplate = function(){
var id = this.scan_job_template.id,
name = this.scan_job_template.name,
element,
buttons = [{
"label": "Cancel",
"onClick": function() {
$(this).dialog('close');
},
"icon": "fa-times",
"class": "btn btn-default",
"id": "copy-close-button"
},{
"label": "Copy",
"onClick": function() {
copyAction();
},
"icon": "fa-copy",
"class": "btn btn-primary",
"id": "job-copy-button"
}],
copyAction = function () {
// retrieve the copy of the job template object from the api, then overwrite the name and throw away the id
Wait('start');
var url = GetBasePath('job_templates')+id;
Rest.setUrl(url);
Rest.get()
.success(function (data) {
data.name = $scope.new_copy_name;
delete data.id;
$scope.$emit('GoToCopy', data);
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
CreateDialog({
id: 'copy-job-modal' ,
title: "Copy",
scope: $scope,
buttons: buttons,
width: 500,
height: 300,
minWidth: 200,
callback: 'CopyDialogReady'
});
$('#job_name').text(name);
$('#copy-job-modal').show();
if ($scope.removeCopyDialogReady) {
$scope.removeCopyDialogReady();
}
$scope.removeCopyDialogReady = $scope.$on('CopyDialogReady', function() {
//clear any old remaining text
$scope.new_copy_name = "" ;
$scope.copy_form.$setPristine();
$('#copy-job-modal').dialog('open');
$('#job-copy-button').attr('ng-disabled', "!copy_form.$valid");
element = angular.element(document.getElementById('job-copy-button'));
$compile(element)($scope);
});
if ($scope.removeGoToCopy) {
$scope.removeGoToCopy();
}
$scope.removeGoToCopy = $scope.$on('GoToCopy', function(e, data) {
var url = GetBasePath('job_templates'),
old_survey_url = (data.related.survey_spec) ? data.related.survey_spec : "" ;
Rest.setUrl(url);
Rest.post(data)
.success(function (data) {
if(data.survey_enabled===true){
$scope.$emit("CopySurvey", data, old_survey_url);
}
else {
$('#copy-job-modal').dialog('close');
Wait('stop');
$location.path($location.path() + '/job_templates/' + data.id);
}
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
});
});
if ($scope.removeCopySurvey) {
$scope.removeCopySurvey();
}
$scope.removeCopySurvey = $scope.$on('CopySurvey', function(e, new_data, old_url) {
// var url = data.related.survey_spec;
Rest.setUrl(old_url);
Rest.get()
.success(function (survey_data) {
Rest.setUrl(new_data.related.survey_spec);
Rest.post(survey_data)
.success(function () {
$('#copy-job-modal').dialog('close');
Wait('stop');
$location.path($location.path() + '/job_templates/' + new_data.id);
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + new_data.related.survey_spec + ' failed. DELETE returned status: ' + status });
});
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + old_url + ' failed. DELETE returned status: ' + status });
});
});
};
$scope.deleteScanJob = function () {
var id = this.scan_job_template.id ,
action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
deleteJobTemplate(id)
.success(function () {
$('#prompt-modal').modal('hide');
$scope.search(form.related.scan_job_templates.iterator);
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'DELETE returned status: ' + status });
});
};
Prompt({
hdr: 'Delete',
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the job template below?</div><div class="Prompt-bodyTarget">' + this.scan_job_template.name + '</div>',
action: action,
actionText: 'DELETE'
});
};
}
export default ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList',
'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit',
'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString',
'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt',
'PlaybookRun', 'CreateDialog', 'deleteJobTemplate', '$state',
InventoriesEdit,
];
@@ -0,0 +1,26 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesEdit from './inventory-edit.controller';
export default {
name: 'inventories.edit',
route: '/:inventory_id',
templateUrl: templateUrl('inventories/inventories'),
controller: InventoriesEdit,
data: {
activityStreamId: 'inventory_id'
},
ncyBreadcrumb: {
label: "INVENTORY EDIT"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};
@@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-edit.route';
import controller from './inventory-edit.controller';
export default
angular.module('inventoryEdit', [])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);
@@ -0,0 +1,364 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesList($scope, $rootScope, $location, $log,
$stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList,
generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller,
ClearScope, ProcessErrors, GetBasePath, Wait,
Find, Empty, $state) {
var list = InventoryList,
defaultUrl = GetBasePath('inventory'),
view = generateList,
paths = $location.path().replace(/^\//, '').split('/'),
mode = (paths[0] === 'inventories') ? 'edit' : 'select';
function ellipsis(a) {
if (a.length > 20) {
return a.substr(0,20) + '...';
}
return a;
}
function attachElem(event, html, title) {
var elem = $(event.target).parent();
try {
elem.tooltip('hide');
elem.popover('destroy');
}
catch(err) {
//ignore
}
$('.popover').each(function() {
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
$(this).remove();
});
$('.tooltip').each( function() {
// close any lingering tool tipss
$(this).hide();
});
elem.attr({
"aw-pop-over": html,
"data-popover-title": title,
"data-placement": "right" });
$compile(elem)($scope);
elem.on('shown.bs.popover', function() {
$('.popover').each(function() {
$compile($(this))($scope); //make nested directives work!
});
$('.popover-content, .popover-title').click(function() {
elem.popover('hide');
});
});
elem.popover('show');
}
view.inject(InventoryList, { mode: mode, scope: $scope });
$rootScope.flashMessage = null;
SearchInit({
scope: $scope,
set: 'inventories',
list: list,
url: defaultUrl
});
PaginateInit({
scope: $scope,
list: list,
url: defaultUrl
});
if ($stateParams.name) {
$scope[InventoryList.iterator + 'InputDisable'] = false;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.name;
$scope[InventoryList.iterator + 'SearchField'] = 'name';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.name.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = null;
}
if ($stateParams.has_active_failures) {
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.has_active_failures;
$scope[InventoryList.iterator + 'SearchField'] = 'has_active_failures';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.has_active_failures.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = ($stateParams.has_active_failures === 'true') ? {
value: 1
} : {
value: 0
};
}
if ($stateParams.has_inventory_sources) {
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.has_inventory_sources;
$scope[InventoryList.iterator + 'SearchField'] = 'has_inventory_sources';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.has_inventory_sources.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = ($stateParams.has_inventory_sources === 'true') ? {
value: 1
} : {
value: 0
};
}
if ($stateParams.inventory_sources_with_failures) {
// pass a value of true, however this field actually contains an integer value
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.inventory_sources_with_failures;
$scope[InventoryList.iterator + 'SearchField'] = 'inventory_sources_with_failures';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.inventory_sources_with_failures.label;
$scope[InventoryList.iterator + 'SearchType'] = 'gtzero';
}
$scope.search(list.iterator);
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
//If we got here by deleting an inventory, stop the spinner and cleanup events
Wait('stop');
try {
$('#prompt-modal').modal('hide');
}
catch(e) {
// ignore
}
$scope.inventories.forEach(function(inventory, idx) {
$scope.inventories[idx].launch_class = "";
if (inventory.has_inventory_sources) {
if (inventory.inventory_sources_with_failures > 0) {
$scope.inventories[idx].syncStatus = 'error';
$scope.inventories[idx].syncTip = inventory.inventory_sources_with_failures + ' groups with sync failures. Click for details';
}
else {
$scope.inventories[idx].syncStatus = 'successful';
$scope.inventories[idx].syncTip = 'No inventory sync failures. Click for details.';
}
}
else {
$scope.inventories[idx].syncStatus = 'na';
$scope.inventories[idx].syncTip = 'Not configured for inventory sync.';
$scope.inventories[idx].launch_class = "btn-disabled";
}
if (inventory.has_active_failures) {
$scope.inventories[idx].hostsStatus = 'error';
$scope.inventories[idx].hostsTip = inventory.hosts_with_active_failures + ' hosts with failures. Click for details.';
}
else if (inventory.total_hosts) {
$scope.inventories[idx].hostsStatus = 'successful';
$scope.inventories[idx].hostsTip = 'No hosts with failures. Click for details.';
}
else {
$scope.inventories[idx].hostsStatus = 'none';
$scope.inventories[idx].hostsTip = 'Inventory contains 0 hosts.';
}
});
});
if ($scope.removeRefreshInventories) {
$scope.removeRefreshInventories();
}
$scope.removeRefreshInventories = $scope.$on('RefreshInventories', function () {
// Reflect changes after inventory properties edit completes
$scope.search(list.iterator);
});
if ($scope.removeHostSummaryReady) {
$scope.removeHostSummaryReady();
}
$scope.removeHostSummaryReady = $scope.$on('HostSummaryReady', function(e, event, data) {
var html, title = "Recent Jobs";
Wait('stop');
if (data.count > 0) {
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
html += "<thead>\n";
html += "<tr>";
html += "<th>Status</th>";
html += "<th>Finished</th>";
html += "<th>Name</th>";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
data.results.forEach(function(row) {
html += "<tr>\n";
html += "<td><a href=\"#/jobs/" + row.id + "\" " + "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>\n";
html += "<td>" + ($filter('longDate')(row.finished)).replace(/ /,'<br />') + "</td>";
html += "<td><a href=\"#/jobs/" + row.id + "\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
". Click for details\" aw-tip-placement=\"top\">" + ellipsis(row.name) + "</a></td>";
html += "</tr>\n";
});
html += "</tbody>\n";
html += "</table>\n";
}
else {
html = "<p>No recent job data available for this inventory.</p>\n";
}
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) {
if (row.related.last_update) {
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";
}
else {
html += "<tr>";
html += "<td><a href=\"\" aw-tool-tip=\"No sync data\" aw-tip-placement=\"top\"><i class=\"fa icon-job-none\"></i></a></td>";
html += "<td>NA</td>";
html += "<td><a href=\"\">" + 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 inventory;
if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
if (inventory.syncStatus !== 'na') {
Wait('start');
Rest.setUrl(inventory.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, inventory, data);
})
.error(function(data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + inventory.related.inventory_sources + ' failed. GET returned status: ' + status
});
});
}
}
};
$scope.showHostSummary = function(event, id) {
var url, inventory;
if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
if (inventory.total_hosts > 0) {
Wait('start');
url = GetBasePath('jobs') + "?type=job&inventory=" + id + "&failed=";
url += (inventory.has_active_failures) ? 'true' : "false";
url += "&order_by=-finished&page_size=5";
Rest.setUrl(url);
Rest.get()
.success( function(data) {
$scope.$emit('HostSummaryReady', event, data);
})
.error( function(data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. GET returned: ' + status
});
});
}
}
};
$scope.viewJob = function(url) {
// Pull the id out of the URL
var id = url.replace(/^\//, '').split('/')[3];
$state.go('inventorySyncStdout', {id: id});
};
$scope.addInventory = function () {
$state.go('inventories.add');
};
$scope.editInventory = function (id) {
$state.go('inventories.edit', {inventory_id: id});
};
$scope.manageInventory = function(id){
$location.path($location.path() + '/' + id + '/manage');
};
$scope.deleteInventory = function (id, name) {
var action = function () {
var url = defaultUrl + id + '/';
Wait('start');
$('#prompt-modal').modal('hide');
Rest.setUrl(url);
Rest.destroy()
.success(function () {
$scope.search(list.iterator);
})
.error(function (data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status
});
});
};
Prompt({
hdr: 'Delete',
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the inventory below?</div><div class="Prompt-bodyTarget">' + $filter('sanitize')(name) + '</div>',
action: action,
actionText: 'DELETE'
});
};
$scope.lookupOrganization = function (organization_id) {
Rest.setUrl(GetBasePath('organizations') + organization_id + '/');
Rest.get()
.success(function (data) {
return data.name;
});
};
// Failed jobs link. Go to the jobs tabs, find all jobs for the inventory and sort by status
$scope.viewJobs = function (id) {
$location.url('/jobs/?inventory__int=' + id);
};
$scope.viewFailedJobs = function (id) {
$location.url('/jobs/?inventory__int=' + id + '&status=failed');
};
}
export default ['$scope', '$rootScope', '$location', '$log',
'$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList',
'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller',
'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', InventoriesList];
@@ -0,0 +1,27 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesList from './inventory-list.controller';
export default {
name: 'inventories',
route: '/inventories',
templateUrl: templateUrl('inventories/inventories'),
controller: InventoriesList,
data: {
activityStream: true,
activityStreamTarget: 'inventory'
},
ncyBreadcrumb: {
label: "INVENTORIES"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};
@@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-list.route';
import controller from './inventory-list.controller';
export default
angular.module('inventoryList', [])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);
+18
View File
@@ -0,0 +1,18 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import inventoryAdd from './add/main';
import inventoryEdit from './edit/main';
import inventoryList from './list/main';
import inventoryManage from './manage/main';
export default
angular.module('inventory', [
inventoryAdd.name,
inventoryEdit.name,
inventoryList.name,
inventoryManage.name,
]);
@@ -0,0 +1,525 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesManage($log, $scope, $rootScope, $location,
$state, $compile, generateList, ClearScope, Empty, Wait, Rest, Alert,
GetBasePath, ProcessErrors, InventoryGroups,
InjectHosts, Find, HostsReload, SearchInit, PaginateInit, GetSyncStatusMsg,
GetHostsStatusMsg, GroupsEdit, InventoryUpdate, GroupsCancelUpdate,
ViewUpdateStatus, GroupsDelete, Store, HostsEdit, HostsDelete,
EditInventoryProperties, ToggleHostEnabled, ShowJobSummary,
InventoryGroupsHelp, HelpDialog,
GroupsCopy, HostsCopy, $stateParams, ParamPass) {
var PreviousSearchParams,
url,
hostScope = $scope.$new();
ClearScope();
// TODO: only display adhoc button if the user has permission to use it.
// TODO: figure out how to get the action-list partial to update so that
// the tooltip can be changed based off things being selected or not.
$scope.adhocButtonTipContents = "Launch adhoc command for the inventory";
// watcher for the group list checkbox changes
$scope.$on('multiSelectList.selectionChanged', function(e, selection) {
if (selection.length > 0) {
$scope.groupsSelected = true;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "selected groups and hosts.";
} else {
$scope.groupsSelected = false;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "inventory.";
}
$scope.groupsSelectedItems = selection.selectedItems;
});
// watcher for the host list checkbox changes
hostScope.$on('multiSelectList.selectionChanged', function(e, selection) {
// you need this so that the event doesn't bubble to the watcher above
// for the host list
e.stopPropagation();
if (selection.length === 0) {
$scope.hostsSelected = false;
} else if (selection.length === 1) {
$scope.systemTrackingTooltip = "Compare host over time";
$scope.hostsSelected = true;
$scope.systemTrackingDisabled = false;
} else if (selection.length === 2) {
$scope.systemTrackingTooltip = "Compare hosts against each other";
$scope.hostsSelected = true;
$scope.systemTrackingDisabled = false;
} else {
$scope.hostsSelected = true;
$scope.systemTrackingDisabled = true;
}
$scope.hostsSelectedItems = selection.selectedItems;
});
$scope.systemTracking = function() {
var hostIds = _.map($scope.hostsSelectedItems, function(x){
return x.id;
});
$state.transitionTo('systemTracking',
{ inventory: $scope.inventory,
inventoryId: $scope.inventory.id,
hosts: $scope.hostsSelectedItems,
hostIds: hostIds
});
};
// populates host patterns based on selected hosts/groups
$scope.populateAdhocForm = function() {
var host_patterns = "all";
if ($scope.hostsSelected || $scope.groupsSelected) {
var allSelectedItems = [];
if ($scope.groupsSelectedItems) {
allSelectedItems = allSelectedItems.concat($scope.groupsSelectedItems);
}
if ($scope.hostsSelectedItems) {
allSelectedItems = allSelectedItems.concat($scope.hostsSelectedItems);
}
if (allSelectedItems) {
host_patterns = _.pluck(allSelectedItems, "name").join(":");
}
}
$rootScope.hostPatterns = host_patterns;
$state.go('inventoryManage.adhoc');
};
$scope.refreshHostsOnGroupRefresh = false;
$scope.selected_group_id = null;
Wait('start');
if ($scope.removeHostReloadComplete) {
$scope.removeHostReloadComplete();
}
$scope.removeHostReloadComplete = $scope.$on('HostReloadComplete', function() {
if ($scope.initial_height) {
var host_height = $('#hosts-container .well').height(),
group_height = $('#group-list-container .well').height(),
new_height;
if (host_height > group_height) {
new_height = host_height - (host_height - group_height);
}
else if (host_height < group_height) {
new_height = host_height + (group_height - host_height);
}
if (new_height) {
$('#hosts-container .well').height(new_height);
}
$scope.initial_height = null;
}
});
if ($scope.removeRowCountReady) {
$scope.removeRowCountReady();
}
$scope.removeRowCountReady = $scope.$on('RowCountReady', function(e, rows) {
// Add hosts view
$scope.show_failures = false;
InjectHosts({
group_scope: $scope,
host_scope: hostScope,
inventory_id: $scope.inventory.id,
tree_id: null,
group_id: null,
pageSize: rows
});
SearchInit({ scope: $scope, set: 'groups', list: InventoryGroups, url: $scope.inventory.related.root_groups });
PaginateInit({ scope: $scope, list: InventoryGroups , url: $scope.inventory.related.root_groups, pageSize: rows });
$scope.search(InventoryGroups.iterator, null, true);
});
if ($scope.removeInventoryLoaded) {
$scope.removeInventoryLoaded();
}
$scope.removeInventoryLoaded = $scope.$on('InventoryLoaded', function() {
var rows;
// Add groups view
generateList.inject(InventoryGroups, {
mode: 'edit',
id: 'group-list-container',
searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12',
scope: $scope
});
rows = 20;
hostScope.host_page_size = rows;
$scope.group_page_size = rows;
$scope.show_failures = false;
InjectHosts({
group_scope: $scope,
host_scope: hostScope,
inventory_id: $scope.inventory.id,
tree_id: null,
group_id: null,
pageSize: rows
});
// Load data
SearchInit({
scope: $scope,
set: 'groups',
list: InventoryGroups,
url: $scope.inventory.related.root_groups
});
PaginateInit({
scope: $scope,
list: InventoryGroups ,
url: $scope.inventory.related.root_groups,
pageSize: rows
});
$scope.search(InventoryGroups.iterator, null, true);
$scope.$emit('WatchUpdateStatus'); // init socket io conneciton and start watching for status updates
});
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function(e, set) {
if (set === 'groups') {
$scope.groups.forEach( function(group, idx) {
var stat, hosts_status;
stat = GetSyncStatusMsg({
status: group.summary_fields.inventory_source.status,
has_inventory_sources: group.has_inventory_sources,
source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null )
}); // from helpers/Groups.js
$scope.groups[idx].status_class = stat['class'];
$scope.groups[idx].status_tooltip = stat.tooltip;
$scope.groups[idx].launch_tooltip = stat.launch_tip;
$scope.groups[idx].launch_class = stat.launch_class;
hosts_status = GetHostsStatusMsg({
active_failures: group.hosts_with_active_failures,
total_hosts: group.total_hosts,
inventory_id: $scope.inventory.id,
group_id: group.id
}); // from helpers/Groups.js
$scope.groups[idx].hosts_status_tip = hosts_status.tooltip;
$scope.groups[idx].show_failures = hosts_status.failures;
$scope.groups[idx].hosts_status_class = hosts_status['class'];
$scope.groups[idx].source = (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null;
$scope.groups[idx].status = (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.status : null;
});
if ($scope.refreshHostsOnGroupRefresh) {
$scope.refreshHostsOnGroupRefresh = false;
HostsReload({
scope: hostScope,
group_id: $scope.selected_group_id,
inventory_id: $scope.inventory.id,
pageSize: hostScope.host_page_size
});
}
else {
Wait('stop');
}
}
});
// Load Inventory
url = GetBasePath('inventory') + $stateParams.inventory_id + '/';
Rest.setUrl(url);
Rest.get()
.success(function (data) {
$scope.inventory = data;
$scope.$emit('InventoryLoaded');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory: ' + $stateParams.inventory_id +
' GET returned status: ' + status });
});
// start watching for real-time updates
if ($rootScope.removeWatchUpdateStatus) {
$rootScope.removeWatchUpdateStatus();
}
$rootScope.removeWatchUpdateStatus = $rootScope.$on('JobStatusChange-inventory', function(e, data) {
var stat, group;
if (data.group_id) {
group = Find({ list: $scope.groups, key: 'id', val: data.group_id });
if (data.status === "failed" || data.status === "successful") {
if (data.group_id === $scope.selected_group_id || group) {
// job completed, fefresh all groups
$log.debug('Update completed. Refreshing the tree.');
$scope.refreshGroups();
}
}
else if (group) {
// incremental update, just update
$log.debug('Status of group: ' + data.group_id + ' changed to: ' + data.status);
stat = GetSyncStatusMsg({
status: data.status,
has_inventory_sources: group.has_inventory_sources,
source: group.source
});
$log.debug('changing tooltip to: ' + stat.tooltip);
group.status = data.status;
group.status_class = stat['class'];
group.status_tooltip = stat.tooltip;
group.launch_tooltip = stat.launch_tip;
group.launch_class = stat.launch_class;
}
}
});
// Load group on selection
function loadGroups(url) {
SearchInit({ scope: $scope, set: 'groups', list: InventoryGroups, url: url });
PaginateInit({ scope: $scope, list: InventoryGroups , url: url, pageSize: $scope.group_page_size });
$scope.search(InventoryGroups.iterator, null, true, false, true);
}
$scope.refreshHosts = function() {
HostsReload({
scope: hostScope,
group_id: $scope.selected_group_id,
inventory_id: $scope.inventory.id,
pageSize: hostScope.host_page_size
});
};
$scope.refreshGroups = function() {
$scope.refreshHostsOnGroupRefresh = true;
$scope.search(InventoryGroups.iterator, null, true, false, true);
};
$scope.restoreSearch = function() {
// Restore search params and related stuff, plus refresh
// groups and hosts lists
SearchInit({
scope: $scope,
set: PreviousSearchParams.set,
list: PreviousSearchParams.list,
url: PreviousSearchParams.defaultUrl,
iterator: PreviousSearchParams.iterator,
sort_order: PreviousSearchParams.sort_order,
setWidgets: false
});
$scope.refreshHostsOnGroupRefresh = true;
$scope.search(InventoryGroups.iterator, null, true, false, true);
};
$scope.groupSelect = function(id) {
var groups = [], group = Find({ list: $scope.groups, key: 'id', val: id });
if($state.params.groups){
groups.push($state.params.groups);
}
groups.push(group.id);
groups = groups.join();
$state.transitionTo('inventoryManage', {inventory_id: $state.params.inventory_id, groups: groups}, { notify: false });
loadGroups(group.related.children, group.id);
};
$scope.createGroup = function () {
PreviousSearchParams = Store('group_current_search_params');
var params = {
scope: $scope,
inventory_id: $scope.inventory.id,
group_id: $scope.selected_group_id,
mode: 'add'
}
ParamPass.set(params);
$state.go('inventoryManage.addGroup');
};
$scope.editGroup = function (id) {
PreviousSearchParams = Store('group_current_search_params');
var params = {
scope: $scope,
inventory_id: $scope.inventory.id,
group_id: id,
mode: 'edit'
}
ParamPass.set(params);
$state.go('inventoryManage.editGroup', {group_id: id});
};
// Launch inventory sync
$scope.updateGroup = function (id) {
var group = Find({ list: $scope.groups, key: 'id', val: id });
if (group) {
if (Empty(group.source)) {
// if no source, do nothing.
} else if (group.status === 'updating') {
Alert('Update in Progress', 'The inventory update process is currently running for group <em>' +
group.name + '</em> Click the <i class="fa fa-refresh"></i> button to monitor the status.', 'alert-info', null, null, null, null, true);
} else {
Wait('start');
Rest.setUrl(group.related.inventory_source);
Rest.get()
.success(function (data) {
InventoryUpdate({
scope: $scope,
url: data.related.update,
group_name: data.summary_fields.group.name,
group_source: data.source,
group_id: group.id,
});
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' +
group.related.inventory_source + ' GET returned status: ' + status });
});
}
}
};
$scope.cancelUpdate = function (id) {
GroupsCancelUpdate({ scope: $scope, id: id });
};
$scope.viewUpdateStatus = function (id) {
ViewUpdateStatus({
scope: $scope,
group_id: id
});
};
$scope.copyGroup = function(id) {
PreviousSearchParams = Store('group_current_search_params');
GroupsCopy({
scope: $scope,
group_id: id
});
};
$scope.deleteGroup = function (id) {
GroupsDelete({
scope: $scope,
group_id: id,
inventory_id: $scope.inventory.id
});
};
$scope.editInventoryProperties = function () {
// EditInventoryProperties({ scope: $scope, inventory_id: $scope.inventory.id });
$location.path('/inventories/' + $scope.inventory.id + '/');
};
hostScope.createHost = function () {
var params = {
host_scope: hostScope,
group_scope: $scope,
mode: 'add',
host_id: null,
selected_group_id: $scope.selected_group_id,
inventory_id: $scope.inventory.id
}
ParamPass.set(params);
$state.go('inventoryManage.addHost');
};
hostScope.editHost = function (host_id) {
var params = {
host_scope: hostScope,
group_scope: $scope,
mode: 'edit',
host_id: host_id,
inventory_id: $scope.inventory.id
}
ParamPass.set(params);
$state.go('inventoryManage.editHost', {host_id: host_id});
};
hostScope.deleteHost = function (host_id, host_name) {
HostsDelete({
parent_scope: $scope,
host_scope: hostScope,
host_id: host_id,
host_name: host_name
});
};
hostScope.copyHost = function(id) {
PreviousSearchParams = Store('group_current_search_params');
HostsCopy({
group_scope: $scope,
host_scope: hostScope,
host_id: id
});
};
hostScope.toggleHostEnabled = function (host_id, external_source) {
ToggleHostEnabled({
parent_scope: $scope,
host_scope: hostScope,
host_id: host_id,
external_source: external_source
});
};
hostScope.showJobSummary = function (job_id) {
ShowJobSummary({
job_id: job_id
});
};
$scope.showGroupHelp = function (params) {
var opts = {
defn: InventoryGroupsHelp
};
if (params) {
opts.autoShow = params.autoShow || false;
}
HelpDialog(opts);
}
;
$scope.showHosts = function (group_id, show_failures) {
// Clicked on group
if (group_id !== null) {
Wait('start');
hostScope.show_failures = show_failures;
$scope.groupSelect(group_id);
hostScope.hosts = [];
$scope.show_failures = show_failures; // turn on failed hosts
// filter in hosts view
} else {
Wait('stop');
}
};
if ($scope.removeGroupDeleteCompleted) {
$scope.removeGroupDeleteCompleted();
}
$scope.removeGroupDeleteCompleted = $scope.$on('GroupDeleteCompleted',
function() {
$scope.refreshGroups();
}
);
}
export default [
'$log', '$scope', '$rootScope', '$location',
'$state', '$compile', 'generateList', 'ClearScope', 'Empty', 'Wait',
'Rest', 'Alert', 'GetBasePath', 'ProcessErrors',
'InventoryGroups', 'InjectHosts', 'Find', 'HostsReload',
'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg',
'GroupsEdit', 'InventoryUpdate', 'GroupsCancelUpdate', 'ViewUpdateStatus',
'GroupsDelete', 'Store', 'HostsEdit', 'HostsDelete',
'EditInventoryProperties', 'ToggleHostEnabled', 'ShowJobSummary',
'InventoryGroupsHelp', 'HelpDialog', 'GroupsCopy',
'HostsCopy', '$stateParams', 'ParamPass', InventoriesManage,
];
@@ -10,9 +10,6 @@
<div id="host-list-container" class="Panel"></div> <div id="host-list-container" class="Panel"></div>
</div> </div>
</div> </div>
<div id="inventory-modal-container"></div>
<div id="group-copy-dialog" style="display: none;"> <div id="group-copy-dialog" style="display: none;">
<div id="copy-group-radio-container" class="well"> <div id="copy-group-radio-container" class="well">
<div class="title"><span class="highlight">1.</span> Copy or move <span ng-bind="name"></span>?</div> <div class="title"><span class="highlight">1.</span> Copy or move <span ng-bind="name"></span>?</div>
@@ -0,0 +1,28 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesManage from './inventory-manage.controller';
export default {
name: 'inventoryManage',
url: '/inventories/:inventory_id/manage?groups',
templateUrl: templateUrl('inventories/manage/inventory-manage'),
controller: InventoriesManage,
data: {
activityStream: true,
activityStreamTarget: 'inventory',
activityStreamId: 'inventory_id'
},
ncyBreadcrumb: {
label: "INVENTORY MANAGE"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};
@@ -0,0 +1,19 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-manage.route';
import manageHosts from './manage-hosts/main';
import manageGroups from './manage-groups/main';
export default
angular.module('inventoryManage', [
manageHosts.name,
manageGroups.name
])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);
@@ -0,0 +1,550 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
function manageGroupsDirectiveController($filter, $rootScope, $location, $log, $stateParams, $compile, $state, $scope, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors,
GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, LookUpInit, Empty, Wait,
GetChoices, UpdateGroup, SourceChange, Find, ParseVariableString, ToJSON, GroupsScheduleListInit,
SourceForm, SetSchedulesInnerDialogSize, CreateSelect2, ParamPass) {
var vm = this;
var params = ParamPass.get();
if(params === undefined) {
params = {};
params.scope = $scope.$new();
}
var parent_scope = params.scope,
group_id = $stateParams.group_id,
mode = $state.current.data.mode, // 'add' or 'edit'
inventory_id = $stateParams.inventory_id,
generator = GenerateForm,
group_created = false,
defaultUrl,
master = {},
choicesReady,
modal_scope = parent_scope.$new(),
properties_scope = parent_scope.$new(),
sources_scope = parent_scope.$new(),
elem, group,
schedules_url = '';
if (mode === 'edit') {
defaultUrl = GetBasePath('groups') + group_id + '/';
} else {
defaultUrl = (group_id !== undefined) ? GetBasePath('groups') + group_id + '/children/' :
GetBasePath('inventory') + inventory_id + '/groups/';
}
Rest.setUrl(defaultUrl);
Rest.get()
.success(function(data) {
group = data;
for (var fld in GroupForm.fields) {
if (data[fld]) {
properties_scope[fld] = data[fld];
master[fld] = properties_scope[fld];
}
}
if(mode === 'edit') {
schedules_url = data.related.inventory_source + 'schedules/';
properties_scope.variable_url = data.related.variable_data;
sources_scope.source_url = data.related.inventory_source;
modal_scope.$emit('LoadSourceData');
}
})
.error(function(data, status) {
ProcessErrors(modal_scope, data, status, {
hdr: 'Error!',
msg: 'Failed to retrieve group: ' + defaultUrl + '. GET status: ' + status
});
});
$('#properties-tab').empty();
$('#sources-tab').empty();
elem = document.getElementById('group-manage-panel');
$compile(elem)(modal_scope);
$scope.parseType = 'yaml';
var form_scope =
generator.inject(GroupForm, {
mode: mode,
id: 'properties-tab',
related: false,
scope: properties_scope,
cancelButton: false,
});
var source_form_scope =
generator.inject(SourceForm, {
mode: mode,
id: 'sources-tab',
related: false,
scope: sources_scope,
cancelButton: false
});
generator.reset();
GetSourceTypeOptions({
scope: sources_scope,
variable: 'source_type_options'
});
sources_scope.source = SourceForm.fields.source['default'];
sources_scope.sourcePathRequired = false;
sources_scope[SourceForm.fields.source_vars.parseTypeName] = 'yaml';
sources_scope.update_cache_timeout = 0;
properties_scope.parseType = 'yaml';
function waitStop() {
Wait('stop');
}
function initSourceChange() {
parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value !== "manual") ? true : false;
SourceChange({
scope: sources_scope,
form: SourceForm
});
}
// JT -- this gets called after the properties & properties variables are loaded, and is emitted from (groupLoaded)
if (modal_scope.removeLoadSourceData) {
modal_scope.removeLoadSourceData();
}
modal_scope.removeLoadSourceData = modal_scope.$on('LoadSourceData', function() {
ParseTypeChange({
scope: form_scope,
variable: 'variables',
parse_variable: 'parseType',
field_id: 'group_variables'
});
if (sources_scope.source_url) {
// get source data
Rest.setUrl(sources_scope.source_url);
Rest.get()
.success(function(data) {
var fld, i, j, flag, found, set, opts, list, form;
form = SourceForm;
for (fld in form.fields) {
if (fld === 'checkbox_group') {
for (i = 0; i < form.fields[fld].fields.length; i++) {
flag = form.fields[fld].fields[i];
if (data[flag.name] !== undefined) {
sources_scope[flag.name] = data[flag.name];
master[flag.name] = sources_scope[flag.name];
}
}
}
if (fld === 'source') {
found = false;
data.source = (data.source === "") ? "manual" : data.source;
for (i = 0; i < sources_scope.source_type_options.length; i++) {
if (sources_scope.source_type_options[i].value === data.source) {
sources_scope.source = sources_scope.source_type_options[i];
found = true;
}
}
if (!found || sources_scope.source.value === "manual") {
sources_scope.groupUpdateHide = true;
} else {
sources_scope.groupUpdateHide = false;
}
master.source = sources_scope.source;
} else if (fld === 'source_vars') {
// Parse source_vars, converting to YAML.
sources_scope.source_vars = ParseVariableString(data.source_vars);
master.source_vars = sources_scope.variables;
} else if (fld === "inventory_script") {
// the API stores it as 'source_script', we call it inventory_script
data.summary_fields['inventory_script'] = data.summary_fields.source_script;
sources_scope.inventory_script = data.source_script;
master.inventory_script = sources_scope.inventory_script;
} else if (fld === "source_regions") {
if (data[fld] === "") {
sources_scope[fld] = data[fld];
master[fld] = sources_scope[fld];
} else {
sources_scope[fld] = data[fld].split(",");
master[fld] = sources_scope[fld];
}
} else if (data[fld] !== undefined) {
sources_scope[fld] = data[fld];
master[fld] = sources_scope[fld];
}
if (form.fields[fld].sourceModel && data.summary_fields &&
data.summary_fields[form.fields[fld].sourceModel]) {
sources_scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
}
}
initSourceChange();
if (data.source_regions) {
if (data.source === 'ec2' ||
data.source === 'rax' ||
data.source === 'gce' ||
data.source === 'azure') {
if (data.source === 'ec2') {
set = sources_scope.ec2_regions;
} else if (data.source === 'rax') {
set = sources_scope.rax_regions;
} else if (data.source === 'gce') {
set = sources_scope.gce_regions;
} else if (data.source === 'azure') {
set = sources_scope.azure_regions;
}
opts = [];
list = data.source_regions.split(',');
for (i = 0; i < list.length; i++) {
for (j = 0; j < set.length; j++) {
if (list[i] === set[j].value) {
opts.push({
id: set [j].value,
text: set [j].label
});
}
}
}
master.source_regions = opts;
CreateSelect2({
element: "#source_source_regions",
opts: opts
});
}
} else {
// If empty, default to all
master.source_regions = [{
id: 'all',
text: 'All'
}];
}
if (data.group_by && data.source === 'ec2') {
set = sources_scope.ec2_group_by;
opts = [];
list = data.group_by.split(',');
for (i = 0; i < list.length; i++) {
for (j = 0; j < set.length; j++) {
if (list[i] === set[j].value) {
opts.push({
id: set [j].value,
text: set [j].label
});
}
}
}
master.group_by = opts;
CreateSelect2({
element: "#source_group_by",
opts: opts
});
}
sources_scope.group_update_url = data.related.update;
})
.error(function(data, status) {
sources_scope.source = "";
ProcessErrors(modal_scope, data, status, null, {
hdr: 'Error!',
msg: 'Failed to retrieve inventory source. GET status: ' + status
});
});
}
});
if (sources_scope.removeScopeSourceTypeOptionsReady) {
sources_scope.removeScopeSourceTypeOptionsReady();
}
sources_scope.removeScopeSourceTypeOptionsReady = sources_scope.$on('sourceTypeOptionsReady', function() {
if (mode === 'add') {
sources_scope.source = Find({
list: sources_scope.source_type_options,
key: 'value',
val: ''
});
modal_scope.showSchedulesTab = false;
}
});
choicesReady = 0;
if (sources_scope.removeChoicesReady) {
sources_scope.removeChoicesReady();
}
sources_scope.removeChoicesReady = sources_scope.$on('choicesReadyGroup', function() {
CreateSelect2({
element: '#source_source',
multiple: false
});
modal_scope.$emit('LoadSourceData');
choicesReady++;
if (choicesReady === 5) {
if (mode !== 'edit') {
properties_scope.variables = "---";
master.variables = properties_scope.variables;
}
}
});
// Load options for source regions
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'rax_regions',
choice_name: 'rax_region_choices',
callback: 'choicesReadyGroup'
});
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'ec2_regions',
choice_name: 'ec2_region_choices',
callback: 'choicesReadyGroup'
});
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'gce_regions',
choice_name: 'gce_region_choices',
callback: 'choicesReadyGroup'
});
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'azure_regions',
choice_name: 'azure_region_choices',
callback: 'choicesReadyGroup'
});
// Load options for group_by
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'group_by',
variable: 'ec2_group_by',
choice_name: 'ec2_group_by_choices',
callback: 'choicesReadyGroup'
});
//Wait('start');
if (parent_scope.removeAddTreeRefreshed) {
parent_scope.removeAddTreeRefreshed();
}
parent_scope.removeAddTreeRefreshed = parent_scope.$on('GroupTreeRefreshed', function() {
// Clean up
Wait('stop');
if (modal_scope.searchCleanUp) {
modal_scope.searchCleanup();
}
try {
//$('#group-modal-dialog').dialog('close');
} catch (e) {
// ignore
}
});
if (modal_scope.removeSaveComplete) {
modal_scope.removeSaveComplete();
}
modal_scope.removeSaveComplete = modal_scope.$on('SaveComplete', function(e, error) {
if (!error) {
modal_scope.cancelPanel();
}
});
if (modal_scope.removeFormSaveSuccess) {
modal_scope.removeFormSaveSuccess();
}
modal_scope.removeFormSaveSuccess = modal_scope.$on('formSaveSuccess', function() {
// Source data gets stored separately from the group. Validate and store Source
// related fields, then call SaveComplete to wrap things up.
var parseError = false,
regions, r, i,
group_by,
data = {
group: group_id,
source: ((sources_scope.source && sources_scope.source.value !== 'manual') ? sources_scope.source.value : ''),
source_path: sources_scope.source_path,
credential: sources_scope.credential,
overwrite: sources_scope.overwrite,
overwrite_vars: sources_scope.overwrite_vars,
source_script: sources_scope.inventory_script,
update_on_launch: sources_scope.update_on_launch,
update_cache_timeout: (sources_scope.update_cache_timeout || 0)
};
// Create a string out of selected list of regions
if (sources_scope.source_regions) {
regions = $('#source_source_regions').select2("data");
r = [];
for (i = 0; i < regions.length; i++) {
r.push(regions[i].id);
}
data.source_regions = r.join();
}
if (sources_scope.source && (sources_scope.source.value === 'ec2')) {
data.instance_filters = sources_scope.instance_filters;
// Create a string out of selected list of regions
group_by = $('#source_group_by').select2("data");
r = [];
for (i = 0; i < group_by.length; i++) {
r.push(group_by[i].id);
}
data.group_by = r.join();
}
if (sources_scope.source && (sources_scope.source.value === 'ec2')) {
// for ec2, validate variable data
data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.source_vars, true);
}
if (sources_scope.source && (sources_scope.source.value === 'custom')) {
data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.extra_vars, true);
}
if (sources_scope.source && (sources_scope.source.value === 'vmware' ||
sources_scope.source.value === 'openstack')) {
data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true);
}
// the API doesn't expect the credential to be passed with a custom inv script
if (sources_scope.source && sources_scope.source.value === 'custom') {
delete(data.credential);
}
if (!parseError) {
Rest.setUrl(sources_scope.source_url);
Rest.put(data)
.success(function() {
modal_scope.$emit('SaveComplete', false);
})
.error(function(data, status) {
$('#group_tabs a:eq(1)').tab('show');
ProcessErrors(sources_scope, data, status, SourceForm, {
hdr: 'Error!',
msg: 'Failed to update group inventory source. PUT status: ' + status
});
});
}
});
// Cancel
modal_scope.cancelPanel = function() {
Wait('stop');
$state.go('inventoryManage', {}, {reload: true});
};
// Save
modal_scope.saveGroup = function() {
Wait('start');
var fld, data, json_data;
try {
json_data = ToJSON(properties_scope.parseType, properties_scope.variables, true);
data = {};
for (fld in GroupForm.fields) {
data[fld] = properties_scope[fld];
}
data.inventory = inventory_id;
Rest.setUrl(defaultUrl);
if (mode === 'edit' || (mode === 'add' && group_created)) {
Rest.put(data)
.success(function() {
modal_scope.$emit('formSaveSuccess');
})
.error(function(data, status) {
$('#group_tabs a:eq(0)').tab('show');
ProcessErrors(properties_scope, data, status, GroupForm, {
hdr: 'Error!',
msg: 'Failed to update group: ' + group_id + '. PUT status: ' + status
});
});
} else {
Rest.post(data)
.success(function(data) {
group_created = true;
group_id = data.id;
sources_scope.source_url = data.related.inventory_source;
modal_scope.$emit('formSaveSuccess');
})
.error(function(data, status) {
$('#group_tabs a:eq(0)').tab('show');
ProcessErrors(properties_scope, data, status, GroupForm, {
hdr: 'Error!',
msg: 'Failed to create group: ' + group_id + '. POST status: ' + status
});
});
}
} catch (e) {
// ignore. ToJSON will have already alerted the user
}
};
// Start the update process
modal_scope.updateGroup = function() {
if (sources_scope.source === "manual" || sources_scope.source === null) {
Alert('Missing Configuration', 'The selected group is not configured for updates. You must first edit the group, provide Source settings, ' +
'and then run an update.', 'alert-info');
} else if (sources_scope.status === 'updating') {
Alert('Update in Progress', 'The inventory update process is currently running for group <em>' +
$filter('sanitize')(sources_scope.summary_fields.group.name) + '</em>. Use the Refresh button to monitor the status.', 'alert-info', null, null, null, null, true);
} else {
InventoryUpdate({
scope: parent_scope,
group_id: group_id,
url: properties_scope.group_update_url,
group_name: properties_scope.name,
group_source: sources_scope.source.value
});
}
};
// Change the lookup and regions when the source changes
sources_scope.sourceChange = function() {
sources_scope.credential_name = "";
sources_scope.credential = "";
if (sources_scope.credential_name_api_error) {
delete sources_scope.credential_name_api_error;
}
initSourceChange();
};
angular.extend(vm, {
cancelPanel : modal_scope.cancelPanel,
saveGroup: modal_scope.saveGroup
});
}
export default ['$filter', '$rootScope', '$location', '$log', '$stateParams', '$compile', '$state', '$scope', 'Rest', 'Alert', 'GroupForm', 'GenerateForm',
'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate',
'LookUpInit', 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find',
'ParseVariableString', 'ToJSON', 'GroupsScheduleListInit', 'SourceForm', 'SetSchedulesInnerDialogSize', 'CreateSelect2', 'ParamPass',
manageGroupsDirectiveController
];
@@ -0,0 +1,25 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
import manageGroupsDirectiveController from './manage-groups.directive.controller';
export default ['templateUrl', 'ParamPass',
function(templateUrl, ParamPass) {
return {
restrict: 'EA',
scope: true,
replace: true,
templateUrl: templateUrl('inventories/manage/manage-groups/directive/manage-groups.directive'),
link: function(scope, element, attrs) {
},
controller: manageGroupsDirectiveController,
controllerAs: 'vm',
bindToController: true
};
}
];
@@ -0,0 +1,20 @@
<div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="vm.cancelPanel()">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div id="group-manage-panel">
<div id="properties-tab"></div>
<div id="sources-tab"></div>
</div>
<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
<div class="ui-dialog-buttonset">
<button type="button" class="btn btn-primary Form-saveButton" id="Inventory-groupManage--okButton" ng-click="vm.saveGroup()">
Save</button>
<button type="button" class="btn btn-default Form-cancelButton" id="Inventory-groupManage--cancelButton" ng-click="vm.cancelPanel()">
Cancel</button>
</div>
</div>
</div>
@@ -0,0 +1,16 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './manage-groups.route';
import manageGroupsDirective from './directive/manage-groups.directive';
export default
angular.module('manage-groups', [])
.directive('manageGroups', manageGroupsDirective)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route.edit);
$stateExtender.addState(route.add);
}]);
@@ -0,0 +1,5 @@
<div class="tab-pane" id="Inventory-groupManage">
<div ng-cloak id="Inventory-groupManage--panel" class="Panel">
<manage-groups></manage-groups>
</div>
</div>
@@ -0,0 +1,46 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {
templateUrl
} from '../../../shared/template-url/template-url.factory';
export default {
edit: {
name: 'inventoryManage.editGroup',
route: '/:group_id/editGroup',
templateUrl: templateUrl('inventories/manage/manage-groups/manage-groups'),
data: {
group_id: 'group_id',
mode: 'edit'
},
ncyBreadcrumb: {
label: "INVENTORY EDIT GROUPS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
},
add: {
name: 'inventoryManage.addGroup',
route: '/addGroup',
templateUrl: templateUrl('inventories/manage/manage-groups/manage-groups'),
ncyBreadcrumb: {
label: "INVENTORY ADD GROUP"
},
data: {
mode: 'add'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
},
};
@@ -0,0 +1,194 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
function manageHostsDirectiveController($rootScope, $location, $log, $stateParams, $state, $scope, Rest, Alert, HostForm,
GenerateForm, Prompt, ProcessErrors, GetBasePath, HostsReload, ParseTypeChange, Wait,
Find, SetStatus, ApplyEllipsis, ToJSON, ParseVariableString, CreateDialog, TextareaResize, ParamPass) {
var vm = this;
var params = ParamPass.get();
if(params === undefined) {
params = {};
params.host_scope = $scope.$new();
params.group_scope = $scope.$new();
}
var parent_scope = params.host_scope,
group_scope = params.group_scope,
inventory_id = $stateParams.inventory_id,
mode = $state.current.data.mode, // 'add' or 'edit'
selected_group_id = params.selected_group_id,
generator = GenerateForm,
form = HostForm,
defaultUrl,
scope = parent_scope.$new(),
master = {},
relatedSets = {},
url, form_scope;
var host_id = $stateParams.host_id || undefined;
form_scope =
generator.inject(HostForm, {
mode: 'edit',
id: 'host-panel-form',
related: false,
scope: scope,
cancelButton: false
});
generator.reset();
// Retrieve detail record and prepopulate the form
if (mode === 'edit') {
defaultUrl = GetBasePath('hosts') + host_id + '/';
Rest.setUrl(defaultUrl);
Rest.get()
.success(function(data) {
var set, fld, related;
for (fld in form.fields) {
if (data[fld]) {
scope[fld] = data[fld];
master[fld] = scope[fld];
}
}
related = data.related;
for (set in form.related) {
if (related[set]) {
relatedSets[set] = {
url: related[set],
iterator: form.related[set].iterator
};
}
}
scope.variable_url = data.related.variable_data;
scope.has_inventory_sources = data.has_inventory_sources;
scope.parseType = 'yaml';
ParseTypeChange({
scope: scope,
field_id: 'host_variables',
});
})
.error(function(data, status) {
ProcessErrors(parent_scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to retrieve host: ' + host_id + '. GET returned status: ' + status
});
});
} else {
if (selected_group_id) {
// adding hosts to a group
url = GetBasePath('groups') + selected_group_id + '/';
} else {
// adding hosts to the top-level (inventory)
url = GetBasePath('inventory') + inventory_id + '/';
}
// Add mode
Rest.setUrl(url);
Rest.get()
.success(function(data) {
scope.has_inventory_sources = data.has_inventory_sources;
scope.enabled = true;
scope.variables = '---';
defaultUrl = data.related.hosts;
//scope.$emit('hostVariablesLoaded');
scope.parseType = 'yaml';
ParseTypeChange({
scope: scope,
field_id: 'host_variables',
});
})
.error(function(data, status) {
ProcessErrors(parent_scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to retrieve group: ' + selected_group_id + '. GET returned status: ' + status
});
});
}
if (scope.removeSaveCompleted) {
scope.removeSaveCompleted();
}
scope.removeSaveCompleted = scope.$on('saveCompleted', function() {
Wait('stop');
try {
$('#host-modal-dialog').dialog('close');
} catch (err) {
// ignore
}
if (group_scope && group_scope.refreshHosts) {
group_scope.refreshHosts();
}
if (parent_scope.refreshHosts) {
parent_scope.refreshHosts();
}
scope.$destroy();
$state.go('inventoryManage', {}, {
reload: true
});
});
// Save changes to the parent
var saveHost = function() {
Wait('start');
var fld, data = {};
try {
data.variables = ToJSON(scope.parseType, scope.variables, true);
for (fld in form.fields) {
data[fld] = scope[fld];
}
data.inventory = inventory_id;
Rest.setUrl(defaultUrl);
if (mode === 'edit') {
Rest.put(data)
.success(function() {
scope.$emit('saveCompleted');
})
.error(function(data, status) {
ProcessErrors(scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to update host: ' + host_id + '. PUT returned status: ' + status
});
});
} else {
Rest.post(data)
.success(function() {
scope.$emit('saveCompleted');
})
.error(function(data, status) {
ProcessErrors(scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to create host. POST returned status: ' + status
});
});
}
} catch (e) {
// ignore. ToJSON will have already alerted the user
}
};
var cancelPanel = function() {
scope.$destroy();
if (scope.codeMirror) {
scope.codeMirror.destroy();
}
$state.go('inventoryManage');
};
angular.extend(vm, {
cancelPanel: cancelPanel,
saveHost: saveHost,
mode: mode
});
}
export default ['$rootScope', '$location', '$log', '$stateParams', '$state', '$scope', 'Rest', 'Alert', 'HostForm',
'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange',
'Wait', 'Find', 'SetStatus', 'ApplyEllipsis', 'ToJSON', 'ParseVariableString',
'CreateDialog', 'TextareaResize', 'ParamPass', manageHostsDirectiveController
];
@@ -0,0 +1,25 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
import manageHostsDirectiveController from './manage-hosts.directive.controller';
export default ['templateUrl', 'ParamPass',
function(templateUrl, ParamPass) {
return {
restrict: 'EA',
scope: true,
replace: true,
templateUrl: templateUrl('inventories/manage/manage-hosts/directive/manage-hosts.directive'),
link: function(scope, element, attrs) {
},
controller: manageHostsDirectiveController,
controllerAs: 'vm',
bindToController: true
};
}
];
@@ -0,0 +1,25 @@
<div>
<div class="Form-header" ng-if="vm.mode === 'add'">
<div class="Form-title ng-binding" >Create Host</div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="vm.cancelPanel()">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
<div class="Form-exitHolder" ng-if="vm.mode === 'edit'">
<button class="Form-exit" ng-click="vm.cancelPanel()">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div id="host-panel-form"></div>
<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
<div class="ui-dialog-buttonset">
<button type="button" class="btn btn-primary Form-saveButton" id="Inventory-hostManage--okButton" ng-click="vm.saveHost()">
Save</button>
<button type="button" class="btn btn-default Form-cancelButton" id="Inventory-hostManage--cancelButton" ng-click="vm.cancelPanel()">
Cancel</button>
</div>
</div>
</div>
@@ -0,0 +1,16 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './manage-hosts.route';
import manageHostsDirective from './directive/manage-hosts.directive';
export default
angular.module('manage-hosts', [])
.directive('manageHosts', manageHostsDirective)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route.edit);
$stateExtender.addState(route.add);
}]);
@@ -0,0 +1,5 @@
<div class="tab-pane" id="Inventory-hostManage">
<div ng-cloak id="Inventory-hostManage--panel" class="Panel">
<manage-hosts></manage-hosts>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More