Merge branch 'release_3.1.0' into multi_worker_callbacks

This commit is contained in:
Matthew Jones 2017-01-18 20:39:15 -05:00 committed by GitHub
commit 7ca496b6ef
178 changed files with 25728 additions and 761 deletions

1
.gitignore vendored
View File

@ -106,7 +106,6 @@ reports
*.log.[0-9]
*.results
local/
*.mo
# AWX python libs populated by requirements.txt
awx/lib/.deps_built

View File

@ -428,7 +428,7 @@ celeryd:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
fi; \
$(PYTHON) manage.py celeryd -l DEBUG -B --autoreload --autoscale=20,3 --schedule=$(CELERY_SCHEDULE_FILE) -Q projects,jobs,default,scheduler,broadcast_all,$(COMPOSE_HOST)
$(PYTHON) manage.py celeryd -l DEBUG -B --autoreload --autoscale=20,3 --schedule=$(CELERY_SCHEDULE_FILE) -Q projects,jobs,default,scheduler,broadcast_all,$(COMPOSE_HOST) -n celery@$(COMPOSE_HOST)
#$(PYTHON) manage.py celery multi show projects jobs default -l DEBUG -Q:projects projects -Q:jobs jobs -Q:default default -c:projects 1 -c:jobs 3 -c:default 3 -Ofair -B --schedule=$(CELERY_SCHEDULE_FILE)
# Run to start the zeromq callback receiver
@ -559,9 +559,12 @@ messages:
fi; \
$(PYTHON) manage.py makemessages -l $(LANG) --keep-pot
# generate l10n .json .mo
languages: $(UI_DEPS_FLAG_FILE) check-po
# generate l10n .json
ui-languages: $(UI_DEPS_FLAG_FILE) check-po
$(NPM_BIN) --prefix awx/ui run languages
# generate l10n .mo
api-languages:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
fi; \
@ -592,8 +595,7 @@ ui-devel: $(UI_DEPS_FLAG_FILE)
ui-release: $(UI_RELEASE_FLAG_FILE)
# todo: include languages target when .po deliverables are added to source control
$(UI_RELEASE_FLAG_FILE): $(UI_DEPS_FLAG_FILE)
$(UI_RELEASE_FLAG_FILE): ui-languages $(UI_DEPS_FLAG_FILE)
$(NPM_BIN) --prefix awx/ui run build-release
touch $(UI_RELEASE_FLAG_FILE)

View File

@ -13,7 +13,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions
from rest_framework import metadata
from rest_framework import serializers
from rest_framework.relations import RelatedField
from rest_framework.relations import RelatedField, ManyRelatedField
from rest_framework.request import clone_request
# Ansible Tower
@ -75,7 +75,7 @@ class Metadata(metadata.SimpleMetadata):
elif getattr(field, 'fields', None):
field_info['children'] = self.get_serializer_info(field)
if hasattr(field, 'choices') and not isinstance(field, RelatedField):
if not isinstance(field, (RelatedField, ManyRelatedField)) and hasattr(field, 'choices'):
field_info['choices'] = [(choice_value, choice_name) for choice_value, choice_name in field.choices.items()]
# Indicate if a field is write-only.

View File

@ -16,6 +16,7 @@ class Pagination(pagination.PageNumberPagination):
if not self.page.has_next():
return None
url = self.request and self.request.get_full_path() or ''
url = url.encode('utf-8')
page_number = self.page.next_page_number()
return replace_query_param(url, self.page_query_param, page_number)
@ -23,5 +24,6 @@ class Pagination(pagination.PageNumberPagination):
if not self.page.has_previous():
return None
url = self.request and self.request.get_full_path() or ''
url = url.encode('utf-8')
page_number = self.page.previous_page_number()
return replace_query_param(url, self.page_query_param, page_number)

View File

@ -251,6 +251,7 @@ class BaseSerializer(serializers.ModelSerializer):
'inventory_update': _('Inventory Sync'),
'system_job': _('Management Job'),
'workflow_job': _('Workflow Job'),
'workflow_job_template': _('Workflow Template'),
}
choices = []
for t in self.get_types():
@ -1619,7 +1620,8 @@ class ResourceAccessListElementSerializer(UserSerializer):
'name': role.name,
'description': role.description,
'team_id': team_role.object_id,
'team_name': team_role.content_object.name
'team_name': team_role.content_object.name,
'team_organization_name': team_role.content_object.organization.name,
}
if role.content_type is not None:
role_dict['resource_name'] = role.content_object.name
@ -2369,7 +2371,7 @@ class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer):
if view and view.request:
request_method = view.request.method
if request_method in ['PATCH']:
obj = view.get_object()
obj = self.instance
char_prompts = copy.copy(obj.char_prompts)
char_prompts.update(self.extract_char_prompts(data))
else:
@ -2708,18 +2710,15 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
variables_needed_to_start = serializers.ReadOnlyField()
survey_enabled = serializers.SerializerMethodField()
extra_vars = VerbatimField(required=False, write_only=True)
warnings = serializers.SerializerMethodField()
workflow_job_template_data = serializers.SerializerMethodField()
class Meta:
model = WorkflowJobTemplate
fields = ('can_start_without_user_input', 'extra_vars', 'warnings',
fields = ('can_start_without_user_input', 'extra_vars',
'survey_enabled', 'variables_needed_to_start',
'node_templates_missing', 'node_prompts_rejected',
'workflow_job_template_data')
def get_warnings(self, obj):
return obj.get_warnings()
def get_survey_enabled(self, obj):
if obj:
return obj.survey_enabled and 'spec' in obj.survey_spec

View File

@ -13,7 +13,7 @@ Which will act on data older than 30 days.
For `cleanup_facts`:
`{"older_than": "4w", `granularity`: "3d"}`
`{"older_than": "4w", "granularity": "3d"}`
Which will reduce the granularity of scan data to one scan per 3 days when the data is older than 4w.

View File

@ -851,6 +851,7 @@ class OrganizationNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView)
serializer_class = NotificationTemplateSerializer
parent_model = Organization
relationship = 'notification_templates_any'
new_in_300 = True
class OrganizationNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView):
@ -859,6 +860,7 @@ class OrganizationNotificationTemplatesErrorList(SubListCreateAttachDetachAPIVie
serializer_class = NotificationTemplateSerializer
parent_model = Organization
relationship = 'notification_templates_error'
new_in_300 = True
class OrganizationNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView):
@ -867,6 +869,7 @@ class OrganizationNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIV
serializer_class = NotificationTemplateSerializer
parent_model = Organization
relationship = 'notification_templates_success'
new_in_300 = True
class OrganizationAccessList(ResourceAccessList):
@ -921,6 +924,7 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
metadata_class = RoleMetadata
parent_model = Team
relationship='member_role.children'
new_in_300 = True
def get_queryset(self):
team = get_object_or_404(Team, pk=self.kwargs['pk'])
@ -1103,6 +1107,7 @@ class ProjectNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
serializer_class = NotificationTemplateSerializer
parent_model = Project
relationship = 'notification_templates_any'
new_in_300 = True
class ProjectNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView):
@ -1111,6 +1116,7 @@ class ProjectNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView):
serializer_class = NotificationTemplateSerializer
parent_model = Project
relationship = 'notification_templates_error'
new_in_300 = True
class ProjectNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView):
@ -1119,6 +1125,7 @@ class ProjectNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView):
serializer_class = NotificationTemplateSerializer
parent_model = Project
relationship = 'notification_templates_success'
new_in_300 = True
class ProjectUpdatesList(SubListAPIView):
@ -1156,6 +1163,7 @@ class ProjectUpdateList(ListAPIView):
model = ProjectUpdate
serializer_class = ProjectUpdateListSerializer
new_in_13 = True
class ProjectUpdateDetail(RetrieveDestroyAPIView):
@ -1196,6 +1204,7 @@ class ProjectUpdateNotificationsList(SubListAPIView):
serializer_class = NotificationSerializer
parent_model = ProjectUpdate
relationship = 'notifications'
new_in_300 = True
class ProjectAccessList(ResourceAccessList):
@ -1271,6 +1280,7 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
parent_model = User
relationship='roles'
permission_classes = (IsAuthenticated,)
new_in_300 = True
def get_queryset(self):
u = get_object_or_404(User, pk=self.kwargs['pk'])
@ -1543,12 +1553,14 @@ class InventoryScriptList(ListCreateAPIView):
model = CustomInventoryScript
serializer_class = CustomInventoryScriptSerializer
new_in_210 = True
class InventoryScriptDetail(RetrieveUpdateDestroyAPIView):
model = CustomInventoryScript
serializer_class = CustomInventoryScriptSerializer
new_in_210 = True
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
@ -2170,6 +2182,7 @@ class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIVi
serializer_class = NotificationTemplateSerializer
parent_model = InventorySource
relationship = 'notification_templates_any'
new_in_300 = True
def post(self, request, *args, **kwargs):
parent = self.get_parent_object()
@ -2281,6 +2294,7 @@ class InventoryUpdateNotificationsList(SubListAPIView):
serializer_class = NotificationSerializer
parent_model = InventoryUpdate
relationship = 'notifications'
new_in_300 = True
class JobTemplateList(ListCreateAPIView):
@ -2316,7 +2330,10 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
always_allow_superuser = False
def update_raw_data(self, data):
obj = self.get_object()
try:
obj = self.get_object()
except PermissionDenied:
return data
extra_vars = data.pop('extra_vars', None) or {}
if obj:
for p in obj.passwords_needed_to_start:
@ -2396,6 +2413,7 @@ class JobTemplateSurveySpec(GenericAPIView):
model = JobTemplate
parent_model = JobTemplate
serializer_class = EmptySerializer
new_in_210 = True
def get(self, request, *args, **kwargs):
obj = self.get_object()
@ -2462,6 +2480,7 @@ class WorkflowJobTemplateSurveySpec(WorkflowsEnforcementMixin, JobTemplateSurvey
model = WorkflowJobTemplate
parent_model = WorkflowJobTemplate
new_in_310 = True
class JobTemplateActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
@ -2479,6 +2498,7 @@ class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
serializer_class = NotificationTemplateSerializer
parent_model = JobTemplate
relationship = 'notification_templates_any'
new_in_300 = True
class JobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView):
@ -2487,6 +2507,7 @@ class JobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView
serializer_class = NotificationTemplateSerializer
parent_model = JobTemplate
relationship = 'notification_templates_error'
new_in_300 = True
class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView):
@ -2495,6 +2516,7 @@ class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIVi
serializer_class = NotificationTemplateSerializer
parent_model = JobTemplate
relationship = 'notification_templates_success'
new_in_300 = True
class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
@ -2884,10 +2906,16 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, GenericAPIView):
def get(self, request, *args, **kwargs):
obj = self.get_object()
data = {}
copy_TF, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
data['can_copy'] = copy_TF
data['warnings'] = messages
can_copy, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
data = OrderedDict([
('can_copy', can_copy), ('can_copy_without_user_input', can_copy),
('templates_unable_to_copy', [] if can_copy else ['all']),
('credentials_unable_to_copy', [] if can_copy else ['all']),
('inventories_unable_to_copy', [] if can_copy else ['all'])
])
if messages and can_copy:
data['can_copy_without_user_input'] = False
data.update(messages)
return Response(data)
def post(self, request, *args, **kwargs):
@ -2918,7 +2946,10 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
always_allow_superuser = False
def update_raw_data(self, data):
obj = self.get_object()
try:
obj = self.get_object()
except PermissionDenied:
return data
extra_vars = data.pop('extra_vars', None) or {}
if obj:
for v in obj.variables_needed_to_start:
@ -2953,6 +2984,14 @@ class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView):
model = WorkflowJob
serializer_class = EmptySerializer
is_job_start = True
new_in_310 = True
def check_object_permissions(self, request, obj):
if request.method == 'POST' and obj:
relaunch_perm, messages = request.user.can_access_with_errors(self.model, 'start', obj)
if not relaunch_perm and 'workflow_job_template' in messages:
self.permission_denied(request, message=messages['workflow_job_template'])
return super(WorkflowJobRelaunch, self).check_object_permissions(request, obj)
def get(self, request, *args, **kwargs):
return Response({})
@ -3129,6 +3168,7 @@ class SystemJobTemplateList(ListAPIView):
model = SystemJobTemplate
serializer_class = SystemJobTemplateSerializer
new_in_210 = True
def get(self, request, *args, **kwargs):
if not request.user.is_superuser and not request.user.is_system_auditor:
@ -3140,6 +3180,7 @@ class SystemJobTemplateDetail(RetrieveAPIView):
model = SystemJobTemplate
serializer_class = SystemJobTemplateSerializer
new_in_210 = True
class SystemJobTemplateLaunch(GenericAPIView):
@ -3147,6 +3188,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
model = SystemJobTemplate
serializer_class = EmptySerializer
is_job_start = True
new_in_210 = True
def get(self, request, *args, **kwargs):
return Response({})
@ -3154,8 +3196,8 @@ class SystemJobTemplateLaunch(GenericAPIView):
def post(self, request, *args, **kwargs):
obj = self.get_object()
new_job = obj.create_unified_job(**request.data)
new_job.signal_start(**request.data)
new_job = obj.create_unified_job(extra_vars=request.data.get('extra_vars', {}))
new_job.signal_start()
data = dict(system_job=new_job.id)
return Response(data, status=status.HTTP_201_CREATED)
@ -3169,6 +3211,7 @@ class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
parent_model = SystemJobTemplate
relationship = 'schedules'
parent_key = 'unified_job_template'
new_in_210 = True
class SystemJobTemplateJobsList(SubListAPIView):
@ -3178,6 +3221,7 @@ class SystemJobTemplateJobsList(SubListAPIView):
parent_model = SystemJobTemplate
relationship = 'jobs'
parent_key = 'system_job_template'
new_in_210 = True
class SystemJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
@ -3186,6 +3230,7 @@ class SystemJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPI
serializer_class = NotificationTemplateSerializer
parent_model = SystemJobTemplate
relationship = 'notification_templates_any'
new_in_300 = True
class SystemJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView):
@ -3194,6 +3239,7 @@ class SystemJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachA
serializer_class = NotificationTemplateSerializer
parent_model = SystemJobTemplate
relationship = 'notification_templates_error'
new_in_300 = True
class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView):
@ -3202,6 +3248,7 @@ class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetac
serializer_class = NotificationTemplateSerializer
parent_model = SystemJobTemplate
relationship = 'notification_templates_success'
new_in_300 = True
class JobList(ListCreateAPIView):
@ -3239,10 +3286,12 @@ class JobLabelList(SubListAPIView):
parent_model = Job
relationship = 'labels'
parent_key = 'job'
new_in_300 = True
class WorkflowJobLabelList(WorkflowsEnforcementMixin, JobLabelList):
parent_model = WorkflowJob
new_in_310 = True
class JobActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
@ -3337,6 +3386,7 @@ class JobNotificationsList(SubListAPIView):
serializer_class = NotificationSerializer
parent_model = Job
relationship = 'notifications'
new_in_300 = True
class BaseJobHostSummariesList(SubListAPIView):
@ -3847,12 +3897,14 @@ class AdHocCommandNotificationsList(SubListAPIView):
serializer_class = NotificationSerializer
parent_model = AdHocCommand
relationship = 'notifications'
new_in_300 = True
class SystemJobList(ListCreateAPIView):
model = SystemJob
serializer_class = SystemJobListSerializer
new_in_210 = True
def get(self, request, *args, **kwargs):
if not request.user.is_superuser and not request.user.is_system_auditor:
@ -3864,6 +3916,7 @@ class SystemJobDetail(RetrieveDestroyAPIView):
model = SystemJob
serializer_class = SystemJobSerializer
new_in_210 = True
class SystemJobCancel(RetrieveAPIView):
@ -3871,6 +3924,7 @@ class SystemJobCancel(RetrieveAPIView):
model = SystemJob
serializer_class = SystemJobCancelSerializer
is_job_cancel = True
new_in_210 = True
def post(self, request, *args, **kwargs):
obj = self.get_object()
@ -3887,6 +3941,7 @@ class SystemJobNotificationsList(SubListAPIView):
serializer_class = NotificationSerializer
parent_model = SystemJob
relationship = 'notifications'
new_in_300 = True
class UnifiedJobTemplateList(ListAPIView):
@ -4009,6 +4064,7 @@ class UnifiedJobStdout(RetrieveAPIView):
class ProjectUpdateStdout(UnifiedJobStdout):
model = ProjectUpdate
new_in_13 = True
class InventoryUpdateStdout(UnifiedJobStdout):
@ -4079,6 +4135,7 @@ class NotificationTemplateNotificationList(SubListAPIView):
parent_model = NotificationTemplate
relationship = 'notifications'
parent_key = 'notification_template'
new_in_300 = True
class NotificationList(ListAPIView):

View File

@ -16,7 +16,10 @@ class ConfConfig(AppConfig):
from .settings import SettingsWrapper
SettingsWrapper.initialize()
if settings.LOG_AGGREGATOR_ENABLED:
LOGGING = settings.LOGGING
LOGGING['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.HTTPSHandler'
configure_logging(settings.LOGGING_CONFIG, LOGGING)
LOGGING_DICT = settings.LOGGING
LOGGING_DICT['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.HTTPSHandler'
if 'awx' in settings.LOG_AGGREGATOR_LOGGERS:
if 'http_receiver' not in LOGGING_DICT['loggers']['awx']['handlers']:
LOGGING_DICT['loggers']['awx']['handlers'] += ['http_receiver']
configure_logging(settings.LOGGING_CONFIG, LOGGING_DICT)
# checks.register(SettingsWrapper._check_settings)

View File

@ -13,7 +13,7 @@ import awx.main.signals
from awx.conf import settings_registry
from awx.conf.models import Setting
from awx.conf.serializers import SettingSerializer
from awx.main.tasks import clear_cache_keys
from awx.main.tasks import process_cache_changes
logger = logging.getLogger('awx.conf.signals')
@ -32,7 +32,7 @@ def handle_setting_change(key, for_delete=False):
cache_keys = set([Setting.get_cache_key(k) for k in setting_keys])
logger.debug('sending signals to delete cache keys(%r)', cache_keys)
cache.delete_many(cache_keys)
clear_cache_keys.delay(list(cache_keys))
process_cache_changes.delay(list(cache_keys))
# Send setting_changed signal with new value for each setting.
for setting_key in setting_keys:

View File

@ -181,7 +181,7 @@ class EventContext(object):
event_data['res'] = {}
event_dict = dict(event=event, event_data=event_data)
for key in event_data.keys():
if key in ('job_id', 'ad_hoc_command_id', 'uuid', 'parent_uuid', 'created', 'artifact_data'):
if key in ('job_id', 'ad_hoc_command_id', 'uuid', 'parent_uuid', 'created',):
event_dict[key] = event_data.pop(key)
elif key in ('verbosity', 'pid'):
event_dict[key] = event_data[key]

View File

@ -19,8 +19,6 @@ from __future__ import (absolute_import, division, print_function)
# Python
import contextlib
import copy
import re
import sys
import uuid
@ -77,45 +75,11 @@ class BaseCallbackModule(CallbackBase):
super(BaseCallbackModule, self).__init__()
self.task_uuids = set()
def censor_result(self, res, no_log=False):
if not isinstance(res, dict):
if no_log:
return "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
return res
if res.get('_ansible_no_log', no_log):
new_res = {}
for k in self.CENSOR_FIELD_WHITELIST:
if k in res:
new_res[k] = res[k]
if k == 'cmd' and k in res:
if isinstance(res['cmd'], list):
res['cmd'] = ' '.join(res['cmd'])
if re.search(r'\s', res['cmd']):
new_res['cmd'] = re.sub(r'^(([^\s\\]|\\\s)+).*$',
r'\1 <censored>',
res['cmd'])
new_res['censored'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
res = new_res
if 'results' in res:
if isinstance(res['results'], list):
for i in xrange(len(res['results'])):
res['results'][i] = self.censor_result(res['results'][i], res.get('_ansible_no_log', no_log))
elif res.get('_ansible_no_log', False):
res['results'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
return res
@contextlib.contextmanager
def capture_event_data(self, event, **event_data):
event_data.setdefault('uuid', str(uuid.uuid4()))
if 'res' in event_data:
event_data['res'] = self.censor_result(copy.copy(event_data['res']))
res = event_data.get('res', None)
if res and isinstance(res, dict):
if 'artifact_data' in res:
event_data['artifact_data'] = res['artifact_data']
if event not in self.EVENTS_WITHOUT_TASK:
task = event_data.pop('task', None)
else:
@ -262,7 +226,7 @@ class BaseCallbackModule(CallbackBase):
if task_uuid in self.task_uuids:
# FIXME: When this task UUID repeats, it means the play is using the
# free strategy, so different hosts may be running different tasks
# within a play.
# within a play.
return
self.task_uuids.add(task_uuid)
self.set_task(task)
@ -319,6 +283,9 @@ class BaseCallbackModule(CallbackBase):
with self.capture_event_data('playbook_on_notify', **event_data):
super(BaseCallbackModule, self).v2_playbook_on_notify(result, handler)
'''
ansible_stats is, retoractively, added in 2.2
'''
def v2_playbook_on_stats(self, stats):
self.clear_play()
# FIXME: Add count of plays/tasks.
@ -329,7 +296,9 @@ class BaseCallbackModule(CallbackBase):
ok=stats.ok,
processed=stats.processed,
skipped=stats.skipped,
artifact_data=stats.custom.get('_run', {}) if hasattr(stats, 'custom') else {}
)
with self.capture_event_data('playbook_on_stats', **event_data):
super(BaseCallbackModule, self).v2_playbook_on_stats(stats)

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -353,7 +353,7 @@ class BaseAccess(object):
# Shortcuts in certain cases by deferring to earlier property
if display_method == 'schedule':
user_capabilities['schedule'] = user_capabilities['edit']
user_capabilities['schedule'] = user_capabilities['start']
continue
elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)):
user_capabilities['delete'] = user_capabilities['edit']
@ -363,27 +363,30 @@ class BaseAccess(object):
continue
# Compute permission
data = {}
access_method = getattr(self, "can_%s" % method)
if method in ['change']: # 3 args
user_capabilities[display_method] = access_method(obj, data)
elif method in ['delete', 'run_ad_hoc_commands', 'copy']:
user_capabilities[display_method] = access_method(obj)
elif method in ['start']:
user_capabilities[display_method] = access_method(obj, validate_license=False)
elif method in ['add']: # 2 args with data
user_capabilities[display_method] = access_method(data)
elif method in ['attach', 'unattach']: # parent/sub-object call
if type(parent_obj) == Team:
relationship = 'parents'
parent_obj = parent_obj.member_role
else:
relationship = 'members'
user_capabilities[display_method] = access_method(
obj, parent_obj, relationship, skip_sub_obj_read_check=True, data=data)
user_capabilities[display_method] = self.get_method_capability(method, obj, parent_obj)
return user_capabilities
def get_method_capability(self, method, obj, parent_obj):
if method in ['change']: # 3 args
return self.can_change(obj, {})
elif method in ['delete', 'run_ad_hoc_commands', 'copy']:
access_method = getattr(self, "can_%s" % method)
return access_method(obj)
elif method in ['start']:
return self.can_start(obj, validate_license=False)
elif method in ['add']: # 2 args with data
return self.can_add({})
elif method in ['attach', 'unattach']: # parent/sub-object call
access_method = getattr(self, "can_%s" % method)
if type(parent_obj) == Team:
relationship = 'parents'
parent_obj = parent_obj.member_role
else:
relationship = 'members'
return access_method(obj, parent_obj, relationship, skip_sub_obj_read_check=True, data={})
return False
class UserAccess(BaseAccess):
'''
@ -982,8 +985,6 @@ class ProjectUpdateAccess(BaseAccess):
@check_superuser
def can_cancel(self, obj):
if not obj.can_cancel:
return False
if self.user == obj.created_by:
return True
# Project updates cascade delete with project, admin role descends from org admin
@ -1040,7 +1041,7 @@ class JobTemplateAccess(BaseAccess):
Project.accessible_objects(self.user, 'use_role').exists() or
Inventory.accessible_objects(self.user, 'use_role').exists())
# if reference_obj is provided, determine if it can be coppied
# if reference_obj is provided, determine if it can be copied
reference_obj = data.get('reference_obj', None)
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
@ -1392,7 +1393,8 @@ class WorkflowJobTemplateNodeAccess(BaseAccess):
qs = self.model.objects.filter(
workflow_job_template__in=WorkflowJobTemplate.accessible_objects(
self.user, 'read_role'))
qs = qs.prefetch_related('success_nodes', 'failure_nodes', 'always_nodes')
qs = qs.prefetch_related('success_nodes', 'failure_nodes', 'always_nodes',
'unified_job_template')
return qs
def can_use_prompted_resources(self, data):
@ -1478,8 +1480,14 @@ class WorkflowJobNodeAccess(BaseAccess):
qs = qs.prefetch_related('success_nodes', 'failure_nodes', 'always_nodes')
return qs
@check_superuser
def can_add(self, data):
return False
if data is None: # Hide direct creation in API browser
return False
return (
self.check_related('unified_job_template', UnifiedJobTemplate, data, role_field='execute_role') and
self.check_related('credential', Credential, data, role_field='use_role') and
self.check_related('inventory', Inventory, data, role_field='use_role'))
def can_change(self, obj, data):
return False
@ -1528,22 +1536,28 @@ class WorkflowJobTemplateAccess(BaseAccess):
def can_copy(self, obj):
if self.save_messages:
wfjt_errors = {}
missing_ujt = []
missing_credentials = []
missing_inventories = []
qs = obj.workflow_job_template_nodes
qs.select_related('unified_job_template', 'inventory', 'credential')
for node in qs.all():
node_errors = {}
if node.inventory and self.user not in node.inventory.use_role:
node_errors['inventory'] = 'Prompted inventory %s can not be coppied.' % node.inventory.name
missing_inventories.append(node.inventory.name)
if node.credential and self.user not in node.credential.use_role:
node_errors['credential'] = 'Prompted credential %s can not be coppied.' % node.credential.name
missing_credentials.append(node.credential.name)
ujt = node.unified_job_template
if ujt and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False):
node_errors['unified_job_template'] = (
'Prompted %s %s can not be coppied.' % (ujt._meta.verbose_name_raw, ujt.name))
missing_ujt.append(ujt.name)
if node_errors:
wfjt_errors[node.id] = node_errors
self.messages.update(wfjt_errors)
if missing_ujt:
self.messages['templates_unable_to_copy'] = missing_ujt
if missing_credentials:
self.messages['credentials_unable_to_copy'] = missing_credentials
if missing_inventories:
self.messages['inventories_unable_to_copy'] = missing_inventories
return self.check_related('organization', Organization, {'reference_obj': obj}, mandatory=True)
@ -1611,11 +1625,19 @@ class WorkflowJobAccess(BaseAccess):
def can_change(self, obj, data):
return False
@check_superuser
def can_delete(self, obj):
if obj.workflow_job_template is None:
# only superusers can delete orphaned workflow jobs
return self.user.is_superuser
return self.user in obj.workflow_job_template.admin_role
return (obj.workflow_job_template and
obj.workflow_job_template.organization and
self.user in obj.workflow_job_template.organization.admin_role)
def get_method_capability(self, method, obj, parent_obj):
if method == 'start':
# Return simplistic permission, will perform detailed check on POST
if not obj.workflow_job_template:
return self.user.is_superuser
return self.user in obj.workflow_job_template.execute_role
return super(WorkflowJobAccess, self).get_method_capability(method, obj, parent_obj)
def can_start(self, obj, validate_license=True):
if validate_license:
@ -1624,7 +1646,29 @@ class WorkflowJobAccess(BaseAccess):
if self.user.is_superuser:
return True
return (obj.workflow_job_template and self.user in obj.workflow_job_template.execute_role)
wfjt = obj.workflow_job_template
# only superusers can relaunch orphans
if not wfjt:
return False
# execute permission to WFJT is mandatory for any relaunch
if self.user not in wfjt.execute_role:
return False
# user's WFJT access doesn't guarentee permission to launch, introspect nodes
return self.can_recreate(obj)
def can_recreate(self, obj):
node_qs = obj.workflow_job_nodes.all().prefetch_related('inventory', 'credential', 'unified_job_template')
node_access = WorkflowJobNodeAccess(user=self.user)
wj_add_perm = True
for node in node_qs:
if not node_access.can_add({'reference_obj': node}):
wj_add_perm = False
if not wj_add_perm and self.save_messages:
self.messages['workflow_job_template'] = _('You do not have permission to the workflow job '
'resources required for relaunch.')
return wj_add_perm
def can_cancel(self, obj):
if not obj.can_cancel:
@ -1912,11 +1956,17 @@ class ScheduleAccess(BaseAccess):
@check_superuser
def can_add(self, data):
return self.check_related('unified_job_template', UnifiedJobTemplate, data, mandatory=True)
return self.check_related('unified_job_template', UnifiedJobTemplate, data, role_field='execute_role', mandatory=True)
@check_superuser
def can_change(self, obj, data):
return self.check_related('unified_job_template', UnifiedJobTemplate, data, obj=obj, mandatory=True)
if self.check_related('unified_job_template', UnifiedJobTemplate, data, obj=obj, mandatory=True):
return True
# Users with execute role can modify the schedules they created
return (
obj.created_by == self.user and
self.check_related('unified_job_template', UnifiedJobTemplate, data, obj=obj, role_field='execute_role', mandatory=True))
def can_delete(self, obj):
return self.can_change(obj, {})

View File

@ -96,12 +96,12 @@ class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--older_than',
dest='older_than',
default=None,
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
default='30d',
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 30d.'),
make_option('--granularity',
dest='granularity',
default=None,
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
default='1w',
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 1w.'),
make_option('--module',
dest='module',
default=None,

View File

@ -12,7 +12,7 @@ from django.db import transaction
from django.utils.timezone import now
# AWX
from awx.main.models import Job, AdHocCommand, ProjectUpdate, InventoryUpdate, SystemJob
from awx.main.models import Job, AdHocCommand, ProjectUpdate, InventoryUpdate, SystemJob, WorkflowJob
class Command(NoArgsCommand):
@ -30,19 +30,22 @@ class Command(NoArgsCommand):
'be removed)'),
make_option('--jobs', dest='only_jobs', action='store_true',
default=False,
help='Only remove jobs'),
help='Remove jobs'),
make_option('--ad-hoc-commands', dest='only_ad_hoc_commands',
action='store_true', default=False,
help='Only remove ad hoc commands'),
help='Remove ad hoc commands'),
make_option('--project-updates', dest='only_project_updates',
action='store_true', default=False,
help='Only remove project updates'),
help='Remove project updates'),
make_option('--inventory-updates', dest='only_inventory_updates',
action='store_true', default=False,
help='Only remove inventory updates'),
help='Remove inventory updates'),
make_option('--management-jobs', default=False,
action='store_true', dest='only_management_jobs',
help='Only remove management jobs')
help='Remove management jobs'),
make_option('--workflow-jobs', default=False,
action='store_true', dest='only_workflow_jobs',
help='Remove workflow jobs')
)
def cleanup_jobs(self):
@ -169,6 +172,28 @@ class Command(NoArgsCommand):
self.logger.addHandler(handler)
self.logger.propagate = False
def cleanup_workflow_jobs(self):
skipped, deleted = 0, 0
for workflow_job in WorkflowJob.objects.all():
workflow_job_display = '"{}" (started {}, {} nodes)'.format(
unicode(workflow_job), unicode(workflow_job.created),
workflow_job.workflow_nodes.count())
if workflow_job.status in ('pending', 'waiting', 'running'):
action_text = 'would skip' if self.dry_run else 'skipping'
self.logger.debug('%s %s job %s', action_text, workflow_job.status, workflow_job_display)
skipped += 1
elif workflow_job.created >= self.cutoff:
action_text = 'would skip' if self.dry_run else 'skipping'
self.logger.debug('%s %s', action_text, workflow_job_display)
skipped += 1
else:
action_text = 'would delete' if self.dry_run else 'deleting'
self.logger.info('%s %s', action_text, workflow_job_display)
if not self.dry_run:
workflow_job.delete()
deleted += 1
return skipped, deleted
@transaction.atomic
def handle_noargs(self, **options):
self.verbosity = int(options.get('verbosity', 1))
@ -179,7 +204,7 @@ class Command(NoArgsCommand):
self.cutoff = now() - datetime.timedelta(days=self.days)
except OverflowError:
raise CommandError('--days specified is too large. Try something less than 99999 (about 270 years).')
model_names = ('jobs', 'ad_hoc_commands', 'project_updates', 'inventory_updates', 'management_jobs')
model_names = ('jobs', 'ad_hoc_commands', 'project_updates', 'inventory_updates', 'management_jobs', 'workflow_jobs')
models_to_cleanup = set()
for m in model_names:
if options.get('only_%s' % m, False):

View File

@ -64,7 +64,7 @@ class MemObject(object):
all_vars = {}
files_found = 0
for suffix in ('', '.yml', '.yaml', '.json'):
path = ''.join([base_path, suffix])
path = ''.join([base_path, suffix]).encode("utf-8")
if not os.path.exists(path):
continue
if not os.path.isfile(path):
@ -462,7 +462,7 @@ class ExecutableJsonLoader(BaseLoader):
# to set their variables
for k,v in self.all_group.all_hosts.iteritems():
if 'hostvars' not in _meta:
data = self.command_to_json([self.source, '--host', k])
data = self.command_to_json([self.source, '--host', k.encode("utf-8")])
else:
data = _meta['hostvars'].get(k, {})
if isinstance(data, dict):

View File

@ -20,7 +20,7 @@ from django.core.urlresolvers import reverse
# AWX
from awx.main.models.base import * # noqa
from awx.main.models.unified_jobs import * # noqa
from awx.main.models.notifications import JobNotificationMixin
from awx.main.models.notifications import JobNotificationMixin, NotificationTemplate
from awx.main.fields import JSONField
logger = logging.getLogger('awx.main.models.ad_hoc_commands')
@ -157,18 +157,20 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
@property
def notification_templates(self):
all_inventory_sources = set()
all_orgs = set()
for h in self.hosts.all():
for invsrc in h.inventory_sources.all():
all_inventory_sources.add(invsrc)
all_orgs.add(h.inventory.organization)
active_templates = dict(error=set(),
success=set(),
any=set())
for invsrc in all_inventory_sources:
notifications_dict = invsrc.notification_templates
for notification_type in active_templates.keys():
for templ in notifications_dict[notification_type]:
active_templates[notification_type].add(templ)
base_notification_templates = NotificationTemplate.objects
for org in all_orgs:
for templ in base_notification_templates.filter(organization_notification_templates_for_errors=org):
active_templates['error'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_success=org):
active_templates['success'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_any=org):
active_templates['any'].add(templ)
active_templates['error'] = list(active_templates['error'])
active_templates['any'] = list(active_templates['any'])
active_templates['success'] = list(active_templates['success'])

View File

@ -1002,9 +1002,8 @@ class InventorySourceOptions(BaseModel):
if r not in valid_regions and r not in invalid_regions:
invalid_regions.append(r)
if invalid_regions:
raise ValidationError(_('Invalid %(source)s region%(plural)s: %(region)s') % {
'source': self.source, 'plural': '' if len(invalid_regions) == 1 else 's',
'region': ', '.join(invalid_regions)})
raise ValidationError(_('Invalid %(source)s region: %(region)s') % {
'source': self.source, 'region': ', '.join(invalid_regions)})
return ','.join(regions)
source_vars_dict = VarsDictProperty('source_vars')
@ -1028,9 +1027,8 @@ class InventorySourceOptions(BaseModel):
if instance_filter_name not in self.INSTANCE_FILTER_NAMES:
invalid_filters.append(instance_filter)
if invalid_filters:
raise ValidationError(_('Invalid filter expression%(plural)s: %(filter)s') %
{'plural': '' if len(invalid_filters) == 1 else 's',
'filter': ', '.join(invalid_filters)})
raise ValidationError(_('Invalid filter expression: %(filter)s') %
{'filter': ', '.join(invalid_filters)})
return instance_filters
def clean_group_by(self):
@ -1047,9 +1045,8 @@ class InventorySourceOptions(BaseModel):
if c not in valid_choices and c not in invalid_choices:
invalid_choices.append(c)
if invalid_choices:
raise ValidationError(_('Invalid group by choice%(plural)s: %(choice)s') %
{'plural': '' if len(invalid_choices) == 1 else 's',
'choice': ', '.join(invalid_choices)})
raise ValidationError(_('Invalid group by choice: %(choice)s') %
{'choice': ', '.join(invalid_choices)})
return ','.join(choices)

View File

@ -606,6 +606,11 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin):
evars.update(extra_vars)
self.update_fields(extra_vars=json.dumps(evars))
def _resources_sufficient_for_launch(self):
if self.job_type == PERM_INVENTORY_SCAN:
return self.inventory_id is not None
return not (self.inventory_id is None or self.project_id is None)
def display_artifacts(self):
'''
Hides artifacts if they are marked as no_log type artifacts.
@ -1175,7 +1180,6 @@ class JobEvent(CreatedModifiedModel):
# Save UUID and parent UUID for determining parent-child relationship.
job_event_uuid = kwargs.get('uuid', None)
parent_event_uuid = kwargs.get('parent_uuid', None)
artifact_dict = kwargs.get('artifact_data', None)
# Sanity check: Don't honor keys that we don't recognize.
valid_keys = {'job_id', 'event', 'event_data', 'playbook', 'play',
@ -1185,6 +1189,11 @@ class JobEvent(CreatedModifiedModel):
if key not in valid_keys:
kwargs.pop(key)
event_data = kwargs.get('event_data', None)
artifact_dict = None
if event_data:
artifact_dict = event_data.pop('artifact_data', None)
# Try to find a parent event based on UUID.
if parent_event_uuid:
cache_key = '{}_{}'.format(kwargs['job_id'], parent_event_uuid)
@ -1208,12 +1217,21 @@ class JobEvent(CreatedModifiedModel):
# Save artifact data to parent job (if provided).
if artifact_dict:
event_data = kwargs.get('event_data', None)
if event_data and isinstance(event_data, dict):
res = event_data.get('res', None)
if res and isinstance(res, dict):
if res.get('_ansible_no_log', False):
artifact_dict['_ansible_no_log'] = True
# Note: Core has not added support for marking artifacts as
# sensitive yet. Going forward, core will not use
# _ansible_no_log to denote sensitive set_stats calls.
# Instead, they plan to add a flag outside of the traditional
# no_log mechanism. no_log will not work for this feature,
# in core, because sensitive data is scrubbed before sending
# data to the callback. The playbook_on_stats is the callback
# in which the set_stats data is used.
# Again, the sensitive artifact feature has not yet landed in
# core. The below is how we mark artifacts payload as
# senstive
# artifact_dict['_ansible_no_log'] = True
#
parent_job = Job.objects.filter(pk=kwargs['job_id']).first()
if parent_job and parent_job.artifacts != artifact_dict:
parent_job.artifacts = artifact_dict

View File

@ -561,6 +561,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
"Override in child classes, None value indicates this is not configurable"
return None
def _resources_sufficient_for_launch(self):
return True
def __unicode__(self):
return u'%s-%s-%s' % (self.created, self.id, self.status)

View File

@ -134,7 +134,7 @@ class WorkflowNodeBase(CreatedModifiedModel):
scan_errors = ujt_obj._extra_job_type_errors(accepted_fields)
ignored_dict.update(scan_errors)
for fd in ['inventory', 'credential']:
if getattr(ujt_obj, fd) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
if getattr(ujt_obj, "{}_id".format(fd)) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
missing_dict[fd] = 'Job Template does not have this field and workflow node does not provide it'
data = {}
@ -418,18 +418,22 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
def can_start_without_user_input(self):
'''Return whether WFJT can be launched without survey passwords.'''
return not bool(self.variables_needed_to_start)
return not bool(
self.variables_needed_to_start or
self.node_templates_missing() or
self.node_prompts_rejected())
def get_warnings(self):
warning_data = {}
for node in self.workflow_job_template_nodes.all():
if node.unified_job_template is None:
warning_data[node.pk] = 'Node is missing a linked unified_job_template'
continue
def node_templates_missing(self):
return [node.pk for node in self.workflow_job_template_nodes.filter(
unified_job_template__isnull=True).all()]
def node_prompts_rejected(self):
node_list = []
for node in self.workflow_job_template_nodes.select_related('unified_job_template').all():
node_prompts_warnings = node.get_prompts_warnings()
if node_prompts_warnings:
warning_data[node.pk] = node_prompts_warnings
return warning_data
node_list.append(node.pk)
return node_list
def user_copy(self, user):
new_wfjt = self.copy_unified_jt()

View File

@ -10,6 +10,7 @@ from sets import Set
from django.conf import settings
from django.db import transaction, connection
from django.db.utils import DatabaseError
from django.utils.translation import ugettext_lazy as _
# AWX
from awx.main.models import * # noqa
@ -114,14 +115,20 @@ class TaskManager():
dag = WorkflowDAG(workflow_job)
spawn_nodes = dag.bfs_nodes_to_run()
for spawn_node in spawn_nodes:
if spawn_node.unified_job_template is None:
continue
kv = spawn_node.get_job_kwargs()
job = spawn_node.unified_job_template.create_unified_job(**kv)
spawn_node.job = job
spawn_node.save()
can_start = job.signal_start(**kv)
if job._resources_sufficient_for_launch():
can_start = job.signal_start(**kv)
else:
can_start = False
if not can_start:
job.status = 'failed'
job.job_explanation = "Workflow job could not start because it was not in the right state or required manual credentials"
job.job_explanation = _("Job spawned from workflow could not start because it "
"was not in the right state or required manual credentials")
job.save(update_fields=['status', 'job_explanation'])
connection.on_commit(lambda: job.websocket_emit_status('failed'))

View File

@ -67,6 +67,8 @@ class WorkflowDAG(SimpleDAG):
obj = n['node_object']
job = obj.job
if obj.unified_job_template is None:
continue
if not job:
return False
# Job is about to run or is running. Hold our horses and wait for

View File

@ -34,11 +34,13 @@ def run_job_complete(job_id):
@task
def run_task_manager():
logger.debug("Running Tower task manager.")
TaskManager().schedule()
@task
def run_fail_inconsistent_running_jobs():
logger.debug("Running task to fail inconsistent running jobs.")
with transaction.atomic():
# Lock
try:

View File

@ -32,7 +32,8 @@ import pexpect
# Celery
from celery import Task, task
from celery.signals import celeryd_init
from celery.signals import celeryd_init, worker_process_init
from celery import current_app
# Django
from django.conf import settings
@ -75,7 +76,8 @@ logger = logging.getLogger('awx.main.tasks')
def celery_startup(conf=None, **kwargs):
# Re-init all schedules
# NOTE: Rework this during the Rampart work
logger.info("Syncing Tower Schedules")
startup_logger = logging.getLogger('awx.main.tasks')
startup_logger.info("Syncing Tower Schedules")
for sch in Schedule.objects.all():
try:
sch.update_computed_fields()
@ -84,7 +86,28 @@ def celery_startup(conf=None, **kwargs):
logger.error("Failed to rebuild schedule {}: {}".format(sch, e))
def uwsgi_reload():
def _setup_tower_logger():
global logger
from django.utils.log import configure_logging
LOGGING_DICT = settings.LOGGING
if settings.LOG_AGGREGATOR_ENABLED:
LOGGING_DICT['handlers']['http_receiver']['class'] = 'awx.main.utils.handlers.HTTPSHandler'
LOGGING_DICT['handlers']['http_receiver']['async'] = False
if 'awx' in settings.LOG_AGGREGATOR_LOGGERS:
if 'http_receiver' not in LOGGING_DICT['loggers']['awx']['handlers']:
LOGGING_DICT['loggers']['awx']['handlers'] += ['http_receiver']
configure_logging(settings.LOGGING_CONFIG, LOGGING_DICT)
logger = logging.getLogger('awx.main.tasks')
@worker_process_init.connect
def task_set_logger_pre_run(*args, **kwargs):
if settings.LOG_AGGREGATOR_ENABLED:
_setup_tower_logger()
logger.debug('Custom Tower logger configured for worker process.')
def _uwsgi_reload():
# http://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands
logger.warn('Initiating uWSGI chain reload of server')
TRIGGER_CHAIN_RELOAD = 'c'
@ -92,14 +115,28 @@ def uwsgi_reload():
awxfifo.write(TRIGGER_CHAIN_RELOAD)
@task(queue='broadcast_all')
def clear_cache_keys(cache_keys):
set_of_keys = set([key for key in cache_keys])
def _reset_celery_logging():
# Worker logger reloaded, now send signal to restart pool
app = current_app._get_current_object()
app.control.broadcast('pool_restart', arguments={'reload': True},
destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False)
def _clear_cache_keys(set_of_keys):
logger.debug('cache delete_many(%r)', set_of_keys)
cache.delete_many(set_of_keys)
@task(queue='broadcast_all')
def process_cache_changes(cache_keys):
logger.warn('Processing cache changes, task args: {0.args!r} kwargs: {0.kwargs!r}'.format(
process_cache_changes.request))
set_of_keys = set([key for key in cache_keys])
_clear_cache_keys(set_of_keys)
for setting_key in set_of_keys:
if setting_key.startswith('LOG_AGGREGATOR_'):
uwsgi_reload()
_uwsgi_reload()
_reset_celery_logging()
break
@ -129,6 +166,7 @@ def send_notifications(notification_list, job_id=None):
@task(bind=True, queue='default')
def run_administrative_checks(self):
logger.warn("Running administrative checks.")
if not settings.TOWER_ADMIN_ALERTS:
return
validation_info = TaskEnhancer().validate_enhancements()
@ -150,11 +188,13 @@ def run_administrative_checks(self):
@task(bind=True, queue='default')
def cleanup_authtokens(self):
logger.warn("Cleaning up expired authtokens.")
AuthToken.objects.filter(expires__lt=now()).delete()
@task(bind=True)
def cluster_node_heartbeat(self):
logger.debug("Cluster node heartbeat task.")
inst = Instance.objects.filter(hostname=settings.CLUSTER_HOST_ID)
if inst.exists():
inst = inst[0]
@ -1832,7 +1872,7 @@ class RunSystemJob(BaseTask):
if 'days' in json_vars and system_job.job_type != 'cleanup_facts':
args.extend(['--days', str(json_vars.get('days', 60))])
if system_job.job_type == 'cleanup_jobs':
args.extend(['--jobs', '--project-updates', '--inventory-updates', '--management-jobs', '--ad-hoc-commands'])
args.extend(['--jobs', '--project-updates', '--inventory-updates', '--management-jobs', '--ad-hoc-commands', '--workflow-jobs'])
if system_job.job_type == 'cleanup_facts':
if 'older_than' in json_vars:
args.extend(['--older_than', str(json_vars['older_than'])])

View File

@ -65,6 +65,17 @@ def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project
}, alice, expect=expect)
@pytest.mark.django_db
def test_reject_dict_extra_vars_patch(patch, job_template_factory, admin_user):
# Expect a string for extra_vars, raise 400 in this case that would
# otherwise have been saved incorrectly
jt = job_template_factory(
'jt', organization='org1', project='prj', inventory='inv', credential='cred'
).job_template
patch(reverse('api:job_template_detail', args=(jt.id,)),
{'extra_vars': {'foo': 5}}, admin_user, expect=400)
@pytest.mark.django_db
def test_edit_playbook(patch, job_template_factory, alice):
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import mock # noqa
import pytest
@ -22,6 +24,84 @@ def team_project_list(organization_factory):
return objects
@pytest.mark.django_db
def test_user_project_paged_list(get, organization_factory):
'Test project listing that spans multiple pages'
# 3 total projects, 1 per page, 3 pages
objects = organization_factory(
'org1',
projects=['project-%s' % i for i in range(3)],
users=['alice'],
roles=['project-%s.admin_role:alice' % i for i in range(3)],
)
# first page has first project and no previous page
pk = objects.users.alice.pk
url = reverse('api:user_projects_list', args=(pk,))
results = get(url, objects.users.alice, QUERY_STRING='page_size=1').data
assert results['count'] == 3
assert len(results['results']) == 1
assert results['previous'] is None
assert results['next'] == (
'/api/v1/users/%s/projects/?page=2&page_size=1' % pk
)
# second page has one more, a previous and next page
results = get(url, objects.users.alice,
QUERY_STRING='page=2&page_size=1').data
assert len(results['results']) == 1
assert results['previous'] == (
'/api/v1/users/%s/projects/?page=1&page_size=1' % pk
)
assert results['next'] == (
'/api/v1/users/%s/projects/?page=3&page_size=1' % pk
)
# third page has last project and a previous page
results = get(url, objects.users.alice,
QUERY_STRING='page=3&page_size=1').data
assert len(results['results']) == 1
assert results['previous'] == (
'/api/v1/users/%s/projects/?page=2&page_size=1' % pk
)
assert results['next'] is None
@pytest.mark.django_db
def test_user_project_paged_list_with_unicode(get, organization_factory):
'Test project listing that contains unicode chars in the next/prev links'
# Create 2 projects that contain a "cloud" unicode character, make sure we
# can search it and properly generate next/previous page links
objects = organization_factory(
'org1',
projects=['project-☁-1','project-☁-2'],
users=['alice'],
roles=['project-☁-1.admin_role:alice','project-☁-2.admin_role:alice'],
)
pk = objects.users.alice.pk
url = reverse('api:user_projects_list', args=(pk,))
# first on first page, next page link contains unicode char
results = get(url, objects.users.alice,
QUERY_STRING='page_size=1&search=%E2%98%81').data
assert results['count'] == 2
assert len(results['results']) == 1
assert results['next'] == (
'/api/v1/users/%s/projects/?page=2&page_size=1&search=%%E2%%98%%81' % pk # noqa
)
# second project on second page, previous page link contains unicode char
results = get(url, objects.users.alice,
QUERY_STRING='page=2&page_size=1&search=%E2%98%81').data
assert results['count'] == 2
assert len(results['results']) == 1
assert results['previous'] == (
'/api/v1/users/%s/projects/?page=1&page_size=1&search=%%E2%%98%%81' % pk # noqa
)
@pytest.mark.django_db
def test_user_project_list(get, organization_factory):
'List of projects a user has access to, filtered by projects you can also see'

View File

@ -259,22 +259,37 @@ def test_associate_label(label, user, job_template):
@pytest.mark.django_db
def test_move_schedule_to_JT_no_access(job_template, rando):
schedule = Schedule.objects.create(
unified_job_template=job_template,
rrule='DTSTART:20151117T050000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1')
job_template.admin_role.members.add(rando)
jt2 = JobTemplate.objects.create(name="other-jt")
access = ScheduleAccess(rando)
assert not access.can_change(schedule, data=dict(unified_job_template=jt2.pk))
class TestJobTemplateSchedules:
rrule = 'DTSTART:20151117T050000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1'
rrule2 = 'DTSTART:20151117T050000Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1'
@pytest.fixture
def jt2(self):
return JobTemplate.objects.create(name="other-jt")
def test_move_schedule_to_JT_no_access(self, job_template, rando, jt2):
schedule = Schedule.objects.create(unified_job_template=job_template, rrule=self.rrule)
job_template.admin_role.members.add(rando)
access = ScheduleAccess(rando)
assert not access.can_change(schedule, data=dict(unified_job_template=jt2.pk))
@pytest.mark.django_db
def test_move_schedule_from_JT_no_access(job_template, rando):
schedule = Schedule.objects.create(
unified_job_template=job_template,
rrule='DTSTART:20151117T050000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1')
jt2 = JobTemplate.objects.create(name="other-jt")
jt2.admin_role.members.add(rando)
access = ScheduleAccess(rando)
assert not access.can_change(schedule, data=dict(unified_job_template=jt2.pk))
def test_move_schedule_from_JT_no_access(self, job_template, rando, jt2):
schedule = Schedule.objects.create(unified_job_template=job_template, rrule=self.rrule)
jt2.admin_role.members.add(rando)
access = ScheduleAccess(rando)
assert not access.can_change(schedule, data=dict(unified_job_template=jt2.pk))
def test_can_create_schedule_with_execute(self, job_template, rando):
job_template.execute_role.members.add(rando)
access = ScheduleAccess(rando)
assert access.can_add({'unified_job_template': job_template})
def test_can_modify_ones_own_schedule(self, job_template, rando):
job_template.execute_role.members.add(rando)
schedule = Schedule.objects.create(unified_job_template=job_template, rrule=self.rrule, created_by=rando)
access = ScheduleAccess(rando)
assert access.can_change(schedule, {'rrule': self.rrule2})

View File

@ -86,11 +86,15 @@ class TestWorkflowJobTemplateNodeAccess:
@pytest.mark.django_db
class TestWorkflowJobAccess:
def test_wfjt_admin_delete(self, wfjt, workflow_job, rando):
wfjt.admin_role.members.add(rando)
access = WorkflowJobAccess(rando)
def test_org_admin_can_delete_workflow_job(self, workflow_job, org_admin):
access = WorkflowJobAccess(org_admin)
assert access.can_delete(workflow_job)
def test_wfjt_admin_can_delete_workflow_job(self, workflow_job, rando):
workflow_job.workflow_job_template.admin_role.members.add(rando)
access = WorkflowJobAccess(rando)
assert not access.can_delete(workflow_job)
def test_cancel_your_own_job(self, wfjt, workflow_job, rando):
wfjt.execute_role.members.add(rando)
workflow_job.created_by = rando
@ -120,13 +124,11 @@ class TestWorkflowJobAccess:
access = WorkflowJobTemplateAccess(rando, save_messages=True)
assert not access.can_copy(wfjt)
warnings = access.messages
assert 1 in warnings
assert 'inventory' in warnings[1]
assert 'inventories_unable_to_copy' in warnings
def test_workflow_copy_warnings_jt(self, wfjt, rando, job_template):
wfjt.workflow_job_template_nodes.create(unified_job_template=job_template)
access = WorkflowJobTemplateAccess(rando, save_messages=True)
assert not access.can_copy(wfjt)
warnings = access.messages
assert 1 in warnings
assert 'unified_job_template' in warnings[1]
assert 'templates_unable_to_copy' in warnings

View File

@ -125,6 +125,7 @@ class TestWorkflowJobTemplateNodeSerializerCharPrompts():
serializer = WorkflowJobTemplateNodeSerializer()
node = WorkflowJobTemplateNode(pk=1)
node.char_prompts = {'limit': 'webservers'}
serializer.instance = node
view = FakeView(node)
view.request = FakeRequest()
view.request.method = "PATCH"

View File

@ -5,7 +5,7 @@ import pytest
# AWX
from awx.main.scheduler.dag_simple import SimpleDAG
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.main.models import Job
from awx.main.models import Job, JobTemplate
from awx.main.models.workflow import WorkflowJobNode
@ -72,6 +72,7 @@ def factory_node():
if status:
j = Job(status=status)
wfn.job = j
wfn.unified_job_template = JobTemplate(name='JT{}'.format(id))
return wfn
return fn

View File

@ -30,6 +30,7 @@ PARAM_NAMES = {
'password': 'LOG_AGGREGATOR_PASSWORD',
'enabled_loggers': 'LOG_AGGREGATOR_LOGGERS',
'indv_facts': 'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
'enabled_flag': 'LOG_AGGREGATOR_ENABLED',
}
@ -48,6 +49,7 @@ class HTTPSHandler(logging.Handler):
def __init__(self, fqdn=False, **kwargs):
super(HTTPSHandler, self).__init__()
self.fqdn = fqdn
self.async = kwargs.get('async', True)
for fd in PARAM_NAMES:
# settings values take precedence over the input params
settings_name = PARAM_NAMES[fd]
@ -100,11 +102,21 @@ class HTTPSHandler(logging.Handler):
payload_str = json.dumps(payload_input)
else:
payload_str = payload_input
return dict(data=payload_str, background_callback=unused_callback)
if self.async:
return dict(data=payload_str, background_callback=unused_callback)
else:
return dict(data=payload_str)
def skip_log(self, logger_name):
if self.host == '' or (not self.enabled_flag):
return True
if not logger_name.startswith('awx.analytics'):
# Tower log emission is only turned off by enablement setting
return False
return self.enabled_loggers is None or logger_name.split('.')[-1] not in self.enabled_loggers
def emit(self, record):
if (self.host == '' or self.enabled_loggers is None or
record.name.split('.')[-1] not in self.enabled_loggers):
if self.skip_log(record.name):
return
try:
payload = self.format(record)
@ -123,7 +135,10 @@ class HTTPSHandler(logging.Handler):
self.session.post(host, **self.get_post_kwargs(fact_payload))
return
self.session.post(host, **self.get_post_kwargs(payload))
if self.async:
self.session.post(host, **self.get_post_kwargs(payload))
else:
requests.post(host, auth=requests.auth.HTTPBasicAuth(self.username, self.password), **self.get_post_kwargs(payload))
except (KeyboardInterrupt, SystemExit):
raise
except:

View File

@ -185,8 +185,9 @@ def vars_validate_or_raise(vars_str):
except ValueError:
pass
try:
yaml.safe_load(vars_str)
return vars_str
r = yaml.safe_load(vars_str)
if not (isinstance(r, basestring) and r.startswith('OrderedDict(')):
return vars_str
except yaml.YAMLError:
pass
raise RestValidationError(_('Must be valid JSON or YAML.'))

View File

@ -115,6 +115,12 @@
chdir: "{{project_path|quote}}/roles"
when: doesRequirementsExist.stat.exists and scm_full_checkout|bool
# format provided by ansible is ["Revision: 12345", "URL: ..."]
- name: parse subversion version string properly
set_fact:
scm_version: "{{scm_version|regex_replace('^.*Revision: ([0-9]+).*$', '\\1')}}"
when: scm_type == 'svn'
- name: Repository Version
debug: msg="Repository Version {{ scm_version }}"
when: scm_version is defined

View File

@ -73,7 +73,7 @@ DATABASES = {
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
@ -154,7 +154,7 @@ STDOUT_MAX_BYTES_DISPLAY = 1048576
# Returned in the header on event api lists as a recommendation to the UI
# on how many events to display before truncating/hiding
RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER = 10000
RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER = 4000
# The maximum size of the ansible callback event's res data structure
# beyond this limit and the value will be removed
@ -167,6 +167,15 @@ JOB_EVENT_WORKERS = 4
JOB_EVENT_MAX_QUEUE_SIZE = 5000
# Disallow sending session cookies over insecure connections
SESSION_COOKIE_SECURE = True
# Disallow sending csrf cookies over insecure connections
CSRF_COOKIE_SECURE = True
# Limit CSRF cookies to browser sessions
CSRF_COOKIE_AGE = None
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
@ -380,6 +389,7 @@ CELERY_ACCEPT_CONTENT = ['json']
CELERY_TRACK_STARTED = True
CELERYD_TASK_TIME_LIMIT = None
CELERYD_TASK_SOFT_TIME_LIMIT = None
CELERYD_POOL_RESTARTS = True
CELERYBEAT_SCHEDULER = 'celery.beat.PersistentScheduler'
CELERYBEAT_MAX_LOOP_INTERVAL = 60
CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend'
@ -882,7 +892,7 @@ LOGGING = {
},
'http_receiver': {
'class': 'awx.main.utils.handlers.HTTPSNullHandler',
'level': 'INFO',
'level': 'DEBUG',
'formatter': 'json',
'host': '',
},
@ -981,7 +991,7 @@ LOGGING = {
'handlers': ['callback_receiver'],
},
'awx.main.tasks': {
'handlers': ['task_system']
'handlers': ['task_system'],
},
'awx.main.scheduler': {
'handlers': ['task_system'],
@ -1009,18 +1019,6 @@ LOGGING = {
'level': 'INFO',
'propagate': False
},
'awx.analytics.job_events': {
'handlers': ['null'],
'level': 'INFO'
},
'awx.analytics.activity_stream': {
'handlers': ['null'],
'level': 'INFO'
},
'awx.analytics.system_tracking': {
'handlers': ['null'],
'level': 'INFO'
},
'django_auth_ldap': {
'handlers': ['console', 'file', 'tower_warnings'],
'level': 'DEBUG',

View File

@ -24,11 +24,11 @@ ALLOWED_HOSTS = ['*']
mimetypes.add_type("image/svg+xml", ".svg", True)
mimetypes.add_type("image/svg+xml", ".svgz", True)
MONGO_HOST = '127.0.0.1'
MONGO_PORT = 27017
MONGO_USERNAME = None
MONGO_PASSWORD = None
MONGO_DB = 'system_tracking_dev'
# Disallow sending session cookies over insecure connections
SESSION_COOKIE_SECURE = False
# Disallow sending csrf cookies over insecure connections
CSRF_COOKIE_SECURE = False
# Override django.template.loaders.cached.Loader in defaults.py
TEMPLATE_LOADERS = (

View File

@ -114,7 +114,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html

View File

@ -71,7 +71,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html

View File

@ -299,7 +299,10 @@ class LDAPGroupTypeField(fields.ChoiceField):
data = super(LDAPGroupTypeField, self).to_internal_value(data)
if not data:
return None
return getattr(django_auth_ldap.config, data)()
if data.endswith('MemberDNGroupType'):
return getattr(django_auth_ldap.config, data)(member_attr='member')
else:
return getattr(django_auth_ldap.config, data)()
class LDAPUserFlagsField(fields.DictField):

View File

@ -52,7 +52,7 @@
<div class="col-sm-6">
</div>
<div class="col-sm-6 footer-copyright">
Copyright &copy; 2016 <a href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.
Copyright &copy; 2017 <a href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.
</div>
</div>
</div>

View File

@ -729,18 +729,6 @@ legend {
.navigation {
margin: 15px 0 15px 0;
}
.modal-body {
.alert {
padding: 0;
border: none;
margin: 0;
}
.alert-danger {
background-color: @default-bg;
border: none;
color: @default-interface-txt;
}
}
.footer-navigation {
margin: 10px 0 10px 0;
@ -1638,17 +1626,19 @@ tr td button i {
}
/* overrides to TB modal */
.modal-content {
padding: 20px;
}
.modal-header {
color: @default-interface-txt;
margin: .1em 0;
white-space: nowrap;
width: 90%;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
border: none;
padding: 12px 14px 0 12px;
padding: 0;
}
.modal {
@ -1677,8 +1667,18 @@ tr td button i {
}
.modal-body {
padding: 20px 14px 7px 14px;
min-height: 120px;
padding: 20px 0;
.alert {
padding: 10px;
margin: 0;
}
.alert-danger {
background-color: @default-bg;
border: none;
color: @default-interface-txt;
}
}
#prompt-modal .modal-body {
@ -1690,15 +1690,15 @@ tr td button i {
}
.modal-footer {
padding: .3em 1em .5em .4em;
padding: 0;
border: none;
margin-top: 0;
.btn.btn-primary {
text-transform: uppercase;
background-color: @default-succ;
border-color: @default-succ;
padding: 5px 15px;
margin: .5em .4em .5em 0;
cursor: pointer;
&:hover {
@ -1720,8 +1720,7 @@ tr td button i {
/* PW progress bar */
.pw-progress {
margin-top: 10px;
.pw-progress { margin-top: 10px;
li {
line-height: normal;
@ -2219,10 +2218,6 @@ a:hover {
font-family: 'Open Sans';
}
.modal-body .alert {
padding: 10px;
}
.WorkflowBadge{
background-color: @b7grey;
border-radius: 10px;

View File

@ -44,11 +44,10 @@
color: @list-header-txt;
font-size: 14px;
font-weight: bold;
padding-bottom: 25px;
min-height: 45px;
word-break: break-all;
max-width: 90%;
word-wrap: break-word;
margin-bottom: 20px;
}
.Form-secondaryTitle{

View File

@ -11,19 +11,19 @@
<!-- Don't indent this properly, you'll break the cow -->
<pre class="About-cowsay--code">
________________
/ Tower {{version_str}} \\
\\<span>{{version}}</span>/
/ Tower {{version_str}} \
\<span>{{version}}</span>/
----------------
\\ ^__^
\\ (oo)\\_______
(__) A )\\/\\
\ ^__^
\ (oo)\_______
(__) A )\/\
||----w |
|| ||
</pre>
</div>
<div class="About-modal--footer">
<img class="About-brand--redhat img-responsive" src="/static/assets/tower-logo-login.svg" />
<p class="text-right">Copyright &copy; 2016 Red Hat, Inc. <br>
<p class="text-right">Copyright &copy; 2017 Red Hat, Inc. <br>
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
</div>
</div>

View File

@ -15,7 +15,8 @@ export default ['templateUrl', '$state',
usersDataset: '=',
teamsDataset: '=',
resourceData: '=',
withoutTeamPermissions: '@'
withoutTeamPermissions: '@',
title: '@'
},
controller: controller,
templateUrl: templateUrl('access/add-rbac-resource/rbac-resource'),

View File

@ -6,9 +6,9 @@
<div class="List-header">
<div class="List-title">
<div class="List-titleText ng-binding">
{{ object.name }}
{{ object.name || object.username }}
<div class="List-titleLockup"></div>
Add Permissions
{{ title }}
</div>
</div>
<div class="Form-exitHolder">

View File

@ -42,8 +42,6 @@ export default ['$compile','templateUrl', 'i18n', 'generateList',
list.listTitleBadge = false;
// @issue - fix field.columnClass values for this view
switch(scope.resourceType){
case 'projects':
@ -51,6 +49,8 @@ export default ['$compile','templateUrl', 'i18n', 'generateList',
name: list.fields.name,
scm_type: list.fields.scm_type
};
list.fields.name.columnClass = 'col-md-5 col-sm-5 col-xs-10';
list.fields.scm_type.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'inventories':
@ -58,6 +58,8 @@ export default ['$compile','templateUrl', 'i18n', 'generateList',
name: list.fields.name,
organization: list.fields.organization
};
list.fields.name.columnClass = 'col-md-5 col-sm-5 col-xs-10';
list.fields.organization.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'job_templates':
@ -67,6 +69,8 @@ export default ['$compile','templateUrl', 'i18n', 'generateList',
name: list.fields.name,
description: list.fields.description
};
list.fields.name.columnClass = 'col-md-5 col-sm-5 col-xs-10';
list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'workflow_templates':
@ -77,12 +81,16 @@ export default ['$compile','templateUrl', 'i18n', 'generateList',
name: list.fields.name,
description: list.fields.description
};
list.fields.name.columnClass = 'col-md-5 col-sm-5 col-xs-10';
list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'credentials':
list.fields = {
name: list.fields.name,
description: list.fields.description
};
list.fields.name.columnClass = 'col-md-5 col-sm-5 col-xs-10';
list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs';
}
list.fields = _.each(list.fields, (field) => field.nosort = true);

View File

@ -11,7 +11,8 @@ export default ['templateUrl',
return {
restrict: 'E',
scope: {
resolve: "="
resolve: "=",
title: "@",
},
controller: controller,
templateUrl: templateUrl('access/add-rbac-user-team/rbac-user-team'),

View File

@ -7,9 +7,9 @@
<div class="List-header">
<div class="List-title">
<div class="List-titleText ng-binding">
{{ owner.name }}
{{ owner.name || owner.username }}
<div class="List-titleLockup"></div>
Add Permissions
{{ title }}
</div>
</div>
<div class="Form-exitHolder">

View File

@ -51,6 +51,10 @@
padding-top: 20px;
}
.AddPermissions-list {
margin-bottom: 20px;
}
.AddPermissions-list .List-searchRow {
height: 0px;
}

View File

@ -34,7 +34,7 @@
username: {
key: true,
label: 'Username',
columnClass: 'col-md-3 col-sm-3 col-xs-9'
columnClass: 'col-md-5 col-sm-5 col-xs-11'
},
},

View File

@ -43,6 +43,8 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
name: list.fields.name,
scm_type: list.fields.scm_type
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.scm_type.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'Inventories':
@ -50,6 +52,8 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
name: list.fields.name,
organization: list.fields.organization
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.organization.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'JobTemplates':
@ -59,6 +63,8 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
name: list.fields.name,
description: list.fields.description
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'WorkflowTemplates':
@ -69,6 +75,8 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
name: list.fields.name,
description: list.fields.description
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
case 'Users':
list.fields = {
@ -76,12 +84,25 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
first_name: list.fields.first_name,
last_name: list.fields.last_name
};
list.fields.username.columnClass = 'col-md-5 col-sm-5 col-xs-11';
list.fields.first_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
list.fields.last_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
break;
case 'Teams':
list.fields = {
name: list.fields.name,
organization: list.fields.organization,
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.organization.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
default:
list.fields = {
name: list.fields.name,
description: list.fields.description
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.description.columnClass = 'col-md-5 col-sm-5 hidden-xs';
}
list_html = generateList.build({

View File

@ -9,7 +9,7 @@
<div class="RoleList-tag"
ng-class="{'RoleList-tag--deletable': entry.explicit && entry.user_capabilities.unattach,
'RoleList-tag--team': entry.team_id}"
aw-tool-tip='{{entry.team_name | sanitize}}' aw-tip-placement='bottom'>
aw-tool-tip='<div>Organization: {{ entry.team_organization_name | sanitize }}</div><div>Team: {{entry.team_name | sanitize}}</div>' aw-tip-placement='bottom'>
<span class="RoleList-name">{{ entry.name }}</span>
<i ng-show='entry.team_id' class="fa fa-users"></i>
</div>

View File

@ -12,6 +12,7 @@
function activityStreamController($scope, $state, subTitle, Stream, GetTargetTitle, list, Dataset) {
init();
initOmitSmartTags();
function init() {
// search init
@ -33,6 +34,20 @@ function activityStreamController($scope, $state, subTitle, Stream, GetTargetTit
});
}
// Specification of smart-tags omission from the UI is done in the route/state init.
// A limitation is that this specficiation is static and the key for which to be omitted from
// the smart-tags must be known at that time.
// In the case of activity stream, we won't to dynamically ommit the resource for which we are
// displaying the activity stream for. i.e. 'project', 'credential', etc.
function initOmitSmartTags() {
let defaults, route = _.find($state.$current.path, (step) => {
return step.params.hasOwnProperty('activity_search');
});
if (route && $state.params.target !== undefined) {
defaults = route.params.activity_search.config.value;
defaults[$state.params.target] = null;
}
}
}
export default ['$scope', '$state', 'subTitle', 'Stream', 'GetTargetTitle', 'StreamList', 'Dataset', activityStreamController];

View File

@ -35,5 +35,6 @@
margin-bottom: 0;
max-height: 200px;
overflow: scroll;
overflow-x: auto;
color: @as-detail-changes-txt;
}

View File

@ -22,7 +22,7 @@
</div>
</div>
<div class="Modal-footer">
<a href="#" data-target="#stream-detail-modal" data-dismiss="modal" id="action_cancel_btn" class="btn btn-primary StreamDetail-actionButton">OK</a>
<a href="#" data-target="#stream-detail-modal" data-dismiss="modal" id="action_cancel_btn" class="btn btn-default StreamDetail-actionButton">OK</a>
</div>
</div>
</div>

View File

@ -47,6 +47,9 @@ export default
order_by: '-timestamp',
page_size: '20',
};
if (streamConfig.activityStreamTarget && streamConfig.activityStreamId) {
stateGoParams.activity_search[streamConfig.activityStreamTarget] = $state.params[streamConfig.activityStreamId];
}
}
else {
stateGoParams.activity_search = {

View File

@ -21,7 +21,7 @@ export default
});
});
// Remove the clone from the dom
$breadcrumbClone.remove();console.log(availableWidth);
$breadcrumbClone.remove();
if(expandedBreadcrumbWidth > availableWidth) {
let widthToTrim = expandedBreadcrumbWidth - availableWidth;
// Sort the crumbs from biggest to smallest

View File

@ -16,7 +16,8 @@
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY'
},
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET'
},
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: {
@ -38,8 +39,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_ORG_KEY'
},
SOCIAL_AUTH_GITHUB_ORG_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_ORG_SECRET'
},
SOCIAL_AUTH_GITHUB_ORG_NAME: {
@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_TEAM_KEY'
},
SOCIAL_AUTH_GITHUB_TEAM_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_TEAM_SECRET'
},
SOCIAL_AUTH_GITHUB_TEAM_ID: {
@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_KEY'
},
SOCIAL_AUTH_GITHUB_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_SECRET'
}
},
@ -24,8 +25,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY'
},
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'
},
SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: {
@ -36,8 +37,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
reset: 'AUTH_LDAP_BIND_DN'
},
AUTH_LDAP_BIND_PASSWORD: {
type: 'password'
type: 'sensitive',
hasShowInputButton: true,
},
AUTH_LDAP_USER_SEARCH: {
type: 'textarea',
@ -84,8 +85,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
reset: 'RADIUS_PORT'
},
RADIUS_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'RADIUS_SECRET'
}
},
@ -29,8 +30,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -20,7 +20,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT'
},
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY'
},
SOCIAL_AUTH_SAML_ORG_INFO: {
@ -56,8 +57,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -12,6 +12,19 @@
float: right
}
.Form-resetAll {
border: none;
padding: 0;
background-color: @white;
margin-right: auto;
color: @default-link;
font-size: 12px;
&:hover {
color: @default-link-hov;
}
}
.Form-tab {
min-width: 77px;
}

View File

@ -64,8 +64,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -22,8 +22,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -30,7 +30,8 @@
reset: 'LOG_AGGREGATOR_USERNAME'
},
LOG_AGGREGATOR_PASSWORD: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'LOG_AGGREGATOR_PASSWORD'
},
LOG_AGGREGATOR_LOGGERS: {
@ -48,8 +49,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -26,8 +26,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -32,8 +32,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -113,7 +113,7 @@ CredentialsList.$inject = ['$scope', '$rootScope', '$location', '$log',
export function CredentialsAdd($scope, $rootScope, $compile, $location, $log,
$stateParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors,
ClearScope, GetBasePath, GetChoices, Empty, KindChange,
ClearScope, GetBasePath, GetChoices, Empty, KindChange, BecomeMethodChange,
OwnerChange, FormSave, $state, CreateSelect2) {
ClearScope();
@ -221,6 +221,10 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log,
KindChange({ scope: $scope, form: form, reset: true });
};
$scope.becomeMethodChange = function() {
BecomeMethodChange({ scope: $scope });
};
// Save
$scope.formSave = function() {
if ($scope[form.name + '_form'].$valid) {
@ -276,14 +280,14 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log,
CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange',
'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'BecomeMethodChange',
'OwnerChange', 'FormSave', '$state', 'CreateSelect2'
];
export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
$stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt,
GetBasePath, GetChoices, KindChange, Empty, OwnerChange, FormSave, Wait,
$state, CreateSelect2, Authorization) {
GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, FormSave, Wait,
$state, CreateSelect2, Authorization, i18n) {
ClearScope();
@ -336,19 +340,15 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
});
}
// if the credential is assigned to an organization, allow permission delegation
// do NOT use $scope.organization in a view directive to determine if a credential is associated with an org
// @todo why not? ^ and what is this type check for a number doing - should this be a type check for undefined?
$scope.disablePermissionAssignment = typeof($scope.organization) === 'number' ? false : true;
if ($scope.disablePermissionAssignment) {
$scope.permissionsTooltip = 'Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.';
}
setAskCheckboxes();
KindChange({
scope: $scope,
form: form,
reset: false
$scope.$watch('organization', function(val) {
if (val === undefined) {
$scope.permissionsTooltip = i18n._('Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.');
} else {
$scope.permissionsTooltip = '';
}
});
setAskCheckboxes();
OwnerChange({ scope: $scope });
$scope.$watch("ssh_key_data", function(val) {
if (val === "" || val === null || val === undefined) {
@ -453,6 +453,13 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
break;
}
}
KindChange({
scope: $scope,
form: form,
reset: false
});
master.kind = $scope.kind;
CreateSelect2({
@ -518,6 +525,10 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
KindChange({ scope: $scope, form: form, reset: true });
};
$scope.becomeMethodChange = function() {
BecomeMethodChange({ scope: $scope });
};
$scope.formCancel = function() {
$state.transitionTo('credentials');
};
@ -612,6 +623,6 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert',
'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices',
'KindChange', 'Empty', 'OwnerChange',
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization'
'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange',
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n',
];

View File

@ -0,0 +1,36 @@
/** @define OwnerList */
@import "./client/src/shared/branding/colors.default.less";
.OwnerList {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.OwnerList-seeBase {
display: flex;
max-width: 100%;
color: @default-link;
text-transform: uppercase;
padding: 2px 15px;
cursor: pointer;
border-radius: 5px;
font-size: 11px;
}
.OwnerList-seeBase:hover {
color: @default-link-hov;
}
.OwnerList-seeLess {
.OwnerList-seeBase;
}
.OwnerList-seeMore {
.OwnerList-seeBase;
}
.OwnerList-Container {
margin-right: 5px;
}

View File

@ -1,5 +1,12 @@
<div ng-repeat="owner in owners_list">
<a ng-if="owner.type === 'organization'" ui-sref="organizations.edit({ organization_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
<a ng-if="owner.type === 'user'" ui-sref="users.edit({ user_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
<a ng-if="owner.type === 'team'" ui-sref="teams.edit({ team_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
</div>
<div class="OwnerList" ng-init="ownersLimit = 5; ownersLimitConst = 5; ">
<div class="OwnerList-Container" ng-repeat="owner in owners_list | limitTo:ownersLimit">
<a ng-if="owner.type === 'organization'" ui-sref="organizations.edit({ organization_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
<a ng-if="owner.type === 'user'" ui-sref="users.edit({ user_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
<a ng-if="owner.type === 'team'" ui-sref="teams.edit({ team_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
</div>
<div class="OwnerList-seeMore" ng-show="owners_list.length > ownersLimitConst && ownersLimit == ownersLimitConst"
ng-click="ownersLimit = owners_list.length">View More</div>
<div class="OwnerList-seeLess" ng-show="owners_list.length > ownersLimitConst && ownersLimit != ownersLimitConst"
ng-click="ownersLimit = ownersLimitConst">View Less</div>
</div>

View File

@ -32,34 +32,21 @@
}
.DashboardList-viewAll {
color: @btn-txt;
background-color: @btn-bg;
font-size: 12px;
border: 1px solid @default-icon-hov;
border-radius: 5px;
font-size: 11px;
margin-right: 15px;
margin-top: 10px;
margin-top: 13px;
margin-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
padding-top: 5px;
transition: background-color 0.2s;
}
.DashboardList-viewAll:hover {
color: @btn-txt;
background-color: @btn-bg-hov;
}
.DashboardList-viewAll:focus {
color: @btn-txt;
}
.DashboardList-container {
flex: 1;
width: 100%;
padding: 20px;
padding-top: 0;
}
.DashboardList-tableHeader--name {

View File

@ -1,3 +1,3 @@
<footer class='Footer'>
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !current_user || !current_user.username}">Copyright &copy 2016 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !current_user || !current_user.username}">Copyright &copy 2017 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
</footer>

View File

@ -288,7 +288,8 @@ export default
dataPlacement: 'right',
dataContainer: "body",
subForm: 'credentialSubForm',
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)',
ngChange: 'becomeMethodChange()',
},
"become_username": {
labelBind: 'becomeUsernameLabel',
@ -420,9 +421,12 @@ export default
related: {
permissions: {
disabled: 'disablePermissionAssignment',
disabled: '(organization === undefined ? true : false)',
// Do not transition the state if organization is undefined
ngClick: `(organization === undefined ? true : false)||$state.go('credentials.edit.permissions')`,
awToolTip: '{{permissionsTooltip}}',
dataTipWatch: 'permissionsTooltip',
awToolTipTabEnabledInEditMode: true,
dataPlacement: 'top',
basePath: 'api/v1/credentials/{{$stateParams.credential_id}}/access_list/',
search: {

View File

@ -43,7 +43,7 @@ export default
label: 'Variables',
type: 'textarea',
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
rows: 12,
rows: 6,
'default': '---',
dataTitle: 'Group Variables',
dataPlacement: 'right',
@ -69,6 +69,11 @@ export default
ngModel: 'source'
},
credential: {
// initializes a default value for this search param
// search params with default values set will not generate user-interactable search tags
search: {
kind: null
},
label: 'Cloud Credential',
type: 'lookup',
list: 'CredentialList',

View File

@ -78,7 +78,7 @@ angular.module('InventoryFormDefinition', ['ScanJobsListDefinition'])
},
close: {
ngClick: 'formCancel()',
ngHide: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
ngShow: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
@ -103,7 +103,7 @@ angular.module('InventoryFormDefinition', ['ScanJobsListDefinition'])
add: {
label: i18n._('Add'),
ngClick: "$state.go('.add')",
awToolTip: 'Add a permission',
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD',
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'

View File

@ -68,7 +68,7 @@ export default
searchType: 'select',
actions: {
add: {
ngClick: "addPermission",
ngClick: "$state.go('.add')",
label: i18n._('Add'),
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',

View File

@ -121,6 +121,7 @@ export default
organizations: {
awToolTip: i18n._('Please save before assigning to organizations'),
basePath: 'api/v1/users/{{$stateParams.user_id}}/organizations',
emptyListText: i18n._('Please add user to an Organization.'),
search: {
page_size: '10'
},

View File

@ -122,7 +122,7 @@ export default
add: {
ngClick: "$state.go('.add')",
label: i18n._('Add'),
awToolTip: 'Add a permission',
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; '+ i18n._('ADD'),
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'

View File

@ -74,6 +74,7 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.projectPopOver = "<p>" + i18n._("The project value") + "</p>";
scope.hostPopOver = "<p>" + i18n._("The host value") + "</p>";
scope.ssh_key_data_api_error = '';
if (!Empty(scope.kind)) {
// Apply kind specific settings
switch (scope.kind.value) {
@ -204,6 +205,111 @@ angular.module('CredentialsHelper', ['Utilities'])
}
])
.factory('BecomeMethodChange', ['Empty', 'i18n',
function (Empty, i18n) {
return function (params) {
console.log('become method has changed');
var scope = params.scope;
if (!Empty(scope.kind)) {
// Apply kind specific settings
switch (scope.kind.value) {
case 'aws':
scope.aws_required = true;
break;
case 'rax':
scope.rackspace_required = true;
scope.username_required = true;
break;
case 'ssh':
scope.usernameLabel = i18n._('Username'); //formally 'SSH Username'
scope.becomeUsernameLabel = i18n._('Privilege Escalation Username');
scope.becomePasswordLabel = i18n._('Privilege Escalation Password');
break;
case 'scm':
scope.sshKeyDataLabel = i18n._('SCM Private Key');
scope.passwordLabel = i18n._('Password');
break;
case 'gce':
scope.usernameLabel = i18n._('Service Account Email Address');
scope.sshKeyDataLabel = i18n._('RSA Private Key');
scope.email_required = true;
scope.key_required = true;
scope.project_required = true;
scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.');
scope.projectLabel = i18n._("Project");
scope.project_required = false;
scope.projectPopOver = "<p>" + i18n._("The Project ID is the " +
"GCE assigned identification. It is constructed as " +
"two words followed by a three digit number. Such " +
"as: ") + "</p><p>adjective-noun-000</p>";
break;
case 'azure':
scope.sshKeyDataLabel = i18n._('Management Certificate');
scope.subscription_required = true;
scope.key_required = true;
scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console.");
break;
case 'azure_rm':
scope.usernameLabel = i18n._("Username");
scope.subscription_required = true;
scope.passwordLabel = i18n._('Password');
scope.azure_rm_required = true;
break;
case 'vmware':
scope.username_required = true;
scope.host_required = true;
scope.password_required = true;
scope.hostLabel = "vCenter Host";
scope.passwordLabel = i18n._('Password');
scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter.");
break;
case 'openstack':
scope.hostLabel = i18n._("Host (Authentication URL)");
scope.projectLabel = i18n._("Project (Tenant Name)");
scope.domainLabel = i18n._("Domain Name");
scope.password_required = true;
scope.project_required = true;
scope.host_required = true;
scope.username_required = true;
scope.projectPopOver = "<p>" + i18n._("This is the tenant name. " +
" This value is usually the same " +
" as the username.") + "</p>";
scope.hostPopOver = "<p>" + i18n._("The host to authenticate with.") +
"<br />" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/");
break;
case 'satellite6':
scope.username_required = true;
scope.password_required = true;
scope.passwordLabel = i18n._('Password');
scope.host_required = true;
scope.hostLabel = i18n._("Satellite 6 URL");
scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" +
"Red Hat Satellite 6 server. %s" +
"For example, %s"), "<br />", "<br />", "https://satellite.example.org");
break;
case 'cloudforms':
scope.username_required = true;
scope.password_required = true;
scope.passwordLabel = i18n._('Password');
scope.host_required = true;
scope.hostLabel = i18n._("CloudForms URL");
scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" +
"corresponds to your CloudForm instance. %s" +
"For example, %s"), "<br />", "<br />", "https://cloudforms.example.org");
break;
case 'net':
scope.username_required = true;
scope.password_required = false;
scope.passwordLabel = i18n._('Password');
scope.sshKeyDataLabel = i18n._('SSH Key');
break;
}
}
};
}
])
.factory('OwnerChange', [
function () {

View File

@ -233,7 +233,7 @@ export default
hdr: hdr,
body: (action_label === 'cancel' || job.status === 'new') ? cancelBody : deleteBody,
action: action,
actionText: (action_label === 'cancel' || job.status === 'new') ? "YES" : "DELETE"
actionText: (action_label === 'cancel' || job.status === 'new') ? "OK" : "DELETE"
});
});

View File

@ -24,10 +24,7 @@ export default
var langUrl = langInfo.replace('-', '_');
//gettextCatalog.debug = true;
gettextCatalog.setCurrentLanguage(langInfo);
// TODO: the line below is commented out temporarily until
// the .po files are received from the i18n team, in order to avoid
// 404 file not found console errors in dev
// gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json');
gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json');
};
}])
.factory('i18n', ['gettextCatalog',

View File

@ -11,10 +11,16 @@
*/
function InventoriesAdd($scope, $rootScope, $compile, $location, $log,
$stateParams, GenerateForm, InventoryForm, Rest, Alert, ProcessErrors,
$stateParams, GenerateForm, InventoryForm, rbacUiControlService, Rest, Alert, ProcessErrors,
ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON,
$state) {
$scope.canAdd = false;
rbacUiControlService.canAdd(GetBasePath('inventory'))
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
Rest.setUrl(GetBasePath('inventory'));
Rest.options()
.success(function(data) {
@ -91,7 +97,7 @@ function InventoriesAdd($scope, $rootScope, $compile, $location, $log,
}
export default ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'GenerateForm', 'InventoryForm', 'Rest', 'Alert',
'$log', '$stateParams', 'GenerateForm', 'InventoryForm', 'rbacUiControlService', 'Rest', 'Alert',
'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange',
'Wait', 'ToJSON', '$state', InventoriesAdd
];

View File

@ -32,7 +32,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
form.formFieldSize = null;
$scope.inventory_id = inventory_id;
$scope.$watch('invnentory_obj.summary_fields.user_capabilities.edit', function(val) {
$scope.$watch('inventory_obj.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}

View File

@ -66,7 +66,19 @@ angular.module('inventory', [
],
ParentObject: ['groupData', function(groupData) {
return groupData;
}]
}],
UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q',
function(Rest, GetBasePath, $stateParams, $q) {
Rest.setUrl(GetBasePath('unified_jobs'));
var val = $q.defer();
Rest.options()
.then(function(data) {
val.resolve(data.data);
}, function(data) {
val.reject(data);
});
return val.promise;
}]
},
views: {
// clear form template when views render in this substate

View File

@ -11,7 +11,7 @@
$scope.item = group;
$scope.submitMode = $stateParams.groups === undefined ? 'move' : 'copy';
$scope['toggle_'+ list.iterator] = function(id){
$scope.toggle_row = function(id){
// toggle off anything else currently selected
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
// yoink the currently selected thing
@ -60,9 +60,6 @@
};
function init(){
var url = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/';
url += $stateParams.group ? '?not__id__in=' + group.id + ',' + _.last($stateParams.group) : '?not__id=' + group.id;
list.basePath = url;
$scope.atRootLevel = $stateParams.group ? false : true;
// search init

View File

@ -8,10 +8,10 @@
['$scope', '$state', '$stateParams', 'generateList', 'HostManageService', 'GetBasePath', 'CopyMoveGroupList', 'host', 'Dataset',
function($scope, $state, $stateParams, GenerateList, HostManageService, GetBasePath, CopyMoveGroupList, host, Dataset){
var list = CopyMoveGroupList;
$scope.item = host;
$scope.submitMode = 'copy';
$scope['toggle_'+ list.iterator] = function(id){
$scope.toggle_row = function(id){
// toggle off anything else currently selected
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
// yoink the currently selected thing

View File

@ -30,8 +30,8 @@ var copyMoveGroupRoute = {
resolve: {
Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', 'group',
function(list, qs, $stateParams, GetBasePath, group) {
$stateParams.copy_search.not__id__in = ($stateParams.group.length > 0 ? group.id + ',' + _.last($stateParams.group) : group.id);
let path = GetBasePath(list.name);
$stateParams.copy_search.not__id__in = ($stateParams.group && $stateParams.group.length > 0 ? group.id + ',' + _.last($stateParams.group) : group.id.toString());
let path = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/';
return qs.search(path, $stateParams.copy_search);
}
],
@ -66,7 +66,7 @@ var copyMoveHostRoute = {
resolve: {
Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = GetBasePath(list.name);
let path = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts/';
return qs.search(path, $stateParams.copy_search);
}
],
@ -80,7 +80,9 @@ var copyMoveHostRoute = {
controller: CopyMoveHostsController,
},
'copyMoveList@inventoryManage.copyMoveHost': {
templateProvider: function(CopyMoveGroupList, generateList) {
templateProvider: function(CopyMoveGroupList, generateList, $stateParams, GetBasePath) {
let list = CopyMoveGroupList;
list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts/';
let html = generateList.build({
list: CopyMoveGroupList,
mode: 'lookup',

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