mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 02:50:02 -03:30
Merge branch 'release_3.2.0' into devel
This commit is contained in:
commit
64415872a0
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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__)
|
||||
|
||||
@ -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):
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 --
|
||||
# ---------------------------
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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])) {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
.Paginate-total {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: -2px;
|
||||
margin: 0 0 -2px 10px;
|
||||
}
|
||||
|
||||
.Paginate-filterLabel{
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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){
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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){
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
$scope.mode = "add";
|
||||
$scope.parseType = 'yaml';
|
||||
$scope.credentialNotPresent = false;
|
||||
$scope.canGetAllRelatedResources = true;
|
||||
|
||||
md5Setup({
|
||||
scope: $scope,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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];
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
});
|
||||
}]
|
||||
});
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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:'
|
||||
}
|
||||
];
|
||||
|
||||
@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user