Merge branch 'release_3.2.0' into devel

This commit is contained in:
Matthew Jones 2017-09-18 10:55:45 -04:00
commit 64415872a0
No known key found for this signature in database
GPG Key ID: 76A4C17A97590C1C
48 changed files with 838 additions and 403 deletions

View File

@ -38,7 +38,7 @@ from rest_framework.utils.serializer_helpers import ReturnList
from polymorphic.models import PolymorphicModel
# AWX
from awx.main.constants import SCHEDULEABLE_PROVIDERS
from awx.main.constants import SCHEDULEABLE_PROVIDERS, ANSI_SGR_PATTERN
from awx.main.models import * # noqa
from awx.main.access import get_user_capabilities
from awx.main.fields import ImplicitRoleField
@ -343,6 +343,8 @@ class BaseSerializer(serializers.ModelSerializer):
continue
summary_fields[fk] = OrderedDict()
for field in related_fields:
if field == 'credential_type_id' and fk == 'credential' and self.version < 2: # TODO: remove version check in 3.3
continue
fval = getattr(fkval, field, None)
@ -2332,8 +2334,13 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
if obj.vault_credential:
res['vault_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.vault_credential.pk})
if self.version > 1:
view = 'api:%s_extra_credentials_list' % camelcase_to_underscore(obj.__class__.__name__)
res['extra_credentials'] = self.reverse(view, kwargs={'pk': obj.pk})
if isinstance(obj, UnifiedJobTemplate):
res['extra_credentials'] = self.reverse(
'api:job_template_extra_credentials_list',
kwargs={'pk': obj.pk}
)
elif isinstance(obj, UnifiedJob):
res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk})
else:
cloud_cred = obj.cloud_credential
if cloud_cred:
@ -3120,6 +3127,14 @@ class JobEventSerializer(BaseSerializer):
max_bytes = settings.EVENT_STDOUT_MAX_BYTES_DISPLAY
if max_bytes > 0 and 'stdout' in ret and len(ret['stdout']) >= max_bytes:
ret['stdout'] = ret['stdout'][:(max_bytes - 1)] + u'\u2026'
set_count = 0
reset_count = 0
for m in ANSI_SGR_PATTERN.finditer(ret['stdout']):
if m.string[m.start():m.end()] == u'\u001b[0m':
reset_count += 1
else:
set_count += 1
ret['stdout'] += u'\u001b[0m' * (set_count - reset_count)
return ret
@ -3151,6 +3166,14 @@ class AdHocCommandEventSerializer(BaseSerializer):
max_bytes = settings.EVENT_STDOUT_MAX_BYTES_DISPLAY
if max_bytes > 0 and 'stdout' in ret and len(ret['stdout']) >= max_bytes:
ret['stdout'] = ret['stdout'][:(max_bytes - 1)] + u'\u2026'
set_count = 0
reset_count = 0
for m in ANSI_SGR_PATTERN.finditer(ret['stdout']):
if m.string[m.start():m.end()] == u'\u001b[0m':
reset_count += 1
else:
set_count += 1
ret['stdout'] += u'\u001b[0m' * (set_count - reset_count)
return ret

View File

@ -2383,24 +2383,24 @@ class InventoryScriptView(RetrieveAPIView):
host = get_object_or_404(obj.hosts, name=hostname, **hosts_q)
data = host.variables_dict
else:
data = OrderedDict()
data = dict()
if obj.variables_dict:
all_group = data.setdefault('all', OrderedDict())
all_group = data.setdefault('all', dict())
all_group['vars'] = obj.variables_dict
if obj.kind == 'smart':
if len(obj.hosts.all()) == 0:
return Response({})
else:
all_group = data.setdefault('all', OrderedDict())
smart_hosts_qs = obj.hosts.all().order_by('name')
all_group = data.setdefault('all', dict())
smart_hosts_qs = obj.hosts.all()
smart_hosts = list(smart_hosts_qs.values_list('name', flat=True))
all_group['hosts'] = smart_hosts
else:
# Add hosts without a group to the all group.
groupless_hosts_qs = obj.hosts.filter(groups__isnull=True, **hosts_q).order_by('name')
groupless_hosts_qs = obj.hosts.filter(groups__isnull=True, **hosts_q)
groupless_hosts = list(groupless_hosts_qs.values_list('name', flat=True))
if groupless_hosts:
all_group = data.setdefault('all', OrderedDict())
all_group = data.setdefault('all', dict())
all_group['hosts'] = groupless_hosts
# Build in-memory mapping of groups and their hosts.
@ -2408,7 +2408,6 @@ class InventoryScriptView(RetrieveAPIView):
if 'enabled' in hosts_q:
group_hosts_kw['host__enabled'] = hosts_q['enabled']
group_hosts_qs = Group.hosts.through.objects.filter(**group_hosts_kw)
group_hosts_qs = group_hosts_qs.order_by('host__name')
group_hosts_qs = group_hosts_qs.values_list('group_id', 'host_id', 'host__name')
group_hosts_map = {}
for group_id, host_id, host_name in group_hosts_qs:
@ -2420,7 +2419,6 @@ class InventoryScriptView(RetrieveAPIView):
from_group__inventory_id=obj.id,
to_group__inventory_id=obj.id,
)
group_parents_qs = group_parents_qs.order_by('from_group__name')
group_parents_qs = group_parents_qs.values_list('from_group_id', 'from_group__name', 'to_group_id')
group_children_map = {}
for from_group_id, from_group_name, to_group_id in group_parents_qs:
@ -2429,15 +2427,15 @@ class InventoryScriptView(RetrieveAPIView):
# Now use in-memory maps to build up group info.
for group in obj.groups.all():
group_info = OrderedDict()
group_info = dict()
group_info['hosts'] = group_hosts_map.get(group.id, [])
group_info['children'] = group_children_map.get(group.id, [])
group_info['vars'] = group.variables_dict
data[group.name] = group_info
if hostvars:
data.setdefault('_meta', OrderedDict())
data['_meta'].setdefault('hostvars', OrderedDict())
data.setdefault('_meta', dict())
data['_meta'].setdefault('hostvars', dict())
for host in obj.hosts.filter(**hosts_q):
data['_meta']['hostvars'][host.name] = host.variables_dict
@ -2669,6 +2667,12 @@ class InventoryUpdateList(ListAPIView):
model = InventoryUpdate
serializer_class = InventoryUpdateListSerializer
def get_queryset(self):
qs = super(InventoryUpdateList, self).get_queryset()
# TODO: remove this defer in 3.3 when we implement https://github.com/ansible/ansible-tower/issues/5436
qs = qs.defer('result_stdout_text')
return qs
class InventoryUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):

View File

@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import APIException
# Tower
from awx.main.utils.common import get_licenser
from awx.main.utils.common import get_licenser, memoize
__all__ = ['LicenseForbids', 'get_license', 'get_licensed_features',
'feature_enabled', 'feature_exists']
@ -40,6 +40,7 @@ def get_licensed_features():
return features
@memoize(cache_name='ephemeral')
def feature_enabled(name):
"""Return True if the requested feature is enabled, False otherwise."""
validated_license_data = _get_validated_license_data()

View File

@ -27,7 +27,7 @@ from awx.main.models import * # noqa
from awx.main.models.unified_jobs import ACTIVE_STATES
from awx.main.models.mixins import ResourceMixin
from awx.conf.license import LicenseForbids
from awx.conf.license import LicenseForbids, feature_enabled
__all__ = ['get_user_queryset', 'check_user_access', 'check_user_access_with_errors',
'user_accessible_objects', 'consumer_access',
@ -140,7 +140,14 @@ def get_user_capabilities(user, instance, **kwargs):
convenient for the user interface to consume and hide or show various
actions in the interface.
'''
access_class = access_registry[instance.__class__]
cls = instance.__class__
# When `.defer()` is used w/ the Django ORM, the result is a subclass of
# the original that represents e.g.,
# awx.main.models.ad_hoc_commands.AdHocCommand_Deferred_result_stdout_text
# We want to do the access registry lookup keyed on the base class name.
if getattr(cls, '_deferred', False):
cls = instance.__class__.__bases__[0]
access_class = access_registry[cls]
return access_class(user).get_user_capabilities(instance, **kwargs)
@ -323,6 +330,10 @@ class BaseAccess(object):
if validation_errors:
user_capabilities[display_method] = False
continue
elif isinstance(obj, (WorkflowJobTemplate, WorkflowJob)):
if not feature_enabled('workflows'):
user_capabilities[display_method] = (display_method == 'delete')
continue
elif display_method == 'copy' and isinstance(obj, WorkflowJobTemplate) and obj.organization_id is None:
user_capabilities[display_method] = self.user.is_superuser
continue
@ -482,8 +493,10 @@ class UserAccess(BaseAccess):
def can_change(self, obj, data):
if data is not None and ('is_superuser' in data or 'is_system_auditor' in data):
if (to_python_boolean(data.get('is_superuser', 'false'), allow_none=True) or
to_python_boolean(data.get('is_system_auditor', 'false'), allow_none=True)) and not self.user.is_superuser:
if to_python_boolean(data.get('is_superuser', 'false'), allow_none=True) and \
not self.user.is_superuser:
return False
if to_python_boolean(data.get('is_system_auditor', 'false'), allow_none=True) and not (self.user.is_superuser or self.user == obj):
return False
# A user can be changed if they are themselves, or by org admins or
# superusers. Change permission implies changing only certain fields
@ -2068,6 +2081,8 @@ class UnifiedJobAccess(BaseAccess):
# 'job_template__project',
# 'job_template__credential',
#)
# TODO: remove this defer in 3.3 when we implement https://github.com/ansible/ansible-tower/issues/5436
qs = qs.defer('result_stdout_text')
return qs.all()

View File

@ -1,8 +1,11 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
import re
from django.utils.translation import ugettext_lazy as _
CLOUD_PROVIDERS = ('azure', 'azure_rm', 'ec2', 'gce', 'rax', 'vmware', 'openstack', 'satellite6', 'cloudforms')
SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom', 'scm',)
PRIVILEGE_ESCALATION_METHODS = [ ('sudo', _('Sudo')), ('su', _('Su')), ('pbrun', _('Pbrun')), ('pfexec', _('Pfexec')), ('dzdo', _('DZDO')), ('pmrun', _('Pmrun')), ('runas', _('Runas'))]
ANSI_SGR_PATTERN = re.compile(r'\x1b\[[0-9;]*m')

View File

@ -17,6 +17,11 @@ class Command(BaseCommand):
Deprovision a Tower cluster node
"""
help = (
'Remove instance from the database. '
'Specify `--hostname` to use this command.'
)
option_list = BaseCommand.option_list + (
make_option('--hostname', dest='hostname', type='string',
help='Hostname used during provisioning'),

View File

@ -16,6 +16,11 @@ class Command(BaseCommand):
Regsiter this instance with the database for HA tracking.
"""
help = (
'Add instance to the database. '
'Specify `--hostname` to use this command.'
)
option_list = BaseCommand.option_list + (
make_option('--hostname', dest='hostname', type='string',
help='Hostname used during provisioning'),

View File

@ -14,7 +14,7 @@ def _create_fact_scan_project(ContentType, Project, org):
ct = ContentType.objects.get_for_model(Project)
name = "Tower Fact Scan - {}".format(org.name if org else "No Organization")
proj = Project(name=name,
scm_url='https://github.com/ansible/tower-fact-modules',
scm_url='https://github.com/ansible/awx-facts-playbooks',
scm_type='git',
scm_update_on_launch=True,
scm_update_cache_timeout=86400,

View File

@ -145,3 +145,17 @@ activity_stream_registrar.connect(WorkflowJob)
# prevent API filtering on certain Django-supplied sensitive fields
prevent_search(User._meta.get_field('password'))
# Always, always, always defer result_stdout_text for polymorphic UnifiedJob rows
# TODO: remove this defer in 3.3 when we implement https://github.com/ansible/ansible-tower/issues/5436
def defer_stdout(f):
def _wrapped(*args, **kwargs):
objs = f(*args, **kwargs)
objs.query.deferred_loading[0].add('result_stdout_text')
return objs
return _wrapped
for cls in UnifiedJob.__subclasses__():
cls.base_objects.filter = defer_stdout(cls.base_objects.filter)

View File

@ -300,7 +300,7 @@ class TaskManager():
# Already processed dependencies for this job
if job.dependent_jobs.all():
return False
latest_inventory_update = InventoryUpdate.objects.filter(inventory_source=inventory_source).order_by("created")
latest_inventory_update = InventoryUpdate.objects.filter(inventory_source=inventory_source).order_by("-created")
if not latest_inventory_update.exists():
return True
latest_inventory_update = latest_inventory_update.first()
@ -323,7 +323,7 @@ class TaskManager():
now = tz_now()
if job.dependent_jobs.all():
return False
latest_project_update = ProjectUpdate.objects.filter(project=job.project).order_by("created")
latest_project_update = ProjectUpdate.objects.filter(project=job.project, job_type='check').order_by("-created")
if not latest_project_update.exists():
return True
latest_project_update = latest_project_update.first()
@ -421,26 +421,40 @@ class TaskManager():
if not found_acceptable_queue:
logger.debug("%s couldn't be scheduled on graph, waiting for next cycle", task.log_format)
def fail_jobs_if_not_in_celery(self, node_jobs, active_tasks, celery_task_start_time):
def fail_jobs_if_not_in_celery(self, node_jobs, active_tasks, celery_task_start_time,
isolated=False):
for task in node_jobs:
if (task.celery_task_id not in active_tasks and not hasattr(settings, 'IGNORE_CELERY_INSPECTOR')):
if isinstance(task, WorkflowJob):
continue
if task.modified > celery_task_start_time:
continue
task.status = 'failed'
task.job_explanation += ' '.join((
'Task was marked as running in Tower but was not present in',
'Celery, so it has been marked as failed.',
))
new_status = 'failed'
if isolated:
new_status = 'error'
task.status = new_status
if isolated:
# TODO: cancel and reap artifacts of lost jobs from heartbeat
task.job_explanation += ' '.join((
'Task was marked as running in Tower but its ',
'controller management daemon was not present in',
'Celery, so it has been marked as failed.',
'Task may still be running, but contactability is unknown.'
))
else:
task.job_explanation += ' '.join((
'Task was marked as running in Tower but was not present in',
'Celery, so it has been marked as failed.',
))
try:
task.save(update_fields=['status', 'job_explanation'])
except DatabaseError:
logger.error("Task {} DB error in marking failed. Job possibly deleted.".format(task.log_format))
continue
awx_tasks._send_notification_templates(task, 'failed')
task.websocket_emit_status('failed')
logger.error("Task {} has no record in celery. Marking as failed".format(task.log_format))
task.websocket_emit_status(new_status)
logger.error("{}Task {} has no record in celery. Marking as failed".format(
'Isolated ' if isolated else '', task.log_format))
def cleanup_inconsistent_celery_tasks(self):
'''
@ -471,26 +485,36 @@ class TaskManager():
self.fail_jobs_if_not_in_celery(waiting_tasks, all_celery_task_ids, celery_task_start_time)
for node, node_jobs in running_tasks.iteritems():
isolated = False
if node in active_queues:
active_tasks = active_queues[node]
else:
'''
Node task list not found in celery. If tower thinks the node is down
then fail all the jobs on the node.
Node task list not found in celery. We may branch into cases:
- instance is unknown to tower, system is improperly configured
- instance is reported as down, then fail all jobs on the node
- instance is an isolated node, then check running tasks
among all allowed controller nodes for management process
'''
try:
instance = Instance.objects.get(hostname=node)
if instance.capacity == 0:
active_tasks = []
else:
continue
except Instance.DoesNotExist:
logger.error("Execution node Instance {} not found in database. "
"The node is currently executing jobs {}".format(node,
[j.log_format for j in node_jobs]))
active_tasks = []
instance = Instance.objects.filter(hostname=node).first()
self.fail_jobs_if_not_in_celery(node_jobs, active_tasks, celery_task_start_time)
if instance is None:
logger.error("Execution node Instance {} not found in database. "
"The node is currently executing jobs {}".format(
node, [j.log_format for j in node_jobs]))
active_tasks = []
elif instance.capacity == 0:
active_tasks = []
elif instance.rampart_groups.filter(controller__isnull=False).exists():
active_tasks = all_celery_task_ids
isolated = True
else:
continue
self.fail_jobs_if_not_in_celery(
node_jobs, active_tasks, celery_task_start_time,
isolated=isolated
)
def calculate_capacity_consumed(self, tasks):
self.graph = InstanceGroup.objects.capacity_values(tasks=tasks, graph=self.graph)

View File

@ -388,6 +388,9 @@ def activity_stream_create(sender, instance, created, **kwargs):
# Skip recording any inventory source directly associated with a group.
if isinstance(instance, InventorySource) and instance.deprecated_group:
return
_type = type(instance)
if getattr(_type, '_deferred', False):
return
object1 = camelcase_to_underscore(instance.__class__.__name__)
changes = model_to_dict(instance, model_serializer_mapping)
# Special case where Job survey password variables need to be hidden
@ -421,6 +424,9 @@ def activity_stream_update(sender, instance, **kwargs):
changes = model_instance_diff(old, new, model_serializer_mapping)
if changes is None:
return
_type = type(instance)
if getattr(_type, '_deferred', False):
return
object1 = camelcase_to_underscore(instance.__class__.__name__)
activity_entry = ActivityStream(
operation='update',
@ -445,6 +451,9 @@ def activity_stream_delete(sender, instance, **kwargs):
# explicitly called with flag on in Inventory.schedule_deletion.
if isinstance(instance, Inventory) and not kwargs.get('inventory_delete_flag', False):
return
_type = type(instance)
if getattr(_type, '_deferred', False):
return
changes = model_to_dict(instance)
object1 = camelcase_to_underscore(instance.__class__.__name__)
activity_entry = ActivityStream(
@ -466,6 +475,9 @@ def activity_stream_associate(sender, instance, **kwargs):
else:
return
obj1 = instance
_type = type(instance)
if getattr(_type, '_deferred', False):
return
object1=camelcase_to_underscore(obj1.__class__.__name__)
obj_rel = sender.__module__ + "." + sender.__name__
@ -476,6 +488,9 @@ def activity_stream_associate(sender, instance, **kwargs):
if not obj2_actual.exists():
continue
obj2_actual = obj2_actual[0]
_type = type(obj2_actual)
if getattr(_type, '_deferred', False):
return
if isinstance(obj2_actual, Role) and obj2_actual.content_object is not None:
obj2_actual = obj2_actual.content_object
object2 = camelcase_to_underscore(obj2_actual.__class__.__name__)

View File

@ -320,7 +320,11 @@ def awx_periodic_scheduler(self):
def _send_notification_templates(instance, status_str):
if status_str not in ['succeeded', 'failed']:
raise ValueError(_("status_str must be either succeeded or failed"))
notification_templates = instance.get_notification_templates()
try:
notification_templates = instance.get_notification_templates()
except:
logger.warn("No notification template defined for emitting notification")
notification_templates = None
if notification_templates:
if status_str == 'succeeded':
notification_template_type = 'success'
@ -482,6 +486,7 @@ class BaseTask(LogErrorsTask):
model = None
abstract = True
cleanup_paths = []
proot_show_paths = []
def update_model(self, pk, _attempt=0, **updates):
"""Reload the model instance from the database and update the
@ -793,6 +798,7 @@ class BaseTask(LogErrorsTask):
# May have to serialize the value
kwargs['private_data_files'] = self.build_private_data_files(instance, **kwargs)
kwargs['passwords'] = self.build_passwords(instance, **kwargs)
kwargs['proot_show_paths'] = self.proot_show_paths
args = self.build_args(instance, **kwargs)
safe_args = self.build_safe_args(instance, **kwargs)
output_replacements = self.build_output_replacements(instance, **kwargs)
@ -1068,6 +1074,7 @@ class RunJob(BaseTask):
env['VMWARE_USER'] = cloud_cred.username
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
env['VMWARE_HOST'] = cloud_cred.host
env['VMWARE_VALIDATE_CERTS'] = str(settings.VMWARE_VALIDATE_CERTS)
elif cloud_cred and cloud_cred.kind == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = cred_files.get(cloud_cred, '')
@ -1285,6 +1292,10 @@ class RunProjectUpdate(BaseTask):
name = 'awx.main.tasks.run_project_update'
model = ProjectUpdate
@property
def proot_show_paths(self):
return [settings.PROJECTS_ROOT]
def build_private_data(self, project_update, **kwargs):
'''
Return SSH private key data needed for this project update.
@ -1298,7 +1309,7 @@ class RunProjectUpdate(BaseTask):
}
}
'''
handle, self.revision_path = tempfile.mkstemp(dir=settings.AWX_PROOT_BASE_PATH)
handle, self.revision_path = tempfile.mkstemp(dir=settings.PROJECTS_ROOT)
self.cleanup_paths.append(self.revision_path)
private_data = {'credentials': {}}
if project_update.credential:
@ -1591,6 +1602,12 @@ class RunProjectUpdate(BaseTask):
if status == 'successful' and instance.launch_type != 'sync':
self._update_dependent_inventories(instance, dependent_inventory_sources)
def should_use_proot(self, instance, **kwargs):
'''
Return whether this task should use proot.
'''
return getattr(settings, 'AWX_PROOT_ENABLED', False)
class RunInventoryUpdate(BaseTask):

View File

@ -44,6 +44,12 @@ def test_system_auditor_is_system_auditor(system_auditor):
assert system_auditor.is_system_auditor
@pytest.mark.django_db
def test_system_auditor_can_modify_self(system_auditor):
access = UserAccess(system_auditor)
assert access.can_change(obj=system_auditor, data=dict(is_system_auditor='true'))
@pytest.mark.django_db
def test_user_queryset(user):
u = user('pete', False)

View File

@ -21,7 +21,7 @@ class TestCleanupInconsistentCeleryTasks():
@mock.patch.object(TaskManager, 'get_active_tasks', return_value=([], {}))
@mock.patch.object(TaskManager, 'get_running_tasks', return_value=({'host1': [Job(id=2), Job(id=3),]}, []))
@mock.patch.object(InstanceGroup.objects, 'prefetch_related', return_value=[])
@mock.patch.object(Instance.objects, 'get', side_effect=Instance.DoesNotExist)
@mock.patch.object(Instance.objects, 'filter', return_value=mock.MagicMock(first=lambda: None))
@mock.patch('awx.main.scheduler.logger')
def test_instance_does_not_exist(self, logger_mock, *args):
logger_mock.error = mock.MagicMock(side_effect=RuntimeError("mocked"))

View File

@ -181,6 +181,8 @@ class TestJobExecution:
EXAMPLE_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxyz==\n-----END PRIVATE KEY-----'
def setup_method(self, method):
if not os.path.exists(settings.PROJECTS_ROOT):
os.mkdir(settings.PROJECTS_ROOT)
self.project_path = tempfile.mkdtemp(prefix='awx_project_')
with open(os.path.join(self.project_path, 'helloworld.yml'), 'w') as f:
f.write('---')
@ -281,6 +283,15 @@ class TestGenericRun(TestJobExecution):
args, cwd, env, stdout = call_args
assert args[0] == 'bwrap'
def test_bwrap_virtualenvs_are_readonly(self):
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert '--ro-bind %s %s' % (settings.ANSIBLE_VENV_PATH, settings.ANSIBLE_VENV_PATH) in ' '.join(args) # noqa
assert '--ro-bind %s %s' % (settings.AWX_VENV_PATH, settings.AWX_VENV_PATH) in ' '.join(args) # noqa
def test_awx_task_env(self):
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
patch.start()
@ -1096,6 +1107,27 @@ class TestProjectUpdateCredentials(TestJobExecution):
]
}
def test_bwrap_exposes_projects_root(self):
ssh = CredentialType.defaults['ssh']()
self.instance.scm_type = 'git'
self.instance.credential = Credential(
pk=1,
credential_type=ssh,
)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
call_args, call_kwargs = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert ' '.join(args).startswith('bwrap')
' '.join([
'--bind',
settings.PROJECTS_ROOT,
settings.PROJECTS_ROOT,
]) in ' '.join(args)
assert '"scm_revision_output": "/projects/tmp' in ' '.join(args)
def test_username_and_password_auth(self, scm_type):
ssh = CredentialType.defaults['ssh']()
self.instance.scm_type = scm_type

View File

@ -108,13 +108,14 @@ class RequireDebugTrueOrTest(logging.Filter):
return settings.DEBUG or 'test' in sys.argv
def memoize(ttl=60, cache_key=None):
def memoize(ttl=60, cache_key=None, cache_name='default'):
'''
Decorator to wrap a function and cache its result.
'''
from django.core.cache import cache
from django.core.cache import caches
def _memoizer(f, *args, **kwargs):
cache = caches[cache_name]
key = cache_key or slugify('%s %r %r' % (f.__name__, args, kwargs))
value = cache.get(key)
if value is None:
@ -696,8 +697,13 @@ def wrap_args_with_proot(args, cwd, **kwargs):
show_paths = [cwd, kwargs['private_data_dir']]
else:
show_paths = [cwd]
show_paths.extend([settings.ANSIBLE_VENV_PATH, settings.AWX_VENV_PATH])
for venv in (
settings.ANSIBLE_VENV_PATH,
settings.AWX_VENV_PATH
):
new_args.extend(['--ro-bind', venv, venv])
show_paths.extend(getattr(settings, 'AWX_PROOT_SHOW_PATHS', None) or [])
show_paths.extend(kwargs.get('proot_show_paths', []))
for path in sorted(set(show_paths)):
if not os.path.exists(path):
continue

View File

@ -49,6 +49,7 @@ Command line arguments:
- tenant
- ad_user
- password
- cloud_environment
Environment variables:
- AZURE_PROFILE
@ -58,6 +59,7 @@ Environment variables:
- AZURE_TENANT
- AZURE_AD_USER
- AZURE_PASSWORD
- AZURE_CLOUD_ENVIRONMENT
Run for Specific Host
-----------------------
@ -190,22 +192,27 @@ import json
import os
import re
import sys
import inspect
import traceback
from packaging.version import Version
from os.path import expanduser
import ansible.module_utils.six.moves.urllib.parse as urlparse
HAS_AZURE = True
HAS_AZURE_EXC = None
try:
from msrestazure.azure_exceptions import CloudError
from msrestazure import azure_cloud
from azure.mgmt.compute import __version__ as azure_compute_version
from azure.common import AzureMissingResourceHttpError, AzureHttpError
from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials
from azure.mgmt.network.network_management_client import NetworkManagementClient
from azure.mgmt.resource.resources.resource_management_client import ResourceManagementClient
from azure.mgmt.compute.compute_management_client import ComputeManagementClient
from azure.mgmt.network import NetworkManagementClient
from azure.mgmt.resource.resources import ResourceManagementClient
from azure.mgmt.compute import ComputeManagementClient
except ImportError as exc:
HAS_AZURE_EXC = exc
HAS_AZURE = False
@ -218,7 +225,8 @@ AZURE_CREDENTIAL_ENV_MAPPING = dict(
secret='AZURE_SECRET',
tenant='AZURE_TENANT',
ad_user='AZURE_AD_USER',
password='AZURE_PASSWORD'
password='AZURE_PASSWORD',
cloud_environment='AZURE_CLOUD_ENVIRONMENT',
)
AZURE_CONFIG_SETTINGS = dict(
@ -232,7 +240,7 @@ AZURE_CONFIG_SETTINGS = dict(
group_by_tag='AZURE_GROUP_BY_TAG'
)
AZURE_MIN_VERSION = "0.30.0rc5"
AZURE_MIN_VERSION = "2.0.0"
def azure_id_to_dict(id):
@ -249,6 +257,7 @@ class AzureRM(object):
def __init__(self, args):
self._args = args
self._cloud_environment = None
self._compute_client = None
self._resource_client = None
self._network_client = None
@ -262,6 +271,26 @@ class AzureRM(object):
self.fail("Failed to get credentials. Either pass as parameters, set environment variables, "
"or define a profile in ~/.azure/credentials.")
# if cloud_environment specified, look up/build Cloud object
raw_cloud_env = self.credentials.get('cloud_environment')
if not raw_cloud_env:
self._cloud_environment = azure_cloud.AZURE_PUBLIC_CLOUD # SDK default
else:
# try to look up "well-known" values via the name attribute on azure_cloud members
all_clouds = [x[1] for x in inspect.getmembers(azure_cloud) if isinstance(x[1], azure_cloud.Cloud)]
matched_clouds = [x for x in all_clouds if x.name == raw_cloud_env]
if len(matched_clouds) == 1:
self._cloud_environment = matched_clouds[0]
elif len(matched_clouds) > 1:
self.fail("Azure SDK failure: more than one cloud matched for cloud_environment name '{0}'".format(raw_cloud_env))
else:
if not urlparse.urlparse(raw_cloud_env).scheme:
self.fail("cloud_environment must be an endpoint discovery URL or one of {0}".format([x.name for x in all_clouds]))
try:
self._cloud_environment = azure_cloud.get_cloud_from_metadata_endpoint(raw_cloud_env)
except Exception as e:
self.fail("cloud_environment {0} could not be resolved: {1}".format(raw_cloud_env, e.message))
if self.credentials.get('subscription_id', None) is None:
self.fail("Credentials did not include a subscription_id value.")
self.log("setting subscription_id")
@ -272,16 +301,23 @@ class AzureRM(object):
self.credentials.get('tenant') is not None:
self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'],
secret=self.credentials['secret'],
tenant=self.credentials['tenant'])
tenant=self.credentials['tenant'],
cloud_environment=self._cloud_environment)
elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None:
self.azure_credentials = UserPassCredentials(self.credentials['ad_user'], self.credentials['password'])
tenant = self.credentials.get('tenant')
if not tenant:
tenant = 'common'
self.azure_credentials = UserPassCredentials(self.credentials['ad_user'],
self.credentials['password'],
tenant=tenant,
cloud_environment=self._cloud_environment)
else:
self.fail("Failed to authenticate with provided credentials. Some attributes were missing. "
"Credentials must include client_id, secret and tenant or ad_user and password.")
def log(self, msg):
if self.debug:
print (msg + u'\n')
print(msg + u'\n')
def fail(self, msg):
raise Exception(msg)
@ -341,6 +377,10 @@ class AzureRM(object):
self.log('Received credentials from parameters.')
return arg_credentials
if arg_credentials['ad_user'] is not None:
self.log('Received credentials from parameters.')
return arg_credentials
# try environment
env_credentials = self._get_env_credentials()
if env_credentials:
@ -372,7 +412,12 @@ class AzureRM(object):
def network_client(self):
self.log('Getting network client')
if not self._network_client:
self._network_client = NetworkManagementClient(self.azure_credentials, self.subscription_id)
self._network_client = NetworkManagementClient(
self.azure_credentials,
self.subscription_id,
base_url=self._cloud_environment.endpoints.resource_manager,
api_version='2017-06-01'
)
self._register('Microsoft.Network')
return self._network_client
@ -380,14 +425,24 @@ class AzureRM(object):
def rm_client(self):
self.log('Getting resource manager client')
if not self._resource_client:
self._resource_client = ResourceManagementClient(self.azure_credentials, self.subscription_id)
self._resource_client = ResourceManagementClient(
self.azure_credentials,
self.subscription_id,
base_url=self._cloud_environment.endpoints.resource_manager,
api_version='2017-05-10'
)
return self._resource_client
@property
def compute_client(self):
self.log('Getting compute client')
if not self._compute_client:
self._compute_client = ComputeManagementClient(self.azure_credentials, self.subscription_id)
self._compute_client = ComputeManagementClient(
self.azure_credentials,
self.subscription_id,
base_url=self._cloud_environment.endpoints.resource_manager,
api_version='2017-03-30'
)
self._register('Microsoft.Compute')
return self._compute_client
@ -440,7 +495,7 @@ class AzureInventory(object):
self.include_powerstate = False
self.get_inventory()
print (self._json_format_dict(pretty=self._args.pretty))
print(self._json_format_dict(pretty=self._args.pretty))
sys.exit(0)
def _parse_cli_args(self):
@ -448,13 +503,13 @@ class AzureInventory(object):
parser = argparse.ArgumentParser(
description='Produce an Ansible Inventory file for an Azure subscription')
parser.add_argument('--list', action='store_true', default=True,
help='List instances (default: True)')
help='List instances (default: True)')
parser.add_argument('--debug', action='store_true', default=False,
help='Send debug messages to STDOUT')
help='Send debug messages to STDOUT')
parser.add_argument('--host', action='store',
help='Get all information about an instance')
help='Get all information about an instance')
parser.add_argument('--pretty', action='store_true', default=False,
help='Pretty print JSON output(default: False)')
help='Pretty print JSON output(default: False)')
parser.add_argument('--profile', action='store',
help='Azure profile contained in ~/.azure/credentials')
parser.add_argument('--subscription_id', action='store',
@ -465,10 +520,12 @@ class AzureInventory(object):
help='Azure Client Secret')
parser.add_argument('--tenant', action='store',
help='Azure Tenant Id')
parser.add_argument('--ad-user', action='store',
parser.add_argument('--ad_user', action='store',
help='Active Directory User')
parser.add_argument('--password', action='store',
help='password')
parser.add_argument('--cloud_environment', action='store',
help='Azure Cloud Environment name or metadata discovery URL')
parser.add_argument('--resource-groups', action='store',
help='Return inventory for comma separated list of resource group names')
parser.add_argument('--tags', action='store',
@ -486,8 +543,7 @@ class AzureInventory(object):
try:
virtual_machines = self._compute_client.virtual_machines.list(resource_group)
except Exception as exc:
sys.exit("Error: fetching virtual machines for resource group {0} - {1}".format(resource_group,
str(exc)))
sys.exit("Error: fetching virtual machines for resource group {0} - {1}".format(resource_group, str(exc)))
if self._args.host or self.tags:
selected_machines = self._selected_machines(virtual_machines)
self._load_machines(selected_machines)
@ -510,7 +566,7 @@ class AzureInventory(object):
for machine in machines:
id_dict = azure_id_to_dict(machine.id)
#TODO - The API is returning an ID value containing resource group name in ALL CAPS. If/when it gets
# TODO - The API is returning an ID value containing resource group name in ALL CAPS. If/when it gets
# fixed, we should remove the .lower(). Opened Issue
# #574: https://github.com/Azure/azure-sdk-for-python/issues/574
resource_group = id_dict['resourceGroups'].lower()
@ -538,7 +594,7 @@ class AzureInventory(object):
mac_address=None,
plan=(machine.plan.name if machine.plan else None),
virtual_machine_size=machine.hardware_profile.vm_size,
computer_name=machine.os_profile.computer_name,
computer_name=(machine.os_profile.computer_name if machine.os_profile else None),
provisioning_state=machine.provisioning_state,
)
@ -559,7 +615,7 @@ class AzureInventory(object):
)
# Add windows details
if machine.os_profile.windows_configuration is not None:
if machine.os_profile is not None and machine.os_profile.windows_configuration is not None:
host_vars['windows_auto_updates_enabled'] = \
machine.os_profile.windows_configuration.enable_automatic_updates
host_vars['windows_timezone'] = machine.os_profile.windows_configuration.time_zone
@ -790,13 +846,10 @@ class AzureInventory(object):
def main():
if not HAS_AZURE:
sys.exit("The Azure python sdk is not installed (try `pip install 'azure>=2.0.0rc5' --upgrade`) - {0}".format(HAS_AZURE_EXC))
if Version(azure_compute_version) < Version(AZURE_MIN_VERSION):
sys.exit("Expecting azure.mgmt.compute.__version__ to be {0}. Found version {1} "
"Do you have Azure >= 2.0.0rc5 installed? (try `pip install 'azure>=2.0.0rc5' --upgrade`)".format(AZURE_MIN_VERSION, azure_compute_version))
sys.exit("The Azure python sdk is not installed (try `pip install 'azure>={0}' --upgrade`) - {1}".format(AZURE_MIN_VERSION, HAS_AZURE_EXC))
AzureInventory()
if __name__ == '__main__':
main()

View File

@ -481,6 +481,9 @@ if is_testing():
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'ephemeral': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
}
else:
CACHES = {
@ -488,6 +491,9 @@ else:
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': 'memcached:11211',
},
'ephemeral': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
}
# Social Auth configuration.
@ -716,7 +722,7 @@ VMWARE_GROUP_FILTER = r'^.+$'
VMWARE_HOST_FILTER = r'^.+$'
VMWARE_EXCLUDE_EMPTY_GROUPS = True
VMWARE_VALIDATE_CERTS = False
# ---------------------------
# -- Google Compute Engine --
# ---------------------------

View File

@ -100,7 +100,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "license_type !== 'enterprise' || form.$invalid || form.$pending"
ngDisabled: "license_type !== 'enterprise' && license_type !== 'open' || configuration_ldap_template_form.$invalid || configuration_ldap_template_form.$pending"
}
}
};

View File

@ -39,7 +39,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "license_type !== 'enterprise' || form.$invalid || form.$pending"
ngDisabled: "license_type !== 'enterprise' && license_type !== 'open' || configuration_radius_template_form.$invalid || configuration_radius_template_form.$pending"
}
}
};

View File

@ -92,7 +92,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "license_type !== 'enterprise' || form.$invalid || form.$pending"
ngDisabled: "license_type !== 'enterprise' && license_type !== 'open' || configuration_saml_template_form.$invalid || configuration_saml_template_form.$pending"
}
}
};

View File

@ -52,7 +52,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "license_type !== 'enterprise' || form.$invalid || form.$pending"
ngDisabled: "license_type !== 'enterprise' && license_type !== 'open' || configuration_tacacs_template_form.$invalid || configuration_tacacs_template_form.$pending"
}
}
};

View File

@ -95,7 +95,11 @@ export default [
} else {
if (key === "LICENSE") {
$scope.license_type = data[key].license_type;
if (_.isEmpty(data[key])) {
$scope.license_type = "open";
} else {
$scope.license_type = data[key].license_type;
}
}
//handle nested objects
if(ConfigurationUtils.isEmpty(data[key])) {

View File

@ -19,7 +19,10 @@
</div>
<div class="Modal-body">
<div>
<div class="Prompt-bodyQuery"><span translate>Are you sure you want to disassociate the host below from</span> {{disassociateGroup.name}}?</div>
<div class="Prompt-bodyQuery">
<span translate>Are you sure you want to disassociate the host below from</span> {{disassociateGroup.name}}?<br /><br />
<span translate>Note that only hosts directly in this group can be disassociated. Hosts in sub-groups must be disassociated directly from the sub-group level that they belong.</span>
</div>
<div class="Prompt-bodyTarget">{{ host.name }}</div>
</div>
<div class="Modal-footer">

View File

@ -81,9 +81,12 @@
<label class="JobResults-resultRowLabel" translate>
Explanation
</label>
<div class="JobResults-resultRowText" ng-show="!previousTaskFailed">
{{ job.job_explanation }}
</div>
<div class="JobResults-resultRowText">
{{task_detail | limitTo:explanationLimit}}
<span ng-show="explanationLimit && task_detail.length > explanationLimit">
<span ng-show="previousTaskFailed && explanationLimit && task_detail.length > explanationLimit">
<span>... </span>
<span class="JobResults-seeMoreLess" ng-click="explanationLimit=undefined">Show More</span>
</span>

View File

@ -44,16 +44,7 @@ export default
Rest.get()
.success(function (data) {
if(params.updateAllSources) {
let userCanUpdateAllSources = true;
_.forEach(data, function(inventory_source){
if (!inventory_source.can_update) {
userCanUpdateAllSources = false;
}
});
if(userCanUpdateAllSources) {
scope.$emit('StartTheUpdate', {});
}
scope.$emit('StartTheUpdate', {});
}
else {
inventory_source = data;
@ -67,7 +58,7 @@ export default
}
} else {
Wait('stop');
Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.',
Alert('Error Launching Sync', 'Unable to execute the inventory sync. Please contact your system administrator.',
'alert-danger');
}
}

View File

@ -54,9 +54,16 @@ export default ['$scope', '$rootScope', '$location', '$stateParams',
.catch(({data, status}) => {
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to add new organization. Post returned status: ' + status
msg: 'Failed to save instance groups. POST returned status: ' + status
});
});
})
.catch(({data, status}) => {
let explanation = _.has(data, "name") ? data.name[0] : "";
ProcessErrors($scope, data, status, OrganizationForm, {
hdr: 'Error!',
msg: `Failed to save organization. PUT status: ${status}. ${explanation}`
});
});
};

View File

@ -120,6 +120,7 @@ angular.module('Utilities', ['RestServices', 'Utilities'])
if (action) {
action();
}
$('.modal-backdrop').remove();
});
$('#alert-modal').on('shown.bs.modal', function() {
$('#alert_ok_btn').focus();

View File

@ -29,10 +29,8 @@ export default ['templateUrl', function(templateUrl) {
$scope.init = function() {
let list = $scope.list;
if($state.params.selected) {
$scope.currentSelection = {
name: null,
id: parseInt($state.params.selected)
};
let selection = $scope[list.name].find(({id}) => id === parseInt($state.params.selected));
$scope.currentSelection = _.pick(selection, 'id', 'name');
}
$scope.$watch(list.name, function(){
selectRowIfPresent();

View File

@ -51,7 +51,7 @@
.Paginate-total {
display: flex;
align-items: flex-end;
margin-bottom: -2px;
margin: 0 0 -2px 10px;
}
.Paginate-filterLabel{

View File

@ -32,7 +32,7 @@
.SmartSearch-searchTermContainer {
flex: initial;
width: 100%;
flex-grow: 1;
border: 1px solid @b7grey;
border-radius: 4px;
display: flex;
@ -167,7 +167,7 @@
color: @default-interface-txt;
border: 1px solid @b7grey;
cursor: pointer;
width: 70px;
min-width: 70px;
height: 34px;
line-height: 20px;
}
@ -240,15 +240,6 @@
margin-right: 5px;
}
// Additional modal specific styles
.modal-body, #add-permissions-modal,
.JobResults {
.SmartSearch-searchTermContainer {
width: 100%;
}
}
@media (max-width: 700px) {
.SmartSearch-bar {
width: 100%;

View File

@ -161,10 +161,15 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', '
terms = (terms) ? terms.trim() : "";
if(terms && terms !== '') {
// Split the terms up
let splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
_.forEach(splitTerms, (term) => {
let splitTerms;
if ($scope.singleSearchParam === 'host_filter') {
splitTerms = SmartSearchService.splitFilterIntoTerms(terms);
} else {
splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
}
_.forEach(splitTerms, (term) => {
let termParts = SmartSearchService.splitTermIntoParts(term);
function combineSameSearches(a,b){

View File

@ -1,5 +1,45 @@
export default [function() {
return {
/**
* For the Smart Host Filter, values with spaces are wrapped with double quotes on input.
* To avoid having these quoted values split up and treated as terms themselves, some
* work is done to encode quotes in quoted values and the spaces within those quoted
* values before calling to `splitSearchIntoTerms`.
*/
splitFilterIntoTerms (searchString) {
if (!searchString) {
return null;
}
let groups = [];
let quoted;
searchString.split(' ').forEach(substring => {
if (/:"/g.test(substring)) {
if (/"$/.test(substring)) {
groups.push(this.encode(substring));
} else {
quoted = substring;
}
} else if (quoted) {
quoted += ` ${substring}`;
if (/"/g.test(substring)) {
groups.push(this.encode(quoted));
quoted = undefined;
}
} else {
groups.push(substring);
}
});
return this.splitSearchIntoTerms(groups.join(' '));
},
encode (string) {
string = string.replace(/'/g, '%27');
return string.replace(/("| )/g, match => encodeURIComponent(match));
},
splitSearchIntoTerms(searchString) {
return searchString.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
},

View File

@ -35,6 +35,7 @@ export default
self.socket.onopen = function () {
$log.debug("Websocket connection opened.");
socketPromise.resolve();
console.log('promise resolved, and readyState: '+ self.readyState);
self.checkStatus();
if(needsResubscribing){
self.subscribe(self.getLast());
@ -116,6 +117,8 @@ export default
disconnect: function(){
if(this.socket){
this.socket.close();
delete this.socket;
console.log("Socket deleted: "+this.socket);
}
},
subscribe: function(state){
@ -186,14 +189,20 @@ export default
var self = this;
$log.debug('Sent to Websocket Server: ' + data);
socketPromise.promise.then(function(){
self.socket.send(data, function () {
var args = arguments;
self.scope.$apply(function () {
if (callback) {
callback.apply(self.socket, args);
}
console.log("socket readyState at emit: " + self.socket.readyState);
// if(self.socket.readyState === 0){
// self.subscribe(self.getLast());
// }
if(self.socket.readyState === 1){
self.socket.send(data, function () {
var args = arguments;
self.scope.$apply(function () {
if (callback) {
callback.apply(self.socket, args);
}
});
});
});
}
});
},
addStateResolve: function(state, id){

View File

@ -37,6 +37,7 @@
$scope.mode = "add";
$scope.parseType = 'yaml';
$scope.credentialNotPresent = false;
$scope.canGetAllRelatedResources = true;
md5Setup({
scope: $scope,

View File

@ -18,13 +18,15 @@ export default
'Empty', 'Prompt', 'ToJSON', 'GetChoices', 'CallbackHelpInit',
'InitiatePlaybookRun' , 'initSurvey', '$state', 'CreateSelect2',
'ToggleNotification','$q', 'InstanceGroupsService', 'InstanceGroupsData', 'MultiCredentialService', 'availableLabels',
'canGetProject', 'canGetInventory', 'jobTemplateData', 'ParseVariableString',
function(
$filter, $scope, $rootScope,
$location, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert,
ProcessErrors, GetBasePath, md5Setup,
ParseTypeChange, Wait, selectedLabels, i18n,
Empty, Prompt, ToJSON, GetChoices, CallbackHelpInit, InitiatePlaybookRun, SurveyControllerInit, $state,
CreateSelect2, ToggleNotification, $q, InstanceGroupsService, InstanceGroupsData, MultiCredentialService, availableLabels
CreateSelect2, ToggleNotification, $q, InstanceGroupsService, InstanceGroupsData, MultiCredentialService, availableLabels,
canGetProject, canGetInventory, jobTemplateData, ParseVariableString
) {
$scope.$watch('job_template_obj.summary_fields.user_capabilities.edit', function(val) {
@ -47,6 +49,7 @@ export default
function init() {
CallbackHelpInit({ scope: $scope });
$scope.playbook_options = null;
$scope.playbook = null;
$scope.mode = 'edit';
@ -253,7 +256,175 @@ export default
$scope.rmoveLoadJobs();
}
$scope.removeLoadJobs = $scope.$on('LoadJobs', function() {
$scope.fillJobTemplate();
$scope.job_template_obj = jobTemplateData;
$scope.name = jobTemplateData.name;
var fld, i;
for (fld in form.fields) {
if (fld !== 'variables' && fld !== 'survey' && fld !== 'forks' && jobTemplateData[fld] !== null && jobTemplateData[fld] !== undefined) {
if (form.fields[fld].type === 'select') {
if ($scope[fld + '_options'] && $scope[fld + '_options'].length > 0) {
for (i = 0; i < $scope[fld + '_options'].length; i++) {
if (jobTemplateData[fld] === $scope[fld + '_options'][i].value) {
$scope[fld] = $scope[fld + '_options'][i];
}
}
} else {
$scope[fld] = jobTemplateData[fld];
}
} else {
$scope[fld] = jobTemplateData[fld];
if(!Empty(jobTemplateData.summary_fields.survey)) {
$scope.survey_exists = true;
}
}
master[fld] = $scope[fld];
}
if (fld === 'forks') {
if (jobTemplateData[fld] !== 0) {
$scope[fld] = jobTemplateData[fld];
master[fld] = $scope[fld];
}
}
if (fld === 'variables') {
// Parse extra_vars, converting to YAML.
$scope.variables = ParseVariableString(jobTemplateData.extra_vars);
master.variables = $scope.variables;
}
if (form.fields[fld].type === 'lookup' && jobTemplateData.summary_fields[form.fields[fld].sourceModel]) {
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
jobTemplateData.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField];
}
if (form.fields[fld].type === 'checkbox_group') {
for(var j=0; j<form.fields[fld].fields.length; j++) {
$scope[form.fields[fld].fields[j].name] = jobTemplateData[form.fields[fld].fields[j].name];
}
}
}
Wait('stop');
$scope.url = jobTemplateData.url;
$scope.survey_enabled = jobTemplateData.survey_enabled;
$scope.ask_variables_on_launch = (jobTemplateData.ask_variables_on_launch) ? true : false;
master.ask_variables_on_launch = $scope.ask_variables_on_launch;
$scope.ask_verbosity_on_launch = (jobTemplateData.ask_verbosity_on_launch) ? true : false;
master.ask_verbosity_on_launch = $scope.ask_verbosity_on_launch;
$scope.ask_limit_on_launch = (jobTemplateData.ask_limit_on_launch) ? true : false;
master.ask_limit_on_launch = $scope.ask_limit_on_launch;
$scope.ask_tags_on_launch = (jobTemplateData.ask_tags_on_launch) ? true : false;
master.ask_tags_on_launch = $scope.ask_tags_on_launch;
$scope.ask_skip_tags_on_launch = (jobTemplateData.ask_skip_tags_on_launch) ? true : false;
master.ask_skip_tags_on_launch = $scope.ask_skip_tags_on_launch;
$scope.ask_diff_mode_on_launch = (jobTemplateData.ask_diff_mode_on_launch) ? true : false;
master.ask_diff_mode_on_launch = $scope.ask_diff_mode_on_launch;
$scope.job_tag_options = (jobTemplateData.job_tags) ? jobTemplateData.job_tags.split(',')
.map((i) => ({name: i, label: i, value: i})) : [];
$scope.job_tags = $scope.job_tag_options;
master.job_tags = $scope.job_tags;
$scope.skip_tag_options = (jobTemplateData.skip_tags) ? jobTemplateData.skip_tags.split(',')
.map((i) => ({name: i, label: i, value: i})) : [];
$scope.skip_tags = $scope.skip_tag_options;
master.skip_tags = $scope.skip_tags;
$scope.ask_job_type_on_launch = (jobTemplateData.ask_job_type_on_launch) ? true : false;
master.ask_job_type_on_launch = $scope.ask_job_type_on_launch;
$scope.ask_inventory_on_launch = (jobTemplateData.ask_inventory_on_launch) ? true : false;
master.ask_inventory_on_launch = $scope.ask_inventory_on_launch;
$scope.ask_credential_on_launch = (jobTemplateData.ask_credential_on_launch) ? true : false;
master.ask_credential_on_launch = $scope.ask_credential_on_launch;
if (jobTemplateData.host_config_key) {
$scope.example_config_key = jobTemplateData.host_config_key;
}
$scope.example_template_id = id;
$scope.setCallbackHelp();
$scope.callback_url = $scope.callback_server_path + ((jobTemplateData.related.callback) ? jobTemplateData.related.callback :
GetBasePath('job_templates') + id + '/callback/');
master.callback_url = $scope.callback_url;
$scope.can_edit = jobTemplateData.summary_fields.user_capabilities.edit;
if($scope.job_template_obj.summary_fields.user_capabilities.edit) {
MultiCredentialService.loadCredentials(jobTemplateData)
.then(([selectedCredentials, credTypes, credTypeOptions,
credTags, credentialGetPermissionDenied]) => {
$scope.canGetAllRelatedResources = canGetProject && canGetInventory && !credentialGetPermissionDenied ? true : false;
$scope.selectedCredentials = selectedCredentials;
$scope.credential_types = credTypes;
$scope.credentialTypeOptions = credTypeOptions;
$scope.credentialsToPost = credTags;
$scope.$emit('jobTemplateLoaded', master);
});
}
else {
if (jobTemplateData.summary_fields.credential) {
$scope.selectedCredentials.machine = jobTemplateData.summary_fields.credential;
}
if (jobTemplateData.summary_fields.vault_credential) {
$scope.selectedCredentials.vault = jobTemplateData.summary_fields.vault_credential;
}
if (jobTemplateData.summary_fields.extra_credentials) {
$scope.selectedCredentials.extra = jobTemplateData.summary_fields.extra_credentials;
}
MultiCredentialService.getCredentialTypes()
.then(({credential_types, credentialTypeOptions}) => {
let typesArray = Object.keys(credential_types).map(key => credential_types[key]);
let credTypeOptions = credentialTypeOptions;
let machineAndVaultCreds = [],
extraCreds = [];
if($scope.selectedCredentials.machine) {
machineAndVaultCreds.push($scope.selectedCredentials.machine);
}
if($scope.selectedCredentials.vault) {
machineAndVaultCreds.push($scope.selectedCredentials.vault);
}
machineAndVaultCreds.map(cred => ({
name: cred.name,
id: cred.id,
postType: cred.postType,
kind: typesArray
.filter(type => {
return cred.kind === type.kind || parseInt(cred.credential_type) === type.value;
})[0].name + ":"
}));
if($scope.selectedCredentials.extra && $scope.selectedCredentials.extra.length > 0) {
extraCreds = extraCreds.concat($scope.selectedCredentials.extra).map(cred => ({
name: cred.name,
id: cred.id,
postType: cred.postType,
kind: credTypeOptions
.filter(type => {
return parseInt(cred.credential_type_id) === type.value;
})[0].name + ":"
}));
}
$scope.credentialsToPost = machineAndVaultCreds.concat(extraCreds);
$scope.$emit('jobTemplateLoaded', master);
});
}
});
if ($scope.removeChoicesReady) {

View File

@ -2,14 +2,7 @@ export default
function CallbackHelpInit($q, $location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $stateParams, ProcessErrors,
ParseVariableString, Empty, Wait, MultiCredentialService, $rootScope) {
return function(params) {
var scope = params.scope,
defaultUrl = GetBasePath('job_templates'),
// generator = GenerateForm,
form = JobTemplateForm(),
// loadingFinishedCount = 0,
// base = $location.path().replace(/^\//, '').split('/')[0],
master = {},
id = $stateParams.job_template_id;
var scope = params.scope;
// checkSCMStatus, getPlaybooks, callback,
// choicesCount = 0;
@ -44,211 +37,6 @@ export default
scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a';
scope.example_template_id = 'N';
scope.setCallbackHelp();
// this fills the job template form both on copy of the job template
// and on edit
scope.fillJobTemplate = function(){
// id = id || $rootScope.copy.id;
// Retrieve detail record and prepopulate the form
Rest.setUrl(defaultUrl + id);
Rest.get()
.success(function (data) {
scope.job_template_obj = data;
scope.name = data.name;
var fld, i;
for (fld in form.fields) {
if (fld !== 'variables' && fld !== 'survey' && fld !== 'forks' && data[fld] !== null && data[fld] !== undefined) {
if (form.fields[fld].type === 'select') {
if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) {
for (i = 0; i < scope[fld + '_options'].length; i++) {
if (data[fld] === scope[fld + '_options'][i].value) {
scope[fld] = scope[fld + '_options'][i];
}
}
} else {
scope[fld] = data[fld];
}
} else {
scope[fld] = data[fld];
if(!Empty(data.summary_fields.survey)) {
scope.survey_exists = true;
}
}
master[fld] = scope[fld];
}
if (fld === 'forks') {
if (data[fld] !== 0) {
scope[fld] = data[fld];
master[fld] = scope[fld];
}
}
if (fld === 'variables') {
// Parse extra_vars, converting to YAML.
scope.variables = ParseVariableString(data.extra_vars);
master.variables = scope.variables;
}
if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) {
scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField];
}
if (form.fields[fld].type === 'checkbox_group') {
for(var j=0; j<form.fields[fld].fields.length; j++) {
scope[form.fields[fld].fields[j].name] = data[form.fields[fld].fields[j].name];
}
}
}
Wait('stop');
scope.url = data.url;
scope.survey_enabled = data.survey_enabled;
scope.ask_variables_on_launch = (data.ask_variables_on_launch) ? true : false;
master.ask_variables_on_launch = scope.ask_variables_on_launch;
scope.ask_verbosity_on_launch = (data.ask_verbosity_on_launch) ? true : false;
master.ask_verbosity_on_launch = scope.ask_verbosity_on_launch;
scope.ask_limit_on_launch = (data.ask_limit_on_launch) ? true : false;
master.ask_limit_on_launch = scope.ask_limit_on_launch;
scope.ask_tags_on_launch = (data.ask_tags_on_launch) ? true : false;
master.ask_tags_on_launch = scope.ask_tags_on_launch;
scope.ask_skip_tags_on_launch = (data.ask_skip_tags_on_launch) ? true : false;
master.ask_skip_tags_on_launch = scope.ask_skip_tags_on_launch;
scope.ask_diff_mode_on_launch = (data.ask_diff_mode_on_launch) ? true : false;
master.ask_diff_mode_on_launch = scope.ask_diff_mode_on_launch;
scope.job_tag_options = (data.job_tags) ? data.job_tags.split(',')
.map((i) => ({name: i, label: i, value: i})) : [];
scope.job_tags = scope.job_tag_options;
master.job_tags = scope.job_tags;
scope.skip_tag_options = (data.skip_tags) ? data.skip_tags.split(',')
.map((i) => ({name: i, label: i, value: i})) : [];
scope.skip_tags = scope.skip_tag_options;
master.skip_tags = scope.skip_tags;
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;
scope.ask_inventory_on_launch = (data.ask_inventory_on_launch) ? true : false;
master.ask_inventory_on_launch = scope.ask_inventory_on_launch;
scope.ask_credential_on_launch = (data.ask_credential_on_launch) ? true : false;
master.ask_credential_on_launch = scope.ask_credential_on_launch;
if (data.host_config_key) {
scope.example_config_key = data.host_config_key;
}
scope.example_template_id = id;
scope.setCallbackHelp();
scope.callback_url = scope.callback_server_path + ((data.related.callback) ? data.related.callback :
GetBasePath('job_templates') + id + '/callback/');
master.callback_url = scope.callback_url;
scope.can_edit = data.summary_fields.user_capabilities.edit;
if(scope.job_template_obj.summary_fields.user_capabilities.edit) {
MultiCredentialService.loadCredentials(data)
.then(([selectedCredentials, credTypes, credTypeOptions,
credTags]) => {
scope.selectedCredentials = selectedCredentials;
scope.credential_types = credTypes;
scope.credentialTypeOptions = credTypeOptions;
scope.credentialsToPost = credTags;
scope.$emit('jobTemplateLoaded', master);
});
}
else {
if (data.summary_fields.credential) {
scope.selectedCredentials.machine = data.summary_fields.credential;
}
if (data.summary_fields.vault_credential) {
scope.selectedCredentials.vault = data.summary_fields.vault_credential;
}
// Extra credentials are not included in summary_fields so we have to go
// out and get them ourselves.
let defers = [],
typesArray = [],
credTypeOptions;
Rest.setUrl(data.related.extra_credentials);
defers.push(Rest.get()
.then((data) => {
scope.selectedCredentials.extra = data.data.results;
})
.catch(({data, status}) => {
ProcessErrors(null, data, status, null,
{
hdr: 'Error!',
msg: 'Failed to get extra credentials. ' +
'Get returned status: ' +
status
});
}));
defers.push(MultiCredentialService.getCredentialTypes()
.then(({credential_types, credentialTypeOptions}) => {
typesArray = Object.keys(credential_types).map(key => credential_types[key]);
credTypeOptions = credentialTypeOptions;
})
);
return $q.all(defers).then(() => {
let machineAndVaultCreds = [],
extraCreds = [];
if(scope.selectedCredentials.machine) {
machineAndVaultCreds.push(scope.selectedCredentials.machine);
}
if(scope.selectedCredentials.vault) {
machineAndVaultCreds.push(scope.selectedCredentials.vault);
}
machineAndVaultCreds.map(cred => ({
name: cred.name,
id: cred.id,
postType: cred.postType,
kind: typesArray
.filter(type => {
return cred.kind === type.kind || parseInt(cred.credential_type) === type.value;
})[0].name + ":"
}));
extraCreds = extraCreds.concat(scope.selectedCredentials.extra).map(cred => ({
name: cred.name,
id: cred.id,
postType: cred.postType,
kind: credTypeOptions
.filter(type => {
return parseInt(cred.credential_type) === type.value;
})[0].name + ":"
}));
scope.credentialsToPost = machineAndVaultCreds.concat(extraCreds);
scope.$emit('jobTemplateLoaded', master);
});
}
})
.error(function (data, status) {
ProcessErrors(scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to retrieve job template: ' + $stateParams.id + '. GET status: ' + status
});
});
};
};
}

View File

@ -85,7 +85,7 @@ function(NotificationsList, CompletedJobsList, i18n) {
ngChange: 'job_template_form.inventory_name.$validate()',
text: i18n._('Prompt on launch')
},
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate) || !canGetAllRelatedResources'
},
project: {
label: i18n._('Project'),
@ -100,13 +100,14 @@ function(NotificationsList, CompletedJobsList, i18n) {
dataTitle: i18n._('Project'),
dataPlacement: 'right',
dataContainer: "body",
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate) || !canGetAllRelatedResources',
awLookupWhen: 'canGetAllRelatedResources'
},
playbook: {
label: i18n._('Playbook'),
type:'select',
ngOptions: 'book for book in playbook_options track by book',
ngDisabled: "!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate) || disablePlaybookBecausePermissionDenied",
ngDisabled: "!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate) || !canGetAllRelatedResources",
id: 'playbook-select',
required: true,
column: 1,

View File

@ -14,7 +14,7 @@ export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile'
.then(kinds => {
scope.credentialKinds = kinds;
scope.credentialKind = "" + kinds.Machine;
scope.credentialKind = scope.selectedCredentials.machine && scope.selectedCredentials.machine.readOnly ? (scope.selectedCredentials.vault && scope.selectedCredentials.vault.readOnly ? "" + kinds.Network : "" + kinds.Vault) : "" + kinds.Machine;
scope.showModal = function() {
$('#multi-credential-modal').modal('show');
@ -50,6 +50,22 @@ export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile'
.then(({credential_types, credentialTypeOptions}) => {
scope.credential_types = credential_types;
scope.credentialTypeOptions = credentialTypeOptions;
scope.allCredentialTypeOptions = _.cloneDeep(credentialTypeOptions);
// We want to hide machine and vault dropdown options if a credential
// has already been selected for those types and the user interacting
// with the form doesn't have the ability to change them
for(let i=scope.credentialTypeOptions.length - 1; i >=0; i--) {
if((scope.selectedCredentials.machine &&
scope.selectedCredentials.machine.credential_type_id === scope.credentialTypeOptions[i].value &&
scope.selectedCredentials.machine.readOnly) ||
(scope.selectedCredentials.vault &&
scope.selectedCredentials.vault.credential_type_id === scope.credentialTypeOptions[i].value &&
scope.selectedCredentials.vault.readOnly)) {
scope.credentialTypeOptions.splice(i, 1);
}
}
scope.$emit('multiCredentialModalLinked');
});
});
@ -70,7 +86,7 @@ export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile'
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.credentialTypeOptions);
$scope.allCredentialTypeOptions);
};
let updateMachineCredentialList = function() {
@ -85,7 +101,7 @@ export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile'
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.credentialTypeOptions);
$scope.allCredentialTypeOptions);
};
let updateVaultCredentialList = function() {
@ -100,7 +116,7 @@ export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile'
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.credentialTypeOptions);
$scope.allCredentialTypeOptions);
};
let uncheckAllCredentials = function() {
@ -110,7 +126,7 @@ export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile'
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.credentialTypeOptions);
$scope.allCredentialTypeOptions);
};
let init = function() {

View File

@ -27,13 +27,13 @@
class="MultiCredential-tagContainer ng-scope"
ng-repeat="tag in credTags track by $index">
<div class="MultiCredential-deleteContainer"
ng-click="removeCredential(tag.id)">
ng-click="removeCredential(tag.id)"
ng-if="!tag.readOnly">
<i class="fa fa-times
MultiCredential-tagDelete">
</i>
</div>
<div class="MultiCredential-tag
MultiCredential-tag--deletable">
<div class="MultiCredential-tag" ng-class="tag.readOnly ? 'MultiCredential-tag--disabled' : 'MultiCredential-tag--deletable'">
<span
class="MultiCredential-name--label
ng-binding">

View File

@ -51,6 +51,10 @@
padding-left: 15px;
}
.MultiCredential-tag--disabled {
background-color: @default-icon;
}
.MultiCredential-tag--deletable {
margin-right: 0px;
border-top-left-radius: 0px;

View File

@ -23,12 +23,12 @@
ng-repeat="tag in credentialsToPost track by $index">
<div class="MultiCredential-deleteContainer"
ng-click="removeCredential(tag.id)"
ng-hide="fieldIsDisabled">
ng-hide="fieldIsDisabled || tag.readOnly">
<i class="fa fa-times MultiCredential-tagDelete">
</i>
</div>
<div class="MultiCredential-tag"
ng-class="{'MultiCredential-tag--deletable': !fieldIsDisabled}">
ng-class="{'MultiCredential-tag--deletable': !fieldIsDisabled && !tag.readOnly, 'MultiCredential-tag--disabled': tag.readOnly}">
<span class="MultiCredential-name--label
ng-binding">
{{ tag.kind }}

View File

@ -138,6 +138,7 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro
name: cred.name,
id: cred.id,
postType: cred.postType,
readOnly: cred.readOnly ? true : false,
kind: typeOpts
.filter(type => {
return parseInt(cred.credential_type) === type.value;
@ -178,6 +179,8 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro
let credDefers = [];
let job_template_obj = data;
let credentialGetPermissionDenied = false;
// get machine credential
if (data.related.credential) {
Rest.setUrl(data.related.credential);
@ -188,8 +191,10 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the machine credential, so use summary_fields */
credentialGetPermissionDenied = true;
selectedCredentials.machine = job_template_obj.summary_fields.credential;
selectedCredentials.machine.credential_type = job_template_obj.summary_fields.credential.credential_type_id;
selectedCredentials.machine.readOnly = true;
} else {
ProcessErrors(
null, data, status, null,
@ -212,8 +217,10 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the vault credential, so use summary_fields */
credentialGetPermissionDenied = true;
selectedCredentials.vault = job_template_obj.summary_fields.vault_credential;
selectedCredentials.vault.credential_type = job_template_obj.summary_fields.vault_credential.credential_type_id;
selectedCredentials.vault.readOnly = true;
} else {
ProcessErrors(
null, data, status, null,
@ -237,9 +244,11 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the extra credentials, so use summary_fields */
credentialGetPermissionDenied = true;
selectedCredentials.extra = job_template_obj.summary_fields.extra_credentials;
_.map(selectedCredentials.extra, (cred) => {
cred.credential_type = cred.credential_type_id;
cred.readOnly = true;
return cred;
});
} else {
@ -267,7 +276,7 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro
.updateCredentialTags(selectedCredentials, credTypeOptions);
return [selectedCredentials, credTypes, credTypeOptions,
credTags];
credTags, credentialGetPermissionDenied];
});
};

View File

@ -137,6 +137,61 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.
},
resolve: {
edit: {
jobTemplateData: ['$stateParams', 'TemplatesService', 'ProcessErrors',
function($stateParams, TemplatesService, ProcessErrors) {
return TemplatesService.getJobTemplate($stateParams.job_template_id)
.then(function(res) {
return res.data;
}).catch(function(response){
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get job template. GET returned status: ' +
response.status
});
});
}],
canGetProject: ['Rest', 'ProcessErrors', 'jobTemplateData',
function(Rest, ProcessErrors, jobTemplateData) {
Rest.setUrl(jobTemplateData.related.project);
return Rest.get()
.then(() => {
return true;
})
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the project, no problem. */
} else {
ProcessErrors(null, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get project. GET returned ' +
'status: ' + status
});
}
return false;
});
}],
canGetInventory: ['Rest', 'ProcessErrors', 'jobTemplateData',
function(Rest, ProcessErrors, jobTemplateData) {
Rest.setUrl(jobTemplateData.related.inventory);
return Rest.get()
.then(() => {
return true;
})
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the project, no problem. */
} else {
ProcessErrors(null, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get project. GET returned ' +
'status: ' + status
});
}
return false;
});
}],
InstanceGroupsData: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
function($stateParams, Rest, GetBasePath, ProcessErrors){
let path = `${GetBasePath('job_templates')}${$stateParams.job_template_id}/instance_groups/`;
@ -155,32 +210,32 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.
});
});
}],
availableLabels: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', 'TemplatesService',
function(Rest, $stateParams, GetBasePath, ProcessErrors, TemplatesService) {
return TemplatesService.getAllLabelOptions()
.then(function(labels){
return labels;
}).catch(function(response){
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get labels. GET returned status: ' +
response.status
});
availableLabels: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', 'TemplatesService',
function(Rest, $stateParams, GetBasePath, ProcessErrors, TemplatesService) {
return TemplatesService.getAllLabelOptions()
.then(function(labels){
return labels;
}).catch(function(response){
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get labels. GET returned status: ' +
response.status
});
}],
selectedLabels: ['Rest', '$stateParams', 'GetBasePath', 'TemplatesService', 'ProcessErrors',
function(Rest, $stateParams, GetBasePath, TemplatesService, ProcessErrors) {
return TemplatesService.getAllJobTemplateLabels($stateParams.job_template_id)
.then(function(labels){
return labels;
}).catch(function(response){
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get workflow job template labels. GET returned status: ' +
response.status
});
});
}],
selectedLabels: ['Rest', '$stateParams', 'GetBasePath', 'TemplatesService', 'ProcessErrors',
function(Rest, $stateParams, GetBasePath, TemplatesService, ProcessErrors) {
return TemplatesService.getAllJobTemplateLabels($stateParams.job_template_id)
.then(function(labels){
return labels;
}).catch(function(response){
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get workflow job template labels. GET returned status: ' +
response.status
});
}]
});
}]
}
}
});

View File

@ -1,4 +1,4 @@
'use strict'
'use strict';
describe('MultiCredentialService', () => {
let MultiCredentialService;
@ -79,7 +79,7 @@ describe('MultiCredentialService', () => {
expect(equal).toBe(true);
});
it('should return array of selected credentials (populated)', () => {
it('should return array of selected credentials (populated, not read only)', () => {
let creds = {
machine: {
credential_type: 1,
@ -120,18 +120,92 @@ describe('MultiCredentialService', () => {
name: 'ssh',
id: 3,
postType: 'machine',
readOnly: false,
kind: 'SSH:'
},
{
name: 'aws',
id: 4,
postType: 'extra',
readOnly: false,
kind: 'Amazon Web Services:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
readOnly: false,
kind: 'Google Compute Engine:'
}
];
let actual = MultiCredentialService
.updateCredentialTags(creds, typeOpts);
let equal = _.isEqual(expected.sort(), actual.sort());
expect(equal).toBe(true);
});
it('should return array of selected credentials (populated, read only)', () => {
let creds = {
machine: {
credential_type: 1,
id: 3,
name: 'ssh',
readOnly: true
},
extra: [
{
credential_type: 2,
id: 4,
name: 'aws',
readOnly: true
},
{
credential_type: 3,
id: 5,
name: 'gce',
readOnly: true
}
]
};
let typeOpts = [
{
name: 'SSH',
value: 1
},
{
name: 'Amazon Web Services',
value: 2
},
{
name: 'Google Compute Engine',
value: 3
}
];
let expected = [
{
name: 'ssh',
id: 3,
postType: 'machine',
readOnly: true,
kind: 'SSH:'
},
{
name: 'aws',
id: 4,
postType: 'extra',
readOnly: true,
kind: 'Amazon Web Services:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
readOnly: true,
kind: 'Google Compute Engine:'
}
];

View File

@ -42,4 +42,22 @@ describe('Service: SmartSearch', () => {
});
});
describe('fn splitFilterIntoTerms', () => {
it('should convert the filter term to a key and value with encode quotes and spaces', () => {
expect(SmartSearchService.splitFilterIntoTerms()).toEqual(null);
expect(SmartSearchService.splitFilterIntoTerms('foo')).toEqual(["foo"]);
expect(SmartSearchService.splitFilterIntoTerms('foo bar')).toEqual(["foo", "bar"]);
expect(SmartSearchService.splitFilterIntoTerms('name:foo bar')).toEqual(["name:foo", "bar"]);
expect(SmartSearchService.splitFilterIntoTerms('name:foo description:bar')).toEqual(["name:foo", "description:bar"]);
expect(SmartSearchService.splitFilterIntoTerms('name:"foo bar"')).toEqual(["name:%22foo%20bar%22"]);
expect(SmartSearchService.splitFilterIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:%22foo%20bar%22", "description:%22bar%20foo%22"]);
expect(SmartSearchService.splitFilterIntoTerms('name:"foo bar" a b c')).toEqual(["name:%22foo%20bar%22", 'a', 'b', 'c']);
expect(SmartSearchService.splitFilterIntoTerms('name:"1"')).toEqual(["name:%221%22"]);
expect(SmartSearchService.splitFilterIntoTerms('name:1')).toEqual(["name:1"]);
expect(SmartSearchService.splitFilterIntoTerms('name:"foo ba\'r" a b c')).toEqual(["name:%22foo%20ba%27r%22", 'a', 'b', 'c']);
expect(SmartSearchService.splitFilterIntoTerms('name:"foobar" other:"barbaz"')).toEqual(["name:%22foobar%22", "other:%22barbaz%22"]);
expect(SmartSearchService.splitFilterIntoTerms('name:"foobar" other:"bar baz"')).toEqual(["name:%22foobar%22", "other:%22bar%20baz%22"]);
});
});
});

View File

@ -1,5 +1,18 @@
apache-libcloud==2.0.0
azure==2.0.0rc6
# azure deps from https://github.com/ansible/ansible/blob/fe1153c0afa1ffd648147af97454e900560b3532/packaging/requirements/requirements-azure.txt
azure-mgmt-compute>=2.0.0,<3
azure-mgmt-network>=1.3.0,<2
azure-mgmt-storage>=1.2.0,<2
azure-mgmt-resource>=1.1.0,<2
azure-storage>=0.35.1,<0.36
azure-cli-core>=2.0.12,<3
msrestazure>=0.4.11,<0.5
azure-mgmt-dns>=1.0.1,<2
azure-mgmt-keyvault>=0.40.0,<0.41
azure-mgmt-batch>=4.1.0,<5
azure-mgmt-sql>=0.7.1,<0.8
azure-mgmt-web>=0.32.0,<0.33
azure-mgmt-containerservice>=1.0.0
backports.ssl-match-hostname==3.5.0.1
kombu==3.0.37
boto==2.46.1

View File

@ -4,30 +4,30 @@
#
# pip-compile --output-file requirements/requirements_ansible.txt requirements/requirements_ansible.in
#
adal==0.4.5 # via msrestazure
adal==0.4.7 # via azure-cli-core, msrestazure
amqp==1.4.9 # via kombu
anyjson==0.3.3 # via kombu
apache-libcloud==2.0.0
appdirs==1.4.3 # via os-client-config, python-ironicclient, setuptools
applicationinsights==0.11.0 # via azure-cli-core
argcomplete==1.9.2 # via azure-cli-core
asn1crypto==0.22.0 # via cryptography
azure-batch==1.0.0 # via azure
azure-common[autorest]==1.1.4 # via azure-batch, azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-keyvault, azure-mgmt-logic, azure-mgmt-network, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-storage, azure-servicebus, azure-servicemanagement-legacy, azure-storage
azure-mgmt-batch==1.0.0 # via azure-mgmt
azure-mgmt-compute==0.30.0rc6 # via azure-mgmt
azure-mgmt-keyvault==0.30.0rc6 # via azure-mgmt
azure-mgmt-logic==1.0.0 # via azure-mgmt
azure-mgmt-network==0.30.0rc6 # via azure-mgmt
azure-mgmt-nspkg==2.0.0 # via azure-batch, azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-keyvault, azure-mgmt-logic, azure-mgmt-network, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-scheduler, azure-mgmt-storage
azure-mgmt-redis==1.0.0 # via azure-mgmt
azure-mgmt-resource==0.30.0rc6 # via azure-mgmt
azure-mgmt-scheduler==1.0.0 # via azure-mgmt
azure-mgmt-storage==0.30.0rc6 # via azure-mgmt
azure-mgmt==0.30.0rc6 # via azure
azure-nspkg==2.0.0 # via azure-common, azure-mgmt-nspkg, azure-storage
azure-servicebus==0.20.3 # via azure
azure-servicemanagement-legacy==0.20.4 # via azure
azure-storage==0.33.0 # via azure
azure==2.0.0rc6
azure-cli-core==2.0.15
azure-cli-nspkg==3.0.1 # via azure-cli-core
azure-common==1.1.8 # via azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-keyvault, azu
azure-mgmt-batch==4.1.0
azure-mgmt-compute==2.1.0
azure-mgmt-containerservice==1.0.0
azure-mgmt-dns==1.0.1
azure-mgmt-keyvault==0.40.0
azure-mgmt-network==1.4.0
azure-mgmt-nspkg==2.0.0 # via azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-keyvault, azu
azure-mgmt-resource==1.1.0
azure-mgmt-sql==0.7.1
azure-mgmt-storage==1.2.1
azure-mgmt-web==0.32.0
azure-nspkg==2.0.0 # via azure-cli-nspkg, azure-common, azure-mgmt-nspkg, azure-storage
azure-storage==0.35.1
babel==2.3.4 # via osc-lib, oslo.i18n, python-cinderclient, python-glanceclient, python-neutronclient, python-novaclient, python-openstackclient
backports.ssl-match-hostname==3.5.0.1
boto3==1.4.4
@ -37,7 +37,8 @@ certifi==2017.4.17 # via msrest
cffi==1.10.0 # via cryptography
cliff==2.7.0 # via osc-lib, python-designateclient, python-neutronclient, python-openstackclient
cmd2==0.7.2 # via cliff
cryptography==1.9 # via adal, azure-storage, pyopenssl, secretstorage
colorama==0.3.9 # via azure-cli-core
cryptography==2.0.3 # via adal, azure-storage, paramiko, pyopenssl, secretstorage
debtcollector==1.15.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient
decorator==4.0.11 # via shade
deprecation==1.0.1 # via openstacksdk
@ -47,6 +48,7 @@ enum34==1.1.6 # via cryptography, msrest
funcsigs==1.0.2 # via debtcollector, oslo.utils
functools32==3.2.3.post2 # via jsonschema
futures==3.1.1 # via azure-storage, s3transfer, shade
humanfriendly==4.4.1 # via azure-cli-core
idna==2.5 # via cryptography
ipaddress==1.0.18 # via cryptography, shade
iso8601==0.1.11 # via keystoneauth1, oslo.utils, python-neutronclient, python-novaclient
@ -61,8 +63,8 @@ kombu==3.0.37
lxml==3.8.0 # via pyvmomi
monotonic==1.3 # via oslo.utils
msgpack-python==0.4.8 # via oslo.serialization
msrest==0.4.10 # via azure-common, msrestazure
msrestazure==0.4.9 # via azure-common
msrest==0.4.14 # via azure-cli-core, msrestazure
msrestazure==0.4.13
munch==2.1.1 # via shade
netaddr==0.7.19 # via oslo.config, oslo.utils, python-neutronclient
netifaces==0.10.6 # via oslo.utils, shade
@ -83,9 +85,10 @@ prettytable==0.7.2 # via cliff, python-cinderclient, python-glanceclient,
psphere==0.5.2
psutil==5.2.2
pycparser==2.17 # via cffi
pyjwt==1.5.0 # via adal
pygments==2.2.0 # via azure-cli-core
pyjwt==1.5.2 # via adal, azure-cli-core
pykerberos==1.1.14 # via requests-kerberos
pyopenssl==17.0.0 # via pyvmomi
pyopenssl==17.2.0 # via azure-cli-core, python-glanceclient, pyvmomi
pyparsing==2.2.0 # via cliff, cmd2, oslo.utils, packaging
python-cinderclient==2.2.0 # via python-openstackclient, shade
python-dateutil==2.6.0 # via adal, azure-storage, botocore
@ -114,6 +117,7 @@ simplejson==3.11.1 # via osc-lib, python-cinderclient, python-neutronclie
six==1.10.0 # via cliff, cmd2, cryptography, debtcollector, keystoneauth1, munch, ntlm-auth, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, packaging, pyopenssl, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-ironicclient, python-keystoneclient, python-memcached, python-neutronclient, python-novaclient, python-openstackclient, pyvmomi, pywinrm, setuptools, shade, stevedore, warlock
stevedore==1.23.0 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient
suds==0.4 # via psphere
tabulate==0.7.7 # via azure-cli-core
unicodecsv==0.14.1 # via cliff
warlock==1.2.0 # via python-glanceclient
wrapt==1.10.10 # via debtcollector, positional, python-glanceclient