Merge branch 'release_3.0.2' into stable

* release_3.0.2: (150 commits)
  Updating changelogs for 3.0.2 release
  fixing deprecated_team.organization credential migration
  Fix issue when installing bundled ansible on el6
  fixed localed date stuff
  update test to check org_auditor access
  ensure team organizations are assigned to credentials
  temporarily pin the pytest version until the ldap error can be fixed
  fixed locale
  fix date locale angular scheduler
  Make sure org admins can see credential after migration, comment updates on related tests add clause in test to verify automatic setting of org of new team credential
  Update team admin credential migration test to current state-of-knowledge
  fix ng-toast rel, resolves #3197 (#3316)
  allow users to edit their first and last name
  Revert "Prevent ignored task from being displayed as failing."
  Revert "Modify job event save behavior"
  fixing old tests for new user creation permissions
  Clean venv on 'make clean'
  Resolve KeyError by coercing instance_id to a str
  Update cloudforms dynamic inventory
  Update foreman inventory script
  ...
This commit is contained in:
Matthew Jones
2016-08-31 14:30:58 -04:00
124 changed files with 2405 additions and 1430 deletions

View File

@@ -172,7 +172,7 @@ endif
.DEFAULT_GOAL := build .DEFAULT_GOAL := build
.PHONY: clean clean-tmp rebase push requirements requirements_dev \ .PHONY: clean clean-tmp clean-venv rebase push requirements requirements_dev \
requirements_jenkins \ requirements_jenkins \
develop refresh adduser migrate dbchange dbshell runserver celeryd \ develop refresh adduser migrate dbchange dbshell runserver celeryd \
receiver test test_unit test_coverage coverage_html test_jenkins dev_build \ receiver test test_unit test_coverage coverage_html test_jenkins dev_build \
@@ -182,8 +182,8 @@ endif
websocket-proxy browser-sync browser-sync-reload brocolli-watcher \ websocket-proxy browser-sync browser-sync-reload brocolli-watcher \
devjs minjs testjs_ci \ devjs minjs testjs_ci \
deb deb-src debian debsign pbuilder reprepro setup_tarball \ deb deb-src debian debsign pbuilder reprepro setup_tarball \
virtualbox-ovf virtualbox-centos-7 virtualbox-centos-6 \ vagrant-virtualbox virtualbox-centos-7 virtualbox-centos-6 \
clean-bundle setup_bundle_tarball vagrant-vmware clean-bundle setup_bundle_tarball
# Remove setup build files # Remove setup build files
clean-tar: clean-tar:
@@ -229,8 +229,11 @@ clean-build-test:
clean-tmp: clean-tmp:
rm -rf tmp/ rm -rf tmp/
clean-venv:
rm -rf venv/
# Remove temporary build files, compiled Python files. # Remove temporary build files, compiled Python files.
clean: clean-rpm clean-deb clean-grunt clean-ui clean-static clean-build-test clean-tar clean-packer clean-bundle clean: clean-rpm clean-deb clean-grunt clean-ui clean-static clean-build-test clean-tar clean-packer clean-bundle clean-venv
rm -rf awx/lib/site-packages rm -rf awx/lib/site-packages
rm -rf awx/lib/.deps_built rm -rf awx/lib/.deps_built
rm -rf dist/* rm -rf dist/*
@@ -448,13 +451,22 @@ check: flake8 pep8 # pyflakes pylint
TEST_DIRS=awx/main/tests TEST_DIRS=awx/main/tests
# Run all API unit tests. # Run all API unit tests.
test: test:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
fi; \
py.test $(TEST_DIRS) py.test $(TEST_DIRS)
test_unit: test_unit:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
fi; \
py.test awx/main/tests/unit py.test awx/main/tests/unit
# Run all API unit tests with coverage enabled. # Run all API unit tests with coverage enabled.
test_coverage: test_coverage:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
fi; \
py.test --create-db --cov=awx --cov-report=xml --junitxml=./reports/junit.xml $(TEST_DIRS) py.test --create-db --cov=awx --cov-report=xml --junitxml=./reports/junit.xml $(TEST_DIRS)
# Output test coverage as HTML (into htmlcov directory). # Output test coverage as HTML (into htmlcov directory).
@@ -848,30 +860,24 @@ reprepro: deb-build/$(DEB_NVRA).deb reprepro/conf
amazon-ebs: 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 cd packaging/packer && $(PACKER) build -only $@ $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
# virtualbox # Vagrant box using virtualbox provider
virtualbox-ovf: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box vagrant-virtualbox: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box
packaging/packer/ansible-tower-$(VERSION)-virtualbox.box: packaging/packer/output-virtualbox-iso/centos-7.ovf 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 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: packaging/packer/output-virtualbox-iso/centos-7.ovf:
cd packaging/packer && $(PACKER) build -only virtualbox-iso packer-centos-7.json 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 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: packaging/packer/output-vmware-iso/centos-7.vmx:
cd packaging/packer && $(PACKER) build -only vmware-iso packer-centos-7.json cd packaging/packer && $(PACKER) build -only vmware-iso packer-centos-7.json
vmware-iso: packaging/packer/output-vmware-iso/centos-7.vmx packaging/packer/ansible-tower-$(VERSION)-vmware.box: 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
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 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 # TODO - figure out how to build the front-end and python requirements with

View File

@@ -5,7 +5,7 @@ import os
import sys import sys
import warnings import warnings
__version__ = '3.0.1' __version__ = '3.0.2'
__all__ = ['__version__'] __all__ = ['__version__']

View File

@@ -19,7 +19,7 @@ from awx.main.utils import get_object_or_400
logger = logging.getLogger('awx.api.permissions') logger = logging.getLogger('awx.api.permissions')
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
'TaskPermission', 'ProjectUpdatePermission'] 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission']
class ModelAccessPermission(permissions.BasePermission): class ModelAccessPermission(permissions.BasePermission):
''' '''
@@ -202,3 +202,10 @@ class ProjectUpdatePermission(ModelAccessPermission):
def check_post_permissions(self, request, view, obj=None): def check_post_permissions(self, request, view, obj=None):
project = get_object_or_400(view.model, pk=view.kwargs['pk']) project = get_object_or_400(view.model, pk=view.kwargs['pk'])
return check_user_access(request.user, view.model, 'start', project) return check_user_access(request.user, view.model, 'start', project)
class UserPermission(ModelAccessPermission):
def check_post_permissions(self, request, view, obj=None):
if request.user.is_superuser:
return True
raise PermissionDenied()

View File

@@ -526,8 +526,10 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
serializer_class = InventorySourceSerializer serializer_class = InventorySourceSerializer
elif isinstance(obj, JobTemplate): elif isinstance(obj, JobTemplate):
serializer_class = JobTemplateSerializer serializer_class = JobTemplateSerializer
elif isinstance(obj, SystemJobTemplate):
serializer_class = SystemJobTemplateSerializer
if serializer_class: if serializer_class:
serializer = serializer_class(instance=obj) serializer = serializer_class(instance=obj, context=self.context)
return serializer.to_representation(obj) return serializer.to_representation(obj)
else: else:
return super(UnifiedJobTemplateSerializer, self).to_representation(obj) return super(UnifiedJobTemplateSerializer, self).to_representation(obj)
@@ -590,7 +592,7 @@ class UnifiedJobSerializer(BaseSerializer):
elif isinstance(obj, SystemJob): elif isinstance(obj, SystemJob):
serializer_class = SystemJobSerializer serializer_class = SystemJobSerializer
if serializer_class: if serializer_class:
serializer = serializer_class(instance=obj) serializer = serializer_class(instance=obj, context=self.context)
ret = serializer.to_representation(obj) ret = serializer.to_representation(obj)
else: else:
ret = super(UnifiedJobSerializer, self).to_representation(obj) ret = super(UnifiedJobSerializer, self).to_representation(obj)
@@ -637,7 +639,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
elif isinstance(obj, SystemJob): elif isinstance(obj, SystemJob):
serializer_class = SystemJobListSerializer serializer_class = SystemJobListSerializer
if serializer_class: if serializer_class:
serializer = serializer_class(instance=obj) serializer = serializer_class(instance=obj, context=self.context)
ret = serializer.to_representation(obj) ret = serializer.to_representation(obj)
else: else:
ret = super(UnifiedJobListSerializer, self).to_representation(obj) ret = super(UnifiedJobListSerializer, self).to_representation(obj)
@@ -1285,7 +1287,8 @@ class CustomInventoryScriptSerializer(BaseSerializer):
request = self.context.get('request', None) request = self.context.get('request', None)
if request.user not in obj.admin_role and \ if request.user not in obj.admin_role and \
not request.user.is_superuser 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 ret['script'] = None
return ret return ret
@@ -1713,19 +1716,21 @@ class CredentialSerializerCreate(CredentialSerializer):
attrs.pop(field) attrs.pop(field)
if not owner_fields: if not owner_fields:
raise serializers.ValidationError({"detail": "Missing 'user', 'team', or 'organization'."}) 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) return super(CredentialSerializerCreate, self).validate(attrs)
def create(self, validated_data): def create(self, validated_data):
user = validated_data.pop('user', None) user = validated_data.pop('user', None)
team = validated_data.pop('team', None) team = validated_data.pop('team', None)
if team:
validated_data['organization'] = team.organization
credential = super(CredentialSerializerCreate, self).create(validated_data) credential = super(CredentialSerializerCreate, self).create(validated_data)
if user: if user:
credential.admin_role.members.add(user) credential.admin_role.members.add(user)
if team: 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 return credential
@@ -1823,7 +1828,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
class Meta: class Meta:
model = JobTemplate model = JobTemplate
fields = ('*', 'host_config_key', 'ask_variables_on_launch', 'ask_limit_on_launch', 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') 'ask_credential_on_launch', 'survey_enabled', 'become_enabled', 'allow_simultaneous')
def get_related(self, obj): def get_related(self, obj):
@@ -1907,6 +1912,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
passwords_needed_to_start = serializers.ReadOnlyField() passwords_needed_to_start = serializers.ReadOnlyField()
ask_variables_on_launch = serializers.ReadOnlyField() ask_variables_on_launch = serializers.ReadOnlyField()
ask_limit_on_launch = serializers.ReadOnlyField() ask_limit_on_launch = serializers.ReadOnlyField()
ask_skip_tags_on_launch = serializers.ReadOnlyField()
ask_tags_on_launch = serializers.ReadOnlyField() ask_tags_on_launch = serializers.ReadOnlyField()
ask_job_type_on_launch = serializers.ReadOnlyField() ask_job_type_on_launch = serializers.ReadOnlyField()
ask_inventory_on_launch = serializers.ReadOnlyField() ask_inventory_on_launch = serializers.ReadOnlyField()
@@ -1915,8 +1921,8 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
class Meta: class Meta:
model = Job model = Job
fields = ('*', 'job_template', 'passwords_needed_to_start', 'ask_variables_on_launch', 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_limit_on_launch', 'ask_tags_on_launch', 'ask_skip_tags_on_launch',
'ask_inventory_on_launch', 'ask_credential_on_launch') 'ask_job_type_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch')
def get_related(self, obj): def get_related(self, obj):
res = super(JobSerializer, self).get_related(obj) res = super(JobSerializer, self).get_related(obj)
@@ -1977,7 +1983,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
return ret return ret
if 'job_template' in ret and not obj.job_template: if 'job_template' in ret and not obj.job_template:
ret['job_template'] = None 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() ret['extra_vars'] = obj.display_extra_vars()
return ret return ret
@@ -2280,13 +2286,14 @@ class JobLaunchSerializer(BaseSerializer):
fields = ('can_start_without_user_input', 'passwords_needed_to_start', fields = ('can_start_without_user_input', 'passwords_needed_to_start',
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory', 'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
'credential', 'ask_variables_on_launch', 'ask_tags_on_launch', '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', 'ask_inventory_on_launch', 'ask_credential_on_launch',
'survey_enabled', 'variables_needed_to_start', 'survey_enabled', 'variables_needed_to_start',
'credential_needed_to_start', 'inventory_needed_to_start', 'credential_needed_to_start', 'inventory_needed_to_start',
'job_template_data', 'defaults') 'job_template_data', 'defaults')
read_only_fields = ('ask_variables_on_launch', 'ask_limit_on_launch', read_only_fields = (
'ask_tags_on_launch', 'ask_job_type_on_launch', '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') 'ask_inventory_on_launch', 'ask_credential_on_launch')
extra_kwargs = { extra_kwargs = {
'credential': {'write_only': True,}, 'credential': {'write_only': True,},
@@ -2674,6 +2681,8 @@ class ActivityStreamSerializer(BaseSerializer):
fval = getattr(thisItem, field, None) fval = getattr(thisItem, field, None)
if fval is not None: if fval is not None:
thisItemDict[field] = fval thisItemDict[field] = fval
if fk == 'group':
thisItemDict['inventory_id'] = getattr(thisItem, 'inventory_id', None)
summary_fields[fk].append(thisItemDict) summary_fields[fk].append(thisItemDict)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass

View File

@@ -8,6 +8,8 @@ The response will include the following fields:
configured to prompt for variables upon launch (boolean, read-only) configured to prompt for variables upon launch (boolean, read-only)
* `ask_tags_on_launch`: Flag indicating whether the job_template is * `ask_tags_on_launch`: Flag indicating whether the job_template is
configured to prompt for tags upon launch (boolean, read-only) 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 * `ask_job_type_on_launch`: Flag indicating whether the job_template is
configured to prompt for job_type upon launch (boolean, read-only) configured to prompt for job_type upon launch (boolean, read-only)
* `ask_limit_on_launch`: Flag indicating whether the job_template is * `ask_limit_on_launch`: Flag indicating whether the job_template is

View File

@@ -201,7 +201,7 @@ class ApiV1ConfigView(APIView):
'''Return various sitewide configuration settings.''' '''Return various sitewide configuration settings.'''
license_reader = TaskSerializer() 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']: 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 license_data['features']['activity_streams'] &= tower_settings.ACTIVITY_STREAM_ENABLED
@@ -225,7 +225,10 @@ class ApiV1ConfigView(APIView):
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
data['user_ldap_fields'] = user_ldap_fields data['user_ldap_fields'] = user_ldap_fields
if request.user.is_superuser or Organization.accessible_objects(request.user, '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( data.update(dict(
project_base_dir = settings.PROJECTS_ROOT, project_base_dir = settings.PROJECTS_ROOT,
project_local_paths = Project.get_local_path_choices(), project_local_paths = Project.get_local_path_choices(),
@@ -876,12 +879,19 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
data = dict(msg="Role 'id' field is missing.") data = dict(msg="Role 'id' field is missing.")
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
role = Role.objects.get(pk=sub_id) role = get_object_or_400(Role, pk=sub_id)
content_type = ContentType.objects.get_for_model(Organization) org_content_type = ContentType.objects.get_for_model(Organization)
if role.content_type == content_type: if role.content_type == org_content_type:
data = dict(msg="You cannot assign an Organization role as a child role for a Team.") 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) 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) return super(TeamRolesList, self).post(request, *args, **kwargs)
class TeamObjectRolesList(SubListAPIView): class TeamObjectRolesList(SubListAPIView):
@@ -1143,6 +1153,7 @@ class UserList(ListCreateAPIView):
model = User model = User
serializer_class = UserSerializer serializer_class = UserSerializer
permission_classes = (UserPermission,)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
ret = super(UserList, self).post( request, *args, **kwargs) ret = super(UserList, self).post( request, *args, **kwargs)
@@ -1205,7 +1216,24 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
if sub_id == self.request.user.admin_role.pk: 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) return super(UserRolesList, self).post(request, *args, **kwargs)
@@ -1291,7 +1319,7 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
can_admin = request.user.can_access(User, 'admin', obj, request.data) can_admin = request.user.can_access(User, 'admin', obj, request.data)
su_only_edit_fields = ('is_superuser', 'is_system_auditor') su_only_edit_fields = ('is_superuser', 'is_system_auditor')
admin_only_edit_fields = ('last_name', 'first_name', 'username', 'is_active') admin_only_edit_fields = ('username', 'is_active')
fields_to_check = () fields_to_check = ()
if not request.user.is_superuser: if not request.user.is_superuser:
@@ -1384,8 +1412,8 @@ class TeamCredentialsList(SubListCreateAPIView):
self.check_parent_access(team) self.check_parent_access(team)
visible_creds = Credential.accessible_objects(self.request.user, 'read_role') visible_creds = Credential.accessible_objects(self.request.user, 'read_role')
team_creds = Credential.objects.filter(admin_role__parents=team.member_role) team_creds = Credential.objects.filter(Q(use_role__parents=team.member_role) | Q(admin_role__parents=team.member_role))
return team_creds & visible_creds return (team_creds & visible_creds).distinct()
class OrganizationCredentialList(SubListCreateAPIView): class OrganizationCredentialList(SubListCreateAPIView):
@@ -3623,7 +3651,6 @@ class RoleDetail(RetrieveAPIView):
model = Role model = Role
serializer_class = RoleSerializer serializer_class = RoleSerializer
permission_classes = (IsAuthenticated,)
new_in_300 = True new_in_300 = True
@@ -3646,6 +3673,26 @@ class RoleUsersList(SubListCreateAttachDetachAPIView):
if not sub_id: if not sub_id:
data = dict(msg="User 'id' field is missing.") data = dict(msg="User 'id' field is missing.")
return Response(data, status=status.HTTP_400_BAD_REQUEST) 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) return super(RoleUsersList, self).post(request, *args, **kwargs)
@@ -3670,13 +3717,20 @@ class RoleTeamsList(SubListAPIView):
data = dict(msg="Team 'id' field is missing.") data = dict(msg="Team 'id' field is missing.")
return Response(data, status=status.HTTP_400_BAD_REQUEST) 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']) 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.") 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) 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' action = 'attach'
if request.data.get('disassociate', None): if request.data.get('disassociate', None):
action = 'unattach' action = 'unattach'

View File

@@ -428,8 +428,8 @@ class HostAccess(BaseAccess):
return obj and self.user in obj.inventory.read_role return obj and self.user in obj.inventory.read_role
def can_add(self, data): def can_add(self, data):
if not data or 'inventory' not in data: if not data: # So the browseable API will work
return False return Inventory.accessible_objects(self.user, 'admin_role').exists()
# Checks for admin or change permission on inventory. # Checks for admin or change permission on inventory.
inventory_pk = get_pk_from_dict(data, 'inventory') inventory_pk = get_pk_from_dict(data, 'inventory')
@@ -654,23 +654,14 @@ class CredentialAccess(BaseAccess):
if not obj: if not obj:
return False return False
# Check access to organizations # 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') 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 and (not obj.organization or organization_pk != obj.organization.id)) \
if organization_pk: or (not organization_pk and obj.organization):
# 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 return False
if obj.organization: print(self.user in obj.admin_role)
if self.user in obj.organization.admin_role:
return True
return self.user in obj.admin_role return self.user in obj.admin_role
def can_delete(self, obj): def can_delete(self, obj):
@@ -981,8 +972,11 @@ class JobTemplateAccess(BaseAccess):
field_whitelist = [ field_whitelist = [
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars', 'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch', 'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_skip_tags_on_launch',
'ask_credential_on_launch', 'survey_enabled' '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(): for k, v in data.items():
@@ -1089,7 +1083,8 @@ class JobAccess(BaseAccess):
def can_delete(self, obj): def can_delete(self, obj):
if obj.inventory is not None and self.user in obj.inventory.organization.admin_role: if obj.inventory is not None and self.user in obj.inventory.organization.admin_role:
return True 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 True
return False 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]) project_qs = self.user.get_queryset(Project).filter(scm_type__in=[s[0] for s in Project.SCM_TYPE_CHOICES])
inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES) inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES)
job_template_qs = self.user.get_queryset(JobTemplate) job_template_qs = self.user.get_queryset(JobTemplate)
system_job_template_qs = self.user.get_queryset(SystemJobTemplate)
qs = qs.filter(Q(Project___in=project_qs) | qs = qs.filter(Q(Project___in=project_qs) |
Q(InventorySource___in=inventory_source_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( qs = qs.select_related(
'created_by', 'created_by',
'modified_by', 'modified_by',
@@ -1569,21 +1566,22 @@ class ActivityStreamAccess(BaseAccess):
inventory_set = Inventory.accessible_objects(self.user, 'read_role') inventory_set = Inventory.accessible_objects(self.user, 'read_role')
credential_set = Credential.accessible_objects(self.user, 'read_role') credential_set = Credential.accessible_objects(self.user, 'read_role')
organization_set = Organization.accessible_objects(self.user, 'read_role') auditing_orgs = (
admin_of_orgs = Organization.accessible_objects(self.user, 'admin_role') Organization.accessible_objects(self.user, 'admin_role') |
group_set = Group.objects.filter(inventory__in=inventory_set) Organization.accessible_objects(self.user, 'auditor_role')
).distinct().values_list('id', flat=True)
project_set = Project.accessible_objects(self.user, 'read_role') project_set = Project.accessible_objects(self.user, 'read_role')
jt_set = JobTemplate.accessible_objects(self.user, 'read_role') jt_set = JobTemplate.accessible_objects(self.user, 'read_role')
team_set = Team.accessible_objects(self.user, 'read_role') team_set = Team.accessible_objects(self.user, 'read_role')
return qs.filter( return qs.filter(
Q(ad_hoc_command__inventory__in=inventory_set) | 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(user=self.user) |
Q(organization__in=organization_set) | Q(organization__in=auditing_orgs) |
Q(inventory__in=inventory_set) | Q(inventory__in=inventory_set) |
Q(host__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_source__inventory__in=inventory_set) |
Q(inventory_update__inventory_source__inventory__in=inventory_set) | Q(inventory_update__inventory_source__inventory__in=inventory_set) |
Q(credential__in=credential_set) | Q(credential__in=credential_set) |
@@ -1592,10 +1590,10 @@ class ActivityStreamAccess(BaseAccess):
Q(project_update__project__in=project_set) | Q(project_update__project__in=project_set) |
Q(job_template__in=jt_set) | Q(job_template__in=jt_set) |
Q(job__job_template__in=jt_set) | Q(job__job_template__in=jt_set) |
Q(notification_template__organization__in=admin_of_orgs) | Q(notification_template__organization__in=auditing_orgs) |
Q(notification__notification_template__organization__in=admin_of_orgs) | Q(notification__notification_template__organization__in=auditing_orgs) |
Q(label__organization__in=organization_set) | Q(label__organization__in=auditing_orgs) |
Q(role__in=Role.visible_roles(self.user)) Q(role__in=Role.visible_roles(self.user) if auditing_orgs else [])
).distinct() ).distinct()
def can_add(self, data): def can_add(self, data):
@@ -1667,14 +1665,8 @@ class RoleAccess(BaseAccess):
if self.user.is_superuser or self.user.is_system_auditor: if self.user.is_superuser or self.user.is_system_auditor:
return True return True
if obj.object_id: return Role.filter_visible_roles(
sister_roles = Role.objects.filter( self.user, Role.objects.filter(pk=obj.id)).exists()
content_type = obj.content_type,
object_id = obj.object_id
)
else:
sister_roles = obj
return self.user.roles.filter(descendents__in=sister_roles).exists()
def can_add(self, obj, data): def can_add(self, obj, data):
# Unsupported for now # Unsupported for now

View File

@@ -54,10 +54,6 @@ class AutoOneToOneField(models.OneToOneField):
AutoSingleRelatedObjectDescriptor(related)) AutoSingleRelatedObjectDescriptor(related))
def resolve_role_field(obj, field): def resolve_role_field(obj, field):
ret = [] ret = []
@@ -71,8 +67,8 @@ def resolve_role_field(obj, field):
return [] return []
if len(field_components) == 1: if len(field_components) == 1:
Role_ = get_current_apps().get_model('main', 'Role') role_cls = str(get_current_apps().get_model('main', 'Role'))
if type(obj) is not Role_: if not str(type(obj)) == role_cls:
raise Exception(smart_text('{} refers to a {}, not a Role'.format(field, type(obj)))) raise Exception(smart_text('{} refers to a {}, not a Role'.format(field, type(obj))))
ret.append(obj.id) ret.append(obj.id)
else: else:

View File

@@ -22,6 +22,7 @@ import yaml
from django.conf import settings from django.conf import settings
from django.core.management.base import NoArgsCommand, CommandError from django.core.management.base import NoArgsCommand, CommandError
from django.db import connection, transaction from django.db import connection, transaction
from django.utils.encoding import smart_text
# AWX # AWX
from awx.main.models import * # noqa from awx.main.models import * # noqa
@@ -606,7 +607,7 @@ class Command(NoArgsCommand):
break break
instance_id = from_dict.get(key, default) instance_id = from_dict.get(key, default)
from_dict = instance_id from_dict = instance_id
return instance_id return smart_text(instance_id)
def _get_enabled(self, from_dict, default=None): def _get_enabled(self, from_dict, default=None):
''' '''

View 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),
),
]

View 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),
),
]

View 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),
]

View File

@@ -0,0 +1,30 @@
# -*- 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.infer_credential_org_from_team),
migrations.RunPython(rbac.rebuild_role_hierarchy),
]

View File

@@ -2,7 +2,9 @@ import logging
from time import time from time import time
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.db.utils import IntegrityError
from collections import defaultdict from collections import defaultdict
from awx.main.utils import getattrd from awx.main.utils import getattrd
@@ -490,3 +492,11 @@ def rebuild_role_hierarchy(apps, schema_editor):
logger.info('Done.') logger.info('Done.')
def infer_credential_org_from_team(apps, schema_editor):
Credential = apps.get_model('main', "Credential")
for cred in Credential.objects.exclude(deprecated_team__isnull=True):
try:
with transaction.atomic():
_update_credential_parents(cred.deprecated_team.organization, cred)
except IntegrityError:
logger.info("Organization<{}> credential for old Team<{}> credential already created".format(cred.deprecated_team.organization.pk, cred.pk))

View 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()

View File

@@ -215,11 +215,11 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
parent_role=[ parent_role=[
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
'organization.admin_role',
], ],
) )
use_role = ImplicitRoleField( use_role = ImplicitRoleField(
parent_role=[ parent_role=[
'organization.admin_role',
'admin_role', 'admin_role',
] ]
) )

View File

@@ -6,6 +6,7 @@ import hmac
import json import json
import yaml import yaml
import logging import logging
import time
from urlparse import urljoin from urlparse import urljoin
# Django # Django
@@ -26,7 +27,7 @@ from awx.main.models.unified_jobs import * # noqa
from awx.main.models.notifications import NotificationTemplate from awx.main.models.notifications import NotificationTemplate
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
from awx.main.utils import emit_websocket_notification 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.conf import tower_settings
from awx.main.fields import ImplicitRoleField from awx.main.fields import ImplicitRoleField
from awx.main.models.mixins import ResourceMixin from awx.main.models.mixins import ResourceMixin
@@ -199,6 +200,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
blank=True, blank=True,
default=False, default=False,
) )
ask_skip_tags_on_launch = models.BooleanField(
blank=True,
default=False,
)
ask_job_type_on_launch = models.BooleanField( ask_job_type_on_launch = models.BooleanField(
blank=True, blank=True,
default=False, default=False,
@@ -244,7 +249,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
'playbook', 'credential', 'cloud_credential', 'network_credential', 'forks', 'schedule', 'playbook', 'credential', 'cloud_credential', 'network_credential', 'forks', 'schedule',
'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type', 'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type',
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled', 'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled',
'labels',] 'labels', 'survey_passwords']
def resource_validation_data(self): def resource_validation_data(self):
''' '''
@@ -418,7 +423,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
extra_vars=self.ask_variables_on_launch, extra_vars=self.ask_variables_on_launch,
limit=self.ask_limit_on_launch, limit=self.ask_limit_on_launch,
job_tags=self.ask_tags_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, job_type=self.ask_job_type_on_launch,
inventory=self.ask_inventory_on_launch, inventory=self.ask_inventory_on_launch,
credential=self.ask_credential_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: if field == 'extra_vars' and self.survey_enabled and self.survey_spec:
# Accept vars defined in the survey and no others # Accept vars defined in the survey and no others
survey_vars = [question['variable'] for question in self.survey_spec.get('spec', [])] 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: if key in survey_vars:
prompted_fields[field][key] = kwargs[field][key] prompted_fields[field][key] = extra_vars[key]
else: else:
ignored_fields[field][key] = kwargs[field][key] ignored_fields[field][key] = extra_vars[key]
else: else:
ignored_fields[field] = kwargs[field] ignored_fields[field] = kwargs[field]
@@ -509,6 +524,11 @@ class Job(UnifiedJob, JobOptions):
editable=False, editable=False,
through='JobHostSummary', through='JobHostSummary',
) )
survey_passwords = JSONField(
blank=True,
default={},
editable=False,
)
@classmethod @classmethod
def _get_parent_field_name(cls): def _get_parent_field_name(cls):
@@ -550,6 +570,12 @@ class Job(UnifiedJob, JobOptions):
return self.job_template.ask_tags_on_launch return self.job_template.ask_tags_on_launch
return False 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 @property
def ask_job_type_on_launch(self): def ask_job_type_on_launch(self):
if self.job_template is not None: if self.job_template is not None:
@@ -670,9 +696,17 @@ class Job(UnifiedJob, JobOptions):
dependencies.append(source.create_inventory_update(launch_type='dependency')) dependencies.append(source.create_inventory_update(launch_type='dependency'))
return dependencies return dependencies
def notification_data(self): def notification_data(self, block=5):
data = super(Job, self).notification_data() data = super(Job, self).notification_data()
all_hosts = {} 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(): for h in self.job_host_summaries.all():
all_hosts[h.host_name] = dict(failed=h.failed, all_hosts[h.host_name] = dict(failed=h.failed,
changed=h.changed, changed=h.changed,
@@ -711,15 +745,11 @@ class Job(UnifiedJob, JobOptions):
''' '''
Hides fields marked as passwords in survey. Hides fields marked as passwords in survey.
''' '''
if self.extra_vars and self.job_template and self.job_template.survey_enabled: if self.survey_passwords:
try:
extra_vars = json.loads(self.extra_vars) extra_vars = json.loads(self.extra_vars)
for key in self.job_template.survey_password_variables(): extra_vars.update(self.survey_passwords)
if key in extra_vars:
extra_vars[key] = REPLACE_STR
return json.dumps(extra_vars) return json.dumps(extra_vars)
except ValueError: else:
pass
return self.extra_vars return self.extra_vars
def _survey_search_and_replace(self, content): def _survey_search_and_replace(self, content):

View File

@@ -32,7 +32,7 @@ from djcelery.models import TaskMeta
from awx.main.models.base import * # noqa from awx.main.models.base import * # noqa
from awx.main.models.schedules import Schedule from awx.main.models.schedules import Schedule
from awx.main.utils import decrypt_field, emit_websocket_notification, _inventory_updates 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'] __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() 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: class Meta:
app_label = 'main' app_label = 'main'
unique_together = [('polymorphic_ctype', 'name')] unique_together = [('polymorphic_ctype', 'name')]
@@ -343,6 +346,14 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
create_kwargs[field_name] = getattr(self, field_name) create_kwargs[field_name] = getattr(self, field_name)
new_kwargs = self._update_unified_job_kwargs(**create_kwargs) new_kwargs = self._update_unified_job_kwargs(**create_kwargs)
unified_job = unified_job_class(**new_kwargs) unified_job = unified_job_class(**new_kwargs)
# 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() unified_job.save()
for field_name, src_field_value in m2m_fields.iteritems(): for field_name, src_field_value in m2m_fields.iteritems():
dest_field = getattr(unified_job, field_name) dest_field = getattr(unified_job, field_name)
@@ -367,6 +378,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
PASSWORD_FIELDS = ('start_args',) 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: class Meta:
app_label = 'main' app_label = 'main'

View File

@@ -214,6 +214,9 @@ def handle_work_success(self, result, task_actual):
friendly_name = "System Job" friendly_name = "System Job"
else: else:
return return
all_notification_templates = set(notification_templates.get('success', []) + notification_templates.get('any', []))
if len(all_notification_templates):
notification_body = instance.notification_data() notification_body = instance.notification_data()
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name, notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
task_actual['id'], task_actual['id'],
@@ -221,7 +224,7 @@ def handle_work_success(self, result, task_actual):
notification_body['url']) notification_body['url'])
notification_body['friendly_name'] = friendly_name notification_body['friendly_name'] = friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
for n in set(notification_templates.get('success', []) + notification_templates.get('any', []))], for n in all_notification_templates],
job_id=task_actual['id']) job_id=task_actual['id'])
@task(bind=True) @task(bind=True)
@@ -277,6 +280,9 @@ def handle_work_error(self, task_id, subtasks=None):
(first_task_type, first_task_name, first_task_id) (first_task_type, first_task_name, first_task_id)
instance.save() instance.save()
instance.socketio_emit_status("failed") instance.socketio_emit_status("failed")
all_notification_templates = set(notification_templates.get('error', []) + notification_templates.get('any', []))
if len(all_notification_templates):
notification_body = first_task.notification_data() notification_body = first_task.notification_data()
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name, notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name,
first_task_id, first_task_id,
@@ -284,7 +290,7 @@ def handle_work_error(self, task_id, subtasks=None):
notification_body['url']) notification_body['url'])
notification_body['friendly_name'] = first_task_friendly_name notification_body['friendly_name'] = first_task_friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
for n in set(notification_templates.get('error', []) + notification_templates.get('any', []))], for n in all_notification_templates],
job_id=first_task_id) job_id=first_task_id)
@@ -1313,9 +1319,14 @@ class RunInventoryUpdate(BaseTask):
credential = inventory_update.credential credential = inventory_update.credential
if credential: if credential:
cp.set(section, 'hostname', credential.host) cp.set(section, 'url', credential.host)
cp.set(section, 'username', credential.username) cp.set(section, 'username', credential.username)
cp.set(section, 'password', decrypt_field(credential, 'password')) cp.set(section, 'password', decrypt_field(credential, 'password'))
cp.set(section, 'ssl_verify', "false")
section = 'cache'
cp.add_section(section)
cp.set(section, 'max_age', "0")
elif inventory_update.source == 'azure_rm': elif inventory_update.source == 'azure_rm':
section = 'azure' section = 'azure'

View File

@@ -26,16 +26,16 @@ def survey_spec_factory():
return create_survey_spec return create_survey_spec
@pytest.fixture @pytest.fixture
def job_with_secret_key_factory(job_template_factory): def job_template_with_survey_passwords_factory(job_template_factory):
def rf(persisted): def rf(persisted):
"Returns job with linked JT survey with password survey questions" "Returns job with linked JT survey with password survey questions"
objects = job_template_factory('jt', organization='org1', survey=[ objects = job_template_factory('jt', organization='org1', survey=[
{'variable': 'submitter_email', 'type': 'text', 'default': 'foobar@redhat.com'}, {'variable': 'submitter_email', 'type': 'text', 'default': 'foobar@redhat.com'},
{'variable': 'secret_key', 'default': '6kQngg3h8lgiSTvIEb21', 'type': 'password'}, {'variable': 'secret_key', 'default': '6kQngg3h8lgiSTvIEb21', 'type': 'password'},
{'variable': 'SSN', 'type': 'password'}], jobs=[1], persisted=persisted) {'variable': 'SSN', 'type': 'password'}], persisted=persisted)
return objects.jobs[1] return objects.job_template
return rf return rf
@pytest.fixture @pytest.fixture
def job_with_secret_key_unit(job_with_secret_key_factory): def job_template_with_survey_passwords_unit(job_template_with_survey_passwords_factory):
return job_with_secret_key_factory(persisted=False) return job_template_with_survey_passwords_factory(persisted=False)

View File

@@ -68,7 +68,7 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob
# #
@pytest.mark.django_db @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'), { response = post(reverse('api:credential_list'), {
'team': team.id, 'team': team.id,
'name': 'Some name', 'name': 'Some name',
@@ -80,6 +80,9 @@ def test_create_team_credential(post, get, team, org_admin, team_member):
assert response.status_code == 200 assert response.status_code == 200
assert response.data['count'] == 1 assert response.data['count'] == 1
# Assure that credential's organization is implictly set to team's org
assert response.data['results'][0]['summary_fields']['organization']['id'] == team.organization.id
@pytest.mark.django_db @pytest.mark.django_db
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member): def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member):
response = post(reverse('api:team_credentials_list', args=(team.pk,)), { response = post(reverse('api:team_credentials_list', args=(team.pk,)), {
@@ -94,25 +97,159 @@ def test_create_team_credential_via_team_credentials_list(post, get, team, org_a
assert response.data['count'] == 1 assert response.data['count'] == 1
@pytest.mark.django_db @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'), { response = post(reverse('api:credential_list'), {
'team': team.id, 'team': team.id,
'organization': organization.id,
'name': 'Some name', 'name': 'Some name',
'username': 'someusername' 'username': 'someusername'
}, alice) }, alice)
assert response.status_code == 403 assert response.status_code == 403
@pytest.mark.django_db @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? # Members can't add credentials, only org admins.. for now?
response = post(reverse('api:credential_list'), { response = post(reverse('api:credential_list'), {
'team': team.id, 'team': team.id,
'organization': organization.id,
'name': 'Some name', 'name': 'Some name',
'username': 'someusername' 'username': 'someusername'
}, team_member) }, team_member)
assert response.status_code == 403 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 # organization credentials
@@ -177,6 +314,37 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me
assert response.data['count'] == 0 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 # Openstack Credentials
@@ -224,33 +392,3 @@ def test_create_credential_missing_user_team_org_xfail(post, admin):
}, admin) }, admin)
assert response.status_code == 400 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

View File

@@ -37,6 +37,7 @@ def job_template_prompts(project, inventory, machine_credential):
name='deploy-job-template', name='deploy-job-template',
ask_variables_on_launch=on_off, ask_variables_on_launch=on_off,
ask_tags_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_job_type_on_launch=on_off,
ask_inventory_on_launch=on_off, ask_inventory_on_launch=on_off,
ask_limit_on_launch=on_off, ask_limit_on_launch=on_off,
@@ -54,6 +55,7 @@ def job_template_prompts_null(project):
name='deploy-job-template', name='deploy-job-template',
ask_variables_on_launch=True, ask_variables_on_launch=True,
ask_tags_on_launch=True, ask_tags_on_launch=True,
ask_skip_tags_on_launch=True,
ask_job_type_on_launch=True, ask_job_type_on_launch=True,
ask_inventory_on_launch=True, ask_inventory_on_launch=True,
ask_limit_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) 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.django_db
@pytest.mark.job_runtime_vars @pytest.mark.job_runtime_vars
def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, post, rando, mocker): def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, post, rando, mocker):

View File

@@ -3,12 +3,14 @@ import mock
# AWX # AWX
from awx.api.serializers import JobTemplateSerializer, JobLaunchSerializer 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.models.projects import ProjectOptions
from awx.main.migrations import _save_password_keys as save_password_keys
# Django # Django
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.apps import apps
@property @property
def project_playbooks(self): def project_playbooks(self):
@@ -103,9 +105,10 @@ def test_edit_nonsenstive(patch, job_template_factory, alice):
'extra_vars': '--', 'extra_vars': '--',
'job_tags': 'sometags', 'job_tags': 'sometags',
'force_handlers': True, 'force_handlers': True,
'skip_tags': True, 'skip_tags': 'thistag,thattag',
'ask_variables_on_launch':True, 'ask_variables_on_launch':True,
'ask_tags_on_launch':True, 'ask_tags_on_launch':True,
'ask_skip_tags_on_launch':True,
'ask_job_type_on_launch':True, 'ask_job_type_on_launch':True,
'ask_inventory_on_launch':True, 'ask_inventory_on_launch':True,
'ask_credential_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() objects.job_template.create_unified_job()
delete_response = delete(reverse('api:job_template_detail', args=[objects.job_template.pk]), user=admin_user) delete_response = delete(reverse('api:job_template_detail', args=[objects.job_template.pk]), user=admin_user)
assert delete_response.status_code == 409 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$'}

View File

@@ -193,7 +193,8 @@ def test_launch_with_non_empty_survey_spec_no_license(job_template_factory, post
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.survey @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] AS_record = ActivityStream.objects.filter(object1='job').all()[0]
changes_dict = json.loads(AS_record.changes) changes_dict = json.loads(AS_record.changes)
extra_vars = json.loads(changes_dict['extra_vars']) extra_vars = json.loads(changes_dict['extra_vars'])

View File

@@ -160,7 +160,7 @@ def organization(instance):
@pytest.fixture @pytest.fixture
def credential(): 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 @pytest.fixture
def machine_credential(): def machine_credential():
@@ -168,7 +168,7 @@ def machine_credential():
@pytest.fixture @pytest.fixture
def org_credential(organization): 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 @pytest.fixture
def inventory(organization): def inventory(organization):
@@ -206,8 +206,8 @@ def notification(notification_template):
subject='email subject') subject='email subject')
@pytest.fixture @pytest.fixture
def job_with_secret_key(job_with_secret_key_factory): def job_template_with_survey_passwords(job_template_with_survey_passwords_factory):
return job_with_secret_key_factory(persisted=True) return job_template_with_survey_passwords_factory(persisted=True)
@pytest.fixture @pytest.fixture
def admin(user): def admin(user):

View File

@@ -54,21 +54,40 @@ def test_credential_migration_team_member(credential, team, user, permissions):
rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
# Admin permissions post migration # User permissions post migration
assert u in credential.use_role assert u in credential.use_role
assert u not in credential.admin_role
@pytest.mark.django_db @pytest.mark.django_db
def test_credential_migration_team_admin(credential, team, user, permissions): def test_credential_migration_team_admin(credential, team, user, permissions):
u = user('user', False) u = user('user', False)
team.member_role.members.add(u) team.admin_role.members.add(u)
credential.deprecated_team = team credential.deprecated_team = team
credential.save() credential.save()
assert u not in credential.use_role assert u not in credential.use_role
# Usage permissions post migration # Admin permissions post migration
rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
assert u in credential.use_role assert u in credential.admin_role
@pytest.mark.django_db
def test_credential_migration_org_auditor(credential, team, org_auditor):
# Team's organization is the org_auditor's org
credential.deprecated_team = team
credential.save()
# No permissions pre-migration (this happens automatically so we patch this)
team.admin_role.children.remove(credential.admin_role)
team.member_role.children.remove(credential.use_role)
assert org_auditor not in credential.read_role
rbac.migrate_credential(apps, None)
rbac.infer_credential_org_from_team(apps, None)
# Read permissions post migration
assert org_auditor not in credential.use_role
assert org_auditor in credential.read_role
def test_credential_access_superuser(): def test_credential_access_superuser():
u = User(username='admin', is_superuser=True) u = User(username='admin', is_superuser=True)
@@ -133,29 +152,6 @@ def test_org_credential_access_member(alice, org_credential, credential):
'description': 'New description.', 'description': 'New description.',
'organization': None}) '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 @pytest.mark.django_db
def test_cred_job_template_xfail(user, deploy_jobtemplate): def test_cred_job_template_xfail(user, deploy_jobtemplate):
' Personal credential migration ' ' Personal credential migration '
@@ -248,7 +244,6 @@ def test_single_cred_multi_job_template_multi_org(user, organizations, credentia
orgs[0].admin_role.members.add(a) orgs[0].admin_role.members.add(a)
orgs[1].admin_role.members.add(a) orgs[1].admin_role.members.add(a)
access = CredentialAccess(a)
rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
for jt in jts: for jt in jts:
@@ -256,11 +251,6 @@ def test_single_cred_multi_job_template_multi_org(user, organizations, credentia
credential.refresh_from_db() credential.refresh_from_db()
assert jts[0].credential != jts[1].credential 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 @pytest.mark.django_db
def test_cred_inventory_source(user, inventory, credential): def test_cred_inventory_source(user, inventory, credential):

View File

@@ -92,6 +92,12 @@ def test_null_related_delete_denied(normal_job, rando):
access = JobAccess(rando) access = JobAccess(rando)
assert not access.can_delete(normal_job) 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 @pytest.mark.django_db
def test_inventory_org_admin_delete_allowed(normal_job, org_admin): 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 normal_job.project = None # do this so we test job->inventory->org->admin connection

View File

@@ -192,8 +192,12 @@ class UsersTest(BaseTest):
self.post(url, expect=403, data=new_user, auth=self.get_other_credentials()) self.post(url, expect=403, data=new_user, auth=self.get_other_credentials())
self.post(url, expect=201, data=new_user, auth=self.get_super_credentials()) self.post(url, expect=201, data=new_user, auth=self.get_super_credentials())
self.post(url, expect=400, data=new_user, auth=self.get_super_credentials()) self.post(url, expect=400, data=new_user, auth=self.get_super_credentials())
self.post(url, expect=201, data=new_user2, auth=self.get_normal_credentials()) # org admin cannot create orphaned users
self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials()) self.post(url, expect=403, data=new_user2, auth=self.get_normal_credentials())
# org admin can create org users
org_url = reverse('api:organization_users_list', args=(self.organizations[0].pk,))
self.post(org_url, expect=201, data=new_user2, auth=self.get_normal_credentials())
self.post(org_url, expect=400, data=new_user2, auth=self.get_normal_credentials())
# Normal user cannot add users after his org is marked inactive. # Normal user cannot add users after his org is marked inactive.
self.organizations[0].delete() self.organizations[0].delete()
new_user3 = dict(username='blippy3') new_user3 = dict(username='blippy3')
@@ -325,9 +329,9 @@ class UsersTest(BaseTest):
detail_url = reverse('api:user_detail', args=(self.other_django_user.pk,)) detail_url = reverse('api:user_detail', args=(self.other_django_user.pk,))
data = self.get(detail_url, expect=200, auth=self.get_other_credentials()) data = self.get(detail_url, expect=200, auth=self.get_other_credentials())
# can't change first_name, last_name, etc # can change first_name, last_name, etc
data['last_name'] = "NewLastName" data['last_name'] = "NewLastName"
self.put(detail_url, data, expect=403, auth=self.get_other_credentials()) self.put(detail_url, data, expect=200, auth=self.get_other_credentials())
# can't change username # can't change username
data['username'] = 'newUsername' data['username'] = 'newUsername'
@@ -367,23 +371,20 @@ class UsersTest(BaseTest):
url = reverse('api:user_list') url = reverse('api:user_list')
data = dict(username='username', password='password') data = dict(username='username', password='password')
data2 = dict(username='username2', password='password2') data2 = dict(username='username2', password='password2')
data = self.post(url, expect=201, data=data, auth=self.get_normal_credentials())
# but a regular user cannot create users
self.post(url, expect=403, data=data2, auth=self.get_other_credentials())
# org admins cannot create orphaned users
self.post(url, expect=403, data=data2, auth=self.get_normal_credentials())
# a super user can create new users
self.post(url, expect=201, data=data, auth=self.get_super_credentials())
# verify that the login works... # verify that the login works...
self.get(url, expect=200, auth=('username', 'password')) self.get(url, expect=200, auth=('username', 'password'))
# but a regular user cannot
data = self.post(url, expect=403, data=data2, auth=self.get_other_credentials())
# a super user can also create new users
data = self.post(url, expect=201, data=data2, auth=self.get_super_credentials())
# verify that the login works
self.get(url, expect=200, auth=('username2', 'password2'))
# verify that if you post a user with a pk, you do not alter that user's password info # verify that if you post a user with a pk, you do not alter that user's password info
mod = dict(id=self.super_django_user.pk, username='change', password='change') mod = dict(id=self.super_django_user.pk, username='change', password='change')
data = self.post(url, expect=201, data=mod, auth=self.get_super_credentials()) self.post(url, expect=201, data=mod, auth=self.get_super_credentials())
orig = User.objects.get(pk=self.super_django_user.pk) orig = User.objects.get(pk=self.super_django_user.pk)
self.assertTrue(orig.username != 'change') self.assertTrue(orig.username != 'change')

View 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

View File

@@ -1,22 +1,11 @@
import mock import mock
import pytest 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 ( from awx.api.views import (
ApiV1RootView, ApiV1RootView,
TeamRolesList,
JobTemplateLabelList, JobTemplateLabelList,
) )
from awx.main.models import (
User,
Role,
)
@pytest.fixture @pytest.fixture
def mock_response_new(mocker): def mock_response_new(mocker):
m = mocker.patch('awx.api.views.Response.__new__') m = mocker.patch('awx.api.views.Response.__new__')
@@ -71,27 +60,3 @@ class TestJobTemplateLabelList:
super(JobTemplateLabelList, view).unattach(mock_request, None, None) super(JobTemplateLabelList, view).unattach(mock_request, None, None)
assert mixin_unattach.called_with(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

View File

@@ -1,4 +1,5 @@
import pytest import pytest
import json
def test_missing_project_error(job_template_factory): 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 'inventory' in validation_errors
assert 'credential' 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 @pytest.mark.survey
def test_survey_password_list(job_with_secret_key_unit): def test_job_template_survey_password_redaction(job_template_with_survey_passwords_unit):
"""Verify that survey_password_variables method gives a list of survey passwords""" """Tests the JobTemplate model's funciton to redact passwords from
assert job_with_secret_key_unit.job_template.survey_password_variables() == ['secret_key', 'SSN'] extra_vars - used when creating a new job"""
assert job_template_with_survey_passwords_unit.survey_password_variables() == ['secret_key', 'SSN']

View File

@@ -2,6 +2,7 @@ import pytest
import json import json
from awx.main.tasks import RunJob from awx.main.tasks import RunJob
from awx.main.models import Job
@pytest.fixture @pytest.fixture
@@ -14,9 +15,19 @@ def job(mocker):
'launch_type': 'manual'}) 'launch_type': 'manual'})
@pytest.mark.survey @pytest.mark.survey
def test_job_redacted_extra_vars(job_with_secret_key_unit): def test_job_survey_password_redaction():
"""Verify that this method redacts vars marked as passwords in a survey""" """Tests the Job model's funciton to redact passwords from
assert json.loads(job_with_secret_key_unit.display_extra_vars()) == { 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', 'submitter_email': 'foobar@redhat.com',
'secret_key': '$encrypted$', 'secret_key': '$encrypted$',
'SSN': '$encrypted$'} 'SSN': '$encrypted$'}

View File

@@ -41,8 +41,8 @@ def test_net_cred_ssh_agent(mocker, options):
'credential': None, 'cloud_credential': None, 'network_credential': Credential(**options), 'credential': None, 'cloud_credential': None, 'network_credential': Credential(**options),
'become_enabled': False, 'become_method': None, 'become_username': None, 'become_enabled': False, 'become_method': None, 'become_username': None,
'inventory': mocker.MagicMock(spec=Inventory, id=2), 'force_handlers': False, 'inventory': mocker.MagicMock(spec=Inventory, id=2), 'force_handlers': False,
'limit': None, 'verbosity': None, 'job_tags': None, 'skip_tags': False, 'limit': None, 'verbosity': None, 'job_tags': None, 'skip_tags': None,
'start_at_task': False, 'pk': 1, 'launch_type': 'normal', 'job_template':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'} 'created_by': None, 'extra_vars_dict': None, 'project':None, 'playbook': 'test.yml'}
mock_job = mocker.MagicMock(spec=Job, **mock_job_attrs) mock_job = mocker.MagicMock(spec=Job, **mock_job_attrs)

View File

@@ -1,126 +1,462 @@
#!/usr/bin/python #!/usr/bin/python
# vim: set fileencoding=utf-8 :
#
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
#
# This script is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with it. If not, see <http://www.gnu.org/licenses/>.
#
# This is loosely based on the foreman inventory script
# -- Josh Preston <jpreston@redhat.com>
#
''' from __future__ import print_function
CloudForms external inventory script
==================================================
Generates inventory that Ansible can understand by making API request to CloudForms.
Modeled after https://raw.githubusercontent.com/ansible/ansible/stable-1.9/plugins/inventory/ec2.py
jlabocki <at> redhat.com or @jameslabocki on twitter
'''
import os
import argparse import argparse
import ConfigParser import ConfigParser
import os
import re
from time import time
import requests import requests
import json from requests.auth import HTTPBasicAuth
import warnings
try:
import json
except ImportError:
import simplejson as json
# This disables warnings and is not a good idea, but hey, this is a demo
# http://urllib3.readthedocs.org/en/latest/security.html#disabling-warnings
requests.packages.urllib3.disable_warnings()
class CloudFormsInventory(object): class CloudFormsInventory(object):
def _empty_inventory(self):
return {"_meta" : {"hostvars" : {}}}
def __init__(self): def __init__(self):
''' Main execution path ''' """
Main execution path
"""
self.inventory = dict() # A list of groups and the hosts in that group
self.hosts = dict() # Details about hosts in the inventory
# Inventory grouped by instance IDs, tags, security groups, regions, # Parse CLI arguments
# and availability zones
self.inventory = self._empty_inventory()
# Index of hostname (address) to instance ID
self.index = {}
# Read CLI arguments
self.read_settings()
self.parse_cli_args() self.parse_cli_args()
# Get Hosts # Read settings
if self.args.list: self.read_settings()
self.get_hosts()
# This doesn't exist yet and needs to be added # Cache
if self.args.refresh_cache or not self.is_cache_valid():
self.update_cache()
else:
self.load_inventory_from_cache()
self.load_hosts_from_cache()
data_to_print = ""
# Data to print
if self.args.host: if self.args.host:
data2 = { } if self.args.debug:
print json.dumps(data2, indent=2) print("Fetching host [%s]" % self.args.host)
data_to_print += self.get_host_info(self.args.host)
else:
self.inventory['_meta'] = {'hostvars': {}}
for hostname in self.hosts:
self.inventory['_meta']['hostvars'][hostname] = {
'cloudforms': self.hosts[hostname],
}
# include the ansible_ssh_host in the top level
if 'ansible_ssh_host' in self.hosts[hostname]:
self.inventory['_meta']['hostvars'][hostname]['ansible_ssh_host'] = self.hosts[hostname]['ansible_ssh_host']
def parse_cli_args(self): data_to_print += self.json_format_dict(self.inventory, self.args.pretty)
''' Command line argument processing '''
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms') print(data_to_print)
parser.add_argument('--list', action='store_true', default=False,
help='List instances (default: False)') def is_cache_valid(self):
parser.add_argument('--host', action='store', """
help='Get all the variables about a specific instance') Determines if the cache files have expired, or if it is still valid
self.args = parser.parse_args() """
if self.args.debug:
print("Determining if cache [%s] is still valid (< %s seconds old)" % (self.cache_path_hosts, self.cache_max_age))
if os.path.isfile(self.cache_path_hosts):
mod_time = os.path.getmtime(self.cache_path_hosts)
current_time = time()
if (mod_time + self.cache_max_age) > current_time:
if os.path.isfile(self.cache_path_inventory):
if self.args.debug:
print("Cache is still valid!")
return True
if self.args.debug:
print("Cache is stale or does not exist.")
return False
def read_settings(self): def read_settings(self):
''' Reads the settings from the cloudforms.ini file ''' """
Reads the settings from the cloudforms.ini file
"""
config = ConfigParser.SafeConfigParser() config = ConfigParser.SafeConfigParser()
config_paths = [ config_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), 'cloudforms.ini'), os.path.dirname(os.path.realpath(__file__)) + '/cloudforms.ini',
"/opt/rh/cloudforms.ini", "/etc/ansible/cloudforms.ini",
] ]
env_value = os.environ.get('CLOUDFORMS_INI_PATH') env_value = os.environ.get('CLOUDFORMS_INI_PATH')
if env_value is not None: if env_value is not None:
config_paths.append(os.path.expanduser(os.path.expandvars(env_value))) config_paths.append(os.path.expanduser(os.path.expandvars(env_value)))
if self.args.debug:
for config_path in config_paths:
print("Reading from configuration file [%s]" % config_path)
config.read(config_paths) config.read(config_paths)
# Version # CloudForms API related
if config.has_option('cloudforms', 'version'): if config.has_option('cloudforms', 'url'):
self.cloudforms_version = config.get('cloudforms', 'version') self.cloudforms_url = config.get('cloudforms', 'url')
else: else:
self.cloudforms_version = "none" self.cloudforms_url = None
# CloudForms Endpoint if not self.cloudforms_url:
if config.has_option('cloudforms', 'hostname'): warnings.warn("No url specified, expected something like 'https://cfme.example.com'")
self.cloudforms_hostname = config.get('cloudforms', 'hostname')
else:
self.cloudforms_hostname = None
# CloudForms Username
if config.has_option('cloudforms', 'username'): if config.has_option('cloudforms', 'username'):
self.cloudforms_username = config.get('cloudforms', 'username') self.cloudforms_username = config.get('cloudforms', 'username')
else: else:
self.cloudforms_username = "none" self.cloudforms_username = None
if not self.cloudforms_username:
warnings.warn("No username specified, you need to specify a CloudForms username.")
# CloudForms Password
if config.has_option('cloudforms', 'password'): if config.has_option('cloudforms', 'password'):
self.cloudforms_password = config.get('cloudforms', 'password') self.cloudforms_pw = config.get('cloudforms', 'password')
else: else:
self.cloudforms_password = "none" self.cloudforms_pw = None
def get_hosts(self): if not self.cloudforms_pw:
''' Gets host from CloudForms ''' warnings.warn("No password specified, you need to specify a password for the CloudForms user.")
r = requests.get("https://" + self.cloudforms_hostname + "/api/vms?expand=resources&attributes=name,power_state", auth=(self.cloudforms_username,self.cloudforms_password), verify=False)
obj = r.json() if config.has_option('cloudforms', 'ssl_verify'):
self.cloudforms_ssl_verify = config.getboolean('cloudforms', 'ssl_verify')
else:
self.cloudforms_ssl_verify = True
#Remove objects that don't matter if config.has_option('cloudforms', 'version'):
del obj["count"] self.cloudforms_version = config.get('cloudforms', 'version')
del obj["subcount"] else:
del obj["name"] self.cloudforms_version = None
#Create a new list to grab VMs with power_state on to add to a new list if config.has_option('cloudforms', 'limit'):
#I'm sure there is a cleaner way to do this self.cloudforms_limit = config.getint('cloudforms', 'limit')
newlist = [] else:
getnext = False self.cloudforms_limit = 100
for x in obj.items():
for y in x[1]: if config.has_option('cloudforms', 'purge_actions'):
for z in y.items(): self.cloudforms_purge_actions = config.getboolean('cloudforms', 'purge_actions')
if getnext == True: else:
newlist.append(z[1]) self.cloudforms_purge_actions = True
getnext = False
if ( z[0] == "power_state" and z[1] == "on" ): if config.has_option('cloudforms', 'clean_group_keys'):
getnext = True self.cloudforms_clean_group_keys = config.getboolean('cloudforms', 'clean_group_keys')
newdict = {'hosts': newlist} else:
newdict2 = {'Dynamic_CloudForms': newdict} self.cloudforms_clean_group_keys = True
print json.dumps(newdict2, indent=2)
if config.has_option('cloudforms', 'nest_tags'):
self.cloudforms_nest_tags = config.getboolean('cloudforms', 'nest_tags')
else:
self.cloudforms_nest_tags = False
# Ansible related
try:
group_patterns = config.get('ansible', 'group_patterns')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
group_patterns = "[]"
self.group_patterns = eval(group_patterns)
# Cache related
try:
cache_path = os.path.expanduser(config.get('cache', 'path'))
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
cache_path = '.'
(script, ext) = os.path.splitext(os.path.basename(__file__))
self.cache_path_hosts = cache_path + "/%s.hosts" % script
self.cache_path_inventory = cache_path + "/%s.inventory" % script
self.cache_max_age = config.getint('cache', 'max_age')
if self.args.debug:
print("CloudForms settings:")
print("cloudforms_url = %s" % self.cloudforms_url)
print("cloudforms_username = %s" % self.cloudforms_username)
print("cloudforms_pw = %s" % self.cloudforms_pw)
print("cloudforms_ssl_verify = %s" % self.cloudforms_ssl_verify)
print("cloudforms_version = %s" % self.cloudforms_version)
print("cloudforms_limit = %s" % self.cloudforms_limit)
print("cloudforms_purge_actions = %s" % self.cloudforms_purge_actions)
print("Cache settings:")
print("cache_max_age = %s" % self.cache_max_age)
print("cache_path_hosts = %s" % self.cache_path_hosts)
print("cache_path_inventory = %s" % self.cache_path_inventory)
def parse_cli_args(self):
"""
Command line argument processing
"""
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms managed VMs')
parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)')
parser.add_argument('--host', action='store', help='Get all the variables about a specific instance')
parser.add_argument('--pretty', action='store_true', default=False, help='Pretty print JSON output (default: False)')
parser.add_argument('--refresh-cache', action='store_true', default=False,
help='Force refresh of cache by making API requests to CloudForms (default: False - use cache files)')
parser.add_argument('--debug', action='store_true', default=False, help='Show debug output while running (default: False)')
self.args = parser.parse_args()
def _get_json(self, url):
"""
Make a request and return the JSON
"""
results = []
ret = requests.get(url,
auth=HTTPBasicAuth(self.cloudforms_username, self.cloudforms_pw),
verify=self.cloudforms_ssl_verify)
ret.raise_for_status()
try:
results = json.loads(ret.text)
except ValueError:
warnings.warn("Unexpected response from {0} ({1}): {2}".format(self.cloudforms_url, ret.status_code, ret.reason))
results = {}
if self.args.debug:
print("=======================================================================")
print("=======================================================================")
print("=======================================================================")
print(ret.text)
print("=======================================================================")
print("=======================================================================")
print("=======================================================================")
return results
def _get_hosts(self):
"""
Get all hosts by paging through the results
"""
limit = self.cloudforms_limit
page = 0
last_page = False
results = []
while not last_page:
offset = page * limit
ret = self._get_json("%s/api/vms?offset=%s&limit=%s&expand=resources,tags,hosts,&attributes=ipaddresses" % (self.cloudforms_url, offset, limit))
results += ret['resources']
if ret['subcount'] < limit:
last_page = True
page += 1
return results
def update_cache(self):
"""
Make calls to cloudforms and save the output in a cache
"""
self.groups = dict()
self.hosts = dict()
if self.args.debug:
print("Updating cache...")
for host in self._get_hosts():
# Ignore VMs that are not powered on
if host['power_state'] != 'on':
if self.args.debug:
print("Skipping %s because power_state = %s" % (host['name'], host['power_state']))
continue
# purge actions
if self.cloudforms_purge_actions and 'actions' in host:
del host['actions']
# Create ansible groups for tags
if 'tags' in host:
# Create top-level group
if 'tags' not in self.inventory:
self.inventory['tags'] = dict(children=[], vars={}, hosts=[])
if not self.cloudforms_nest_tags:
# don't expand tags, just use them in a safe way
for group in host['tags']:
# Add sub-group, as a child of top-level
safe_key = self.to_safe(group['name'])
if safe_key:
if self.args.debug:
print("Adding sub-group '%s' to parent 'tags'" % safe_key)
if safe_key not in self.inventory['tags']['children']:
self.push(self.inventory['tags'], 'children', safe_key)
self.push(self.inventory, safe_key, host['name'])
if self.args.debug:
print("Found tag [%s] for host which will be mapped to [%s]" % (group['name'], safe_key))
else:
# expand the tags into nested groups / sub-groups
# Create nested groups for tags
safe_parent_tag_name = 'tags'
for tag in host['tags']:
tag_hierarchy = tag['name'][1:].split('/')
if self.args.debug:
print("Working on list %s" % tag_hierarchy)
for tag_name in tag_hierarchy:
if self.args.debug:
print("Working on tag_name = %s" % tag_name)
safe_tag_name = self.to_safe(tag_name)
if self.args.debug:
print("Using sanitized name %s" % safe_tag_name)
# Create sub-group
if safe_tag_name not in self.inventory:
self.inventory[safe_tag_name] = dict(children=[], vars={}, hosts=[])
# Add sub-group, as a child of top-level
if safe_parent_tag_name:
if self.args.debug:
print("Adding sub-group '%s' to parent '%s'" % (safe_tag_name, safe_parent_tag_name))
if safe_tag_name not in self.inventory[safe_parent_tag_name]['children']:
self.push(self.inventory[safe_parent_tag_name], 'children', safe_tag_name)
# Make sure the next one uses this one as it's parent
safe_parent_tag_name = safe_tag_name
# Add the host to the last tag
self.push(self.inventory[safe_parent_tag_name], 'hosts', host['name'])
# Set ansible_ssh_host to the first available ip address
if 'ipaddresses' in host and host['ipaddresses'] and isinstance(host['ipaddresses'], list):
host['ansible_ssh_host'] = host['ipaddresses'][0]
# Create additional groups
for key in ('location', 'type', 'vendor'):
safe_key = self.to_safe(host[key])
# Create top-level group
if key not in self.inventory:
self.inventory[key] = dict(children=[], vars={}, hosts=[])
# Create sub-group
if safe_key not in self.inventory:
self.inventory[safe_key] = dict(children=[], vars={}, hosts=[])
# Add sub-group, as a child of top-level
if safe_key not in self.inventory[key]['children']:
self.push(self.inventory[key], 'children', safe_key)
if key in host:
# Add host to sub-group
self.push(self.inventory[safe_key], 'hosts', host['name'])
self.hosts[host['name']] = host
self.push(self.inventory, 'all', host['name'])
if self.args.debug:
print("Saving cached data")
self.write_to_cache(self.hosts, self.cache_path_hosts)
self.write_to_cache(self.inventory, self.cache_path_inventory)
def get_host_info(self, host):
"""
Get variables about a specific host
"""
if not self.hosts or len(self.hosts) == 0:
# Need to load cache from cache
self.load_hosts_from_cache()
if host not in self.hosts:
if self.args.debug:
print("[%s] not found in cache." % host)
# try updating the cache
self.update_cache()
if host not in self.hosts:
if self.args.debug:
print("[%s] does not exist after cache update." % host)
# host might not exist anymore
return self.json_format_dict({}, self.args.pretty)
return self.json_format_dict(self.hosts[host], self.args.pretty)
def push(self, d, k, v):
"""
Safely puts a new entry onto an array.
"""
if k in d:
d[k].append(v)
else:
d[k] = [v]
def load_inventory_from_cache(self):
"""
Reads the inventory from the cache file sets self.inventory
"""
cache = open(self.cache_path_inventory, 'r')
json_inventory = cache.read()
self.inventory = json.loads(json_inventory)
def load_hosts_from_cache(self):
"""
Reads the cache from the cache file sets self.hosts
"""
cache = open(self.cache_path_hosts, 'r')
json_cache = cache.read()
self.hosts = json.loads(json_cache)
def write_to_cache(self, data, filename):
"""
Writes data in JSON format to a file
"""
json_data = self.json_format_dict(data, True)
cache = open(filename, 'w')
cache.write(json_data)
cache.close()
def to_safe(self, word):
"""
Converts 'bad' characters in a string to underscores so they can be used as Ansible groups
"""
if self.cloudforms_clean_group_keys:
regex = "[^A-Za-z0-9\_]"
return re.sub(regex, "_", word.replace(" ", ""))
else:
return word
def json_format_dict(self, data, pretty=False):
"""
Converts a dict to a JSON object and dumps it as a formatted string
"""
if pretty:
return json.dumps(data, sort_keys=True, indent=2)
else:
return json.dumps(data)
# Run the script
CloudFormsInventory() CloudFormsInventory()

View File

@@ -1,8 +1,6 @@
#!/usr/bin/python #!/usr/bin/env python
# vim: set fileencoding=utf-8 : # vim: set fileencoding=utf-8 :
# #
# NOTE FOR TOWER: change foreman_ to sattelite_ for the group prefix
#
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org> # Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
# #
# This script is free software: you can redistribute it and/or modify # This script is free software: you can redistribute it and/or modify
@@ -41,6 +39,7 @@ class ForemanInventory(object):
self.inventory = dict() # A list of groups and the hosts in that group self.inventory = dict() # A list of groups and the hosts in that group
self.cache = dict() # Details about hosts in the inventory self.cache = dict() # Details about hosts in the inventory
self.params = dict() # Params of each host self.params = dict() # Params of each host
self.facts = dict() # Facts of each host
self.hostgroups = dict() # host groups self.hostgroups = dict() # host groups
# Read settings and parse CLI arguments # Read settings and parse CLI arguments
@@ -55,6 +54,7 @@ class ForemanInventory(object):
else: else:
self.load_inventory_from_cache() self.load_inventory_from_cache()
self.load_params_from_cache() self.load_params_from_cache()
self.load_facts_from_cache()
self.load_cache_from_cache() self.load_cache_from_cache()
data_to_print = "" data_to_print = ""
@@ -69,6 +69,9 @@ class ForemanInventory(object):
'foreman': self.cache[hostname], 'foreman': self.cache[hostname],
'foreman_params': self.params[hostname], 'foreman_params': self.params[hostname],
} }
if self.want_facts:
self.inventory['_meta']['hostvars'][hostname]['foreman_facts'] = self.facts[hostname]
data_to_print += self.json_format_dict(self.inventory, True) data_to_print += self.json_format_dict(self.inventory, True)
print(data_to_print) print(data_to_print)
@@ -81,7 +84,8 @@ class ForemanInventory(object):
current_time = time() current_time = time()
if (mod_time + self.cache_max_age) > current_time: if (mod_time + self.cache_max_age) > current_time:
if (os.path.isfile(self.cache_path_inventory) and if (os.path.isfile(self.cache_path_inventory) and
os.path.isfile(self.cache_path_params)): os.path.isfile(self.cache_path_params) and
os.path.isfile(self.cache_path_facts)):
return True return True
return False return False
@@ -114,6 +118,16 @@ class ForemanInventory(object):
self.group_patterns = eval(group_patterns) self.group_patterns = eval(group_patterns)
try:
self.group_prefix = config.get('ansible', 'group_prefix')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
self.group_prefix = "foreman_"
try:
self.want_facts = config.getboolean('ansible', 'want_facts')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
self.want_facts = True
# Cache related # Cache related
try: try:
cache_path = os.path.expanduser(config.get('cache', 'path')) cache_path = os.path.expanduser(config.get('cache', 'path'))
@@ -123,6 +137,7 @@ class ForemanInventory(object):
self.cache_path_cache = cache_path + "/%s.cache" % script self.cache_path_cache = cache_path + "/%s.cache" % script
self.cache_path_inventory = cache_path + "/%s.index" % script self.cache_path_inventory = cache_path + "/%s.index" % script
self.cache_path_params = cache_path + "/%s.params" % script self.cache_path_params = cache_path + "/%s.params" % script
self.cache_path_facts = cache_path + "/%s.facts" % script
self.cache_max_age = config.getint('cache', 'max_age') self.cache_max_age = config.getint('cache', 'max_age')
def parse_cli_args(self): def parse_cli_args(self):
@@ -135,7 +150,7 @@ class ForemanInventory(object):
help='Force refresh of cache by making API requests to foreman (default: False - use cache files)') help='Force refresh of cache by making API requests to foreman (default: False - use cache files)')
self.args = parser.parse_args() self.args = parser.parse_args()
def _get_json(self, url): def _get_json(self, url, ignore_errors=None):
page = 1 page = 1
results = [] results = []
while True: while True:
@@ -143,10 +158,14 @@ class ForemanInventory(object):
auth=HTTPBasicAuth(self.foreman_user, self.foreman_pw), auth=HTTPBasicAuth(self.foreman_user, self.foreman_pw),
verify=self.foreman_ssl_verify, verify=self.foreman_ssl_verify,
params={'page': page, 'per_page': 250}) params={'page': page, 'per_page': 250})
if ignore_errors and ret.status_code in ignore_errors:
break
ret.raise_for_status() ret.raise_for_status()
json = ret.json() json = ret.json()
if not json.has_key('results'): if not json.has_key('results'):
return json return json
if type(json['results']) == type({}):
return json['results']
results = results + json['results'] results = results + json['results']
if len(results) >= json['total']: if len(results) >= json['total']:
break break
@@ -162,38 +181,44 @@ class ForemanInventory(object):
self.hostgroups[hid] = self._get_json(url) self.hostgroups[hid] = self._get_json(url)
return self.hostgroups[hid] return self.hostgroups[hid]
def _get_params_by_id(self, hid): def _get_all_params_by_id(self, hid):
url = "%s/api/v2/hosts/%s/parameters" % (self.foreman_url, hid) url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid)
ret = self._get_json(url, [404])
if ret == []: ret = {}
return ret.get('all_parameters', {})
def _get_facts_by_id(self, hid):
url = "%s/api/v2/hosts/%s/facts" % (self.foreman_url, hid)
return self._get_json(url) return self._get_json(url)
def _resolve_params(self, host): def _resolve_params(self, host):
""" """
Resolve all host group params of the host using the top level Fetch host params and convert to dict
hostgroup and the ancestry.
""" """
hostgroup_id = host['hostgroup_id']
paramgroups = []
params = {} params = {}
if hostgroup_id: for param in self._get_all_params_by_id(host['id']):
hostgroup = self._get_hostgroup_by_id(hostgroup_id)
ancestry_path = hostgroup.get('ancestry', '')
ancestry = ancestry_path.split('/') if ancestry_path is not None else []
# Append top level hostgroup last to overwrite lower levels
# values
ancestry.append(hostgroup_id)
paramgroups = [self._get_hostgroup_by_id(hostgroup_id)['parameters']
for hostgroup_id in ancestry]
paramgroups += [self._get_params_by_id(host['id'])]
for paramgroup in paramgroups:
for param in paramgroup:
name = param['name'] name = param['name']
params[name] = param['value'] params[name] = param['value']
return params return params
def _get_facts(self, host):
"""
Fetch all host facts of the host
"""
if not self.want_facts:
return {}
ret = self._get_facts_by_id(host['id'])
if len(ret.values()) == 0:
facts = {}
elif len(ret.values()) == 1:
facts = ret.values()[0]
else:
raise ValueError("More than one set of facts returned for '%s'" % host)
return facts
def update_cache(self): def update_cache(self):
"""Make calls to foreman and save the output in a cache""" """Make calls to foreman and save the output in a cache"""
@@ -203,11 +228,17 @@ class ForemanInventory(object):
for host in self._get_hosts(): for host in self._get_hosts():
dns_name = host['name'] dns_name = host['name']
# Create ansible groups for hostgroup, location and organization # Create ansible groups for hostgroup, environment, location and organization
for group in ['hostgroup', 'location', 'organization']: for group in ['hostgroup', 'environment', 'location', 'organization']:
val = host.get('%s_name' % group) val = host.get('%s_name' % group)
if val: if val:
safe_key = self.to_safe('satellite_%s_%s' % (group, val.lower())) safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower()))
self.push(self.inventory, safe_key, dns_name)
for group in ['lifecycle_environment', 'content_view']:
val = host.get('content_facet_attributes', {}).get('%s_name' % group)
if val:
safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower()))
self.push(self.inventory, safe_key, dns_name) self.push(self.inventory, safe_key, dns_name)
params = self._resolve_params(host) params = self._resolve_params(host)
@@ -231,11 +262,13 @@ class ForemanInventory(object):
self.cache[dns_name] = host self.cache[dns_name] = host
self.params[dns_name] = params self.params[dns_name] = params
self.facts[dns_name] = self._get_facts(host)
self.push(self.inventory, 'all', dns_name) self.push(self.inventory, 'all', dns_name)
self.write_to_cache(self.cache, self.cache_path_cache) self.write_to_cache(self.cache, self.cache_path_cache)
self.write_to_cache(self.inventory, self.cache_path_inventory) self.write_to_cache(self.inventory, self.cache_path_inventory)
self.write_to_cache(self.params, self.cache_path_params) self.write_to_cache(self.params, self.cache_path_params)
self.write_to_cache(self.facts, self.cache_path_facts)
def get_host_info(self): def get_host_info(self):
""" Get variables about a specific host """ """ Get variables about a specific host """
@@ -274,6 +307,14 @@ class ForemanInventory(object):
json_params = cache.read() json_params = cache.read()
self.params = json.loads(json_params) self.params = json.loads(json_params)
def load_facts_from_cache(self):
""" Reads the index from the cache file sets self.index """
if not self.want_facts:
return
cache = open(self.cache_path_facts, 'r')
json_facts = cache.read()
self.facts = json.loads(json_facts)
def load_cache_from_cache(self): def load_cache_from_cache(self):
""" Reads the cache from the cache file sets self.cache """ """ Reads the cache from the cache file sets self.cache """
@@ -301,4 +342,7 @@ class ForemanInventory(object):
else: else:
return json.dumps(data) return json.dumps(data)
ForemanInventory() if __name__ == '__main__':
ForemanInventory()

View File

@@ -682,6 +682,16 @@ SATELLITE6_HOST_FILTER = r'^.+$'
SATELLITE6_EXCLUDE_EMPTY_GROUPS = True SATELLITE6_EXCLUDE_EMPTY_GROUPS = True
SATELLITE6_INSTANCE_ID_VAR = 'foreman.id' 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 -- # -- Activity Stream --
# --------------------- # ---------------------

View File

@@ -1,2 +1,21 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import threading
# Monkeypatch xmlsec.initialize() to only run once (https://github.com/ansible/ansible-tower/issues/3241).
xmlsec_init_lock = threading.Lock()
xmlsec_initialized = False
import dm.xmlsec.binding
original_xmlsec_initialize = dm.xmlsec.binding.initialize
def xmlsec_initialize(*args, **kwargs):
global xmlsec_init_lock, xmlsec_initialized, original_xmlsec_initialize
with xmlsec_init_lock:
if not xmlsec_initialized:
original_xmlsec_initialize(*args, **kwargs)
xmlsec_initialized = True
dm.xmlsec.binding.initialize = xmlsec_initialize

View File

@@ -80,12 +80,6 @@ a.red-txt:active {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.name-column {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
blockquote { blockquote {
font-size: 14px; font-size: 14px;
} }

View File

@@ -44,9 +44,11 @@
color: @list-header-txt; color: @list-header-txt;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
white-space: nowrap;
padding-bottom: 25px; padding-bottom: 25px;
min-height: 45px; min-height: 45px;
word-break: break-all;
max-width: 90%;
word-wrap: break-word;
} }
.Form-secondaryTitle{ .Form-secondaryTitle{
@@ -55,7 +57,10 @@
min-height: 40px; 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; height:15px;
color: @default-interface-txt; color: @default-interface-txt;
background-color: @default-list-header-bg; background-color: @default-list-header-bg;
@@ -353,6 +358,11 @@
border-color: transparent transparent @field-dropdown-icon transparent!important; 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{ .select2-dropdown{
border:1px solid @field-border; border:1px solid @field-border;

View File

@@ -78,6 +78,7 @@ table, tbody {
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
border-top:0px!important; border-top:0px!important;
word-wrap: break-word;
} }
.List-tableCell.description-column { .List-tableCell.description-column {
@@ -383,6 +384,7 @@ table, tbody {
.List-action--showTooltipOnDisabled { .List-action--showTooltipOnDisabled {
display: inline-block; display: inline-block;
cursor: not-allowed;
} }
.List-action--showTooltipOnDisabled .btn[disabled] { .List-action--showTooltipOnDisabled .btn[disabled] {

View File

@@ -1,6 +1,6 @@
{ {
"name": "angular-codemirror", "name": "angular-codemirror",
"version": "1.0.2", "version": "1.0.3",
"dependencies": { "dependencies": {
"angular": "latest", "angular": "latest",
"angular-route": "latest", "angular-route": "latest",
@@ -13,14 +13,13 @@
"codemirror": "latest" "codemirror": "latest"
}, },
"homepage": "https://github.com/chouseknecht/angular-codemirror", "homepage": "https://github.com/chouseknecht/angular-codemirror",
"_release": "1.0.2", "_release": "1.0.3",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v1.0.2", "tag": "1.0.3",
"commit": "94b7aac548b036f4fbd94e56129ed9574e472616" "commit": "b94dc86fde8f60a50b324054806d29d742177d21"
}, },
"_source": "git://github.com/chouseknecht/angular-codemirror.git", "_source": "https://github.com/chouseknecht/angular-codemirror.git",
"_target": "~1.0.2", "_target": "~1.0.3",
"_originalSource": "angular-codemirror", "_originalSource": "angular-codemirror"
"_direct": true
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "angular-codemirror", "name": "angular-codemirror",
"version": "0.0.3", "version": "1.0.2",
"dependencies": { "dependencies": {
"angular": "latest", "angular": "latest",
"angular-route": "latest", "angular-route": "latest",

View File

@@ -108,3 +108,4 @@
.CodeMirror-lint-tooltip { .CodeMirror-lint-tooltip {
z-index: 2060; z-index: 2060;
} }

View File

@@ -30,7 +30,7 @@
angular.module('AngularCodeMirrorModule', []) angular.module('AngularCodeMirrorModule', [])
.factory('AngularCodeMirror', [ function() { .factory('AngularCodeMirror', [ function() {
return function() { return function(readOnly) {
var fn = function() { var fn = function() {
this.myCodeMirror = null; this.myCodeMirror = null;
@@ -43,7 +43,6 @@ angular.module('AngularCodeMirrorModule', [])
model = params.model, model = params.model,
mode = params.mode, mode = params.mode,
onReady = params.onReady, onReady = params.onReady,
onChange = params.onChange,
height = 0; height = 0;
self.element = $(element); self.element = $(element);
@@ -69,6 +68,15 @@ angular.module('AngularCodeMirrorModule', [])
// Initialize CodeMirror // Initialize CodeMirror
self.modes[mode].value = scope[model]; self.modes[mode].value = scope[model];
// if readOnly is passed to AngularCodeMirror, set the
// options for all modes to be readOnly
if (readOnly) {
Object.keys(self.modes).forEach(function(val) {
self.modes[val].readOnly = true;
});
}
self.myCodeMirror = CodeMirror(document.getElementById('cm-' + model + '-container'), self.modes[mode]); self.myCodeMirror = CodeMirror(document.getElementById('cm-' + model + '-container'), self.modes[mode]);
// Adjust the height // Adjust the height
@@ -85,14 +93,7 @@ angular.module('AngularCodeMirrorModule', [])
// Update the model on change // Update the model on change
self.myCodeMirror.on('change', function() { self.myCodeMirror.on('change', function() {
setTimeout(function() { setTimeout(function() { scope.$apply(function(){ scope[model] = self.myCodeMirror.getValue(); }); }, 500);
scope.$apply(function(){
scope[model] = self.myCodeMirror.getValue();
if (onChange) {
onChange();
}
});
}, 500);
}); });
}; };

View File

@@ -1,5 +1,5 @@
/*************************************************************************** /***************************************************************************
* angular-scheruler.js * angular-scheduler.js
* *
* Copyright (c) 2014 Ansible, Inc. * Copyright (c) 2014 Ansible, Inc.
* *
@@ -13,16 +13,32 @@
/* global RRule */ /* global RRule */
'use strict'; (function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
angular.module('underscore',[]) define(['lodash', 'angular', 'jquery', 'jquery-ui', 'moment'], factory);
.factory('_', [ function() { } else if (typeof module === 'object' && module.exports) {
return window._; // Node. Does not work with strict CommonJS, but
}]); // only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('lodash'), require('angular'), require('jquery'), require('jquery-ui'), require('moment'));
angular.module('AngularScheduler', ['underscore']) } else {
// Browser globals (root is window)
root.returnExports = factory(root._, root.angular, root.$, root.$.ui, root.moment);
}
}(this, function(_, angular, $, ui, moment) {
return angular.module('AngularScheduler', [])
.filter('schedulerDate', ['moment', function(moment) {
return function(input) {
var date;
if(input === null){
return "";
}else {
date = moment(input.split(" ")[0]);
return date.format('l') + input.slice(input.indexOf(" "));
}
};
}])
.constant('AngularScheduler.partials', '/lib/') .constant('AngularScheduler.partials', '/lib/')
.constant('AngularScheduler.useTimezone', false) .constant('AngularScheduler.useTimezone', false)
@@ -30,9 +46,9 @@ angular.module('AngularScheduler', ['underscore'])
// Initialize supporting scope variables and functions. Returns a scheduler object with getString(), // Initialize supporting scope variables and functions. Returns a scheduler object with getString(),
// setString() and inject() methods. // setString() and inject() methods.
.factory('SchedulerInit', ['$log', '$filter', '$timezones', 'LoadLookupValues', 'SetDefaults', 'CreateObject', '_', .factory('SchedulerInit', ['$log', '$filter', '$timezones', 'LoadLookupValues', 'SetDefaults', 'CreateObject',
'AngularScheduler.useTimezone', 'AngularScheduler.showUTCField', 'InRange', 'AngularScheduler.useTimezone', 'AngularScheduler.showUTCField', 'InRange',
function($log, $filter, $timezones, LoadLookupValues, SetDefaults, CreateObject, _, useTimezone, showUTCField, InRange) { function($log, $filter, $timezones, LoadLookupValues, SetDefaults, CreateObject, useTimezone, showUTCField, InRange) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
@@ -63,11 +79,9 @@ angular.module('AngularScheduler', ['underscore'])
if (scope.schedulerStartDt === "" || scope.schedulerStartDt === null || scope.schedulerStartDt === undefined) { if (scope.schedulerStartDt === "" || scope.schedulerStartDt === null || scope.schedulerStartDt === undefined) {
scope.startDateError("Provide a valid start date and time"); scope.startDateError("Provide a valid start date and time");
scope.schedulerUTCTime = ''; scope.schedulerUTCTime = '';
} } else if (!(InRange(scope.schedulerStartHour, 0, 23, 2) && InRange(scope.schedulerStartMinute, 0, 59, 2) && InRange(scope.schedulerStartSecond, 0, 59, 2))) {
else if ( !(InRange(scope.schedulerStartHour, 0, 23, 2) && InRange(scope.schedulerStartMinute, 0, 59, 2) && InRange(scope.schedulerStartSecond, 0, 59, 2)) ) {
scope.scheduler_startTime_error = true; scope.scheduler_startTime_error = true;
} } else {
else {
if (useTimezone) { if (useTimezone) {
scope.resetStartDate(); scope.resetStartDate();
try { try {
@@ -79,19 +93,17 @@ angular.module('AngularScheduler', ['underscore'])
scope.schedulerUTCTime = $filter('schDateStrFix')($timezones.toUTC(dateStr, scope.schedulerTimeZone.name).toISOString()); scope.schedulerUTCTime = $filter('schDateStrFix')($timezones.toUTC(dateStr, scope.schedulerTimeZone.name).toISOString());
scope.scheduler_form_schedulerStartDt_error = false; scope.scheduler_form_schedulerStartDt_error = false;
scope.scheduler_startTime_error = false; scope.scheduler_startTime_error = false;
} } catch (e) {
catch(e) {
scope.startDateError("Provide a valid start date and time"); scope.startDateError("Provide a valid start date and time");
} }
} } else {
else {
scope.scheduler_startTime_error = false; scope.scheduler_startTime_error = false;
scope.scheduler_form_schedulerStartDt_error = false; scope.scheduler_form_schedulerStartDt_error = false;
scope.schedulerUTCTime = $filter('schDateStrFix')(scope.schedulerStartDt + 'T' + scope.schedulerStartHour + ':' + scope.schedulerStartMinute + scope.schedulerUTCTime = $filter('schDateStrFix')(scope.schedulerStartDt + 'T' + scope.schedulerStartHour + ':' + scope.schedulerStartMinute +
':' + scope.schedulerStartSecond + '.000Z'); ':' + scope.schedulerStartSecond + '.000Z');
} }
} }
if (callback){ if (callback) {
callback(); callback();
} }
}; };
@@ -122,8 +134,7 @@ angular.module('AngularScheduler', ['underscore'])
scope.schedulerInterval = 1; scope.schedulerInterval = 1;
scope.schedulerShowInterval = true; scope.schedulerShowInterval = true;
scope.schedulerIntervalLabel = scope.schedulerFrequency.intervalLabel; scope.schedulerIntervalLabel = scope.schedulerFrequency.intervalLabel;
} } else {
else {
scope.schedulerShowInterval = false; scope.schedulerShowInterval = false;
scope.schedulerEnd = scope.endOptions[0]; scope.schedulerEnd = scope.endOptions[0];
} }
@@ -138,8 +149,7 @@ angular.module('AngularScheduler', ['underscore'])
scope.monthlyRepeatChange = function() { scope.monthlyRepeatChange = function() {
if (scope.monthlyRepeatOption !== 'day') { if (scope.monthlyRepeatOption !== 'day') {
$('#monthDay').spinner('disable'); $('#monthDay').spinner('disable');
} } else {
else {
$('#monthDay').spinner('enable'); $('#monthDay').spinner('enable');
} }
}; };
@@ -147,8 +157,7 @@ angular.module('AngularScheduler', ['underscore'])
scope.yearlyRepeatChange = function() { scope.yearlyRepeatChange = function() {
if (scope.yearlyRepeatOption !== 'month') { if (scope.yearlyRepeatOption !== 'month') {
$('#yearlyRepeatDay').spinner('disable'); $('#yearlyRepeatDay').spinner('disable');
} } else {
else {
$('#yearlyRepeatDay').spinner('enable'); $('#yearlyRepeatDay').spinner('enable');
} }
}; };
@@ -157,9 +166,8 @@ angular.module('AngularScheduler', ['underscore'])
// Add or remove day when user clicks checkbox button // Add or remove day when user clicks checkbox button
var i = scope.weekDays.indexOf(day); var i = scope.weekDays.indexOf(day);
if (i >= 0) { if (i >= 0) {
scope.weekDays.splice(i,1); scope.weekDays.splice(i, 1);
} } else {
else {
scope.weekDays.push(day); scope.weekDays.push(day);
} }
$(event.target).blur(); $(event.target).blur();
@@ -204,15 +212,15 @@ angular.module('AngularScheduler', ['underscore'])
if (useTimezone) { if (useTimezone) {
// Build list of timezone <select> element options // Build list of timezone <select> element options
$timezones.getZoneList(scope); $timezones.getZoneList(scope);
} } else {
else {
scope.setDefaults(); scope.setDefaults();
} }
return CreateObject(scope, requireFutureStartTime); return CreateObject(scope, requireFutureStartTime);
}; };
}]) }
])
/** /**
Return an AngularScheduler object we can use to get the RRule result from user input, check if Return an AngularScheduler object we can use to get the RRule result from user input, check if
@@ -241,29 +249,25 @@ angular.module('AngularScheduler', ['underscore'])
options.endDate = scope.schedulerEndDt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, function(match, p1, p2, p3) { options.endDate = scope.schedulerEndDt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, function(match, p1, p2, p3) {
return p3 + '-' + p1 + '-' + p2; return p3 + '-' + p1 + '-' + p2;
}) + 'T' + }) + 'T' +
$filter('schZeroPad')(this.scope.schedulerEndHour,2) + ':' + $filter('schZeroPad')(this.scope.schedulerEndHour, 2) + ':' +
$filter('schZeroPad')(this.scope.schedulerEndMinute,2) + ':' + $filter('schZeroPad')(this.scope.schedulerEndMinute, 2) + ':' +
$filter('schZeroPad')(this.scope.schedulerEndSecond,2)+ 'Z'; $filter('schZeroPad')(this.scope.schedulerEndSecond, 2) + 'Z';
} }
if (this.scope.schedulerFrequency.value === 'weekly') { if (this.scope.schedulerFrequency.value === 'weekly') {
options.weekDays = this.scope.weekDays; options.weekDays = this.scope.weekDays;
} } else if (this.scope.schedulerFrequency.value === 'yearly') {
else if (this.scope.schedulerFrequency.value === 'yearly') {
if (this.scope.yearlyRepeatOption === 'month') { if (this.scope.yearlyRepeatOption === 'month') {
options.month = this.scope.yearlyMonth.value; options.month = this.scope.yearlyMonth.value;
options.monthDay = this.scope.yearlyMonthDay; options.monthDay = this.scope.yearlyMonthDay;
} } else {
else {
options.setOccurrence = this.scope.yearlyOccurrence.value; options.setOccurrence = this.scope.yearlyOccurrence.value;
options.weekDays = this.scope.yearlyWeekDay.value; options.weekDays = this.scope.yearlyWeekDay.value;
options.month = this.scope.yearlyOtherMonth.value; options.month = this.scope.yearlyOtherMonth.value;
} }
} } else if (this.scope.schedulerFrequency.value === 'monthly') {
else if (this.scope.schedulerFrequency.value === 'monthly') {
if (this.scope.monthlyRepeatOption === 'day') { if (this.scope.monthlyRepeatOption === 'day') {
options.monthDay = this.scope.monthDay; options.monthDay = this.scope.monthDay;
} } else {
else {
options.setOccurrence = this.scope.monthlyOccurrence.value; options.setOccurrence = this.scope.monthlyOccurrence.value;
options.weekDays = this.scope.monthlyWeekDay.value; options.weekDays = this.scope.monthlyWeekDay.value;
} }
@@ -297,27 +301,26 @@ angular.module('AngularScheduler', ['underscore'])
scope.rrule_nlp_description = rrule.toText(); scope.rrule_nlp_description = rrule.toText();
scope.dateChoice = 'local'; scope.dateChoice = 'local';
scope.occurrence_list = []; scope.occurrence_list = [];
rrule.all(function(date, i){ rrule.all(function(date, i) {
var local, dt; var local, dt;
if (i < 10) { if (i < 10) {
if (useTimezone) { if (useTimezone) {
dt = $timezones.align(date, scope.schedulerTimeZone.name); dt = $timezones.align(date, scope.schedulerTimeZone.name);
local = $filter('schZeroPad')(dt.getMonth() + 1,2) + '/' + local = $filter('schZeroPad')(dt.getMonth() + 1, 2) + '/' +
$filter('schZeroPad')(dt.getDate(),2) + '/' + dt.getFullYear() + ' ' + $filter('schZeroPad')(dt.getDate(), 2) + '/' + dt.getFullYear() + ' ' +
$filter('schZeroPad')(dt.getHours(),2) + ':' + $filter('schZeroPad')(dt.getHours(), 2) + ':' +
$filter('schZeroPad')(dt.getMinutes(),2) + ':' + $filter('schZeroPad')(dt.getMinutes(), 2) + ':' +
$filter('schZeroPad')(dt.getSeconds(),2) + ' ' + $filter('schZeroPad')(dt.getSeconds(), 2) + ' ' +
dt.getTimezoneAbbreviation(); dt.getTimezoneAbbreviation();
} } else {
else {
local = $filter('date')(date, 'MM/dd/yyyy HH:mm:ss Z'); local = $filter('date')(date, 'MM/dd/yyyy HH:mm:ss Z');
} }
scope.occurrence_list.push({ utc: $filter('schDateStrFix')(date.toISOString()), local: local }); scope.occurrence_list.push({ utc: $filter('schedulerDate')($filter('schDateStrFix')(date.toISOString())), local: $filter('schedulerDate')(local) });
return true; return true;
} }
return false; return false;
}); });
scope.rrule_nlp_description = rrule.toText().replace(/^RRule error.*$/,'Natural language description not available'); scope.rrule_nlp_description = rrule.toText().replace(/^RRule error.*$/, 'Natural language description not available');
scope.rrule = rrule.toString(); scope.rrule = rrule.toString();
} }
}; };
@@ -351,7 +354,7 @@ angular.module('AngularScheduler', ['underscore'])
this.scope.scheduler_yearlyMonthDay_error = true; this.scope.scheduler_yearlyMonthDay_error = true;
validity = false; validity = false;
} }
if ( !(InRange(scope.schedulerStartHour, 0, 23, 2) && InRange(scope.schedulerStartMinute, 0, 59, 2) && InRange(scope.schedulerStartSecond, 0, 59, 2)) ) { if (!(InRange(scope.schedulerStartHour, 0, 23, 2) && InRange(scope.schedulerStartMinute, 0, 59, 2) && InRange(scope.schedulerStartSecond, 0, 59, 2))) {
this.scope.scheduler_startTime_error = true; this.scope.scheduler_startTime_error = true;
validity = false; validity = false;
} }
@@ -379,33 +382,29 @@ angular.module('AngularScheduler', ['underscore'])
now = new Date(); now = new Date();
if (this.useTimezone) { if (this.useTimezone) {
dateStr = now.getFullYear() + '-' + dateStr = now.getFullYear() + '-' +
$filter('schZeroPad')(now.getMonth() + 1, 2)+ '-' + $filter('schZeroPad')(now.getMonth() + 1, 2) + '-' +
$filter('schZeroPad')(now.getDate(),2) + 'T' + $filter('schZeroPad')(now.getDate(), 2) + 'T' +
$filter('schZeroPad')(now.getHours(),2) + ':' + $filter('schZeroPad')(now.getHours(), 2) + ':' +
$filter('schZeroPad')(now.getMinutes(),2) + ':' + $filter('schZeroPad')(now.getMinutes(), 2) + ':' +
$filter('schZeroPad')(now.getSeconds(),2) + '.000Z'; $filter('schZeroPad')(now.getSeconds(), 2) + '.000Z';
adjNow = $timezones.toUTC(dateStr, this.scope.schedulerTimeZone.name); //Adjust to the selected TZ adjNow = $timezones.toUTC(dateStr, this.scope.schedulerTimeZone.name); //Adjust to the selected TZ
timeNow = adjNow.getTime(); timeNow = adjNow.getTime();
} } else {
else {
timeNow = now.getTime(); timeNow = now.getTime();
} }
if (this.requireFutureStartTime && timeNow >= timeFuture) { if (this.requireFutureStartTime && timeNow >= timeFuture) {
this.scope.startDateError("Start time must be in the future"); this.scope.startDateError("Start time must be in the future");
validity = false; validity = false;
} }
} } else {
else {
this.scope.startDateError("Invalid start time"); this.scope.startDateError("Invalid start time");
validity = false; validity = false;
} }
} } catch (e) {
catch(e) {
this.scope.startDateError("Invalid start time"); this.scope.startDateError("Invalid start time");
validity = false; validity = false;
} }
} } else {
else {
this.scope.startDateError("Provide a start time"); this.scope.startDateError("Provide a start time");
validity = false; validity = false;
} }
@@ -491,9 +490,10 @@ angular.module('AngularScheduler', ['underscore'])
}; };
return new fn(); return new fn();
}; };
}]) }
])
.factory('InRange', [ function() { .factory('InRange', [function() {
return function(x, min, max, length) { return function(x, min, max, length) {
var rx = new RegExp("\\d{1," + length + "}"); var rx = new RegExp("\\d{1," + length + "}");
if (!rx.test(x)) { if (!rx.test(x)) {
@@ -526,11 +526,11 @@ angular.module('AngularScheduler', ['underscore'])
}); });
$http({ method: 'GET', url: scheduler_partial + 'angular-scheduler.html' }) $http({ method: 'GET', url: scheduler_partial + 'angular-scheduler.html' })
.success( function(data) { .success(function(data) {
scope.$emit('htmlReady', data); scope.$emit('htmlReady', data);
}) })
.error( function(data, status) { .error(function(data, status) {
throw('Error reading ' + scheduler_partial + 'angular-scheduler.html. ' + status); throw ('Error reading ' + scheduler_partial + 'angular-scheduler.html. ' + status);
//$log.error('Error calling ' + scheduler_partial + '. ' + status); //$log.error('Error calling ' + scheduler_partial + '. ' + status);
}); });
}; };
@@ -555,11 +555,11 @@ angular.module('AngularScheduler', ['underscore'])
}); });
$http({ method: 'GET', url: scheduler_partial + 'angular-scheduler-detail.html' }) $http({ method: 'GET', url: scheduler_partial + 'angular-scheduler-detail.html' })
.success( function(data) { .success(function(data) {
scope.$emit('htmlDetailReady', data); scope.$emit('htmlDetailReady', data);
}) })
.error( function(data, status) { .error(function(data, status) {
throw('Error reading ' + scheduler_partial + 'angular-scheduler-detail.html. ' + status); throw ('Error reading ' + scheduler_partial + 'angular-scheduler-detail.html. ' + status);
//$log.error('Error calling ' + scheduler_partial + '. ' + status); //$log.error('Error calling ' + scheduler_partial + '. ' + status);
}); });
}; };
@@ -580,16 +580,15 @@ angular.module('AngularScheduler', ['underscore'])
monthDay = params.monthDay, // integer, optional, between 1 and 31 monthDay = params.monthDay, // integer, optional, between 1 and 31
weekDays = params.weekDays, // integer, optional, valid value from weekdays weekDays = params.weekDays, // integer, optional, valid value from weekdays
setOccurrence = params.setOccurrence, // integer, optional, valid value from occurrences setOccurrence = params.setOccurrence, // integer, optional, valid value from occurrences
options = {}, i; options = {},
i;
if (angular.isDate(startDate)) { if (angular.isDate(startDate)) {
options.dtstart = startDate; options.dtstart = startDate;
} } else {
else {
try { try {
options.dtstart = new Date(startDate); options.dtstart = new Date(startDate);
} } catch (e) {
catch(e) {
$log.error('Date conversion failed. Attempted to convert ' + startDate + ' to Date. ' + e.message); $log.error('Date conversion failed. Attempted to convert ' + startDate + ' to Date. ' + e.message);
} }
} }
@@ -604,7 +603,7 @@ angular.module('AngularScheduler', ['underscore'])
if (weekDays && angular.isArray(weekDays)) { if (weekDays && angular.isArray(weekDays)) {
options.byweekday = []; options.byweekday = [];
for (i=0; i < weekDays.length; i++) { for (i = 0; i < weekDays.length; i++) {
options.byweekday.push(RRule[weekDays[i].toUpperCase()]); options.byweekday.push(RRule[weekDays[i].toUpperCase()]);
} }
} }
@@ -623,22 +622,18 @@ angular.module('AngularScheduler', ['underscore'])
if (occurrenceCount) { if (occurrenceCount) {
options.count = occurrenceCount; options.count = occurrenceCount;
} } else if (endDate) {
else if (endDate) {
if (angular.isDate(endDate)) { if (angular.isDate(endDate)) {
options.until = endDate; options.until = endDate;
} } else {
else {
try { try {
options.until = new Date(endDate); options.until = new Date(endDate);
} } catch (e) {
catch(e) {
$log.error('Date conversion failed. Attempted to convert ' + endDate + ' to Date. ' + e.message); $log.error('Date conversion failed. Attempted to convert ' + endDate + ' to Date. ' + e.message);
} }
} }
} }
} } else {
else {
// We only want to run 1x // We only want to run 1x
options.freq = RRule.DAILY; options.freq = RRule.DAILY;
options.interval = 1; options.interval = 1;
@@ -648,10 +643,11 @@ angular.module('AngularScheduler', ['underscore'])
}; };
}]) }])
.factory('SetRule', ['AngularScheduler.useTimezone', '_', '$log', '$timezones', '$filter', .factory('SetRule', ['AngularScheduler.useTimezone', '$log', '$timezones', '$filter',
function(useTimezone, _, $log, $timezones, $filter) { function(useTimezone, $log, $timezones, $filter) {
return function(rule, scope) { return function(rule, scope) {
var set, result = '', i, var set, result = '',
i,
setStartDate = false; setStartDate = false;
// Search the set of RRule keys for a particular key, returning its value // Search the set of RRule keys for a particular key, returning its value
@@ -698,11 +694,10 @@ angular.module('AngularScheduler', ['underscore'])
} }
} }
if (key === 'INTERVAL') { if (key === 'INTERVAL') {
if (parseInt(value,10)) { if (parseInt(value, 10)) {
scope.schedulerInterval = parseInt(value,10); scope.schedulerInterval = parseInt(value, 10);
scope.schedulerShowInterval = true; scope.schedulerShowInterval = true;
} } else {
else {
result = 'INTERVAL must contain an integer > 0'; result = 'INTERVAL must contain an integer > 0';
} }
} }
@@ -710,24 +705,21 @@ angular.module('AngularScheduler', ['underscore'])
if (getValue(set, 'FREQ') === 'WEEKLY') { if (getValue(set, 'FREQ') === 'WEEKLY') {
days = value.split(/,/); days = value.split(/,/);
scope.weekDays = []; scope.weekDays = [];
for (j=0; j < days.length; j++) { for (j = 0; j < days.length; j++) {
if (_.contains(['SU','MO','TU','WE','TH','FR','SA'], days[j])) { if (_.contains(['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'], days[j])) {
scope.weekDays.push(days[j].toLowerCase()); scope.weekDays.push(days[j].toLowerCase());
scope['weekDay' + days[j].toUpperCase() + 'Class'] = 'active'; //activate related button scope['weekDay' + days[j].toUpperCase() + 'Class'] = 'active'; //activate related button
} } else {
else {
result = 'BYDAY contains unrecognized day value(s)'; result = 'BYDAY contains unrecognized day value(s)';
} }
} }
} } else if (getValue(set, 'FREQ') === 'MONTHLY') {
else if (getValue(set, 'FREQ') === 'MONTHLY') {
scope.monthlyRepeatOption = 'other'; scope.monthlyRepeatOption = 'other';
scope.monthlyWeekDay = toWeekDays(value); scope.monthlyWeekDay = toWeekDays(value);
if (!scope.monthlyWeekDay) { if (!scope.monthlyWeekDay) {
result = 'BYDAY contains unrecognized day value(s)'; result = 'BYDAY contains unrecognized day value(s)';
} }
} } else {
else {
scope.yearlyRepeatOption = 'other'; scope.yearlyRepeatOption = 'other';
scope.yearlyWeekDay = toWeekDays(value); scope.yearlyWeekDay = toWeekDays(value);
if (!scope.yearlyWeekDay) { if (!scope.yearlyWeekDay) {
@@ -736,11 +728,10 @@ angular.module('AngularScheduler', ['underscore'])
} }
} }
if (key === 'BYMONTHDAY') { if (key === 'BYMONTHDAY') {
if (parseInt(value,10) && parseInt(value,10) > 0 && parseInt(value,10) < 32) { if (parseInt(value, 10) && parseInt(value, 10) > 0 && parseInt(value, 10) < 32) {
scope.monthDay = parseInt(value,10); scope.monthDay = parseInt(value, 10);
scope.monhthlyRepeatOption = 'day'; scope.monhthlyRepeatOption = 'day';
} } else {
else {
result = 'BYMONTHDAY must contain an integer between 1 and 31'; result = 'BYMONTHDAY must contain an integer between 1 and 31';
} }
} }
@@ -750,8 +741,8 @@ angular.module('AngularScheduler', ['underscore'])
if (/\d{8}T\d{6}.*Z/.test(value)) { if (/\d{8}T\d{6}.*Z/.test(value)) {
// date may come in without separators. add them so new Date constructor will work // date may come in without separators. add them so new Date constructor will work
value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/, value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/,
function(match, p1, p2, p3, p4,p5,p6) { function(match, p1, p2, p3, p4, p5, p6) {
return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0,2) + 'Z'; return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0, 2) + 'Z';
}); });
} }
if (useTimezone) { if (useTimezone) {
@@ -759,20 +750,19 @@ angular.module('AngularScheduler', ['underscore'])
month = $filter('schZeroPad')(dt.getMonth() + 1, 2); month = $filter('schZeroPad')(dt.getMonth() + 1, 2);
day = $filter('schZeroPad')(dt.getDate(), 2); day = $filter('schZeroPad')(dt.getDate(), 2);
scope.schedulerStartDt = month + '/' + day + '/' + dt.getFullYear(); scope.schedulerStartDt = month + '/' + day + '/' + dt.getFullYear();
scope.schedulerStartHour = $filter('schZeroPad')(dt.getHours(),2); scope.schedulerStartHour = $filter('schZeroPad')(dt.getHours(), 2);
scope.schedulerStartMinute = $filter('schZeroPad')(dt.getMinutes(),2); scope.schedulerStartMinute = $filter('schZeroPad')(dt.getMinutes(), 2);
scope.schedulerStartSecond = $filter('schZeroPad')(dt.getSeconds(),2); scope.schedulerStartSecond = $filter('schZeroPad')(dt.getSeconds(), 2);
scope.scheduleTimeChange(); // calc UTC scope.scheduleTimeChange(); // calc UTC
} } else {
else {
// expects inbound dates to be in ISO format: 2014-04-02T00:00:00.000Z // expects inbound dates to be in ISO format: 2014-04-02T00:00:00.000Z
scope.schedulerStartDt = value.replace(/T.*$/,'').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) { scope.schedulerStartDt = value.replace(/T.*$/, '').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) {
return p2 + '/' + p3 + '/' + p1; return p2 + '/' + p3 + '/' + p1;
}); });
timeString = value.replace(/^.*T/,''); timeString = value.replace(/^.*T/, '');
scope.schedulerStartHour = $filter('schZeroPad')(timeString.substr(0,2),2); scope.schedulerStartHour = $filter('schZeroPad')(timeString.substr(0, 2), 2);
scope.schedulerStartMinute = $filter('schZeroPad')(timeString.substr(3,2),2); scope.schedulerStartMinute = $filter('schZeroPad')(timeString.substr(3, 2), 2);
scope.schedulerStartSecond = $filter('schZeroPad')(timeString.substr(6,2),2); scope.schedulerStartSecond = $filter('schZeroPad')(timeString.substr(6, 2), 2);
} }
scope.scheduleTimeChange(); scope.scheduleTimeChange();
} }
@@ -780,15 +770,14 @@ angular.module('AngularScheduler', ['underscore'])
if (getValue(set, 'FREQ') === 'YEARLY') { if (getValue(set, 'FREQ') === 'YEARLY') {
scope.yearlRepeatOption = 'other'; scope.yearlRepeatOption = 'other';
scope.yearlyOccurrence = _.find(scope.occurrences, function(x) { scope.yearlyOccurrence = _.find(scope.occurrences, function(x) {
return (x.value === parseInt(value,10)); return (x.value === parseInt(value, 10));
}); });
if (!scope.yearlyOccurrence || !scope.yearlyOccurrence.name) { if (!scope.yearlyOccurrence || !scope.yearlyOccurrence.name) {
result = 'BYSETPOS was not in the set of 1,2,3,4,-1'; result = 'BYSETPOS was not in the set of 1,2,3,4,-1';
} }
} } else {
else {
scope.monthlyOccurrence = _.find(scope.occurrences, function(x) { scope.monthlyOccurrence = _.find(scope.occurrences, function(x) {
return (x.value === parseInt(value,10)); return (x.value === parseInt(value, 10));
}); });
if (!scope.monthlyOccurrence || !scope.monthlyOccurrence.name) { if (!scope.monthlyOccurrence || !scope.monthlyOccurrence.name) {
result = 'BYSETPOS was not in the set of 1,2,3,4,-1'; result = 'BYSETPOS was not in the set of 1,2,3,4,-1';
@@ -797,11 +786,10 @@ angular.module('AngularScheduler', ['underscore'])
} }
if (key === 'COUNT') { if (key === 'COUNT') {
if (parseInt(value,10)) { if (parseInt(value, 10)) {
scope.schedulerEnd = scope.endOptions[1]; scope.schedulerEnd = scope.endOptions[1];
scope.schedulerOccurrenceCount = parseInt(value,10); scope.schedulerOccurrenceCount = parseInt(value, 10);
} } else {
else {
result = "COUNT must be a valid integer > 0"; result = "COUNT must be a valid integer > 0";
} }
} }
@@ -810,34 +798,33 @@ angular.module('AngularScheduler', ['underscore'])
if (/\d{8}T\d{6}.*Z/.test(value)) { if (/\d{8}T\d{6}.*Z/.test(value)) {
// date may come in without separators. add them so new Date constructor will work // date may come in without separators. add them so new Date constructor will work
value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/, value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/,
function(match, p1, p2, p3, p4,p5,p6) { function(match, p1, p2, p3, p4, p5, p6) {
return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0,2) + 'Z'; return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0, 2) + 'Z';
}); });
} }
scope.schedulerEnd = scope.endOptions[2]; scope.schedulerEnd = scope.endOptions[2];
scope.schedulerEndDt = value.replace(/T.*$/,'').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) { scope.schedulerEndDt = value.replace(/T.*$/, '').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) {
return p2 + '/' + p3 + '/' + p1; return p2 + '/' + p3 + '/' + p1;
}); });
timeString = value.replace(/^.*T/,''); timeString = value.replace(/^.*T/, '');
scope.schedulerEndHour = $filter('schZeroPad')(timeString.substr(0,2),2); scope.schedulerEndHour = $filter('schZeroPad')(timeString.substr(0, 2), 2);
scope.schedulerEndMinute = $filter('schZeroPad')(timeString.substr(3,2),2); scope.schedulerEndMinute = $filter('schZeroPad')(timeString.substr(3, 2), 2);
scope.schedulerEndSecond = $filter('schZeroPad')(timeString.substr(6,2),2); scope.schedulerEndSecond = $filter('schZeroPad')(timeString.substr(6, 2), 2);
} }
if (key === 'BYMONTH') { if (key === 'BYMONTH') {
if (getValue(set, 'FREQ') === 'YEARLY' && getValue(set, 'BYDAY')) { if (getValue(set, 'FREQ') === 'YEARLY' && getValue(set, 'BYDAY')) {
scope.yearlRepeatOption = 'other'; scope.yearlRepeatOption = 'other';
scope.yearlyOtherMonth = _.find(scope.months, function(x) { scope.yearlyOtherMonth = _.find(scope.months, function(x) {
return x.value === parseInt(value,10); return x.value === parseInt(value, 10);
}); });
if (!scope.yearlyOtherMonth || !scope.yearlyOtherMonth.name) { if (!scope.yearlyOtherMonth || !scope.yearlyOtherMonth.name) {
result = 'BYMONTH must be an integer between 1 and 12'; result = 'BYMONTH must be an integer between 1 and 12';
} }
} } else {
else {
scope.yearlyOption = 'month'; scope.yearlyOption = 'month';
scope.yearlyMonth = _.find(scope.months, function(x) { scope.yearlyMonth = _.find(scope.months, function(x) {
return x.value === parseInt(value,10); return x.value === parseInt(value, 10);
}); });
if (!scope.yearlyMonth || !scope.yearlyMonth.name) { if (!scope.yearlyMonth || !scope.yearlyMonth.name) {
result = 'BYMONTH must be an integer between 1 and 12'; result = 'BYMONTH must be an integer between 1 and 12';
@@ -846,10 +833,9 @@ angular.module('AngularScheduler', ['underscore'])
} }
if (key === 'BYMONTHDAY') { if (key === 'BYMONTHDAY') {
if (parseInt(value,10)) { if (parseInt(value, 10)) {
scope.yearlyMonthDay = parseInt(value,10); scope.yearlyMonthDay = parseInt(value, 10);
} } else {
else {
result = 'BYMONTHDAY must be an integer between 1 and 31'; result = 'BYMONTHDAY must be an integer between 1 and 31';
} }
} }
@@ -869,7 +855,7 @@ angular.module('AngularScheduler', ['underscore'])
if (rule) { if (rule) {
set = rule.split(/;/); set = rule.split(/;/);
if (angular.isArray(set)) { if (angular.isArray(set)) {
for (i=0; i < set.length; i++) { for (i = 0; i < set.length; i++) {
setValue(set[i], set); setValue(set[i], set);
if (result) { if (result) {
break; break;
@@ -878,12 +864,10 @@ angular.module('AngularScheduler', ['underscore'])
if (!result) { if (!result) {
isValid(); isValid();
} }
} } else {
else {
result = 'No rule entered. Provide a valid RRule string.'; result = 'No rule entered. Provide a valid RRule string.';
} }
} } else {
else {
result = 'No rule entered. Provide a valid RRule string.'; result = 'No rule entered. Provide a valid RRule string.';
} }
if (result) { if (result) {
@@ -891,7 +875,8 @@ angular.module('AngularScheduler', ['underscore'])
} }
return result; return result;
}; };
}]) }
])
.factory('SetDefaults', ['$filter', function($filter) { .factory('SetDefaults', ['$filter', function($filter) {
return function(scope) { return function(scope) {
@@ -938,7 +923,7 @@ angular.module('AngularScheduler', ['underscore'])
}; };
}]) }])
.factory('LoadLookupValues', [ function() { .factory('LoadLookupValues', [function() {
return function(scope) { return function(scope) {
scope.frequencyOptions = [ scope.frequencyOptions = [
@@ -997,15 +982,15 @@ angular.module('AngularScheduler', ['underscore'])
}]) }])
// $filter('schZeroPad')(n, pad) -- or -- {{ n | afZeroPad:pad }} // $filter('schZeroPad')(n, pad) -- or -- {{ n | afZeroPad:pad }}
.filter('schZeroPad', [ function() { .filter('schZeroPad', [function() {
return function (n, pad) { return function(n, pad) {
var str = (Math.pow(10,pad) + '').replace(/^1/,'') + (n + '').trim(); var str = (Math.pow(10, pad) + '').replace(/^1/, '') + (n + '').trim();
return str.substr(str.length - pad); return str.substr(str.length - pad);
}; };
}]) }])
// $filter('schdateStrFix')(s) where s is a date string in ISO format: yyyy-mm-ddTHH:MM:SS.sssZ. Returns string in format: mm/dd/yyyy HH:MM:SS UTC // $filter('schdateStrFix')(s) where s is a date string in ISO format: yyyy-mm-ddTHH:MM:SS.sssZ. Returns string in format: mm/dd/yyyy HH:MM:SS UTC
.filter('schDateStrFix', [ function() { .filter('schDateStrFix', [function() {
return function(dateStr) { return function(dateStr) {
return dateStr.replace(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*Z/, function(match, yy, mm, dd, hh, mi, ss) { return dateStr.replace(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*Z/, function(match, yy, mm, dd, hh, mi, ss) {
return mm + '/' + dd + '/' + yy + ' ' + hh + ':' + mi + ':' + ss + ' UTC'; return mm + '/' + dd + '/' + yy + ' ' + hh + ':' + mi + ':' + ss + ' UTC';
@@ -1013,7 +998,7 @@ angular.module('AngularScheduler', ['underscore'])
}; };
}]) }])
.directive('schTooltip', [ function() { .directive('schTooltip', [function() {
return { return {
link: function(scope, element, attrs) { link: function(scope, element, attrs) {
var placement = (attrs.placement) ? attrs.placement : 'top'; var placement = (attrs.placement) ? attrs.placement : 'top';
@@ -1028,7 +1013,7 @@ angular.module('AngularScheduler', ['underscore'])
}; };
}]) }])
.directive('schDatePicker', [ function() { .directive('schDatePicker', [function() {
return { return {
require: 'ngModel', require: 'ngModel',
link: function(scope, element, attrs) { link: function(scope, element, attrs) {
@@ -1042,7 +1027,7 @@ angular.module('AngularScheduler', ['underscore'])
options.changeMonth = (attrs.changeMonth === "false") ? false : true; options.changeMonth = (attrs.changeMonth === "false") ? false : true;
options.changeYear = (attrs.changeYear === "false") ? false : true; options.changeYear = (attrs.changeYear === "false") ? false : true;
options.beforeShow = function() { options.beforeShow = function() {
setTimeout(function(){ setTimeout(function() {
$('.ui-datepicker').css('z-index', 9999); $('.ui-datepicker').css('z-index', 9999);
}, 100); }, 100);
}; };
@@ -1061,7 +1046,7 @@ angular.module('AngularScheduler', ['underscore'])
zeroPad = attr.zeroPad, zeroPad = attr.zeroPad,
min = attr.min || 1, min = attr.min || 1,
max = attr.max || 999; max = attr.max || 999;
$(element).spinner({ element.spinner({
min: min, min: min,
max: max, max: max,
stop: function() { stop: function() {
@@ -1069,10 +1054,9 @@ angular.module('AngularScheduler', ['underscore'])
setTimeout(function() { setTimeout(function() {
scope.$apply(function() { scope.$apply(function() {
if (zeroPad) { if (zeroPad) {
scope[attr.ngModel] = $filter('schZeroPad')($(element).spinner('value'),zeroPad); scope[attr.ngModel] = $filter('schZeroPad')(element.spinner('value'), zeroPad);
} } else {
else { scope[attr.ngModel] = element.spinner('value');
scope[attr.ngModel] = $(element).spinner('value');
} }
if (attr.ngChange) { if (attr.ngChange) {
scope.$eval(attr.ngChange); scope.$eval(attr.ngChange);
@@ -1090,9 +1074,11 @@ angular.module('AngularScheduler', ['underscore'])
} }
}); });
$(element).on("click", function () { $(element).on("click", function() {
$(element).select(); $(element).select();
}); });
} }
}; };
}]); }]);
}));

View File

@@ -52,8 +52,7 @@
"tag": "2.0.0", "tag": "2.0.0",
"commit": "8a1951c54a956c33964c99b338f3a4830e652689" "commit": "8a1951c54a956c33964c99b338f3a4830e652689"
}, },
"_source": "git://github.com/tameraydin/ngToast.git", "_source": "https://github.com/tameraydin/ngToast.git",
"_target": "~2.0.0", "_target": "~2.0.0",
"_originalSource": "ngtoast", "_originalSource": "ngtoast"
"_direct": true
} }

View File

@@ -51,23 +51,6 @@ export default
PaginateInit({ scope: scope, PaginateInit({ scope: scope,
list: list, url: url, pageSize: 5 }); 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); scope.search(list.iterator);
}); });
} }

View File

@@ -21,6 +21,13 @@
key: true, key: true,
label: 'name' label: 'name'
}, },
organization: {
label: 'organization',
ngBind: 'team.summary_fields.organization.name',
sourceModel: 'organization',
sourceField: 'name',
searchable: true
}
} }
}; };

View File

@@ -66,6 +66,12 @@
display: inline-block; display: inline-block;
color: @default-interface-txt; color: @default-interface-txt;
text-transform: uppercase; 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 { .BreadCrumb-item + .BreadCrumb-item:before {

View File

@@ -46,13 +46,13 @@ export function CredentialsList($scope, $rootScope, $location, $log,
Wait('stop'); Wait('stop');
$('#prompt-modal').modal('hide'); $('#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 // Translate the kind value
for (i = 0; i < $scope.credentials.length; i++) { for (i = 0; i < $scope.credentials.length; i++) {
for (j = 0; j < $scope.credential_kind_options.length; j++) { for (j = 0; j < $scope.credential_kind_options_list.length; j++) {
if ($scope.credential_kind_options[j].value === $scope.credentials[i].kind) { if ($scope.credential_kind_options_list[j].value === $scope.credentials[i].kind) {
$scope.credentials[i].kind = $scope.credential_kind_options[j].label; $scope.credentials[i].kind = $scope.credential_kind_options_list[j].label;
break; break;
} }
} }
@@ -82,7 +82,7 @@ export function CredentialsList($scope, $rootScope, $location, $log,
scope: $scope, scope: $scope,
url: defaultUrl, url: defaultUrl,
field: 'kind', field: 'kind',
variable: 'credential_kind_options', variable: 'credential_kind_options_list',
callback: 'choicesReadyCredential' callback: 'choicesReadyCredential'
}); });
@@ -148,7 +148,7 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log,
url; url;
$scope.keyEntered = false; $scope.keyEntered = false;
$scope.permissionsTooltip = 'Please save before assigning permissions';
generator.inject(form, { mode: 'add', related: false, scope: $scope }); generator.inject(form, { mode: 'add', related: false, scope: $scope });
generator.reset(); generator.reset();
@@ -391,6 +391,12 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
$scope.removeCredentialLoaded(); $scope.removeCredentialLoaded();
} }
$scope.removeCredentialLoaded = $scope.$on('credentialLoaded', function () { $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; var set;
for (set in relatedSets) { for (set in relatedSets) {
$scope.search(relatedSets[set].iterator); $scope.search(relatedSets[set].iterator);

View File

@@ -20,7 +20,8 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
var jobs_scope, scheduled_scope, var jobs_scope, scheduled_scope,
choicesCount = 0, choicesCount = 0,
listCount = 0, listCount = 0,
api_complete = false; api_complete = false,
scheduledJobsList = _.cloneDeep(ScheduledJobsList);
$scope.jobsSelected = true; $scope.jobsSelected = true;
@@ -66,22 +67,23 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
scope: jobs_scope, scope: jobs_scope,
list: AllJobsList, list: AllJobsList,
id: 'active-jobs', id: 'active-jobs',
pageSize: 20,
url: GetBasePath('unified_jobs') + '?status__in=pending,waiting,running,completed,failed,successful,error,canceled,new&order_by=-finished', url: GetBasePath('unified_jobs') + '?status__in=pending,waiting,running,completed,failed,successful,error,canceled,new&order_by=-finished',
pageSize: 20,
searchParams: search_params, searchParams: search_params,
spinner: false spinner: false
}); });
scheduled_scope = $scope.$new(true); scheduled_scope = $scope.$new(true);
scheduledJobsList.basePath = GetBasePath('schedules') + '?next_run__isnull=false';
LoadSchedulesScope({ LoadSchedulesScope({
parent_scope: $scope, parent_scope: $scope,
scope: scheduled_scope, scope: scheduled_scope,
list: ScheduledJobsList, list: scheduledJobsList,
pageSize: 20, pageSize: 20,
id: 'scheduled-jobs-tab', id: 'scheduled-jobs-tab',
searchSize: 'col-lg-4 col-md-4 col-sm-4 col-xs-12', 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() { $scope.refreshJobs = function() {

View File

@@ -358,11 +358,8 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
$scope.editSchedules = function(id) { $scope.editSchedules = function(id) {
var project = Find({ list: $scope.projects, key: 'id', val: id }); var project = Find({ list: $scope.projects, key: 'id', val: id });
if (project.scm_type === "Manual" || Empty(project.scm_type)) { if (!(project.scm_type === "Manual" || Empty(project.scm_type)) && !(project.status === 'updating' || project.status === 'running' || project.status === 'pending')) {
// Nothing to do $state.go('projectSchedules', {id: id});
}
else {
$location.path('/projects/' + id + '/schedules');
} }
}; };
} }

View File

@@ -162,6 +162,7 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
$scope.not_ldap_user = !$scope.ldap_user; $scope.not_ldap_user = !$scope.ldap_user;
$scope.ldap_dn = null; $scope.ldap_dn = null;
$scope.socialAuthUser = false; $scope.socialAuthUser = false;
$scope.external_account = null;
generator.reset(); generator.reset();
@@ -334,6 +335,7 @@ export function UsersEdit($scope, $rootScope, $location,
$scope.not_ldap_user = !$scope.ldap_user; $scope.not_ldap_user = !$scope.ldap_user;
master.ldap_user = $scope.ldap_user; master.ldap_user = $scope.ldap_user;
$scope.socialAuthUser = (data.auth.length > 0) ? true : false; $scope.socialAuthUser = (data.auth.length > 0) ? true : false;
$scope.external_account = data.external_account;
$scope.user_type = $scope.user_type_options[0]; $scope.user_type = $scope.user_type_options[0];
$scope.is_system_auditor = false; $scope.is_system_auditor = false;

View File

@@ -6,8 +6,8 @@
export default export default
['$scope', '$state', '$stateParams', 'PageRangeSetup', 'GetBasePath', 'DashboardHostsList', ['$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){ function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts, $rootScope, SearchInit){
var setJobStatus = function(){ var setJobStatus = function(){
_.forEach($scope.hosts, function(value){ _.forEach($scope.hosts, function(value){
SetStatus({ SetStatus({
@@ -59,6 +59,12 @@ export default
$scope.hosts = hosts.results; $scope.hosts = hosts.results;
setJobStatus(); setJobStatus();
generator.inject(list, {mode: 'edit', scope: $scope}); generator.inject(list, {mode: 'edit', scope: $scope});
SearchInit({
scope: $scope,
set: 'hosts',
list: list,
url: defaultUrl
});
PaginateInit({ PaginateInit({
scope: $scope, scope: $scope,
list: list, list: list,
@@ -77,6 +83,7 @@ export default
$scope.rowBeingEdited = $state.params.id; $scope.rowBeingEdited = $state.params.id;
$scope.listBeingEdited = "hosts"; $scope.listBeingEdited = "hosts";
} }
$scope.search(list.iterator);
}; };
init(); init();
}]; }];

View File

@@ -31,6 +31,7 @@ export default function(){
awTipPlacement: 'right', awTipPlacement: 'right',
dataPlacement: 'right', dataPlacement: 'right',
awPopOver: '{{ host.job_status_html }}', awPopOver: '{{ host.job_status_html }}',
dataTitle: '{{host.job_status_title}}',
ngClick:'viewHost(host.id)', ngClick:'viewHost(host.id)',
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus' columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus'
}, },

View File

@@ -101,10 +101,8 @@
.DashboardList-nameCell { .DashboardList-nameCell {
padding-left: 15px; padding-left: 15px;
text-overflow: ellipsis;
overflow:hidden;
white-space: nowrap;
width: 100%; width: 100%;
word-wrap: break-word;
} }
.DashboardList-nameContainer { .DashboardList-nameContainer {

View File

@@ -404,7 +404,9 @@ export default
related: { related: {
permissions: { permissions: {
awToolTip: 'Please save before assigning permissions', disabled: 'disablePermissionAssignment',
awToolTip: '{{permissionsTooltip}}',
dataTipWatch: 'permissionsTooltip',
dataPlacement: 'top', dataPlacement: 'top',
basePath: 'credentials/:id/access_list/', basePath: 'credentials/:id/access_list/',
type: 'collection', type: 'collection',
@@ -452,7 +454,7 @@ export default
return { return {
permissions: { permissions: {
iterator: 'permission', iterator: 'permission',
url: urls.access_list url: urls.access_list,
} }
}; };
} }

View File

@@ -81,7 +81,7 @@ export default
permissions: { permissions: {
awToolTip: 'Please save before assigning permissions', awToolTip: 'Please save before assigning permissions',
dataPlacement: 'top', dataPlacement: 'top',
basePath: 'projects/:id/access_list/', basePath: 'inventories/:id/access_list/',
type: 'collection', type: 'collection',
title: 'Permissions', title: 'Permissions',
iterator: 'permission', iterator: 'permission',

View File

@@ -228,10 +228,7 @@ export default
column: 2, column: 2,
awPopOver: "<p>Provide a comma separated list of tags.</p>\n" + 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>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. " + "<p>Consult the Ansible documentation for further details on the usage of tags.</p>",
"Suppose the actions have been assigned tag values of &quot;configuration&quot;, &quot;packages&quot; and &quot;install&quot;.</p>" +
"<p>If you just want to run the &quot;configuration&quot; and &quot;packages&quot; actions, you would enter the following here " +
"in the Job Tags field:</p>\n<blockquote>configuration,packages</blockquote>\n",
dataTitle: "Job Tags", dataTitle: "Job Tags",
dataPlacement: "right", dataPlacement: "right",
dataContainer: "body", dataContainer: "body",
@@ -240,6 +237,25 @@ export default
text: 'Prompt on launch' 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: { checkbox_group: {
label: 'Options', label: 'Options',
type: 'checkbox_group', type: 'checkbox_group',
@@ -281,7 +297,7 @@ export default
column: 2, column: 2,
awPopOver: "callback_help", awPopOver: "callback_help",
awPopOverWatch: "callback_help", awPopOverWatch: "callback_help",
dataPlacement: 'right', dataPlacement: 'top',
dataTitle: 'Provisioning Callback URL', dataTitle: 'Provisioning Callback URL',
dataContainer: "body" dataContainer: "body"
}, },

View File

@@ -46,7 +46,7 @@ export default
label: 'Username', label: 'Username',
type: 'text', type: 'text',
awRequiredWhen: { awRequiredWhen: {
reqExpression: "not_ldap_user", reqExpression: "not_ldap_user && external_account === null",
init: true init: true
}, },
autocomplete: false autocomplete: false
@@ -69,7 +69,7 @@ export default
label: 'Password', label: 'Password',
type: 'sensitive', type: 'sensitive',
hasShowInputButton: true, hasShowInputButton: true,
ngShow: 'ldap_user == false && socialAuthUser === false', ngShow: 'ldap_user == false && socialAuthUser === false && external_account === null',
addRequired: true, addRequired: true,
editRequired: false, editRequired: false,
ngChange: "clearPWConfirm('password_confirm')", ngChange: "clearPWConfirm('password_confirm')",
@@ -80,7 +80,7 @@ export default
label: 'Confirm Password', label: 'Confirm Password',
type: 'sensitive', type: 'sensitive',
hasShowInputButton: true, hasShowInputButton: true,
ngShow: 'ldap_user == false && socialAuthUser === false', ngShow: 'ldap_user == false && socialAuthUser === false && external_account === null',
addRequired: true, addRequired: true,
editRequired: false, editRequired: false,
awPassMatch: true, awPassMatch: true,

View File

@@ -55,9 +55,9 @@ export default
// Submit request to run an adhoc comamand // Submit request to run an adhoc comamand
.factory('AdhocRun', ['$location','$stateParams', 'LaunchJob', .factory('AdhocRun', ['$location','$stateParams', 'LaunchJob',
'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors',
'Wait', 'Empty', 'CreateLaunchDialog', 'Wait', 'Empty', 'CreateLaunchDialog', '$state',
function ($location, $stateParams, LaunchJob, PromptForPasswords, function ($location, $stateParams, LaunchJob, PromptForPasswords,
Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog) { Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog, $state) {
return function (params) { return function (params) {
var id = params.project_id, var id = params.project_id,
scope = params.scope.$new(), scope = params.scope.$new(),
@@ -87,24 +87,30 @@ 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) { if (scope.removeStartAdhocRun) {
scope.removeStartAdhocRun(); scope.removeStartAdhocRun();
} }
scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() { scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() {
LaunchJob({ var password,
scope: scope, postData={};
url: url, for (password in scope.passwords) {
callback: 'AdhocLaunchFinished' // send to the adhoc postData[scope.passwords[password]] = scope[
// standard out page 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 });
}); });
}); });

View File

@@ -161,7 +161,7 @@ angular.module('CredentialsHelper', ['Utilities'])
break; break;
case 'net': case 'net':
scope.username_required = true; scope.username_required = true;
scope.password_required = true; scope.password_required = false;
scope.passwordLabel = 'Password'; scope.passwordLabel = 'Password';
scope.sshKeyDataLabel = 'SSH Key'; scope.sshKeyDataLabel = 'SSH Key';
break; break;

View File

@@ -165,6 +165,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
has_inventory_sources = params.has_inventory_sources, has_inventory_sources = params.has_inventory_sources,
launch_class = '', launch_class = '',
launch_tip = 'Start sync process', launch_tip = 'Start sync process',
schedule_tip = 'Schedule future inventory syncs',
stat, stat_class, status_tip; stat, stat_class, status_tip;
stat = status; stat = status;
@@ -225,7 +226,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
"tooltip": status_tip, "tooltip": status_tip,
"status": stat, "status": stat,
"launch_class": launch_class, "launch_class": launch_class,
"launch_tip": launch_tip "launch_tip": launch_tip,
"schedule_tip": schedule_tip
}; };
}; };
} }

View File

@@ -132,6 +132,9 @@ angular.module('JobTemplatesHelper', ['Utilities'])
scope.ask_tags_on_launch = (data.ask_tags_on_launch) ? true : false; scope.ask_tags_on_launch = (data.ask_tags_on_launch) ? true : false;
master.ask_tags_on_launch = scope.ask_tags_on_launch; 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; 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; master.ask_job_type_on_launch = scope.ask_job_type_on_launch;

View File

@@ -25,7 +25,8 @@ export default
fld = (params.variable) ? params.variable : 'variables', fld = (params.variable) ? params.variable : 'variables',
pfld = (params.parse_variable) ? params.parse_variable : 'parseType', pfld = (params.parse_variable) ? params.parse_variable : 'parseType',
onReady = params.onReady, onReady = params.onReady,
onChange = params.onChange; onChange = params.onChange,
readOnly = params.readOnly;
function removeField(fld) { function removeField(fld) {
//set our model to the last change in CodeMirror and then destroy CodeMirror //set our model to the last change in CodeMirror and then destroy CodeMirror
@@ -35,8 +36,7 @@ export default
function createField(onChange, onReady, fld) { function createField(onChange, onReady, fld) {
//hide the textarea and show a fresh CodeMirror with the current mode (json or yaml) //hide the textarea and show a fresh CodeMirror with the current mode (json or yaml)
scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly);
scope[fld + 'codeMirror'] = AngularCodeMirror();
scope[fld + 'codeMirror'].addModes($AnsibleConfig.variable_edit_modes); scope[fld + 'codeMirror'].addModes($AnsibleConfig.variable_edit_modes);
scope[fld + 'codeMirror'].showTextArea({ scope[fld + 'codeMirror'].showTextArea({
scope: scope, scope: scope,

View File

@@ -75,6 +75,9 @@ export default
case 'missing': case 'missing':
result = 'Missing. Click for details'; result = 'Missing. Click for details';
break; break;
case 'canceled':
result = 'Canceled. Click for details';
break;
} }
return result; return result;
}; };

View File

@@ -50,16 +50,9 @@ function InventoriesList($scope, $rootScope, $location, $log,
"aw-pop-over": html, "aw-pop-over": html,
"data-popover-title": title, "data-popover-title": title,
"data-placement": "right" }); "data-placement": "right" });
elem.removeAttr('ng-click');
$compile(elem)($scope); $compile(elem)($scope);
elem.on('shown.bs.popover', function() { $scope.triggerPopover(event);
$('.popover').each(function() {
$compile($(this))($scope); //make nested directives work!
});
$('.popover-content, .popover-title').click(function() {
elem.popover('hide');
});
});
elem.popover('show');
} }
view.inject(InventoryList, { mode: mode, scope: $scope }); view.inject(InventoryList, { mode: mode, scope: $scope });
@@ -250,6 +243,14 @@ function InventoriesList($scope, $rootScope, $location, $log,
}); });
$scope.showGroupSummary = function(event, id) { $scope.showGroupSummary = function(event, id) {
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; var inventory;
if (!Empty(id)) { if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id }); inventory = Find({ list: $scope.inventories, key: 'id', val: id });
@@ -267,9 +268,18 @@ function InventoriesList($scope, $rootScope, $location, $log,
}); });
} }
} }
}
}; };
$scope.showHostSummary = function(event, id) { $scope.showHostSummary = function(event, id) {
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; var url, inventory;
if (!Empty(id)) { if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id }); inventory = Find({ list: $scope.inventories, key: 'id', val: id });
@@ -290,6 +300,7 @@ function InventoriesList($scope, $rootScope, $location, $log,
}); });
} }
} }
}
}; };
$scope.viewJob = function(url) { $scope.viewJob = function(url) {

View File

@@ -242,7 +242,7 @@ function adhocController($q, $scope, $location, $stateParams,
Rest.post(data) Rest.post(data)
.success(function (data) { .success(function (data) {
Wait('stop'); Wait('stop');
$location.path("/ad_hoc_commands/" + data.id); $state.go('adHocJobStdout', {id: data.id});
}) })
.error(function (data, status) { .error(function (data, status) {
ProcessErrors($scope, data, status, adhocForm, { ProcessErrors($scope, data, status, adhocForm, {

View File

@@ -19,18 +19,4 @@
<li class="BreadCrumb-item" ng-if="currentState !== 'inventoryManage'"></li> <li class="BreadCrumb-item" ng-if="currentState !== 'inventoryManage'"></li>
<div class="InventoryManageBreadCrumb-ncy" ng-if="!licenseMissing" ncy-breadcrumb></div> <div class="InventoryManageBreadCrumb-ncy" ng-if="!licenseMissing" ncy-breadcrumb></div>
</ol> </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> </div>

View File

@@ -100,6 +100,7 @@
// equal to case 'ec2' || 'rax' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack' // equal to case 'ec2' || 'rax' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack'
else{ else{
var credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source)); 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; CredentialList.basePath = credentialBasePath;
LookUpInit({ LookUpInit({
scope: $scope, scope: $scope,
@@ -122,7 +123,7 @@
$scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; $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 // 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.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.group_by = null;
$scope.source_regions = null; $scope.source_regions = null;
$scope.credential = null; $scope.credential = null;

View File

@@ -100,6 +100,7 @@
else{ else{
var credentialBasePath = (source.value === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source.value === '' ? '' : '?kind=' + (source.value)); var credentialBasePath = (source.value === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source.value === '' ? '' : '?kind=' + (source.value));
CredentialList.basePath = credentialBasePath; CredentialList.basePath = credentialBasePath;
$scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'ec2' ? true : false;
LookUpInit({ LookUpInit({
scope: $scope, scope: $scope,
url: credentialBasePath, url: credentialBasePath,
@@ -122,7 +123,7 @@
// reset fields // reset fields
// azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint // 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.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.group_by = null;
$scope.source_regions = null; $scope.source_regions = null;
$scope.credential = null; $scope.credential = null;

View File

@@ -151,6 +151,7 @@
{status_tooltip: group_status.tooltip}, {status_tooltip: group_status.tooltip},
{launch_tooltip: group_status.launch_tip}, {launch_tooltip: group_status.launch_tip},
{launch_class: group_status.launch_class}, {launch_class: group_status.launch_class},
{group_schedule_tooltip: group_status.schedule_tip},
{hosts_status_tip: hosts_status.tooltip}, {hosts_status_tip: hosts_status.tooltip},
{hosts_status_class: hosts_status.class}, {hosts_status_class: hosts_status.class},
{source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null}, {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null},

View File

@@ -13,11 +13,6 @@ import GroupsListController from './groups/groups-list.controller';
export default { export default {
name: 'inventoryManage', name: 'inventoryManage',
url: '/inventories/:inventory_id/manage?{group:int}{failed}', url: '/inventories/:inventory_id/manage?{group:int}{failed}',
data: {
activityStream: true,
activityStreamTarget: 'inventory',
activityStreamId: 'inventory_id'
},
params:{ params:{
group:{ group:{
array: true array: true

View File

@@ -38,11 +38,13 @@
break; break;
case 'ok': case 'ok':
params.event = 'runner_on_ok'; params.event = 'runner_on_ok';
params.changed = 'false';
break; break;
case 'failed': case 'failed':
params.failed = true; params.event = 'runner_on_failed';
break; break;
case 'changed': case 'changed':
params.event = 'runner_on_ok';
params.changed = true; params.changed = true;
break; break;
default: default:

View File

@@ -641,7 +641,7 @@ export default
return true; return true;
}); });
//scope.setSearchAll('host'); //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); scope.$emit('LoadPlays', data.related.job_events);
}) })
.error(function(data, status) { .error(function(data, status) {

View File

@@ -154,6 +154,11 @@
<div class="JobDetail-resultRowText">{{ job.job_tags }}</div> <div class="JobDetail-resultRowText">{{ job.job_tags }}</div>
</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"> <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> <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> <textarea rows="6" ng-model="variables" name="variables" class="JobDetail-extraVars" id="pre-formatted-variables"></textarea>

View File

@@ -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; 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){ if(scope.ask_limit_on_launch && scope.other_prompt_data && scope.other_prompt_data.limit){
job_launch_data.limit = scope.other_prompt_data.limit; job_launch_data.limit = scope.other_prompt_data.limit;
} }

View File

@@ -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 // General catch-all for "other prompts" - used in this link function and to hide the Other Prompts tab when
// it should be hidden // 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.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_inventory = data.defaults && data.defaults.inventory && data.defaults.inventory.id;
$scope.has_default_credential = data.defaults && data.defaults.credential && data.defaults.credential.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 : ""; $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) { if($scope.ask_variables_on_launch) {
$scope.jobLaunchVariables = (data.defaults && data.defaults.extra_vars) ? data.defaults.extra_vars : "---"; $scope.jobLaunchVariables = (data.defaults && data.defaults.extra_vars) ? data.defaults.extra_vars : "---";
$scope.other_prompt_data.parseType = 'yaml'; $scope.other_prompt_data.parseType = 'yaml';

View File

@@ -148,7 +148,15 @@
<span class="Form-inputLabel">Job Tags</span> <span class="Form-inputLabel">Job Tags</span>
</label> </label>
<div> <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>
</div> </div>
</form> </form>

View File

@@ -518,6 +518,7 @@
} }
data.ask_tags_on_launch = $scope.ask_tags_on_launch ? $scope.ask_tags_on_launch : false; 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_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_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; data.ask_inventory_on_launch = $scope.ask_inventory_on_launch ? $scope.ask_inventory_on_launch : false;

View File

@@ -640,6 +640,7 @@ export default
} }
data.ask_tags_on_launch = $scope.ask_tags_on_launch ? $scope.ask_tags_on_launch : false; 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_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_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; data.ask_inventory_on_launch = $scope.ask_inventory_on_launch ? $scope.ask_inventory_on_launch : false;

View File

@@ -8,7 +8,7 @@
} }
.LabelList-tagContainer, .LabelList-tagContainer,
.LabelList-seeMore { .LabelList-seeMoreLess {
display: flex; display: flex;
max-width: 100%; max-width: 100%;
} }
@@ -27,7 +27,7 @@
overflow: hidden; overflow: hidden;
} }
.LabelList-seeMore { .LabelList-seeMoreLess {
color: @default-link; color: @default-link;
margin: 4px 0px; margin: 4px 0px;
text-transform: uppercase; text-transform: uppercase;
@@ -37,7 +37,7 @@
font-size: 11px; font-size: 11px;
} }
.LabelList-seeMore:hover { .LabelList-seeMoreLess:hover {
color: @default-link-hov; color: @default-link-hov;
} }

View File

@@ -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) { scope.deleteLabel = function(templateId, templateName, labelId, labelName) {
var action = function () { var action = function () {
$('#prompt-modal').modal('hide'); $('#prompt-modal').modal('hide');
@@ -56,13 +63,13 @@ export default
Rest.setUrl(url); Rest.setUrl(url);
Rest.post({"disassociate": true, "id": labelId}) Rest.post({"disassociate": true, "id": labelId})
.success(function () { .success(function () {
scope.search("job_template"); scope.search("job_template", scope.$parent.job_template_page);
Wait('stop'); Wait('stop');
}) })
.error(function (data, status) { .error(function (data, status) {
Wait('stop'); Wait('stop');
ProcessErrors(scope, data, status, null, { hdr: 'Error!', 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; scope.count = null;
} }
}); });
} }
}; };
} }

View File

@@ -8,5 +8,7 @@
<span class="LabelList-name">{{ label.name }}</span> <span class="LabelList-name">{{ label.name }}</span>
</div> </div>
</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> ng-click="seeMore()">View More</div>
<div class="LabelList-seeMoreLess" ng-show="count > 10 && !seeMoreInactive"
ng-click="seeLess()">View Less</div>

View File

@@ -314,7 +314,7 @@ export default
ngClick: 'submitQuestion($event)', ngClick: 'submitQuestion($event)',
ngDisabled: true, ngDisabled: true,
'class': 'btn btn-sm Form-saveButton', 'class': 'btn btn-sm Form-saveButton',
label: '{{editQuestionIndex === null ? "ADD" : "UPDATE"}}' label: '{{editQuestionIndex === null ? "+ ADD" : "UPDATE"}}'
} }
} }

View File

@@ -48,9 +48,7 @@ export default
columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6', columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6',
ngClick: "viewJobDetails(all_job)", ngClick: "viewJobDetails(all_job)",
defaultSearchField: true, defaultSearchField: true,
awToolTip: "{{ all_job.name | sanitize }}", searchDefault: true,
dataTipWatch: 'all_job.name',
dataPlacement: 'top'
}, },
type: { type: {
label: 'Type', label: 'Type',

View File

@@ -35,6 +35,7 @@ export default
ngClick:"viewJobDetails(completed_job)", ngClick:"viewJobDetails(completed_job)",
searchable: true, searchable: true,
searchType: 'select', searchType: 'select',
defaultSearchField: true,
nosort: true, nosort: true,
searchOptions: [ searchOptions: [
{ label: "Success", value: "successful" }, { label: "Success", value: "successful" },
@@ -54,8 +55,8 @@ export default
name: { name: {
label: 'Name', label: 'Name',
columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6', columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6',
searchable: false,
ngClick: "viewJobDetails(completed_job)", ngClick: "viewJobDetails(completed_job)",
defaultSearchField: true,
awToolTip: "{{ completed_job.name | sanitize }}", awToolTip: "{{ completed_job.name | sanitize }}",
dataPlacement: 'top' dataPlacement: 'top'
}, },
@@ -64,7 +65,7 @@ export default
ngBind: 'completed_job.type_label', ngBind: 'completed_job.type_label',
link: false, link: false,
columnClass: "col-lg-2 col-md-2 hidden-sm hidden-xs", columnClass: "col-lg-2 col-md-2 hidden-sm hidden-xs",
searchable: true, searchable: false,
searchType: 'select', searchType: 'select',
searchOptions: [] // populated via GetChoices() in controller searchOptions: [] // populated via GetChoices() in controller
}, },

View File

@@ -19,6 +19,7 @@ export default
hover: true, hover: true,
'class': 'table-no-border', 'class': 'table-no-border',
multiSelect: true, multiSelect: true,
trackBy: 'group.id',
fields: { fields: {
sync_status: { sync_status: {

View File

@@ -20,6 +20,7 @@ export default
hasChildren: true, hasChildren: true,
'class': 'table-no-border', 'class': 'table-no-border',
multiSelect: true, multiSelect: true,
trackBy: 'host.id',
fields: { fields: {
active_failures: { active_failures: {

View File

@@ -22,7 +22,8 @@ export default
key: true, key: true,
label: 'Name', label: 'Name',
columnClass: 'col-lg-5 col-md-5 col-sm-9 col-xs-8', 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: { description: {
label: 'Description', label: 'Description',

View File

@@ -35,7 +35,8 @@ export default
label: 'Name', label: 'Name',
columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6 List-staticColumnAdjacent', columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6 List-staticColumnAdjacent',
defaultSearchField: true, defaultSearchField: true,
linkTo: '/#/jobs/{{job.id}}' linkTo: '/#/jobs/{{job.id}}',
searchDefault: true
}, },
finished: { finished: {
label: 'Finished', label: 'Finished',

View File

@@ -37,6 +37,7 @@ export default
}, },
name: { name: {
key: true, key: true,
searchDefault: true,
label: 'Name', label: 'Name',
columnClass: "col-lg-4 col-md-4 col-sm-5 col-xs-7 List-staticColumnAdjacent", columnClass: "col-lg-4 col-md-4 col-sm-5 col-xs-7 List-staticColumnAdjacent",
modalColumnClass: 'col-md-8' modalColumnClass: 'col-md-8'

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