mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 20:30:46 -03:30
Merge branch 'release_3.0.2' into devel
* release_3.0.2: (126 commits) Disable permissions tab in Credential > Edit form if Credential is private (#3288) Tweaked the popover text for job and skip tags on JT add/edit Workaround a cascade setnull polymorphic issue flake8 Fixed old test expectations Made it so the credential organization field can't be changed Skip some unit tests Fixed org auditor visibility of team credentials Fix sosreport fix credential kind options for list interpret any code below 300 as success bail when status code is over 300 Make CloudForms inventory_script work Use no_log when handling passwords Prevent ignored task from being displayed as failing. making ec2 cred optional on group->edit making ec2 credential optional for ec2 inventory Revert "Fix to ensure org auditors can see team credentials" Fixed team credential list to work with corrected permissions Making the username and password fields optional ...
This commit is contained in:
commit
8893f8278a
18
Makefile
18
Makefile
@ -689,30 +689,24 @@ reprepro: deb-build/$(DEB_NVRA).deb reprepro/conf
|
||||
amazon-ebs:
|
||||
cd packaging/packer && $(PACKER) build -only $@ $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
|
||||
|
||||
# virtualbox
|
||||
virtualbox-ovf: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box
|
||||
# Vagrant box using virtualbox provider
|
||||
vagrant-virtualbox: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box
|
||||
|
||||
packaging/packer/ansible-tower-$(VERSION)-virtualbox.box: packaging/packer/output-virtualbox-iso/centos-7.ovf
|
||||
cd packaging/packer && $(PACKER) build -only virtualbox-ovf $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
|
||||
|
||||
packaging/packer/output-virtualbox-iso/centos-6.ovf:
|
||||
cd packaging/packer && $(PACKER) build packer-centos-6.json
|
||||
|
||||
packaging/packer/output-virtualbox-iso/centos-7.ovf:
|
||||
cd packaging/packer && $(PACKER) build -only virtualbox-iso packer-centos-7.json
|
||||
|
||||
# virtualbox-iso: packaging/packer/output-virtualbox-iso/centos-6.ovf
|
||||
virtualbox-iso: packaging/packer/output-virtualbox-iso/centos-7.ovf
|
||||
|
||||
# vmware
|
||||
# Vagrant box using VMware provider
|
||||
vagrant-vmware: packaging/packer/ansible-tower-$(VERSION)-vmware.box
|
||||
|
||||
packaging/packer/output-vmware-iso/centos-7.vmx:
|
||||
cd packaging/packer && $(PACKER) build -only vmware-iso packer-centos-7.json
|
||||
|
||||
vmware-iso: packaging/packer/output-vmware-iso/centos-7.vmx
|
||||
|
||||
vmware-vmx: packaging/packer/ansible-tower-$(VERSION)-vmx/ansible-tower-$(VERSION).vmx
|
||||
|
||||
packaging/packer/ansible-tower-$(VERSION)-vmx/ansible-tower-$(VERSION).vmx: packaging/packer/output-vmware-iso/centos-7.vmx
|
||||
packaging/packer/ansible-tower-$(VERSION)-vmware.box: packaging/packer/output-vmware-iso/centos-7.vmx
|
||||
cd packaging/packer && $(PACKER) build -only vmware-vmx $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
|
||||
|
||||
# TODO - figure out how to build the front-end and python requirements with
|
||||
|
||||
@ -526,8 +526,10 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
serializer_class = InventorySourceSerializer
|
||||
elif isinstance(obj, JobTemplate):
|
||||
serializer_class = JobTemplateSerializer
|
||||
elif isinstance(obj, SystemJobTemplate):
|
||||
serializer_class = SystemJobTemplateSerializer
|
||||
if serializer_class:
|
||||
serializer = serializer_class(instance=obj)
|
||||
serializer = serializer_class(instance=obj, context=self.context)
|
||||
return serializer.to_representation(obj)
|
||||
else:
|
||||
return super(UnifiedJobTemplateSerializer, self).to_representation(obj)
|
||||
@ -590,7 +592,7 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
elif isinstance(obj, SystemJob):
|
||||
serializer_class = SystemJobSerializer
|
||||
if serializer_class:
|
||||
serializer = serializer_class(instance=obj)
|
||||
serializer = serializer_class(instance=obj, context=self.context)
|
||||
ret = serializer.to_representation(obj)
|
||||
else:
|
||||
ret = super(UnifiedJobSerializer, self).to_representation(obj)
|
||||
@ -637,7 +639,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
||||
elif isinstance(obj, SystemJob):
|
||||
serializer_class = SystemJobListSerializer
|
||||
if serializer_class:
|
||||
serializer = serializer_class(instance=obj)
|
||||
serializer = serializer_class(instance=obj, context=self.context)
|
||||
ret = serializer.to_representation(obj)
|
||||
else:
|
||||
ret = super(UnifiedJobListSerializer, self).to_representation(obj)
|
||||
@ -1285,7 +1287,8 @@ class CustomInventoryScriptSerializer(BaseSerializer):
|
||||
request = self.context.get('request', None)
|
||||
if request.user not in obj.admin_role and \
|
||||
not request.user.is_superuser and \
|
||||
not request.user.is_system_auditor:
|
||||
not request.user.is_system_auditor and \
|
||||
not (obj.organization is not None and request.user in obj.organization.auditor_role):
|
||||
ret['script'] = None
|
||||
return ret
|
||||
|
||||
@ -1713,19 +1716,21 @@ class CredentialSerializerCreate(CredentialSerializer):
|
||||
attrs.pop(field)
|
||||
if not owner_fields:
|
||||
raise serializers.ValidationError({"detail": "Missing 'user', 'team', or 'organization'."})
|
||||
elif len(owner_fields) > 1:
|
||||
raise serializers.ValidationError({"detail": "Expecting exactly one of 'user', 'team', or 'organization'."})
|
||||
|
||||
return super(CredentialSerializerCreate, self).validate(attrs)
|
||||
|
||||
def create(self, validated_data):
|
||||
user = validated_data.pop('user', None)
|
||||
team = validated_data.pop('team', None)
|
||||
if team:
|
||||
validated_data['organization'] = team.organization
|
||||
credential = super(CredentialSerializerCreate, self).create(validated_data)
|
||||
if user:
|
||||
credential.admin_role.members.add(user)
|
||||
if team:
|
||||
credential.admin_role.parents.add(team.member_role)
|
||||
if not credential.organization or team.organization.id != credential.organization.id:
|
||||
raise serializers.ValidationError({"detail": "Credential organization must be set and match before assigning to a team"})
|
||||
credential.admin_role.parents.add(team.admin_role)
|
||||
credential.use_role.parents.add(team.member_role)
|
||||
return credential
|
||||
|
||||
|
||||
@ -1823,7 +1828,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
class Meta:
|
||||
model = JobTemplate
|
||||
fields = ('*', 'host_config_key', 'ask_variables_on_launch', 'ask_limit_on_launch',
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch',
|
||||
'ask_tags_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch',
|
||||
'ask_credential_on_launch', 'survey_enabled', 'become_enabled', 'allow_simultaneous')
|
||||
|
||||
def get_related(self, obj):
|
||||
@ -1907,6 +1912,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
passwords_needed_to_start = serializers.ReadOnlyField()
|
||||
ask_variables_on_launch = serializers.ReadOnlyField()
|
||||
ask_limit_on_launch = serializers.ReadOnlyField()
|
||||
ask_skip_tags_on_launch = serializers.ReadOnlyField()
|
||||
ask_tags_on_launch = serializers.ReadOnlyField()
|
||||
ask_job_type_on_launch = serializers.ReadOnlyField()
|
||||
ask_inventory_on_launch = serializers.ReadOnlyField()
|
||||
@ -1915,8 +1921,8 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ('*', 'job_template', 'passwords_needed_to_start', 'ask_variables_on_launch',
|
||||
'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch')
|
||||
'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_skip_tags_on_launch',
|
||||
'ask_job_type_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(JobSerializer, self).get_related(obj)
|
||||
@ -1977,7 +1983,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
return ret
|
||||
if 'job_template' in ret and not obj.job_template:
|
||||
ret['job_template'] = None
|
||||
if obj.job_template and obj.job_template.survey_enabled and 'extra_vars' in ret:
|
||||
if 'extra_vars' in ret:
|
||||
ret['extra_vars'] = obj.display_extra_vars()
|
||||
return ret
|
||||
|
||||
@ -2281,14 +2287,15 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
|
||||
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
|
||||
'credential', 'ask_variables_on_launch', 'ask_tags_on_launch',
|
||||
'ask_job_type_on_launch', 'ask_limit_on_launch',
|
||||
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_limit_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch',
|
||||
'survey_enabled', 'variables_needed_to_start',
|
||||
'credential_needed_to_start', 'inventory_needed_to_start',
|
||||
'job_template_data', 'defaults')
|
||||
read_only_fields = ('ask_variables_on_launch', 'ask_limit_on_launch',
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch')
|
||||
read_only_fields = (
|
||||
'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch',
|
||||
'ask_skip_tags_on_launch', 'ask_job_type_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch')
|
||||
extra_kwargs = {
|
||||
'credential': {'write_only': True,},
|
||||
'limit': {'write_only': True,},
|
||||
@ -2675,6 +2682,8 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
fval = getattr(thisItem, field, None)
|
||||
if fval is not None:
|
||||
thisItemDict[field] = fval
|
||||
if fk == 'group':
|
||||
thisItemDict['inventory_id'] = getattr(thisItem, 'inventory_id', None)
|
||||
summary_fields[fk].append(thisItemDict)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
@ -8,6 +8,8 @@ The response will include the following fields:
|
||||
configured to prompt for variables upon launch (boolean, read-only)
|
||||
* `ask_tags_on_launch`: Flag indicating whether the job_template is
|
||||
configured to prompt for tags upon launch (boolean, read-only)
|
||||
* `ask_skip_tags_on_launch`: Flag indicating whether the job_template is
|
||||
configured to prompt for skip_tags upon launch (boolean, read-only)
|
||||
* `ask_job_type_on_launch`: Flag indicating whether the job_template is
|
||||
configured to prompt for job_type upon launch (boolean, read-only)
|
||||
* `ask_limit_on_launch`: Flag indicating whether the job_template is
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
@ -201,7 +200,7 @@ class ApiV1ConfigView(APIView):
|
||||
'''Return various sitewide configuration settings.'''
|
||||
|
||||
license_reader = TaskSerializer()
|
||||
license_data = license_reader.from_database(show_key=request.user.is_superuser)
|
||||
license_data = license_reader.from_database(show_key=request.user.is_superuser or request.user.is_system_auditor)
|
||||
if license_data and 'features' in license_data and 'activity_streams' in license_data['features']:
|
||||
license_data['features']['activity_streams'] &= tower_settings.ACTIVITY_STREAM_ENABLED
|
||||
|
||||
@ -225,7 +224,10 @@ class ApiV1ConfigView(APIView):
|
||||
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
|
||||
data['user_ldap_fields'] = user_ldap_fields
|
||||
|
||||
if request.user.is_superuser or Organization.accessible_objects(request.user, 'admin_role').exists():
|
||||
if request.user.is_superuser \
|
||||
or request.user.is_system_auditor \
|
||||
or Organization.accessible_objects(request.user, 'admin_role').exists() \
|
||||
or Organization.accessible_objects(request.user, 'auditor_role').exists():
|
||||
data.update(dict(
|
||||
project_base_dir = settings.PROJECTS_ROOT,
|
||||
project_local_paths = Project.get_local_path_choices(),
|
||||
@ -880,12 +882,19 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
|
||||
data = dict(msg="Role 'id' field is missing.")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
role = Role.objects.get(pk=sub_id)
|
||||
content_type = ContentType.objects.get_for_model(Organization)
|
||||
if role.content_type == content_type:
|
||||
role = get_object_or_400(Role, pk=sub_id)
|
||||
org_content_type = ContentType.objects.get_for_model(Organization)
|
||||
if role.content_type == org_content_type:
|
||||
data = dict(msg="You cannot assign an Organization role as a child role for a Team.")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
team = get_object_or_404(Team, pk=self.kwargs['pk'])
|
||||
credential_content_type = ContentType.objects.get_for_model(Credential)
|
||||
if role.content_type == credential_content_type:
|
||||
if not role.content_object.organization or role.content_object.organization.id != team.organization.id:
|
||||
data = dict(msg="You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return super(TeamRolesList, self).post(request, *args, **kwargs)
|
||||
|
||||
class TeamObjectRolesList(SubListAPIView):
|
||||
@ -1209,7 +1218,24 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if sub_id == self.request.user.admin_role.pk:
|
||||
raise PermissionDenied('You may not remove your own admin_role.')
|
||||
raise PermissionDenied('You may not perform any action with your own admin_role.')
|
||||
|
||||
user = get_object_or_400(User, pk=self.kwargs['pk'])
|
||||
role = get_object_or_400(Role, pk=sub_id)
|
||||
user_content_type = ContentType.objects.get_for_model(User)
|
||||
if role.content_type == user_content_type:
|
||||
raise PermissionDenied('You may not change the membership of a users admin_role')
|
||||
|
||||
credential_content_type = ContentType.objects.get_for_model(Credential)
|
||||
if role.content_type == credential_content_type:
|
||||
if role.content_object.organization and user not in role.content_object.organization.member_role:
|
||||
data = dict(msg="You cannot grant credential access to a user not in the credentials' organization")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not role.content_object.organization and not request.user.is_superuser:
|
||||
data = dict(msg="You cannot grant private credential access to another user")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
return super(UserRolesList, self).post(request, *args, **kwargs)
|
||||
|
||||
@ -1388,8 +1414,8 @@ class TeamCredentialsList(SubListCreateAPIView):
|
||||
self.check_parent_access(team)
|
||||
|
||||
visible_creds = Credential.accessible_objects(self.request.user, 'read_role')
|
||||
team_creds = Credential.objects.filter(admin_role__parents=team.member_role)
|
||||
return team_creds & visible_creds
|
||||
team_creds = Credential.objects.filter(Q(use_role__parents=team.member_role) | Q(admin_role__parents=team.member_role))
|
||||
return (team_creds & visible_creds).distinct()
|
||||
|
||||
|
||||
class OrganizationCredentialList(SubListCreateAPIView):
|
||||
@ -2975,7 +3001,17 @@ class JobJobTasksList(BaseJobEventsList):
|
||||
# and these are what we're interested in here.
|
||||
STARTING_EVENTS = ('playbook_on_task_start', 'playbook_on_setup')
|
||||
|
||||
queryset = JobEvent.start_event_queryset(parent_task, STARTING_EVENTS)
|
||||
# We need to pull information about each start event.
|
||||
#
|
||||
# This is super tricky, because this table has a one-to-many
|
||||
# relationship with itself (parent-child), and we're getting
|
||||
# information for an arbitrary number of children. This means we
|
||||
# need stats on grandchildren, sorted by child.
|
||||
queryset = (JobEvent.objects.filter(parent__parent=parent_task,
|
||||
parent__event__in=STARTING_EVENTS)
|
||||
.values('parent__id', 'event', 'changed', 'failed')
|
||||
.annotate(num=Count('event'))
|
||||
.order_by('parent__id'))
|
||||
|
||||
# The data above will come back in a list, but we are going to
|
||||
# want to access it based on the parent id, so map it into a
|
||||
@ -3034,10 +3070,13 @@ class JobJobTasksList(BaseJobEventsList):
|
||||
# make appropriate changes to the task data.
|
||||
for child_data in data.get(task_start_event.id, []):
|
||||
if child_data['event'] == 'runner_on_failed':
|
||||
task_data['failed'] = True
|
||||
task_data['host_count'] += child_data['num']
|
||||
task_data['reported_hosts'] += child_data['num']
|
||||
task_data['failed_count'] += child_data['num']
|
||||
if child_data['failed']:
|
||||
task_data['failed'] = True
|
||||
task_data['failed_count'] += child_data['num']
|
||||
else:
|
||||
task_data['skipped_count'] += child_data['num']
|
||||
elif child_data['event'] == 'runner_on_ok':
|
||||
task_data['host_count'] += child_data['num']
|
||||
task_data['reported_hosts'] += child_data['num']
|
||||
@ -3625,7 +3664,6 @@ class RoleDetail(RetrieveAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
|
||||
@ -3648,6 +3686,26 @@ class RoleUsersList(SubListCreateAttachDetachAPIView):
|
||||
if not sub_id:
|
||||
data = dict(msg="User 'id' field is missing.")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user = get_object_or_400(User, pk=sub_id)
|
||||
role = self.get_parent_object()
|
||||
if role == self.request.user.admin_role:
|
||||
raise PermissionDenied('You may not perform any action with your own admin_role.')
|
||||
|
||||
user_content_type = ContentType.objects.get_for_model(User)
|
||||
if role.content_type == user_content_type:
|
||||
raise PermissionDenied('You may not change the membership of a users admin_role')
|
||||
|
||||
credential_content_type = ContentType.objects.get_for_model(Credential)
|
||||
if role.content_type == credential_content_type:
|
||||
if role.content_object.organization and user not in role.content_object.organization.member_role:
|
||||
data = dict(msg="You cannot grant credential access to a user not in the credentials' organization")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not role.content_object.organization and not request.user.is_superuser:
|
||||
data = dict(msg="You cannot grant private credential access to another user")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return super(RoleUsersList, self).post(request, *args, **kwargs)
|
||||
|
||||
|
||||
@ -3672,13 +3730,20 @@ class RoleTeamsList(SubListAPIView):
|
||||
data = dict(msg="Team 'id' field is missing.")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
team = get_object_or_400(Team, pk=sub_id)
|
||||
role = Role.objects.get(pk=self.kwargs['pk'])
|
||||
content_type = ContentType.objects.get_for_model(Organization)
|
||||
if role.content_type == content_type:
|
||||
|
||||
organization_content_type = ContentType.objects.get_for_model(Organization)
|
||||
if role.content_type == organization_content_type:
|
||||
data = dict(msg="You cannot assign an Organization role as a child role for a Team.")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
team = Team.objects.get(pk=sub_id)
|
||||
credential_content_type = ContentType.objects.get_for_model(Credential)
|
||||
if role.content_type == credential_content_type:
|
||||
if not role.content_object.organization or role.content_object.organization.id != team.organization.id:
|
||||
data = dict(msg="You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization")
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
action = 'attach'
|
||||
if request.data.get('disassociate', None):
|
||||
action = 'unattach'
|
||||
|
||||
@ -428,8 +428,8 @@ class HostAccess(BaseAccess):
|
||||
return obj and self.user in obj.inventory.read_role
|
||||
|
||||
def can_add(self, data):
|
||||
if not data or 'inventory' not in data:
|
||||
return False
|
||||
if not data: # So the browseable API will work
|
||||
return Inventory.accessible_objects(self.user, 'admin_role').exists()
|
||||
|
||||
# Checks for admin or change permission on inventory.
|
||||
inventory_pk = get_pk_from_dict(data, 'inventory')
|
||||
@ -654,23 +654,14 @@ class CredentialAccess(BaseAccess):
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
# Check access to organizations
|
||||
organization_pk = get_pk_from_dict(data, 'organization')
|
||||
if data and 'organization' in data and organization_pk != getattr(obj, 'organization_id', None):
|
||||
if organization_pk:
|
||||
# admin permission to destination organization is mandatory
|
||||
new_organization_obj = get_object_or_400(Organization, pk=organization_pk)
|
||||
if self.user not in new_organization_obj.admin_role:
|
||||
return False
|
||||
# admin permission to existing organization is also mandatory
|
||||
if obj.organization:
|
||||
if self.user not in obj.organization.admin_role:
|
||||
return False
|
||||
|
||||
if obj.organization:
|
||||
if self.user in obj.organization.admin_role:
|
||||
return True
|
||||
# Cannot change the organization for a credential after it's been created
|
||||
if data and 'organization' in data:
|
||||
organization_pk = get_pk_from_dict(data, 'organization')
|
||||
if (organization_pk and (not obj.organization or organization_pk != obj.organization.id)) \
|
||||
or (not organization_pk and obj.organization):
|
||||
return False
|
||||
|
||||
print(self.user in obj.admin_role)
|
||||
return self.user in obj.admin_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
@ -981,8 +972,11 @@ class JobTemplateAccess(BaseAccess):
|
||||
field_whitelist = [
|
||||
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
|
||||
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch',
|
||||
'ask_credential_on_launch', 'survey_enabled'
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_skip_tags_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled',
|
||||
|
||||
# These fields are ignored, but it is convenient for QA to allow clients to post them
|
||||
'last_job_run', 'created', 'modified',
|
||||
]
|
||||
|
||||
for k, v in data.items():
|
||||
@ -1089,7 +1083,8 @@ class JobAccess(BaseAccess):
|
||||
def can_delete(self, obj):
|
||||
if obj.inventory is not None and self.user in obj.inventory.organization.admin_role:
|
||||
return True
|
||||
if obj.project is not None and self.user in obj.project.organization.admin_role:
|
||||
if (obj.project is not None and obj.project.organization is not None and
|
||||
self.user in obj.project.organization.admin_role):
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -1299,9 +1294,11 @@ class UnifiedJobTemplateAccess(BaseAccess):
|
||||
project_qs = self.user.get_queryset(Project).filter(scm_type__in=[s[0] for s in Project.SCM_TYPE_CHOICES])
|
||||
inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES)
|
||||
job_template_qs = self.user.get_queryset(JobTemplate)
|
||||
system_job_template_qs = self.user.get_queryset(SystemJobTemplate)
|
||||
qs = qs.filter(Q(Project___in=project_qs) |
|
||||
Q(InventorySource___in=inventory_source_qs) |
|
||||
Q(JobTemplate___in=job_template_qs))
|
||||
Q(JobTemplate___in=job_template_qs) |
|
||||
Q(systemjobtemplate__in=system_job_template_qs))
|
||||
qs = qs.select_related(
|
||||
'created_by',
|
||||
'modified_by',
|
||||
@ -1569,21 +1566,22 @@ class ActivityStreamAccess(BaseAccess):
|
||||
|
||||
inventory_set = Inventory.accessible_objects(self.user, 'read_role')
|
||||
credential_set = Credential.accessible_objects(self.user, 'read_role')
|
||||
organization_set = Organization.accessible_objects(self.user, 'read_role')
|
||||
admin_of_orgs = Organization.accessible_objects(self.user, 'admin_role')
|
||||
group_set = Group.objects.filter(inventory__in=inventory_set)
|
||||
auditing_orgs = (
|
||||
Organization.accessible_objects(self.user, 'admin_role') |
|
||||
Organization.accessible_objects(self.user, 'auditor_role')
|
||||
).distinct().values_list('id', flat=True)
|
||||
project_set = Project.accessible_objects(self.user, 'read_role')
|
||||
jt_set = JobTemplate.accessible_objects(self.user, 'read_role')
|
||||
team_set = Team.accessible_objects(self.user, 'read_role')
|
||||
|
||||
return qs.filter(
|
||||
Q(ad_hoc_command__inventory__in=inventory_set) |
|
||||
Q(user__in=organization_set.values('member_role__members')) |
|
||||
Q(user__in=auditing_orgs.values('member_role__members')) |
|
||||
Q(user=self.user) |
|
||||
Q(organization__in=organization_set) |
|
||||
Q(organization__in=auditing_orgs) |
|
||||
Q(inventory__in=inventory_set) |
|
||||
Q(host__inventory__in=inventory_set) |
|
||||
Q(group__in=group_set) |
|
||||
Q(group__inventory__in=inventory_set) |
|
||||
Q(inventory_source__inventory__in=inventory_set) |
|
||||
Q(inventory_update__inventory_source__inventory__in=inventory_set) |
|
||||
Q(credential__in=credential_set) |
|
||||
@ -1592,10 +1590,10 @@ class ActivityStreamAccess(BaseAccess):
|
||||
Q(project_update__project__in=project_set) |
|
||||
Q(job_template__in=jt_set) |
|
||||
Q(job__job_template__in=jt_set) |
|
||||
Q(notification_template__organization__in=admin_of_orgs) |
|
||||
Q(notification__notification_template__organization__in=admin_of_orgs) |
|
||||
Q(label__organization__in=organization_set) |
|
||||
Q(role__in=Role.visible_roles(self.user))
|
||||
Q(notification_template__organization__in=auditing_orgs) |
|
||||
Q(notification__notification_template__organization__in=auditing_orgs) |
|
||||
Q(label__organization__in=auditing_orgs) |
|
||||
Q(role__in=Role.visible_roles(self.user) if auditing_orgs else [])
|
||||
).distinct()
|
||||
|
||||
def can_add(self, data):
|
||||
@ -1667,14 +1665,8 @@ class RoleAccess(BaseAccess):
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return True
|
||||
|
||||
if obj.object_id:
|
||||
sister_roles = Role.objects.filter(
|
||||
content_type = obj.content_type,
|
||||
object_id = obj.object_id
|
||||
)
|
||||
else:
|
||||
sister_roles = obj
|
||||
return self.user.roles.filter(descendents__in=sister_roles).exists()
|
||||
return Role.filter_visible_roles(
|
||||
self.user, Role.objects.filter(pk=obj.id)).exists()
|
||||
|
||||
def can_add(self, obj, data):
|
||||
# Unsupported for now
|
||||
|
||||
20
awx/main/migrations/0029_v302_add_ask_skip_tags.py
Normal file
20
awx/main/migrations/0029_v302_add_ask_skip_tags.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import awx.main.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0028_v300_org_team_cascade'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_skip_tags_on_launch',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
20
awx/main/migrations/0030_v302_job_survey_passwords.py
Normal file
20
awx/main/migrations/0030_v302_job_survey_passwords.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0029_v302_add_ask_skip_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='survey_passwords',
|
||||
field=jsonfield.fields.JSONField(default={}, editable=False, blank=True),
|
||||
),
|
||||
]
|
||||
18
awx/main/migrations/0031_v302_migrate_survey_passwords.py
Normal file
18
awx/main/migrations/0031_v302_migrate_survey_passwords.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from awx.main.migrations import _save_password_keys
|
||||
from awx.main.migrations import _migration_utils as migration_utils
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0030_v302_job_survey_passwords'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||
migrations.RunPython(_save_password_keys.migrate_survey_passwords),
|
||||
]
|
||||
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.migrations import _migration_utils as migration_utils
|
||||
import awx.main.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0031_v302_migrate_survey_passwords'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_administrator', b'organization.admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='use_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.RunPython(rbac.rebuild_role_hierarchy),
|
||||
]
|
||||
27
awx/main/migrations/_save_password_keys.py
Normal file
27
awx/main/migrations/_save_password_keys.py
Normal file
@ -0,0 +1,27 @@
|
||||
def survey_password_variables(survey_spec):
|
||||
vars = []
|
||||
# Get variables that are type password
|
||||
if 'spec' not in survey_spec:
|
||||
return vars
|
||||
for survey_element in survey_spec['spec']:
|
||||
if 'type' in survey_element and survey_element['type'] == 'password':
|
||||
vars.append(survey_element['variable'])
|
||||
return vars
|
||||
|
||||
|
||||
def migrate_survey_passwords(apps, schema_editor):
|
||||
'''Take the output of the Job Template password list for all that
|
||||
have a survey enabled, and then save it into the job model.
|
||||
'''
|
||||
Job = apps.get_model('main', 'Job')
|
||||
for job in Job.objects.iterator():
|
||||
if not job.job_template:
|
||||
continue
|
||||
jt = job.job_template
|
||||
if jt.survey_spec is not None and jt.survey_enabled:
|
||||
password_list = survey_password_variables(jt.survey_spec)
|
||||
hide_password_dict = {}
|
||||
for password in password_list:
|
||||
hide_password_dict[password] = "$encrypted$"
|
||||
job.survey_passwords = hide_password_dict
|
||||
job.save()
|
||||
@ -215,11 +215,11 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
admin_role = ImplicitRoleField(
|
||||
parent_role=[
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
'organization.admin_role',
|
||||
],
|
||||
)
|
||||
use_role = ImplicitRoleField(
|
||||
parent_role=[
|
||||
'organization.admin_role',
|
||||
'admin_role',
|
||||
]
|
||||
)
|
||||
|
||||
@ -6,6 +6,7 @@ import hmac
|
||||
import json
|
||||
import yaml
|
||||
import logging
|
||||
import time
|
||||
from urlparse import urljoin
|
||||
|
||||
# Django
|
||||
@ -26,7 +27,7 @@ from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.models.notifications import NotificationTemplate
|
||||
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.redact import PlainTextCleaner, REPLACE_STR
|
||||
from awx.main.redact import PlainTextCleaner
|
||||
from awx.main.conf import tower_settings
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
@ -199,6 +200,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
blank=True,
|
||||
default=False,
|
||||
)
|
||||
ask_skip_tags_on_launch = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
)
|
||||
ask_job_type_on_launch = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
@ -244,7 +249,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
'playbook', 'credential', 'cloud_credential', 'network_credential', 'forks', 'schedule',
|
||||
'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type',
|
||||
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled',
|
||||
'labels',]
|
||||
'labels', 'survey_passwords']
|
||||
|
||||
def resource_validation_data(self):
|
||||
'''
|
||||
@ -418,7 +423,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
extra_vars=self.ask_variables_on_launch,
|
||||
limit=self.ask_limit_on_launch,
|
||||
job_tags=self.ask_tags_on_launch,
|
||||
skip_tags=self.ask_tags_on_launch,
|
||||
skip_tags=self.ask_skip_tags_on_launch,
|
||||
job_type=self.ask_job_type_on_launch,
|
||||
inventory=self.ask_inventory_on_launch,
|
||||
credential=self.ask_credential_on_launch
|
||||
@ -442,11 +447,21 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
if field == 'extra_vars' and self.survey_enabled and self.survey_spec:
|
||||
# Accept vars defined in the survey and no others
|
||||
survey_vars = [question['variable'] for question in self.survey_spec.get('spec', [])]
|
||||
for key in kwargs[field]:
|
||||
extra_vars = kwargs[field]
|
||||
if isinstance(extra_vars, basestring):
|
||||
try:
|
||||
extra_vars = json.loads(extra_vars)
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
extra_vars = yaml.safe_load(extra_vars)
|
||||
assert isinstance(extra_vars, dict)
|
||||
except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
|
||||
extra_vars = {}
|
||||
for key in extra_vars:
|
||||
if key in survey_vars:
|
||||
prompted_fields[field][key] = kwargs[field][key]
|
||||
prompted_fields[field][key] = extra_vars[key]
|
||||
else:
|
||||
ignored_fields[field][key] = kwargs[field][key]
|
||||
ignored_fields[field][key] = extra_vars[key]
|
||||
else:
|
||||
ignored_fields[field] = kwargs[field]
|
||||
|
||||
@ -509,6 +524,11 @@ class Job(UnifiedJob, JobOptions):
|
||||
editable=False,
|
||||
through='JobHostSummary',
|
||||
)
|
||||
survey_passwords = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
editable=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_parent_field_name(cls):
|
||||
@ -550,6 +570,12 @@ class Job(UnifiedJob, JobOptions):
|
||||
return self.job_template.ask_tags_on_launch
|
||||
return False
|
||||
|
||||
@property
|
||||
def ask_skip_tags_on_launch(self):
|
||||
if self.job_template is not None:
|
||||
return self.job_template.ask_skip_tags_on_launch
|
||||
return False
|
||||
|
||||
@property
|
||||
def ask_job_type_on_launch(self):
|
||||
if self.job_template is not None:
|
||||
@ -670,9 +696,17 @@ class Job(UnifiedJob, JobOptions):
|
||||
dependencies.append(source.create_inventory_update(launch_type='dependency'))
|
||||
return dependencies
|
||||
|
||||
def notification_data(self):
|
||||
def notification_data(self, block=5):
|
||||
data = super(Job, self).notification_data()
|
||||
all_hosts = {}
|
||||
# NOTE: Probably related to job event slowness, remove at some point -matburt
|
||||
if block:
|
||||
summaries = self.job_host_summaries.all()
|
||||
while block > 0 and not len(summaries):
|
||||
time.sleep(1)
|
||||
block -= 1
|
||||
else:
|
||||
summaries = self.job_host_summaries.all()
|
||||
for h in self.job_host_summaries.all():
|
||||
all_hosts[h.host_name] = dict(failed=h.failed,
|
||||
changed=h.changed,
|
||||
@ -711,16 +745,12 @@ class Job(UnifiedJob, JobOptions):
|
||||
'''
|
||||
Hides fields marked as passwords in survey.
|
||||
'''
|
||||
if self.extra_vars and self.job_template and self.job_template.survey_enabled:
|
||||
try:
|
||||
extra_vars = json.loads(self.extra_vars)
|
||||
for key in self.job_template.survey_password_variables():
|
||||
if key in extra_vars:
|
||||
extra_vars[key] = REPLACE_STR
|
||||
return json.dumps(extra_vars)
|
||||
except ValueError:
|
||||
pass
|
||||
return self.extra_vars
|
||||
if self.survey_passwords:
|
||||
extra_vars = json.loads(self.extra_vars)
|
||||
extra_vars.update(self.survey_passwords)
|
||||
return json.dumps(extra_vars)
|
||||
else:
|
||||
return self.extra_vars
|
||||
|
||||
def _survey_search_and_replace(self, content):
|
||||
# Use job template survey spec to identify password fields.
|
||||
|
||||
@ -32,7 +32,7 @@ from djcelery.models import TaskMeta
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.schedules import Schedule
|
||||
from awx.main.utils import decrypt_field, emit_websocket_notification, _inventory_updates
|
||||
from awx.main.redact import UriCleaner
|
||||
from awx.main.redact import UriCleaner, REPLACE_STR
|
||||
|
||||
__all__ = ['UnifiedJobTemplate', 'UnifiedJob']
|
||||
|
||||
@ -81,6 +81,9 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
|
||||
ALL_STATUS_CHOICES = OrderedDict(PROJECT_STATUS_CHOICES + INVENTORY_SOURCE_STATUS_CHOICES + JOB_TEMPLATE_STATUS_CHOICES + DEPRECATED_STATUS_CHOICES).items()
|
||||
|
||||
# NOTE: Working around a django-polymorphic issue: https://github.com/django-polymorphic/django-polymorphic/issues/229
|
||||
_base_manager = models.Manager()
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
unique_together = [('polymorphic_ctype', 'name')]
|
||||
@ -343,6 +346,14 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
create_kwargs[field_name] = getattr(self, field_name)
|
||||
new_kwargs = self._update_unified_job_kwargs(**create_kwargs)
|
||||
unified_job = unified_job_class(**new_kwargs)
|
||||
# For JobTemplate-based jobs with surveys, save list for perma-redaction
|
||||
if (hasattr(self, 'survey_spec') and getattr(self, 'survey_enabled', False) and
|
||||
not getattr(unified_job, 'survey_passwords', False)):
|
||||
password_list = self.survey_password_variables()
|
||||
hide_password_dict = {}
|
||||
for password in password_list:
|
||||
hide_password_dict[password] = REPLACE_STR
|
||||
unified_job.survey_passwords = hide_password_dict
|
||||
unified_job.save()
|
||||
for field_name, src_field_value in m2m_fields.iteritems():
|
||||
dest_field = getattr(unified_job, field_name)
|
||||
@ -367,6 +378,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
|
||||
PASSWORD_FIELDS = ('start_args',)
|
||||
|
||||
# NOTE: Working around a django-polymorphic issue: https://github.com/django-polymorphic/django-polymorphic/issues/229
|
||||
_base_manager = models.Manager()
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
|
||||
|
||||
@ -214,15 +214,18 @@ def handle_work_success(self, result, task_actual):
|
||||
friendly_name = "System Job"
|
||||
else:
|
||||
return
|
||||
notification_body = instance.notification_data()
|
||||
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
|
||||
task_actual['id'],
|
||||
smart_str(instance_name),
|
||||
notification_body['url'])
|
||||
notification_body['friendly_name'] = friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
for n in set(notification_templates.get('success', []) + notification_templates.get('any', []))],
|
||||
job_id=task_actual['id'])
|
||||
|
||||
all_notification_templates = set(notification_templates.get('success', []) + notification_templates.get('any', []))
|
||||
if len(all_notification_templates):
|
||||
notification_body = instance.notification_data()
|
||||
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
|
||||
task_actual['id'],
|
||||
smart_str(instance_name),
|
||||
notification_body['url'])
|
||||
notification_body['friendly_name'] = friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
for n in all_notification_templates],
|
||||
job_id=task_actual['id'])
|
||||
|
||||
@task(bind=True)
|
||||
def handle_work_error(self, task_id, subtasks=None):
|
||||
@ -277,15 +280,18 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
(first_task_type, first_task_name, first_task_id)
|
||||
instance.save()
|
||||
instance.socketio_emit_status("failed")
|
||||
notification_body = first_task.notification_data()
|
||||
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name,
|
||||
first_task_id,
|
||||
smart_str(first_task_name),
|
||||
notification_body['url'])
|
||||
notification_body['friendly_name'] = first_task_friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
for n in set(notification_templates.get('error', []) + notification_templates.get('any', []))],
|
||||
job_id=first_task_id)
|
||||
|
||||
all_notification_templates = set(notification_templates.get('error', []) + notification_templates.get('any', []))
|
||||
if len(all_notification_templates):
|
||||
notification_body = first_task.notification_data()
|
||||
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name,
|
||||
first_task_id,
|
||||
smart_str(first_task_name),
|
||||
notification_body['url'])
|
||||
notification_body['friendly_name'] = first_task_friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
for n in all_notification_templates],
|
||||
job_id=first_task_id)
|
||||
|
||||
|
||||
@task()
|
||||
|
||||
@ -26,14 +26,14 @@ def survey_spec_factory():
|
||||
return create_survey_spec
|
||||
|
||||
@pytest.fixture
|
||||
def job_with_secret_key_factory(job_template_factory):
|
||||
def job_template_with_survey_passwords_factory(job_template_factory):
|
||||
def rf(persisted):
|
||||
"Returns job with linked JT survey with password survey questions"
|
||||
objects = job_template_factory('jt', organization='org1', survey=[
|
||||
{'variable': 'submitter_email', 'type': 'text', 'default': 'foobar@redhat.com'},
|
||||
{'variable': 'secret_key', 'default': '6kQngg3h8lgiSTvIEb21', 'type': 'password'},
|
||||
{'variable': 'SSN', 'type': 'password'}], jobs=[1], persisted=persisted)
|
||||
return objects.jobs[1]
|
||||
{'variable': 'SSN', 'type': 'password'}], persisted=persisted)
|
||||
return objects.job_template
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
@ -43,3 +43,7 @@ def job_with_secret_key_unit(job_with_secret_key_factory):
|
||||
@pytest.fixture
|
||||
def get_ssh_version(mocker):
|
||||
return mocker.patch('awx.main.tasks.get_ssh_version', return_value='OpenSSH_6.9p1, LibreSSL 2.1.8')
|
||||
|
||||
@pytest.fixture
|
||||
def job_template_with_survey_passwords_unit(job_template_with_survey_passwords_factory):
|
||||
return job_template_with_survey_passwords_factory(persisted=False)
|
||||
|
||||
@ -68,9 +68,10 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob
|
||||
#
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_team_credential(post, get, team, org_admin, team_member):
|
||||
def test_create_team_credential(post, get, team, organization, org_admin, team_member):
|
||||
response = post(reverse('api:credential_list'), {
|
||||
'team': team.id,
|
||||
'organization': organization.id,
|
||||
'name': 'Some name',
|
||||
'username': 'someusername'
|
||||
}, org_admin)
|
||||
@ -94,25 +95,159 @@ def test_create_team_credential_via_team_credentials_list(post, get, team, org_a
|
||||
assert response.data['count'] == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_team_credential_by_urelated_user_xfail(post, team, alice, team_member):
|
||||
def test_create_team_credential_by_urelated_user_xfail(post, team, organization, alice, team_member):
|
||||
response = post(reverse('api:credential_list'), {
|
||||
'team': team.id,
|
||||
'organization': organization.id,
|
||||
'name': 'Some name',
|
||||
'username': 'someusername'
|
||||
}, alice)
|
||||
assert response.status_code == 403
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_team_credential_by_team_member_xfail(post, team, alice, team_member):
|
||||
def test_create_team_credential_by_team_member_xfail(post, team, organization, alice, team_member):
|
||||
# Members can't add credentials, only org admins.. for now?
|
||||
response = post(reverse('api:credential_list'), {
|
||||
'team': team.id,
|
||||
'organization': organization.id,
|
||||
'name': 'Some name',
|
||||
'username': 'someusername'
|
||||
}, team_member)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
#
|
||||
# Permission granting
|
||||
#
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_org_credential_to_org_user_through_role_users(post, credential, organization, org_admin, org_member):
|
||||
credential.organization = organization
|
||||
credential.save()
|
||||
response = post(reverse('api:role_users_list', args=(credential.use_role.id,)), {
|
||||
'id': org_member.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_org_credential_to_org_user_through_user_roles(post, credential, organization, org_admin, org_member):
|
||||
credential.organization = organization
|
||||
credential.save()
|
||||
response = post(reverse('api:user_roles_list', args=(org_member.id,)), {
|
||||
'id': credential.use_role.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_org_credential_to_non_org_user_through_role_users(post, credential, organization, org_admin, alice):
|
||||
credential.organization = organization
|
||||
credential.save()
|
||||
response = post(reverse('api:role_users_list', args=(credential.use_role.id,)), {
|
||||
'id': alice.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_org_credential_to_non_org_user_through_user_roles(post, credential, organization, org_admin, alice):
|
||||
credential.organization = organization
|
||||
credential.save()
|
||||
response = post(reverse('api:user_roles_list', args=(alice.id,)), {
|
||||
'id': credential.use_role.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob):
|
||||
# normal users can't do this
|
||||
credential.admin_role.members.add(alice)
|
||||
response = post(reverse('api:role_users_list', args=(credential.use_role.id,)), {
|
||||
'id': bob.id
|
||||
}, alice)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_private_credential_to_org_user_through_role_users(post, credential, org_admin, org_member):
|
||||
# org admins can't either
|
||||
credential.admin_role.members.add(org_admin)
|
||||
response = post(reverse('api:role_users_list', args=(credential.use_role.id,)), {
|
||||
'id': org_member.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sa_grant_private_credential_to_user_through_role_users(post, credential, admin, bob):
|
||||
# but system admins can
|
||||
response = post(reverse('api:role_users_list', args=(credential.use_role.id,)), {
|
||||
'id': bob.id
|
||||
}, admin)
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob):
|
||||
# normal users can't do this
|
||||
credential.admin_role.members.add(alice)
|
||||
response = post(reverse('api:user_roles_list', args=(bob.id,)), {
|
||||
'id': credential.use_role.id
|
||||
}, alice)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_private_credential_to_org_user_through_user_roles(post, credential, org_admin, org_member):
|
||||
# org admins can't either
|
||||
credential.admin_role.members.add(org_admin)
|
||||
response = post(reverse('api:user_roles_list', args=(org_member.id,)), {
|
||||
'id': credential.use_role.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sa_grant_private_credential_to_user_through_user_roles(post, credential, admin, bob):
|
||||
# but system admins can
|
||||
response = post(reverse('api:user_roles_list', args=(bob.id,)), {
|
||||
'id': credential.use_role.id
|
||||
}, admin)
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_org_credential_to_team_through_role_teams(post, credential, organization, org_admin, org_auditor, team):
|
||||
assert org_auditor not in credential.read_role
|
||||
credential.organization = organization
|
||||
credential.save()
|
||||
response = post(reverse('api:role_teams_list', args=(credential.use_role.id,)), {
|
||||
'id': team.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 204
|
||||
assert org_auditor in credential.read_role
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_grant_org_credential_to_team_through_team_roles(post, credential, organization, org_admin, org_auditor, team):
|
||||
assert org_auditor not in credential.read_role
|
||||
credential.organization = organization
|
||||
credential.save()
|
||||
response = post(reverse('api:team_roles_list', args=(team.id,)), {
|
||||
'id': credential.use_role.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 204
|
||||
assert org_auditor in credential.read_role
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sa_grant_private_credential_to_team_through_role_teams(post, credential, admin, team):
|
||||
# not even a system admin can grant a private cred to a team though
|
||||
response = post(reverse('api:role_teams_list', args=(credential.use_role.id,)), {
|
||||
'id': team.id
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sa_grant_private_credential_to_team_through_team_roles(post, credential, admin, team):
|
||||
# not even a system admin can grant a private cred to a team though
|
||||
response = post(reverse('api:role_teams_list', args=(team.id,)), {
|
||||
'id': credential.use_role.id
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
# organization credentials
|
||||
@ -177,6 +312,37 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me
|
||||
assert response.data['count'] == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cant_change_organization(patch, credential, organization, org_admin):
|
||||
credential.organization = organization
|
||||
credential.save()
|
||||
|
||||
response = patch(reverse('api:credential_detail', args=(organization.id,)), {
|
||||
'name': 'Some new name',
|
||||
}, org_admin)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = patch(reverse('api:credential_detail', args=(organization.id,)), {
|
||||
'name': 'Some new name2',
|
||||
'organization': organization.id, # fine for it to be the same
|
||||
}, org_admin)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = patch(reverse('api:credential_detail', args=(organization.id,)), {
|
||||
'name': 'Some new name3',
|
||||
'organization': None
|
||||
}, org_admin)
|
||||
assert response.status_code == 403
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cant_add_organization(patch, credential, organization, org_admin):
|
||||
assert credential.organization is None
|
||||
response = patch(reverse('api:credential_detail', args=(organization.id,)), {
|
||||
'name': 'Some new name',
|
||||
'organization': organization.id
|
||||
}, org_admin)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
#
|
||||
# Openstack Credentials
|
||||
@ -224,33 +390,3 @@ def test_create_credential_missing_user_team_org_xfail(post, admin):
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_with_user_and_org_xfail(post, organization, admin):
|
||||
# Can only specify one of user, team, or organization
|
||||
response = post(reverse('api:credential_list'), {
|
||||
'name': 'Some name',
|
||||
'username': 'someusername',
|
||||
'user': admin.id,
|
||||
'organization': organization.id,
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_with_team_and_org_xfail(post, organization, team, admin):
|
||||
response = post(reverse('api:credential_list'), {
|
||||
'name': 'Some name',
|
||||
'username': 'someusername',
|
||||
'organization': organization.id,
|
||||
'team': team.id,
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_credential_with_user_and_team_xfail(post, team, admin):
|
||||
response = post(reverse('api:credential_list'), {
|
||||
'name': 'Some name',
|
||||
'username': 'someusername',
|
||||
'user': admin.id,
|
||||
'team': team.id,
|
||||
}, admin)
|
||||
assert response.status_code == 400
|
||||
|
||||
@ -37,6 +37,7 @@ def job_template_prompts(project, inventory, machine_credential):
|
||||
name='deploy-job-template',
|
||||
ask_variables_on_launch=on_off,
|
||||
ask_tags_on_launch=on_off,
|
||||
ask_skip_tags_on_launch=on_off,
|
||||
ask_job_type_on_launch=on_off,
|
||||
ask_inventory_on_launch=on_off,
|
||||
ask_limit_on_launch=on_off,
|
||||
@ -54,6 +55,7 @@ def job_template_prompts_null(project):
|
||||
name='deploy-job-template',
|
||||
ask_variables_on_launch=True,
|
||||
ask_tags_on_launch=True,
|
||||
ask_skip_tags_on_launch=True,
|
||||
ask_job_type_on_launch=True,
|
||||
ask_inventory_on_launch=True,
|
||||
ask_limit_on_launch=True,
|
||||
@ -113,6 +115,20 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, admi
|
||||
|
||||
mock_job.signal_start.assert_called_once_with(**runtime_data)
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
def test_job_accept_null_tags(job_template_prompts, post, admin_user, mocker):
|
||||
job_template = job_template_prompts(True)
|
||||
|
||||
mock_job = mocker.MagicMock(spec=Job, id=968)
|
||||
|
||||
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job', return_value=mock_job):
|
||||
with mocker.patch('awx.api.serializers.JobSerializer.to_representation'):
|
||||
post(reverse('api:job_template_launch', args=[job_template.pk]),
|
||||
{'job_tags': '', 'skip_tags': ''}, admin_user, expect=201)
|
||||
|
||||
mock_job.signal_start.assert_called_once_with(job_tags='', skip_tags='')
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, post, rando, mocker):
|
||||
|
||||
@ -3,12 +3,14 @@ import mock
|
||||
|
||||
# AWX
|
||||
from awx.api.serializers import JobTemplateSerializer, JobLaunchSerializer
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models.jobs import JobTemplate, Job
|
||||
from awx.main.models.projects import ProjectOptions
|
||||
from awx.main.migrations import _save_password_keys as save_password_keys
|
||||
|
||||
# Django
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.apps import apps
|
||||
|
||||
@property
|
||||
def project_playbooks(self):
|
||||
@ -103,9 +105,10 @@ def test_edit_nonsenstive(patch, job_template_factory, alice):
|
||||
'extra_vars': '--',
|
||||
'job_tags': 'sometags',
|
||||
'force_handlers': True,
|
||||
'skip_tags': True,
|
||||
'skip_tags': 'thistag,thattag',
|
||||
'ask_variables_on_launch':True,
|
||||
'ask_tags_on_launch':True,
|
||||
'ask_skip_tags_on_launch':True,
|
||||
'ask_job_type_on_launch':True,
|
||||
'ask_inventory_on_launch':True,
|
||||
'ask_credential_on_launch': True,
|
||||
@ -347,3 +350,20 @@ def test_disallow_template_delete_on_running_job(job_template_factory, delete, a
|
||||
objects.job_template.create_unified_job()
|
||||
delete_response = delete(reverse('api:job_template_detail', args=[objects.job_template.pk]), user=admin_user)
|
||||
assert delete_response.status_code == 409
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_save_survey_passwords_to_job(job_template_with_survey_passwords):
|
||||
"""Test that when a new job is created, the survey_passwords field is
|
||||
given all of the passwords that exist in the JT survey"""
|
||||
job = job_template_with_survey_passwords.create_unified_job()
|
||||
assert job.survey_passwords == {'SSN': '$encrypted$', 'secret_key': '$encrypted$'}
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_save_survey_passwords_on_migration(job_template_with_survey_passwords):
|
||||
"""Test that when upgrading to 3.0.2, the jobs connected to a JT that has
|
||||
a survey with passwords in it, the survey passwords get saved to the
|
||||
job survey_passwords field."""
|
||||
Job.objects.create(job_template=job_template_with_survey_passwords)
|
||||
save_password_keys.migrate_survey_passwords(apps, None)
|
||||
job = job_template_with_survey_passwords.jobs.all()[0]
|
||||
assert job.survey_passwords == {'SSN': '$encrypted$', 'secret_key': '$encrypted$'}
|
||||
|
||||
@ -193,7 +193,8 @@ def test_launch_with_non_empty_survey_spec_no_license(job_template_factory, post
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.survey
|
||||
def test_redact_survey_passwords_in_activity_stream(job_with_secret_key):
|
||||
def test_redact_survey_passwords_in_activity_stream(job_template_with_survey_passwords):
|
||||
job_template_with_survey_passwords.create_unified_job()
|
||||
AS_record = ActivityStream.objects.filter(object1='job').all()[0]
|
||||
changes_dict = json.loads(AS_record.changes)
|
||||
extra_vars = json.loads(changes_dict['extra_vars'])
|
||||
|
||||
@ -160,7 +160,7 @@ def organization(instance):
|
||||
|
||||
@pytest.fixture
|
||||
def credential():
|
||||
return Credential.objects.create(kind='aws', name='test-cred')
|
||||
return Credential.objects.create(kind='aws', name='test-cred', username='something', password='secret')
|
||||
|
||||
@pytest.fixture
|
||||
def machine_credential():
|
||||
@ -168,7 +168,7 @@ def machine_credential():
|
||||
|
||||
@pytest.fixture
|
||||
def org_credential(organization):
|
||||
return Credential.objects.create(kind='aws', name='test-cred', organization=organization)
|
||||
return Credential.objects.create(kind='aws', name='test-cred', username='something', password='secret', organization=organization)
|
||||
|
||||
@pytest.fixture
|
||||
def inventory(organization):
|
||||
@ -206,8 +206,8 @@ def notification(notification_template):
|
||||
subject='email subject')
|
||||
|
||||
@pytest.fixture
|
||||
def job_with_secret_key(job_with_secret_key_factory):
|
||||
return job_with_secret_key_factory(persisted=True)
|
||||
def job_template_with_survey_passwords(job_template_with_survey_passwords_factory):
|
||||
return job_template_with_survey_passwords_factory(persisted=True)
|
||||
|
||||
@pytest.fixture
|
||||
def admin(user):
|
||||
|
||||
@ -133,29 +133,6 @@ def test_org_credential_access_member(alice, org_credential, credential):
|
||||
'description': 'New description.',
|
||||
'organization': None})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_access_org_permissions(
|
||||
org_admin, org_member, organization, org_credential, credential):
|
||||
credential.admin_role.members.add(org_admin)
|
||||
credential.admin_role.members.add(org_member)
|
||||
org_credential.admin_role.members.add(org_member)
|
||||
|
||||
access = CredentialAccess(org_admin)
|
||||
member_access = CredentialAccess(org_member)
|
||||
|
||||
# Org admin can move their own credential into their org
|
||||
assert access.can_change(credential, {'organization': organization.pk})
|
||||
# Org member can not
|
||||
assert not member_access.can_change(credential, {
|
||||
'organization': organization.pk})
|
||||
|
||||
# Org admin can remove a credential from their org
|
||||
assert access.can_change(org_credential, {'organization': None})
|
||||
# Org member can not
|
||||
assert not member_access.can_change(org_credential, {'organization': None})
|
||||
assert not member_access.can_change(org_credential, {
|
||||
'user': org_member.pk, 'organization': None})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_job_template_xfail(user, deploy_jobtemplate):
|
||||
' Personal credential migration '
|
||||
@ -248,7 +225,6 @@ def test_single_cred_multi_job_template_multi_org(user, organizations, credentia
|
||||
orgs[0].admin_role.members.add(a)
|
||||
orgs[1].admin_role.members.add(a)
|
||||
|
||||
access = CredentialAccess(a)
|
||||
rbac.migrate_credential(apps, None)
|
||||
|
||||
for jt in jts:
|
||||
@ -256,11 +232,6 @@ def test_single_cred_multi_job_template_multi_org(user, organizations, credentia
|
||||
credential.refresh_from_db()
|
||||
|
||||
assert jts[0].credential != jts[1].credential
|
||||
assert access.can_change(jts[0].credential, {'organization': org.pk})
|
||||
assert access.can_change(jts[1].credential, {'organization': org.pk})
|
||||
|
||||
orgs[0].admin_role.members.remove(a)
|
||||
assert not access.can_change(jts[0].credential, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_inventory_source(user, inventory, credential):
|
||||
|
||||
@ -92,6 +92,12 @@ def test_null_related_delete_denied(normal_job, rando):
|
||||
access = JobAccess(rando)
|
||||
assert not access.can_delete(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_job_with_orphan_proj(normal_job, rando):
|
||||
normal_job.project.organization = None
|
||||
access = JobAccess(rando)
|
||||
assert not access.can_delete(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_org_admin_delete_allowed(normal_job, org_admin):
|
||||
normal_job.project = None # do this so we test job->inventory->org->admin connection
|
||||
|
||||
102
awx/main/tests/unit/api/test_roles.py
Normal file
102
awx/main/tests/unit/api/test_roles.py
Normal file
@ -0,0 +1,102 @@
|
||||
import mock
|
||||
from mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.test import force_authenticate
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from awx.api.views import (
|
||||
RoleUsersList,
|
||||
UserRolesList,
|
||||
TeamRolesList,
|
||||
)
|
||||
|
||||
from awx.main.models import (
|
||||
User,
|
||||
Role,
|
||||
)
|
||||
|
||||
@pytest.mark.skip(reason="Seeing pk error, suspect weirdness in mocking requests")
|
||||
@pytest.mark.parametrize("pk, err", [
|
||||
(111, "not change the membership"),
|
||||
(1, "may not perform"),
|
||||
])
|
||||
def test_user_roles_list_user_admin_role(pk, err):
|
||||
with mock.patch('awx.api.views.get_object_or_400') as role_get, \
|
||||
mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get:
|
||||
|
||||
role_mock = mock.MagicMock(spec=Role, id=1, pk=1)
|
||||
content_type_mock = mock.MagicMock(spec=ContentType)
|
||||
role_mock.content_type = content_type_mock
|
||||
role_get.return_value = role_mock
|
||||
ct_get.return_value = content_type_mock
|
||||
|
||||
with mock.patch('awx.api.views.User.admin_role', new_callable=PropertyMock, return_value=role_mock):
|
||||
factory = APIRequestFactory()
|
||||
view = UserRolesList.as_view()
|
||||
|
||||
user = User(username="root", is_superuser=True)
|
||||
|
||||
request = factory.post("/user/1/roles", {'id':pk}, format="json")
|
||||
force_authenticate(request, user)
|
||||
|
||||
response = view(request)
|
||||
response.render()
|
||||
|
||||
assert response.status_code == 403
|
||||
assert err in response.content
|
||||
|
||||
@pytest.mark.skip(reason="db access or mocking needed for new tests in role assignment code")
|
||||
@pytest.mark.parametrize("admin_role, err", [
|
||||
(True, "may not perform"),
|
||||
(False, "not change the membership"),
|
||||
])
|
||||
def test_role_users_list_other_user_admin_role(admin_role, err):
|
||||
with mock.patch('awx.api.views.RoleUsersList.get_parent_object') as role_get, \
|
||||
mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get:
|
||||
|
||||
role_mock = mock.MagicMock(spec=Role, id=1)
|
||||
content_type_mock = mock.MagicMock(spec=ContentType)
|
||||
role_mock.content_type = content_type_mock
|
||||
role_get.return_value = role_mock
|
||||
ct_get.return_value = content_type_mock
|
||||
|
||||
user_admin_role = role_mock if admin_role else None
|
||||
with mock.patch('awx.api.views.User.admin_role', new_callable=PropertyMock, return_value=user_admin_role):
|
||||
factory = APIRequestFactory()
|
||||
view = RoleUsersList.as_view()
|
||||
|
||||
user = User(username="root", is_superuser=True, pk=1, id=1)
|
||||
request = factory.post("/role/1/users", {'id':1}, format="json")
|
||||
force_authenticate(request, user)
|
||||
|
||||
response = view(request)
|
||||
response.render()
|
||||
|
||||
assert response.status_code == 403
|
||||
assert err in response.content
|
||||
|
||||
def test_team_roles_list_post_org_roles():
|
||||
with mock.patch('awx.api.views.get_object_or_400') as role_get, \
|
||||
mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get:
|
||||
|
||||
role_mock = mock.MagicMock(spec=Role)
|
||||
content_type_mock = mock.MagicMock(spec=ContentType)
|
||||
role_mock.content_type = content_type_mock
|
||||
role_get.return_value = role_mock
|
||||
ct_get.return_value = content_type_mock
|
||||
|
||||
factory = APIRequestFactory()
|
||||
view = TeamRolesList.as_view()
|
||||
|
||||
request = factory.post("/team/1/roles", {'id':1}, format="json")
|
||||
force_authenticate(request, User(username="root", is_superuser=True))
|
||||
|
||||
response = view(request)
|
||||
response.render()
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'cannot assign' in response.content
|
||||
@ -1,22 +1,11 @@
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.test import force_authenticate
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from awx.api.views import (
|
||||
ApiV1RootView,
|
||||
TeamRolesList,
|
||||
JobTemplateLabelList,
|
||||
)
|
||||
|
||||
from awx.main.models import (
|
||||
User,
|
||||
Role,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_response_new(mocker):
|
||||
m = mocker.patch('awx.api.views.Response.__new__')
|
||||
@ -68,30 +57,6 @@ class TestJobTemplateLabelList:
|
||||
with mock.patch('awx.api.generics.DeleteLastUnattachLabelMixin.unattach') as mixin_unattach:
|
||||
view = JobTemplateLabelList()
|
||||
mock_request = mock.MagicMock()
|
||||
|
||||
|
||||
super(JobTemplateLabelList, view).unattach(mock_request, None, None)
|
||||
assert mixin_unattach.called_with(mock_request, None, None)
|
||||
|
||||
@pytest.mark.parametrize("url", ["/team/1/roles", "/role/1/teams"])
|
||||
def test_team_roles_list_post_org_roles(url):
|
||||
with mock.patch('awx.api.views.Role.objects.get') as role_get, \
|
||||
mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get:
|
||||
|
||||
role_mock = mock.MagicMock(spec=Role)
|
||||
content_type_mock = mock.MagicMock(spec=ContentType)
|
||||
role_mock.content_type = content_type_mock
|
||||
role_get.return_value = role_mock
|
||||
ct_get.return_value = content_type_mock
|
||||
|
||||
factory = APIRequestFactory()
|
||||
view = TeamRolesList.as_view()
|
||||
|
||||
request = factory.post(url, {'id':1}, format="json")
|
||||
force_authenticate(request, User(username="root", is_superuser=True))
|
||||
|
||||
response = view(request)
|
||||
response.render()
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'cannot assign' in response.content
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
import json
|
||||
|
||||
|
||||
def test_missing_project_error(job_template_factory):
|
||||
@ -34,7 +35,18 @@ def test_inventory_credential_contradictions(job_template_factory):
|
||||
assert 'inventory' in validation_errors
|
||||
assert 'credential' in validation_errors
|
||||
|
||||
def test_survey_answers_as_string(job_template_factory):
|
||||
objects = job_template_factory(
|
||||
'job-template-with-survey',
|
||||
survey=['var1'],
|
||||
persisted=False)
|
||||
jt = objects.job_template
|
||||
user_extra_vars = json.dumps({'var1': 'asdf'})
|
||||
accepted, ignored = jt._accept_or_ignore_job_kwargs(extra_vars=user_extra_vars)
|
||||
assert 'var1' in accepted['extra_vars']
|
||||
|
||||
@pytest.mark.survey
|
||||
def test_survey_password_list(job_with_secret_key_unit):
|
||||
"""Verify that survey_password_variables method gives a list of survey passwords"""
|
||||
assert job_with_secret_key_unit.job_template.survey_password_variables() == ['secret_key', 'SSN']
|
||||
def test_job_template_survey_password_redaction(job_template_with_survey_passwords_unit):
|
||||
"""Tests the JobTemplate model's funciton to redact passwords from
|
||||
extra_vars - used when creating a new job"""
|
||||
assert job_template_with_survey_passwords_unit.survey_password_variables() == ['secret_key', 'SSN']
|
||||
|
||||
@ -2,6 +2,7 @@ import pytest
|
||||
import json
|
||||
|
||||
from awx.main.tasks import RunJob
|
||||
from awx.main.models import Job
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -14,9 +15,19 @@ def job(mocker):
|
||||
'launch_type': 'manual'})
|
||||
|
||||
@pytest.mark.survey
|
||||
def test_job_redacted_extra_vars(job_with_secret_key_unit):
|
||||
"""Verify that this method redacts vars marked as passwords in a survey"""
|
||||
assert json.loads(job_with_secret_key_unit.display_extra_vars()) == {
|
||||
def test_job_survey_password_redaction():
|
||||
"""Tests the Job model's funciton to redact passwords from
|
||||
extra_vars - used when displaying job information"""
|
||||
job = Job(
|
||||
name="test-job-with-passwords",
|
||||
extra_vars=json.dumps({
|
||||
'submitter_email': 'foobar@redhat.com',
|
||||
'secret_key': '6kQngg3h8lgiSTvIEb21',
|
||||
'SSN': '123-45-6789'}),
|
||||
survey_passwords={
|
||||
'secret_key': '$encrypted$',
|
||||
'SSN': '$encrypted$'})
|
||||
assert json.loads(job.display_extra_vars()) == {
|
||||
'submitter_email': 'foobar@redhat.com',
|
||||
'secret_key': '$encrypted$',
|
||||
'SSN': '$encrypted$'}
|
||||
|
||||
@ -64,8 +64,8 @@ def test_net_cred_ssh_agent(mocker, get_ssh_version):
|
||||
'credential': None, 'cloud_credential': None, 'network_credential': Credential(**options),
|
||||
'become_enabled': False, 'become_method': None, 'become_username': None,
|
||||
'inventory': mocker.MagicMock(spec=Inventory, id=2), 'force_handlers': False,
|
||||
'limit': None, 'verbosity': None, 'job_tags': None, 'skip_tags': False,
|
||||
'start_at_task': False, 'pk': 1, 'launch_type': 'normal', 'job_template':None,
|
||||
'limit': None, 'verbosity': None, 'job_tags': None, 'skip_tags': None,
|
||||
'start_at_task': None, 'pk': 1, 'launch_type': 'normal', 'job_template':None,
|
||||
'created_by': None, 'extra_vars_dict': None, 'project':None, 'playbook': 'test.yml'}
|
||||
mock_job = mocker.MagicMock(spec=Job, **mock_job_attrs)
|
||||
|
||||
|
||||
@ -18,10 +18,11 @@ import json
|
||||
# http://urllib3.readthedocs.org/en/latest/security.html#disabling-warnings
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
|
||||
class CloudFormsInventory(object):
|
||||
|
||||
def _empty_inventory(self):
|
||||
return {"_meta" : {"hostvars" : {}}}
|
||||
return {"_meta": {"hostvars": {}}}
|
||||
|
||||
def __init__(self):
|
||||
''' Main execution path '''
|
||||
@ -43,7 +44,7 @@ class CloudFormsInventory(object):
|
||||
|
||||
# This doesn't exist yet and needs to be added
|
||||
if self.args.host:
|
||||
data2 = { }
|
||||
data2 = {}
|
||||
print json.dumps(data2, indent=2)
|
||||
|
||||
def parse_cli_args(self):
|
||||
@ -51,9 +52,9 @@ class CloudFormsInventory(object):
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms')
|
||||
parser.add_argument('--list', action='store_true', default=False,
|
||||
help='List instances (default: False)')
|
||||
help='List instances (default: False)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all the variables about a specific instance')
|
||||
help='Get all the variables about a specific instance')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def read_settings(self):
|
||||
@ -97,30 +98,47 @@ class CloudFormsInventory(object):
|
||||
|
||||
def get_hosts(self):
|
||||
''' Gets host from CloudForms '''
|
||||
r = requests.get("https://" + self.cloudforms_hostname + "/api/vms?expand=resources&attributes=name,power_state", auth=(self.cloudforms_username,self.cloudforms_password), verify=False)
|
||||
|
||||
r = requests.get("https://{0}/api/vms?expand=resources&attributes=all".format(self.cloudforms_hostname),
|
||||
auth=(self.cloudforms_username, self.cloudforms_password), verify=False)
|
||||
obj = r.json()
|
||||
|
||||
#Remove objects that don't matter
|
||||
del obj["count"]
|
||||
del obj["subcount"]
|
||||
del obj["name"]
|
||||
# Create groups+hosts based on host data
|
||||
for resource in obj.get('resources', []):
|
||||
|
||||
#Create a new list to grab VMs with power_state on to add to a new list
|
||||
#I'm sure there is a cleaner way to do this
|
||||
newlist = []
|
||||
getnext = False
|
||||
for x in obj.items():
|
||||
for y in x[1]:
|
||||
for z in y.items():
|
||||
if getnext == True:
|
||||
newlist.append(z[1])
|
||||
getnext = False
|
||||
if ( z[0] == "power_state" and z[1] == "on" ):
|
||||
getnext = True
|
||||
newdict = {'hosts': newlist}
|
||||
newdict2 = {'Dynamic_CloudForms': newdict}
|
||||
print json.dumps(newdict2, indent=2)
|
||||
# Maintain backwards compat by creating `Dynamic_CloudForms` group
|
||||
if 'Dynamic_CloudForms' not in self.inventory:
|
||||
self.inventory['Dynamic_CloudForms'] = []
|
||||
self.inventory['Dynamic_CloudForms'].append(resource['name'])
|
||||
|
||||
# Add host to desired groups
|
||||
for key in ('vendor', 'type', 'location'):
|
||||
if key in resource:
|
||||
# Create top-level group
|
||||
if key not in self.inventory:
|
||||
self.inventory[key] = dict(children=[], vars={}, hosts=[])
|
||||
# if resource['name'] not in self.inventory[key]['hosts']:
|
||||
# self.inventory[key]['hosts'].append(resource['name'])
|
||||
|
||||
# Create sub-group
|
||||
if resource[key] not in self.inventory:
|
||||
self.inventory[resource[key]] = dict(children=[], vars={}, hosts=[])
|
||||
# self.inventory[resource[key]]['hosts'].append(resource['name'])
|
||||
|
||||
# Add sub-group, as a child of top-level
|
||||
if resource[key] not in self.inventory[key]['children']:
|
||||
self.inventory[key]['children'].append(resource[key])
|
||||
|
||||
# Add host to sub-group
|
||||
if resource['name'] not in self.inventory[resource[key]]:
|
||||
self.inventory[resource[key]]['hosts'].append(resource['name'])
|
||||
|
||||
# Delete 'actions' key
|
||||
del resource['actions']
|
||||
|
||||
# Add _meta hostvars
|
||||
self.inventory['_meta']['hostvars'][resource['name']] = resource
|
||||
|
||||
print json.dumps(self.inventory, indent=2)
|
||||
|
||||
# Run the script
|
||||
CloudFormsInventory()
|
||||
|
||||
@ -697,6 +697,16 @@ SATELLITE6_HOST_FILTER = r'^.+$'
|
||||
SATELLITE6_EXCLUDE_EMPTY_GROUPS = True
|
||||
SATELLITE6_INSTANCE_ID_VAR = 'foreman.id'
|
||||
|
||||
# ---------------------
|
||||
# ----- CloudForms -----
|
||||
# ---------------------
|
||||
CLOUDFORMS_ENABLED_VAR = 'power_state'
|
||||
CLOUDFORMS_ENABLED_VALUE = 'on'
|
||||
CLOUDFORMS_GROUP_FILTER = r'^.+$'
|
||||
CLOUDFORMS_HOST_FILTER = r'^.+$'
|
||||
CLOUDFORMS_EXCLUDE_EMPTY_GROUPS = True
|
||||
CLOUDFORMS_INSTANCE_ID_VAR = 'id'
|
||||
|
||||
# ---------------------
|
||||
# -- Activity Stream --
|
||||
# ---------------------
|
||||
|
||||
@ -80,12 +80,6 @@ a.red-txt:active {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.name-column {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -44,9 +44,11 @@
|
||||
color: @list-header-txt;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 25px;
|
||||
min-height: 45px;
|
||||
word-break: break-all;
|
||||
max-width: 90%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.Form-secondaryTitle{
|
||||
@ -55,7 +57,10 @@
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.Form-title--is_superuser, .Form-title--is_system_auditor, .Form-title--is_ldap_user{
|
||||
.Form-title--is_superuser,
|
||||
.Form-title--is_system_auditor,
|
||||
.Form-title--is_ldap_user,
|
||||
.Form-title--is_external_account{
|
||||
height:15px;
|
||||
color: @default-interface-txt;
|
||||
background-color: @default-list-header-bg;
|
||||
@ -353,6 +358,11 @@
|
||||
border-color: transparent transparent @field-dropdown-icon transparent!important;
|
||||
}
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.select2-dropdown{
|
||||
border:1px solid @field-border;
|
||||
|
||||
|
||||
@ -78,6 +78,7 @@ table, tbody {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
border-top:0px!important;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.List-tableCell.description-column {
|
||||
@ -383,6 +384,7 @@ table, tbody {
|
||||
|
||||
.List-action--showTooltipOnDisabled {
|
||||
display: inline-block;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.List-action--showTooltipOnDisabled .btn[disabled] {
|
||||
|
||||
@ -51,23 +51,6 @@ export default
|
||||
PaginateInit({ scope: scope,
|
||||
list: list, url: url, pageSize: 5 });
|
||||
|
||||
if (scope.removePostRefresh) {
|
||||
scope.removePostRefresh();
|
||||
}
|
||||
scope.removePostRefresh = scope.$on('PostRefresh', function () {
|
||||
if(scope.allSelected && scope.allSelected.length > 0) {
|
||||
// We need to check to see if any of the selected items are now in our list!
|
||||
for(var i=0; i<scope.allSelected.length; i++) {
|
||||
for(var j=0; j<scope[set].length; j++) {
|
||||
if(scope.allSelected[i].id === scope[set][j].id && scope.allSelected[i].type === scope[set][j].type) {
|
||||
// If so, let's go ahead and mark it as selected so that select-list-item knows to check the box
|
||||
scope[set][j].isSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scope.search(list.iterator);
|
||||
});
|
||||
}
|
||||
|
||||
@ -21,6 +21,13 @@
|
||||
key: true,
|
||||
label: 'name'
|
||||
},
|
||||
organization: {
|
||||
label: 'organization',
|
||||
ngBind: 'team.summary_fields.organization.name',
|
||||
sourceModel: 'organization',
|
||||
sourceField: 'name',
|
||||
searchable: true
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -66,6 +66,12 @@
|
||||
display: inline-block;
|
||||
color: @default-interface-txt;
|
||||
text-transform: uppercase;
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.BreadCrumb-item + .BreadCrumb-item:before {
|
||||
|
||||
@ -46,13 +46,13 @@ export function CredentialsList($scope, $rootScope, $location, $log,
|
||||
Wait('stop');
|
||||
$('#prompt-modal').modal('hide');
|
||||
|
||||
list.fields.kind.searchOptions = $scope.credential_kind_options;
|
||||
list.fields.kind.searchOptions = $scope.credential_kind_options_list;
|
||||
|
||||
// Translate the kind value
|
||||
for (i = 0; i < $scope.credentials.length; i++) {
|
||||
for (j = 0; j < $scope.credential_kind_options.length; j++) {
|
||||
if ($scope.credential_kind_options[j].value === $scope.credentials[i].kind) {
|
||||
$scope.credentials[i].kind = $scope.credential_kind_options[j].label;
|
||||
for (j = 0; j < $scope.credential_kind_options_list.length; j++) {
|
||||
if ($scope.credential_kind_options_list[j].value === $scope.credentials[i].kind) {
|
||||
$scope.credentials[i].kind = $scope.credential_kind_options_list[j].label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -82,7 +82,7 @@ export function CredentialsList($scope, $rootScope, $location, $log,
|
||||
scope: $scope,
|
||||
url: defaultUrl,
|
||||
field: 'kind',
|
||||
variable: 'credential_kind_options',
|
||||
variable: 'credential_kind_options_list',
|
||||
callback: 'choicesReadyCredential'
|
||||
});
|
||||
|
||||
@ -148,7 +148,7 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log,
|
||||
url;
|
||||
|
||||
$scope.keyEntered = false;
|
||||
|
||||
$scope.permissionsTooltip = 'Please save before assigning permissions';
|
||||
generator.inject(form, { mode: 'add', related: false, scope: $scope });
|
||||
generator.reset();
|
||||
|
||||
@ -391,6 +391,12 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
|
||||
$scope.removeCredentialLoaded();
|
||||
}
|
||||
$scope.removeCredentialLoaded = $scope.$on('credentialLoaded', function () {
|
||||
// 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
|
||||
$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.';
|
||||
}
|
||||
var set;
|
||||
for (set in relatedSets) {
|
||||
$scope.search(relatedSets[set].iterator);
|
||||
|
||||
@ -20,7 +20,8 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
|
||||
var jobs_scope, scheduled_scope,
|
||||
choicesCount = 0,
|
||||
listCount = 0,
|
||||
api_complete = false;
|
||||
api_complete = false,
|
||||
scheduledJobsList = _.cloneDeep(ScheduledJobsList);
|
||||
|
||||
$scope.jobsSelected = true;
|
||||
|
||||
@ -66,22 +67,23 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
|
||||
scope: jobs_scope,
|
||||
list: AllJobsList,
|
||||
id: 'active-jobs',
|
||||
pageSize: 20,
|
||||
url: GetBasePath('unified_jobs') + '?status__in=pending,waiting,running,completed,failed,successful,error,canceled,new&order_by=-finished',
|
||||
pageSize: 20,
|
||||
searchParams: search_params,
|
||||
spinner: false
|
||||
});
|
||||
|
||||
|
||||
scheduled_scope = $scope.$new(true);
|
||||
scheduledJobsList.basePath = GetBasePath('schedules') + '?next_run__isnull=false';
|
||||
LoadSchedulesScope({
|
||||
parent_scope: $scope,
|
||||
scope: scheduled_scope,
|
||||
list: ScheduledJobsList,
|
||||
list: scheduledJobsList,
|
||||
pageSize: 20,
|
||||
id: 'scheduled-jobs-tab',
|
||||
searchSize: 'col-lg-4 col-md-4 col-sm-4 col-xs-12',
|
||||
url: GetBasePath('schedules') + '?next_run__isnull=false'
|
||||
url: scheduledJobsList.basePath
|
||||
});
|
||||
|
||||
$scope.refreshJobs = function() {
|
||||
|
||||
@ -358,11 +358,8 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
|
||||
$scope.editSchedules = function(id) {
|
||||
var project = Find({ list: $scope.projects, key: 'id', val: id });
|
||||
if (project.scm_type === "Manual" || Empty(project.scm_type)) {
|
||||
// Nothing to do
|
||||
}
|
||||
else {
|
||||
$location.path('/projects/' + id + '/schedules');
|
||||
if (!(project.scm_type === "Manual" || Empty(project.scm_type)) && !(project.status === 'updating' || project.status === 'running' || project.status === 'pending')) {
|
||||
$state.go('projectSchedules', {id: id});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -162,6 +162,7 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
|
||||
$scope.not_ldap_user = !$scope.ldap_user;
|
||||
$scope.ldap_dn = null;
|
||||
$scope.socialAuthUser = false;
|
||||
$scope.external_account = null;
|
||||
|
||||
generator.reset();
|
||||
|
||||
@ -334,6 +335,7 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
$scope.not_ldap_user = !$scope.ldap_user;
|
||||
master.ldap_user = $scope.ldap_user;
|
||||
$scope.socialAuthUser = (data.auth.length > 0) ? true : false;
|
||||
$scope.external_account = data.external_account;
|
||||
|
||||
$scope.user_type = $scope.user_type_options[0];
|
||||
$scope.is_system_auditor = false;
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
export default
|
||||
['$scope', '$state', '$stateParams', 'PageRangeSetup', 'GetBasePath', 'DashboardHostsList',
|
||||
'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts', '$rootScope',
|
||||
function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts, $rootScope){
|
||||
'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts', '$rootScope', 'SearchInit',
|
||||
function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts, $rootScope, SearchInit){
|
||||
var setJobStatus = function(){
|
||||
_.forEach($scope.hosts, function(value){
|
||||
SetStatus({
|
||||
@ -59,6 +59,12 @@ export default
|
||||
$scope.hosts = hosts.results;
|
||||
setJobStatus();
|
||||
generator.inject(list, {mode: 'edit', scope: $scope});
|
||||
SearchInit({
|
||||
scope: $scope,
|
||||
set: 'hosts',
|
||||
list: list,
|
||||
url: defaultUrl
|
||||
});
|
||||
PaginateInit({
|
||||
scope: $scope,
|
||||
list: list,
|
||||
@ -77,6 +83,7 @@ export default
|
||||
$scope.rowBeingEdited = $state.params.id;
|
||||
$scope.listBeingEdited = "hosts";
|
||||
}
|
||||
$scope.search(list.iterator);
|
||||
};
|
||||
init();
|
||||
}];
|
||||
|
||||
@ -31,6 +31,7 @@ export default function(){
|
||||
awTipPlacement: 'right',
|
||||
dataPlacement: 'right',
|
||||
awPopOver: '{{ host.job_status_html }}',
|
||||
dataTitle: '{{host.job_status_title}}',
|
||||
ngClick:'viewHost(host.id)',
|
||||
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus'
|
||||
},
|
||||
|
||||
@ -101,10 +101,8 @@
|
||||
|
||||
.DashboardList-nameCell {
|
||||
padding-left: 15px;
|
||||
text-overflow: ellipsis;
|
||||
overflow:hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.DashboardList-nameContainer {
|
||||
|
||||
@ -404,7 +404,9 @@ export default
|
||||
|
||||
related: {
|
||||
permissions: {
|
||||
awToolTip: 'Please save before assigning permissions',
|
||||
disabled: 'disablePermissionAssignment',
|
||||
awToolTip: '{{permissionsTooltip}}',
|
||||
dataTipWatch: 'permissionsTooltip',
|
||||
dataPlacement: 'top',
|
||||
basePath: 'credentials/:id/access_list/',
|
||||
type: 'collection',
|
||||
@ -452,7 +454,7 @@ export default
|
||||
return {
|
||||
permissions: {
|
||||
iterator: 'permission',
|
||||
url: urls.access_list
|
||||
url: urls.access_list,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ export default
|
||||
permissions: {
|
||||
awToolTip: 'Please save before assigning permissions',
|
||||
dataPlacement: 'top',
|
||||
basePath: 'projects/:id/access_list/',
|
||||
basePath: 'inventories/:id/access_list/',
|
||||
type: 'collection',
|
||||
title: 'Permissions',
|
||||
iterator: 'permission',
|
||||
|
||||
@ -228,10 +228,7 @@ export default
|
||||
column: 2,
|
||||
awPopOver: "<p>Provide a comma separated list of tags.</p>\n" +
|
||||
"<p>Tags are useful when you have a large playbook, and you want to run a specific part of a play or task.</p>" +
|
||||
"<p>For example, you might have a task consisting of a long list of actions. Tag values can be assigned to each action. " +
|
||||
"Suppose the actions have been assigned tag values of "configuration", "packages" and "install".</p>" +
|
||||
"<p>If you just want to run the "configuration" and "packages" actions, you would enter the following here " +
|
||||
"in the Job Tags field:</p>\n<blockquote>configuration,packages</blockquote>\n",
|
||||
"<p>Consult the Ansible documentation for further details on the usage of tags.</p>",
|
||||
dataTitle: "Job Tags",
|
||||
dataPlacement: "right",
|
||||
dataContainer: "body",
|
||||
@ -240,6 +237,25 @@ export default
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
skip_tags: {
|
||||
label: 'Skip Tags',
|
||||
type: 'textarea',
|
||||
rows: 5,
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
'elementClass': 'Form-textInput',
|
||||
column: 2,
|
||||
awPopOver: "<p>Provide a comma separated list of tags.</p>\n" +
|
||||
"<p>Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task.</p>" +
|
||||
"<p>Consult the Ansible documentation for further details on the usage of tags.</p>",
|
||||
dataTitle: "Skip Tags",
|
||||
dataPlacement: "right",
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_skip_tags_on_launch',
|
||||
text: 'Prompt on launch'
|
||||
}
|
||||
},
|
||||
checkbox_group: {
|
||||
label: 'Options',
|
||||
type: 'checkbox_group',
|
||||
@ -281,7 +297,7 @@ export default
|
||||
column: 2,
|
||||
awPopOver: "callback_help",
|
||||
awPopOverWatch: "callback_help",
|
||||
dataPlacement: 'right',
|
||||
dataPlacement: 'top',
|
||||
dataTitle: 'Provisioning Callback URL',
|
||||
dataContainer: "body"
|
||||
},
|
||||
|
||||
@ -46,7 +46,7 @@ export default
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "not_ldap_user",
|
||||
reqExpression: "not_ldap_user && external_account === null",
|
||||
init: true
|
||||
},
|
||||
autocomplete: false
|
||||
@ -69,7 +69,7 @@ export default
|
||||
label: 'Password',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
ngShow: 'ldap_user == false && socialAuthUser === false',
|
||||
ngShow: 'ldap_user == false && socialAuthUser === false && external_account === null',
|
||||
addRequired: true,
|
||||
editRequired: false,
|
||||
ngChange: "clearPWConfirm('password_confirm')",
|
||||
@ -80,7 +80,7 @@ export default
|
||||
label: 'Confirm Password',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
ngShow: 'ldap_user == false && socialAuthUser === false',
|
||||
ngShow: 'ldap_user == false && socialAuthUser === false && external_account === null',
|
||||
addRequired: true,
|
||||
editRequired: false,
|
||||
awPassMatch: true,
|
||||
|
||||
@ -55,9 +55,9 @@ export default
|
||||
// Submit request to run an adhoc comamand
|
||||
.factory('AdhocRun', ['$location','$stateParams', 'LaunchJob',
|
||||
'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors',
|
||||
'Wait', 'Empty', 'CreateLaunchDialog',
|
||||
'Wait', 'Empty', 'CreateLaunchDialog', '$state',
|
||||
function ($location, $stateParams, LaunchJob, PromptForPasswords,
|
||||
Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog) {
|
||||
Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog, $state) {
|
||||
return function (params) {
|
||||
var id = params.project_id,
|
||||
scope = params.scope.$new(),
|
||||
@ -87,25 +87,31 @@ export default
|
||||
});
|
||||
});
|
||||
|
||||
if (scope.removeAdhocLaunchFinished) {
|
||||
scope.removeAdhocLaunchFinished();
|
||||
}
|
||||
scope.removeAdhocLaunchFinished = scope.$on('AdhocLaunchFinished',
|
||||
function(e, data) {
|
||||
$location.path('/ad_hoc_commands/' + data.id);
|
||||
});
|
||||
|
||||
if (scope.removeStartAdhocRun) {
|
||||
scope.removeStartAdhocRun();
|
||||
}
|
||||
|
||||
scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() {
|
||||
LaunchJob({
|
||||
scope: scope,
|
||||
url: url,
|
||||
callback: 'AdhocLaunchFinished' // send to the adhoc
|
||||
// standard out page
|
||||
});
|
||||
var password,
|
||||
postData={};
|
||||
for (password in scope.passwords) {
|
||||
postData[scope.passwords[password]] = scope[
|
||||
scope.passwords[password]
|
||||
];
|
||||
}
|
||||
// Re-launch the adhoc job
|
||||
Rest.setUrl(url);
|
||||
Rest.post(postData)
|
||||
.success(function (data) {
|
||||
Wait('stop');
|
||||
$state.go('adHocJobStdout', {id: data.id});
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors(scope, data, status, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to launch adhoc command. POST ' +
|
||||
'returned status: ' + status });
|
||||
});
|
||||
});
|
||||
|
||||
// start routine only if passwords need to be prompted
|
||||
|
||||
@ -161,7 +161,7 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
break;
|
||||
case 'net':
|
||||
scope.username_required = true;
|
||||
scope.password_required = true;
|
||||
scope.password_required = false;
|
||||
scope.passwordLabel = 'Password';
|
||||
scope.sshKeyDataLabel = 'SSH Key';
|
||||
break;
|
||||
|
||||
@ -165,6 +165,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
has_inventory_sources = params.has_inventory_sources,
|
||||
launch_class = '',
|
||||
launch_tip = 'Start sync process',
|
||||
schedule_tip = 'Schedule future inventory syncs',
|
||||
stat, stat_class, status_tip;
|
||||
|
||||
stat = status;
|
||||
@ -225,7 +226,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
"tooltip": status_tip,
|
||||
"status": stat,
|
||||
"launch_class": launch_class,
|
||||
"launch_tip": launch_tip
|
||||
"launch_tip": launch_tip,
|
||||
"schedule_tip": schedule_tip
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -132,6 +132,9 @@ angular.module('JobTemplatesHelper', ['Utilities'])
|
||||
scope.ask_tags_on_launch = (data.ask_tags_on_launch) ? true : false;
|
||||
master.ask_tags_on_launch = scope.ask_tags_on_launch;
|
||||
|
||||
scope.ask_skip_tags_on_launch = (data.ask_skip_tags_on_launch) ? true : false;
|
||||
master.ask_skip_tags_on_launch = scope.ask_skip_tags_on_launch;
|
||||
|
||||
scope.ask_job_type_on_launch = (data.ask_job_type_on_launch) ? true : false;
|
||||
master.ask_job_type_on_launch = scope.ask_job_type_on_launch;
|
||||
|
||||
|
||||
@ -34,7 +34,8 @@ export default
|
||||
fld = (params.variable) ? params.variable : 'variables',
|
||||
pfld = (params.parse_variable) ? params.parse_variable : 'parseType',
|
||||
onReady = params.onReady,
|
||||
onChange = params.onChange;
|
||||
onChange = params.onChange,
|
||||
readOnly = params.readOnly;
|
||||
|
||||
function removeField(fld) {
|
||||
//set our model to the last change in CodeMirror and then destroy CodeMirror
|
||||
|
||||
@ -75,6 +75,9 @@ export default
|
||||
case 'missing':
|
||||
result = 'Missing. Click for details';
|
||||
break;
|
||||
case 'canceled':
|
||||
result = 'Canceled. Click for details';
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -50,16 +50,9 @@ function InventoriesList($scope, $rootScope, $location, $log,
|
||||
"aw-pop-over": html,
|
||||
"data-popover-title": title,
|
||||
"data-placement": "right" });
|
||||
elem.removeAttr('ng-click');
|
||||
$compile(elem)($scope);
|
||||
elem.on('shown.bs.popover', function() {
|
||||
$('.popover').each(function() {
|
||||
$compile($(this))($scope); //make nested directives work!
|
||||
});
|
||||
$('.popover-content, .popover-title').click(function() {
|
||||
elem.popover('hide');
|
||||
});
|
||||
});
|
||||
elem.popover('show');
|
||||
$scope.triggerPopover(event);
|
||||
}
|
||||
|
||||
view.inject(InventoryList, { mode: mode, scope: $scope });
|
||||
@ -250,44 +243,62 @@ function InventoriesList($scope, $rootScope, $location, $log,
|
||||
});
|
||||
|
||||
$scope.showGroupSummary = function(event, id) {
|
||||
var inventory;
|
||||
if (!Empty(id)) {
|
||||
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
|
||||
if (inventory.syncStatus !== 'na') {
|
||||
Wait('start');
|
||||
Rest.setUrl(inventory.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5');
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.$emit('GroupSummaryReady', event, inventory, data);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + inventory.related.inventory_sources + ' failed. GET returned status: ' + status
|
||||
try{
|
||||
var elem = $(event.target).parent();
|
||||
// if the popover is visible already, then exit the function here
|
||||
if(elem.data()['bs.popover'].tip().hasClass('in')){
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(err){
|
||||
var inventory;
|
||||
if (!Empty(id)) {
|
||||
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
|
||||
if (inventory.syncStatus !== 'na') {
|
||||
Wait('start');
|
||||
Rest.setUrl(inventory.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5');
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.$emit('GroupSummaryReady', event, inventory, data);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + inventory.related.inventory_sources + ' failed. GET returned status: ' + status
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showHostSummary = function(event, id) {
|
||||
var url, inventory;
|
||||
if (!Empty(id)) {
|
||||
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
|
||||
if (inventory.total_hosts > 0) {
|
||||
Wait('start');
|
||||
url = GetBasePath('jobs') + "?type=job&inventory=" + id + "&failed=";
|
||||
url += (inventory.has_active_failures) ? 'true' : "false";
|
||||
url += "&order_by=-finished&page_size=5";
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success( function(data) {
|
||||
$scope.$emit('HostSummaryReady', event, data);
|
||||
})
|
||||
.error( function(data, status) {
|
||||
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + ' failed. GET returned: ' + status
|
||||
try{
|
||||
var elem = $(event.target).parent();
|
||||
// if the popover is visible already, then exit the function here
|
||||
if(elem.data()['bs.popover'].tip().hasClass('in')){
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(err){
|
||||
var url, inventory;
|
||||
if (!Empty(id)) {
|
||||
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
|
||||
if (inventory.total_hosts > 0) {
|
||||
Wait('start');
|
||||
url = GetBasePath('jobs') + "?type=job&inventory=" + id + "&failed=";
|
||||
url += (inventory.has_active_failures) ? 'true' : "false";
|
||||
url += "&order_by=-finished&page_size=5";
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success( function(data) {
|
||||
$scope.$emit('HostSummaryReady', event, data);
|
||||
})
|
||||
.error( function(data, status) {
|
||||
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + ' failed. GET returned: ' + status
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -242,7 +242,7 @@ function adhocController($q, $scope, $location, $stateParams,
|
||||
Rest.post(data)
|
||||
.success(function (data) {
|
||||
Wait('stop');
|
||||
$location.path("/ad_hoc_commands/" + data.id);
|
||||
$state.go('adHocJobStdout', {id: data.id});
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, adhocForm, {
|
||||
|
||||
@ -19,18 +19,4 @@
|
||||
<li class="BreadCrumb-item" ng-if="currentState !== 'inventoryManage'"></li>
|
||||
<div class="InventoryManageBreadCrumb-ncy" ng-if="!licenseMissing" ncy-breadcrumb></div>
|
||||
</ol>
|
||||
<div class="BreadCrumb-menuLink"
|
||||
id="bread_crumb_activity_stream"
|
||||
aw-tool-tip="View Activity Stream"
|
||||
data-placement="left"
|
||||
data-trigger="hover"
|
||||
data-container="body"
|
||||
ng-class="{'BreadCrumb-menuLinkActive' : activityStreamActive}"
|
||||
ng-if="showActivityStreamButton"
|
||||
ng-hide= "licenseMissing"
|
||||
ng-click="openActivityStream()">
|
||||
<i class="BreadCrumb-menuLinkImage icon-activity-stream"
|
||||
alt="Activity Stream">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -100,6 +100,7 @@
|
||||
// equal to case 'ec2' || 'rax' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack'
|
||||
else{
|
||||
var credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source));
|
||||
$scope.cloudCredentialRequired = source !== '' && source !== 'custom' && source !== 'ec2' ? true : false;
|
||||
CredentialList.basePath = credentialBasePath;
|
||||
LookUpInit({
|
||||
scope: $scope,
|
||||
@ -122,7 +123,7 @@
|
||||
$scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null;
|
||||
// azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint
|
||||
$scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions'];
|
||||
$scope.cloudCredentialRequired = source !== '' && source !== 'custom' ? true : false;
|
||||
$scope.cloudCredentialRequired = source !== '' && source !== 'custom' && source !== 'ec2' ? true : false;
|
||||
$scope.group_by = null;
|
||||
$scope.source_regions = null;
|
||||
$scope.credential = null;
|
||||
|
||||
@ -100,6 +100,7 @@
|
||||
else{
|
||||
var credentialBasePath = (source.value === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source.value === '' ? '' : '?kind=' + (source.value));
|
||||
CredentialList.basePath = credentialBasePath;
|
||||
$scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'ec2' ? true : false;
|
||||
LookUpInit({
|
||||
scope: $scope,
|
||||
url: credentialBasePath,
|
||||
@ -122,7 +123,7 @@
|
||||
// reset fields
|
||||
// azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint
|
||||
$scope.source_region_choices = source.value === 'azure_rm' ? $scope.azure_regions : $scope[source.value + '_regions'];
|
||||
$scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' ? true : false;
|
||||
$scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'ec2' ? true : false;
|
||||
$scope.group_by = null;
|
||||
$scope.source_regions = null;
|
||||
$scope.credential = null;
|
||||
|
||||
@ -151,6 +151,7 @@
|
||||
{status_tooltip: group_status.tooltip},
|
||||
{launch_tooltip: group_status.launch_tip},
|
||||
{launch_class: group_status.launch_class},
|
||||
{group_schedule_tooltip: group_status.schedule_tip},
|
||||
{hosts_status_tip: hosts_status.tooltip},
|
||||
{hosts_status_class: hosts_status.class},
|
||||
{source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null},
|
||||
|
||||
@ -13,11 +13,6 @@ import GroupsListController from './groups/groups-list.controller';
|
||||
export default {
|
||||
name: 'inventoryManage',
|
||||
url: '/inventories/:inventory_id/manage?{group:int}{failed}',
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'inventory',
|
||||
activityStreamId: 'inventory_id'
|
||||
},
|
||||
params:{
|
||||
group:{
|
||||
array: true
|
||||
|
||||
@ -38,11 +38,13 @@
|
||||
break;
|
||||
case 'ok':
|
||||
params.event = 'runner_on_ok';
|
||||
params.changed = 'false';
|
||||
break;
|
||||
case 'failed':
|
||||
params.failed = true;
|
||||
params.event = 'runner_on_failed';
|
||||
break;
|
||||
case 'changed':
|
||||
params.event = 'runner_on_ok';
|
||||
params.changed = true;
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -641,7 +641,7 @@ export default
|
||||
return true;
|
||||
});
|
||||
//scope.setSearchAll('host');
|
||||
ParseTypeChange({ scope: scope, field_id: 'pre-formatted-variables' });
|
||||
ParseTypeChange({ scope: scope, field_id: 'pre-formatted-variables', readOnly: true });
|
||||
scope.$emit('LoadPlays', data.related.job_events);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
|
||||
@ -154,6 +154,11 @@
|
||||
<div class="JobDetail-resultRowText">{{ job.job_tags }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.skip_tags">
|
||||
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Skip Tags</label>
|
||||
<div class="JobDetail-resultRowText">{{ job.skip_tags }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group JobDetail-resultRow JobDetail-resultRow--variables toggle-show" ng-show="variables">
|
||||
<label class="JobDetail-resultRowLabel JobDetail-extraVarsLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Extra Variables</label>
|
||||
<textarea rows="6" ng-model="variables" name="variables" class="JobDetail-extraVars" id="pre-formatted-variables"></textarea>
|
||||
|
||||
@ -42,10 +42,14 @@ export default
|
||||
|
||||
}
|
||||
|
||||
if(scope.ask_tags_on_launch && scope.other_prompt_data && scope.other_prompt_data.job_tags){
|
||||
if(scope.ask_tags_on_launch && scope.other_prompt_data && typeof scope.other_prompt_data.job_tags === 'string'){
|
||||
job_launch_data.job_tags = scope.other_prompt_data.job_tags;
|
||||
}
|
||||
|
||||
if(scope.ask_skip_tags_on_launch && scope.other_prompt_data && typeof scope.other_prompt_data.skip_tags === 'string'){
|
||||
job_launch_data.skip_tags = scope.other_prompt_data.skip_tags;
|
||||
}
|
||||
|
||||
if(scope.ask_limit_on_launch && scope.other_prompt_data && scope.other_prompt_data.limit){
|
||||
job_launch_data.limit = scope.other_prompt_data.limit;
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ export default
|
||||
|
||||
// General catch-all for "other prompts" - used in this link function and to hide the Other Prompts tab when
|
||||
// it should be hidden
|
||||
$scope.has_other_prompts = (data.ask_job_type_on_launch || data.ask_limit_on_launch || data.ask_tags_on_launch || data.ask_variables_on_launch) ? true : false;
|
||||
$scope.has_other_prompts = (data.ask_job_type_on_launch || data.ask_limit_on_launch || data.ask_tags_on_launch || data.ask_skip_tags_on_launch || data.ask_variables_on_launch) ? true : false;
|
||||
$scope.password_needed = data.passwords_needed_to_start && data.passwords_needed_to_start.length > 0;
|
||||
$scope.has_default_inventory = data.defaults && data.defaults.inventory && data.defaults.inventory.id;
|
||||
$scope.has_default_credential = data.defaults && data.defaults.credential && data.defaults.credential.id;
|
||||
@ -172,6 +172,10 @@ export default
|
||||
$scope.other_prompt_data.job_tags = (data.defaults && data.defaults.job_tags) ? data.defaults.job_tags : "";
|
||||
}
|
||||
|
||||
if($scope.ask_skip_tags_on_launch) {
|
||||
$scope.other_prompt_data.skip_tags = (data.defaults && data.defaults.skip_tags) ? data.defaults.skip_tags : "";
|
||||
}
|
||||
|
||||
if($scope.ask_variables_on_launch) {
|
||||
$scope.jobLaunchVariables = (data.defaults && data.defaults.extra_vars) ? data.defaults.extra_vars : "---";
|
||||
$scope.other_prompt_data.parseType = 'yaml';
|
||||
|
||||
@ -148,7 +148,15 @@
|
||||
<span class="Form-inputLabel">Job Tags</span>
|
||||
</label>
|
||||
<div>
|
||||
<textarea rows="1" ng-model="other_prompt_data.job_tags" name="tags" class="form-control Form-textArea Form-textInput"></textarea>
|
||||
<textarea rows="5" ng-model="other_prompt_data.job_tags" name="tags" class="form-control Form-textArea Form-textInput"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="ask_skip_tags_on_launch">
|
||||
<label for="skip_tags">
|
||||
<span class="Form-inputLabel">Skip Tags</span>
|
||||
</label>
|
||||
<div>
|
||||
<textarea rows="5" ng-model="other_prompt_data.skip_tags" name="skip_tags" class="form-control Form-textArea Form-textInput"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -518,6 +518,7 @@
|
||||
}
|
||||
|
||||
data.ask_tags_on_launch = $scope.ask_tags_on_launch ? $scope.ask_tags_on_launch : false;
|
||||
data.ask_skip_tags_on_launch = $scope.ask_skip_tags_on_launch ? $scope.ask_skip_tags_on_launch : false;
|
||||
data.ask_limit_on_launch = $scope.ask_limit_on_launch ? $scope.ask_limit_on_launch : false;
|
||||
data.ask_job_type_on_launch = $scope.ask_job_type_on_launch ? $scope.ask_job_type_on_launch : false;
|
||||
data.ask_inventory_on_launch = $scope.ask_inventory_on_launch ? $scope.ask_inventory_on_launch : false;
|
||||
|
||||
@ -640,6 +640,7 @@ export default
|
||||
}
|
||||
|
||||
data.ask_tags_on_launch = $scope.ask_tags_on_launch ? $scope.ask_tags_on_launch : false;
|
||||
data.ask_skip_tags_on_launch = $scope.ask_skip_tags_on_launch ? $scope.ask_skip_tags_on_launch : false;
|
||||
data.ask_limit_on_launch = $scope.ask_limit_on_launch ? $scope.ask_limit_on_launch : false;
|
||||
data.ask_job_type_on_launch = $scope.ask_job_type_on_launch ? $scope.ask_job_type_on_launch : false;
|
||||
data.ask_inventory_on_launch = $scope.ask_inventory_on_launch ? $scope.ask_inventory_on_launch : false;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.LabelList-tagContainer,
|
||||
.LabelList-seeMore {
|
||||
.LabelList-seeMoreLess {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -27,7 +27,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.LabelList-seeMore {
|
||||
.LabelList-seeMoreLess {
|
||||
color: @default-link;
|
||||
margin: 4px 0px;
|
||||
text-transform: uppercase;
|
||||
@ -37,7 +37,7 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.LabelList-seeMore:hover {
|
||||
.LabelList-seeMoreLess:hover {
|
||||
color: @default-link-hov;
|
||||
}
|
||||
|
||||
|
||||
@ -47,6 +47,13 @@ export default
|
||||
});
|
||||
};
|
||||
|
||||
scope.seeLess = function() {
|
||||
// Trim the labels array back down to 10 items
|
||||
scope.labels = scope.labels.slice(0, 10);
|
||||
// Re-set the seeMoreInteractive flag so that the "See More" will be displayed
|
||||
scope.seeMoreInactive = true;
|
||||
};
|
||||
|
||||
scope.deleteLabel = function(templateId, templateName, labelId, labelName) {
|
||||
var action = function () {
|
||||
$('#prompt-modal').modal('hide');
|
||||
@ -56,13 +63,13 @@ export default
|
||||
Rest.setUrl(url);
|
||||
Rest.post({"disassociate": true, "id": labelId})
|
||||
.success(function () {
|
||||
scope.search("job_template");
|
||||
scope.search("job_template", scope.$parent.job_template_page);
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Could not disacssociate label from JT. Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
msg: 'Could not disassociate label from JT. Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
@ -86,6 +93,7 @@ export default
|
||||
scope.count = null;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,5 +8,7 @@
|
||||
<span class="LabelList-name">{{ label.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="LabelList-seeMore" ng-show="count > 10 && seeMoreInactive"
|
||||
<div class="LabelList-seeMoreLess" ng-show="count > 10 && seeMoreInactive"
|
||||
ng-click="seeMore()">View More</div>
|
||||
<div class="LabelList-seeMoreLess" ng-show="count > 10 && !seeMoreInactive"
|
||||
ng-click="seeLess()">View Less</div>
|
||||
|
||||
@ -314,7 +314,7 @@ export default
|
||||
ngClick: 'submitQuestion($event)',
|
||||
ngDisabled: true,
|
||||
'class': 'btn btn-sm Form-saveButton',
|
||||
label: '{{editQuestionIndex === null ? "ADD" : "UPDATE"}}'
|
||||
label: '{{editQuestionIndex === null ? "+ ADD" : "UPDATE"}}'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -48,9 +48,7 @@ export default
|
||||
columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6',
|
||||
ngClick: "viewJobDetails(all_job)",
|
||||
defaultSearchField: true,
|
||||
awToolTip: "{{ all_job.name | sanitize }}",
|
||||
dataTipWatch: 'all_job.name',
|
||||
dataPlacement: 'top'
|
||||
searchDefault: true,
|
||||
},
|
||||
type: {
|
||||
label: 'Type',
|
||||
|
||||
@ -35,6 +35,7 @@ export default
|
||||
ngClick:"viewJobDetails(completed_job)",
|
||||
searchable: true,
|
||||
searchType: 'select',
|
||||
defaultSearchField: true,
|
||||
nosort: true,
|
||||
searchOptions: [
|
||||
{ label: "Success", value: "successful" },
|
||||
@ -54,8 +55,8 @@ export default
|
||||
name: {
|
||||
label: 'Name',
|
||||
columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6',
|
||||
searchable: false,
|
||||
ngClick: "viewJobDetails(completed_job)",
|
||||
defaultSearchField: true,
|
||||
awToolTip: "{{ completed_job.name | sanitize }}",
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
@ -64,7 +65,7 @@ export default
|
||||
ngBind: 'completed_job.type_label',
|
||||
link: false,
|
||||
columnClass: "col-lg-2 col-md-2 hidden-sm hidden-xs",
|
||||
searchable: true,
|
||||
searchable: false,
|
||||
searchType: 'select',
|
||||
searchOptions: [] // populated via GetChoices() in controller
|
||||
},
|
||||
|
||||
@ -19,6 +19,7 @@ export default
|
||||
hover: true,
|
||||
'class': 'table-no-border',
|
||||
multiSelect: true,
|
||||
trackBy: 'group.id',
|
||||
|
||||
fields: {
|
||||
sync_status: {
|
||||
|
||||
@ -20,6 +20,7 @@ export default
|
||||
hasChildren: true,
|
||||
'class': 'table-no-border',
|
||||
multiSelect: true,
|
||||
trackBy: 'host.id',
|
||||
|
||||
fields: {
|
||||
active_failures: {
|
||||
|
||||
@ -22,7 +22,8 @@ export default
|
||||
key: true,
|
||||
label: 'Name',
|
||||
columnClass: 'col-lg-5 col-md-5 col-sm-9 col-xs-8',
|
||||
linkTo: '/#/job_templates/{{job_template.id}}'
|
||||
linkTo: '/#/job_templates/{{job_template.id}}',
|
||||
searchDefault: true
|
||||
},
|
||||
description: {
|
||||
label: 'Description',
|
||||
|
||||
@ -35,7 +35,8 @@ export default
|
||||
label: 'Name',
|
||||
columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6 List-staticColumnAdjacent',
|
||||
defaultSearchField: true,
|
||||
linkTo: '/#/jobs/{{job.id}}'
|
||||
linkTo: '/#/jobs/{{job.id}}',
|
||||
searchDefault: true
|
||||
},
|
||||
finished: {
|
||||
label: 'Finished',
|
||||
|
||||
@ -37,6 +37,7 @@ export default
|
||||
},
|
||||
name: {
|
||||
key: true,
|
||||
searchDefault: true,
|
||||
label: 'Name',
|
||||
columnClass: "col-lg-4 col-md-4 col-sm-5 col-xs-7 List-staticColumnAdjacent",
|
||||
modalColumnClass: 'col-md-8'
|
||||
|
||||
@ -149,6 +149,12 @@ export default
|
||||
if(field.type === 'number'){
|
||||
$scope[i] = Number($scope[i]);
|
||||
}
|
||||
if(field.name === "username" && $scope.notification_type.value === "email" && value === null){
|
||||
$scope[i] = "";
|
||||
}
|
||||
if(field.type === 'sensitive' && value === null){
|
||||
$scope[i] = "";
|
||||
}
|
||||
return $scope[i];
|
||||
}
|
||||
|
||||
|
||||
@ -223,6 +223,12 @@ export default
|
||||
if(field.type === 'number'){
|
||||
$scope[i] = Number($scope[i]);
|
||||
}
|
||||
if(field.name === "username" && $scope.notification_type.value === "email" && value === null){
|
||||
$scope[i] = "";
|
||||
}
|
||||
if(field.type === 'sensitive' && value === null){
|
||||
$scope[i] = "";
|
||||
}
|
||||
return $scope[i];
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ export default
|
||||
Wait('stop');
|
||||
if (scope.notification_templates) {
|
||||
scope.notification_templates.forEach(function(notification_template, i) {
|
||||
setStatus(notification_template);
|
||||
scope.notification_type_options.forEach(function(type) {
|
||||
if (type.value === notification_template.notification_type) {
|
||||
scope.notification_templates[i].notification_type = type.label;
|
||||
@ -74,78 +75,33 @@ export default
|
||||
callback: 'choicesReadyNotifierList'
|
||||
});
|
||||
|
||||
function attachElem(event, html, title) {
|
||||
var elem = $(event.target).parent();
|
||||
try {
|
||||
elem.tooltip('hide');
|
||||
elem.popover('destroy');
|
||||
}
|
||||
catch(err) {
|
||||
//ignore
|
||||
}
|
||||
function setStatus(notification_template) {
|
||||
var html, recent_notifications = notification_template.summary_fields.recent_notifications;
|
||||
if (recent_notifications.length > 0) {
|
||||
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
|
||||
html += "<thead>\n";
|
||||
html += "<tr>";
|
||||
html += "<th>Status</th>";
|
||||
html += "<th>Time</th>";
|
||||
html += "</tr>\n";
|
||||
html += "</thead>\n";
|
||||
html += "<tbody>\n";
|
||||
|
||||
$('.popover').each(function() {
|
||||
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
|
||||
$(this).remove();
|
||||
});
|
||||
$('.tooltip').each( function() {
|
||||
// close any lingering tool tipss
|
||||
$(this).hide();
|
||||
});
|
||||
elem.attr({
|
||||
"aw-pop-over": html,
|
||||
"data-popover-title": title,
|
||||
"data-placement": "right" });
|
||||
$compile(elem)(scope);
|
||||
elem.on('shown.bs.popover', function() {
|
||||
$('.popover').each(function() {
|
||||
$compile($(this))(scope); //make nested directives work!
|
||||
recent_notifications.forEach(function(row) {
|
||||
html += "<tr>\n";
|
||||
html += `<td><i class=\"SmartStatus-tooltip--${row.status} fa icon-job-${row.status}"></i></td>`;
|
||||
html += "<td>" + ($filter('longDate')(row.created)).replace(/ /,'<br />') + "</td>\n";
|
||||
html += "</tr>\n";
|
||||
});
|
||||
$('.popover-content, .popover-title').click(function() {
|
||||
elem.popover('hide');
|
||||
});
|
||||
});
|
||||
elem.popover('show');
|
||||
html += "</tbody>\n";
|
||||
html += "</table>\n";
|
||||
}
|
||||
else {
|
||||
html = "<p>No recent notifications.</p>\n";
|
||||
}
|
||||
notification_template.template_status_html = html;
|
||||
}
|
||||
|
||||
scope.showSummary = function(event, id) {
|
||||
setTimeout(function(){
|
||||
if (!Empty(id)) {
|
||||
var recent_notifications,
|
||||
html, title = "Recent Notifications";
|
||||
|
||||
scope.notification_templates.forEach(function(notification_template){
|
||||
if(notification_template.id === id){
|
||||
recent_notifications = notification_template.summary_fields.recent_notifications;
|
||||
}
|
||||
});
|
||||
if (recent_notifications.length > 0) {
|
||||
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
|
||||
html += "<thead>\n";
|
||||
html += "<tr>";
|
||||
html += "<th>Status</th>";
|
||||
html += "<th>Time</th>";
|
||||
html += "</tr>\n";
|
||||
html += "</thead>\n";
|
||||
html += "<tbody>\n";
|
||||
|
||||
recent_notifications.forEach(function(row) {
|
||||
html += "<tr>\n";
|
||||
html += `<td><i class=\"SmartStatus-tooltip--${row.status} fa icon-job-${row.status}"></i></td>`;
|
||||
html += "<td>" + ($filter('longDate')(row.created)).replace(/ /,'<br />') + "</td>\n";
|
||||
html += "</tr>\n";
|
||||
});
|
||||
html += "</tbody>\n";
|
||||
html += "</table>\n";
|
||||
}
|
||||
else {
|
||||
html = "<p>No recent notifications.</p>\n";
|
||||
}
|
||||
attachElem(event, html, title);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
scope.testNotification = function(){
|
||||
var name = $filter('sanitize')(this.notification_template.name),
|
||||
pending_retries = 10;
|
||||
|
||||
@ -59,10 +59,6 @@ export default function() {
|
||||
username: {
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "email_required",
|
||||
init: "false"
|
||||
},
|
||||
ngShow: "notification_type.value == 'email' ",
|
||||
subForm: 'typeSubForm'
|
||||
},
|
||||
|
||||
@ -19,17 +19,14 @@ export default function(){
|
||||
fields: {
|
||||
status: {
|
||||
label: '',
|
||||
columnClass: 'List-staticColumn--smallStatus',
|
||||
iconOnly: true,
|
||||
searchable: false,
|
||||
nosort: true,
|
||||
ngClick: "null",
|
||||
iconOnly: true,
|
||||
excludeModal: true,
|
||||
icons: [{
|
||||
icon: "{{ 'icon-job-' + notification_template.status }}",
|
||||
ngClick: "showSummary($event, notification_template.id)",
|
||||
ngClass: ""
|
||||
}]
|
||||
icon: 'icon-job-{{ notification_template.status }}',
|
||||
awPopOver: '{{ notification_template.template_status_html }}',
|
||||
dataTitle: "Recent Notifications",
|
||||
dataPlacement: 'right',
|
||||
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus'
|
||||
},
|
||||
name: {
|
||||
key: true,
|
||||
|
||||
@ -36,6 +36,8 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest',
|
||||
disassociate: 1
|
||||
};
|
||||
}
|
||||
// Show the working spinner
|
||||
Wait('start');
|
||||
Rest.setUrl(url);
|
||||
Rest.post(params)
|
||||
.success( function(data) {
|
||||
@ -43,9 +45,8 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest',
|
||||
scope.$emit(callback, data.id);
|
||||
notifier[column] = !notifier[column];
|
||||
}
|
||||
else {
|
||||
Wait('stop');
|
||||
}
|
||||
// Hide the working spinner
|
||||
Wait('stop');
|
||||
})
|
||||
.error( function(data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
|
||||
@ -28,7 +28,7 @@ function () {
|
||||
obj.passwordLabel = ' Password';
|
||||
obj.email_required = true;
|
||||
obj.port_required = true;
|
||||
obj.password_required = true;
|
||||
obj.password_required = false;
|
||||
break;
|
||||
case 'slack':
|
||||
obj.tokenLabel =' Token';
|
||||
|
||||
@ -334,11 +334,8 @@ export default ['$scope', '$rootScope', '$location', '$log',
|
||||
|
||||
$scope.editSchedules = function(id) {
|
||||
var project = Find({ list: $scope.projects, key: 'id', val: id });
|
||||
if (project.scm_type === "Manual" || Empty(project.scm_type)) {
|
||||
// Nothing to do
|
||||
}
|
||||
else {
|
||||
$location.path('/projects/' + id + '/schedules');
|
||||
if (!(project.scm_type === "Manual" || Empty(project.scm_type)) && !(project.status === 'updating' || project.status === 'running' || project.status === 'pending')) {
|
||||
$state.go('projectSchedules', {id: id});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -34,6 +34,10 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu
|
||||
type = 'text';
|
||||
}
|
||||
|
||||
if (field.searchDefault) {
|
||||
obj.default = true;
|
||||
}
|
||||
|
||||
obj.id = id;
|
||||
obj.value = value;
|
||||
obj.label = label;
|
||||
@ -76,10 +80,13 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu
|
||||
passThrough = partitionedOptions[1];
|
||||
|
||||
var joinOptions = function() {
|
||||
return _.sortBy(_
|
||||
var options = _.sortBy(_
|
||||
.flatten([needsRequest, passThrough]), function(opt) {
|
||||
return opt.id;
|
||||
});
|
||||
|
||||
// put default first
|
||||
return _.flatten(_.partition(options, opt => opt.default));
|
||||
};
|
||||
|
||||
if (needsRequest.length) {
|
||||
|
||||
@ -390,46 +390,57 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
||||
|
||||
// lookup Validate lookup value against API
|
||||
//
|
||||
.directive('awlookup', ['Rest', function(Rest) {
|
||||
.directive('awlookup', ['Rest', '$timeout', function(Rest, $timeout) {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attrs, ctrl) {
|
||||
|
||||
var restTimeout;
|
||||
|
||||
ctrl.$parsers.unshift( function(viewValue) {
|
||||
if (viewValue !== '' && viewValue !== null) {
|
||||
var url = elm.attr('data-url');
|
||||
url = url.replace(/\:value/, encodeURI(viewValue));
|
||||
scope[elm.attr('data-source')] = null;
|
||||
Rest.setUrl(url);
|
||||
Rest.get().then( function(data) {
|
||||
var results = data.data.results;
|
||||
if (results.length > 0) {
|
||||
scope[elm.attr('data-source')] = results[0].id;
|
||||
if(restTimeout) {
|
||||
$timeout.cancel(restTimeout);
|
||||
}
|
||||
restTimeout = $timeout( function(){
|
||||
Rest.setUrl(url);
|
||||
Rest.get().then( function(data) {
|
||||
var results = data.data.results;
|
||||
if (results.length > 0) {
|
||||
scope[elm.attr('data-source')] = results[0].id;
|
||||
|
||||
// For user lookups the API endpoint doesn't
|
||||
// have a `name` property, so this is `undefined`
|
||||
// which causes the input to clear after typing
|
||||
// a valid value O_o
|
||||
//
|
||||
// Only assign if there is a value, so that we avoid
|
||||
// this situation.
|
||||
//
|
||||
// TODO: Evaluate if assigning name on the scope is
|
||||
// even necessary at all.
|
||||
//
|
||||
if (!_.isEmpty(results[0].name)) {
|
||||
scope[elm.attr('name')] = results[0].name;
|
||||
// For user lookups the API endpoint doesn't
|
||||
// have a `name` property, so this is `undefined`
|
||||
// which causes the input to clear after typing
|
||||
// a valid value O_o
|
||||
//
|
||||
// Only assign if there is a value, so that we avoid
|
||||
// this situation.
|
||||
//
|
||||
// TODO: Evaluate if assigning name on the scope is
|
||||
// even necessary at all.
|
||||
//
|
||||
if (!_.isEmpty(results[0].name)) {
|
||||
scope[elm.attr('name')] = results[0].name;
|
||||
}
|
||||
|
||||
ctrl.$setValidity('required', true);
|
||||
ctrl.$setValidity('awlookup', true);
|
||||
return viewValue;
|
||||
}
|
||||
|
||||
ctrl.$setValidity('required', true);
|
||||
ctrl.$setValidity('awlookup', true);
|
||||
return viewValue;
|
||||
}
|
||||
ctrl.$setValidity('required', true);
|
||||
ctrl.$setValidity('awlookup', false);
|
||||
return undefined;
|
||||
});
|
||||
ctrl.$setValidity('awlookup', false);
|
||||
return undefined;
|
||||
});
|
||||
}, 750);
|
||||
}
|
||||
else {
|
||||
if(restTimeout) {
|
||||
$timeout.cancel(restTimeout);
|
||||
}
|
||||
ctrl.$setValidity('awlookup', true);
|
||||
scope[elm.attr('data-source')] = null;
|
||||
}
|
||||
@ -477,7 +488,8 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
||||
return {
|
||||
link: function(scope, element, attrs) {
|
||||
var delay = (attrs.delay !== undefined && attrs.delay !== null) ? attrs.delay : ($AnsibleConfig) ? $AnsibleConfig.tooltip_delay : {show: 500, hide: 100},
|
||||
placement;
|
||||
placement,
|
||||
stateChangeWatcher;
|
||||
if (attrs.awTipPlacement) {
|
||||
placement = attrs.awTipPlacement;
|
||||
}
|
||||
@ -493,6 +505,22 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
||||
template = '<div class="tooltip Tooltip" role="tooltip"><div class="tooltip-arrow Tooltip-arrow"></div><div class="tooltip-inner Tooltip-inner"></div></div>';
|
||||
}
|
||||
|
||||
// This block helps clean up tooltips that may get orphaned by a click event
|
||||
$(element).on('mouseenter', function() {
|
||||
if(stateChangeWatcher) {
|
||||
// Un-bind - we don't want a bunch of listeners firing
|
||||
stateChangeWatcher();
|
||||
}
|
||||
stateChangeWatcher = scope.$on('$stateChangeStart', function() {
|
||||
// Go ahead and force the tooltip setTimeout to expire (if it hasn't already fired)
|
||||
$(element).tooltip('hide');
|
||||
// Clean up any existing tooltips including this one
|
||||
$('.tooltip').each(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$(element).on('hidden.bs.tooltip', function( ) {
|
||||
// TB3RC1 is leaving behind tooltip <div> elements. This will remove them
|
||||
// after a tooltip fades away. If not, they lay overtop of other elements and
|
||||
@ -548,6 +576,10 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
||||
template = '<div id="' + element[0].id + '_popover_container" class="popover" role="tooltip"><div class="arrow"></div><h3 id="' + element[0].id + '_popover_title" class="popover-title"></h3><div id="' + element[0].id + '_popover_content" class="popover-content"></div></div>';
|
||||
}
|
||||
|
||||
scope.triggerPopover = function(e){
|
||||
showPopover(e);
|
||||
};
|
||||
|
||||
if (attrs.awPopOverWatch) {
|
||||
$(element).popover({
|
||||
placement: placement,
|
||||
|
||||
@ -1487,6 +1487,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
"ng-show='is_system_auditor'>Auditor</span>";
|
||||
html+= "<span class=\"Form-title--is_ldap_user\" "+
|
||||
"ng-show='ldap_user'>LDAP</span>";
|
||||
html+= "<span class=\"Form-title--is_external_account\" "+
|
||||
"ng-show='external_account'>{{external_account}}</span>";
|
||||
}
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"Form-header--fields\">";
|
||||
@ -1524,8 +1526,13 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
collection = this.form.related[itm];
|
||||
html += `<div id="${itm}_tab"`+
|
||||
`class="Form-tab"`+
|
||||
`ng-click="${this.form.related[itm].disabled} || toggleFormTabs($event)"` +
|
||||
`ng-class="{'is-selected': ${itm}Selected ` ;
|
||||
`ng-click="${this.form.related[itm].disabled} || toggleFormTabs($event)"`;
|
||||
if (collection.awToolTip){
|
||||
html += `aw-tool-tip="${collection.awToolTip}"` +
|
||||
`aw-tip-placement="${collection.dataPlacement}"` +
|
||||
`data-tip-watch="${collection.dataTipWatch}"`;
|
||||
}
|
||||
html += `ng-class="{'is-selected': ${itm}Selected ` ;
|
||||
if(this.form.related[itm].disabled){
|
||||
html += `, 'Form-tab--disabled' : ${this.form.related[itm].disabled }`;
|
||||
}
|
||||
|
||||
@ -371,7 +371,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
} else if (field.link || (field.key && (field.link === undefined || field.link))) {
|
||||
html += "<a href=\"#/" + base + "/{{" + list.iterator + ".id }}\" ";
|
||||
} else {
|
||||
html += "<a href=\"\">";
|
||||
html += "<a href=\"\"";
|
||||
}
|
||||
if (field.awDroppable) {
|
||||
html += Attr(field, 'awDroppable');
|
||||
@ -394,7 +394,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
if (field.awPopOver) {
|
||||
html += "aw-pop-over=\"" + field.awPopOver + "\" ";
|
||||
html += (field.dataPlacement) ? "data-placement=\"" + field.dataPlacement + "\" " : "";
|
||||
html += (field.dataTitle) ? "data-title=\"" + field.dataTitle + "\" " : "";
|
||||
html += (field.dataTitle) ? "over-title=\"" + field.dataTitle + "\" " : "";
|
||||
}
|
||||
html += (field.ngClass) ? Attr(field, 'ngClass') : '';
|
||||
html += (field.ngEllipsis) ? "data-ng-bind=\"" + list.iterator + "." + fld + "\" data-ellipsis " : "";
|
||||
|
||||
@ -466,7 +466,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
innerTable += "ng-class-odd=\"'List-tableRow--oddRow'\" ";
|
||||
innerTable += "ng-class-even=\"'List-tableRow--evenRow'\" ";
|
||||
innerTable += "ng-repeat=\"" + list.iterator + " in " + list.name;
|
||||
innerTable += (list.trackBy) ? " track by " + list.trackBy : " track by $index";
|
||||
innerTable += (list.trackBy) ? " track by " + list.trackBy : "";
|
||||
innerTable += (list.orderBy) ? " | orderBy:'" + list.orderBy + "'" : "";
|
||||
innerTable += (list.filterBy) ? " | filter: " + list.filterBy : "";
|
||||
innerTable += "\">\n";
|
||||
|
||||
@ -63,9 +63,16 @@ export default ['$scope',
|
||||
* {@link multiSelectList.controller:multiSelectList#decorateItem `decorateItem`}
|
||||
*/
|
||||
this.registerItem = function(item) {
|
||||
var decoratedItem = this.decorateItem(item);
|
||||
$scope.items = $scope.items.concat(decoratedItem);
|
||||
return decoratedItem;
|
||||
var foundItem = _.find($scope.items, function(existingItem) { return existingItem.id === item.id; });
|
||||
|
||||
if(foundItem) {
|
||||
return foundItem;
|
||||
}
|
||||
else {
|
||||
var decoratedItem = this.decorateItem(item);
|
||||
$scope.items = $scope.items.concat(decoratedItem);
|
||||
return decoratedItem;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -99,6 +106,7 @@ export default ['$scope',
|
||||
this.decorateItem = function(item) {
|
||||
return {
|
||||
isSelected: false,
|
||||
id: item.id,
|
||||
value: item
|
||||
};
|
||||
};
|
||||
@ -129,11 +137,11 @@ export default ['$scope',
|
||||
* Triggers {@link multiSelectList.selectionChanged `multiSelectList.selectionChanged`}
|
||||
*/
|
||||
this.deselectAll = function() {
|
||||
$scope.items.forEach(function(item) {
|
||||
item.isSelected = false;
|
||||
});
|
||||
$scope.selection.isExtended = false;
|
||||
rebuildSelections();
|
||||
$scope.items.forEach(function(item) {
|
||||
item.isSelected = false;
|
||||
});
|
||||
$scope.selection.isExtended = false;
|
||||
rebuildSelections();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -33,11 +33,8 @@ export default
|
||||
template: '<input type="checkbox" data-multi-select-list-item ng-model="isSelected" ng-change="userInteractionSelect()">',
|
||||
link: function(scope, element, attrs, multiSelectList) {
|
||||
|
||||
var initializeItem = function() {
|
||||
scope.decoratedItem = multiSelectList.registerItem(scope.item);
|
||||
scope.isSelected = scope.item.isSelected ? true : false;
|
||||
scope.decoratedItem.isSelected = scope.item.isSelected ? true : false;
|
||||
};
|
||||
scope.decoratedItem = multiSelectList.registerItem(scope.item);
|
||||
scope.isSelected = scope.decoratedItem.isSelected ? true : false;
|
||||
|
||||
scope.$watch('isSelected', function(value) {
|
||||
if (value === true) {
|
||||
@ -47,23 +44,10 @@ export default
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch('item', function() {
|
||||
// This is necessary for page changes where $scope.item gets updated via ng-repeat
|
||||
// but this link function never gets triggered (and scope.decoratedItem) never
|
||||
// gets updated.
|
||||
initializeItem();
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
multiSelectList.deregisterItem(scope.decoratedItem);
|
||||
});
|
||||
|
||||
scope.userInteractionSelect = function() {
|
||||
scope.$emit("selectedOrDeselected", scope.decoratedItem);
|
||||
};
|
||||
|
||||
initializeItem();
|
||||
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
@ -74,6 +74,7 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
||||
$scope.limit = data.limit;
|
||||
$scope.verbosity = data.verbosity;
|
||||
$scope.job_tags = data.job_tags;
|
||||
$scope.job.module_name = data.module_name;
|
||||
if (data.extra_vars) {
|
||||
$scope.variables = ParseVariableString(data.extra_vars);
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<body data-user-agent="{{userAgent}}">
|
||||
|
||||
<main-menu></main-menu>
|
||||
<bread-crumb ng-if="!includesCurrentState('inventoryManage')"></bread-crumb><div ui-view="groupBreadcrumbs" ng-if="includesCurrentState('inventoryManage')"></div>
|
||||
<bread-crumb ng-show="!includesCurrentState('inventoryManage')"></bread-crumb><div ui-view="groupBreadcrumbs" ng-show="includesCurrentState('inventoryManage')"></div>
|
||||
<toast></toast>
|
||||
|
||||
<div class="container-fluid" id="content-container">
|
||||
|
||||
@ -14,8 +14,11 @@ attempt=0
|
||||
while [[ $attempt -lt $retry_attempts ]]
|
||||
do
|
||||
status_code=`curl -s -i --data "host_config_key=$2" http://$1/api/v1/job_templates/$3/callback/ | head -n 1 | awk '{print $2}'`
|
||||
if [[ $status_code == 202 ]]
|
||||
if [[ $status_code -ge 300 ]]
|
||||
then
|
||||
echo "${status_code} received, encountered problem, halting."
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
attempt=$(( attempt + 1 ))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user