mirror of
https://github.com/ansible/awx.git
synced 2026-04-10 12:39:22 -02:30
Merge branch 'release_3.1.3' into devel
* release_3.1.3: (52 commits) ack fact scan messages making ldap user/group search fields into codemirror instances removing UI parsing for LDAP User and Group Search fields Allow exception view to accept all valid HTTP methods. Restore ability of parsing extra_vars string for provisioning callback. Fix up backup/restore role broken in f7a8e45809758322d9ee41c5305850dd70ed5faf Stop / start ansible-tower-service during restores value_to_python should encode lookup fields as ascii fix brace interpolation on standard out pane Adjust some hardcoded usages of 'awx' to use 'aw_user' and 'aw_group'. Pull Spanish updates from Zanata Temporarily grant awx user createdb role Stop giving ownership of backups to postgres don't display chunked lines' Add dropdown li truncation with ellipsis CTiT -> adhoc modules should allow the user to add new modules Remove task that was replacing the supervisor systemd tmp file Fix failing supervisorctl commands on RH-based distros Give ownership of the supervisor socket to awx Setting for external log emissions cert verification ...
This commit is contained in:
18
Makefile
18
Makefile
@@ -410,13 +410,13 @@ uwsgi: collectstatic
|
|||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/tower/bin/activate; \
|
. $(VENV_BASE)/tower/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
uwsgi -b 32768 --socket :8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/awxfifo --lazy-apps
|
uwsgi -b 32768 --socket 127.0.0.1:8050 --module=awx.wsgi:application --home=/venv/tower --chdir=/tower_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/awxfifo --lazy-apps
|
||||||
|
|
||||||
daphne:
|
daphne:
|
||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/tower/bin/activate; \
|
. $(VENV_BASE)/tower/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
daphne -b 0.0.0.0 -p 8051 awx.asgi:channel_layer
|
daphne -b 127.0.0.1 -p 8051 awx.asgi:channel_layer
|
||||||
|
|
||||||
runworker:
|
runworker:
|
||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
@@ -702,13 +702,19 @@ rpm-build:
|
|||||||
mkdir -p $@
|
mkdir -p $@
|
||||||
|
|
||||||
rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE) tar-build/$(SETUP_TAR_FILE)
|
rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE) tar-build/$(SETUP_TAR_FILE)
|
||||||
cp packaging/rpm/$(NAME).spec rpm-build/
|
ansible localhost \
|
||||||
|
-m template \
|
||||||
|
-a "src=packaging/rpm/$(NAME).spec.j2 dest=rpm-build/$(NAME).spec" \
|
||||||
|
-e tower_version=$(VERSION) \
|
||||||
|
-e tower_release=$(RELEASE)
|
||||||
|
|
||||||
cp packaging/rpm/tower.te rpm-build/
|
cp packaging/rpm/tower.te rpm-build/
|
||||||
cp packaging/rpm/tower.fc rpm-build/
|
cp packaging/rpm/tower.fc rpm-build/
|
||||||
cp packaging/rpm/$(NAME).sysconfig rpm-build/
|
cp packaging/rpm/$(NAME).sysconfig rpm-build/
|
||||||
cp packaging/remove_tower_source.py rpm-build/
|
cp packaging/remove_tower_source.py rpm-build/
|
||||||
cp packaging/bytecompile.sh rpm-build/
|
cp packaging/bytecompile.sh rpm-build/
|
||||||
cp tar-build/$(SETUP_TAR_FILE) rpm-build/
|
cp tar-build/$(SETUP_TAR_FILE) rpm-build/
|
||||||
|
|
||||||
if [ "$(OFFICIAL)" != "yes" ] ; then \
|
if [ "$(OFFICIAL)" != "yes" ] ; then \
|
||||||
(cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \
|
(cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \
|
||||||
(cd dist/ && mv $(NAME)-$(VERSION)-$(BUILD) $(NAME)-$(VERSION)) ; \
|
(cd dist/ && mv $(NAME)-$(VERSION)-$(BUILD) $(NAME)-$(VERSION)) ; \
|
||||||
@@ -764,8 +770,7 @@ requirements/requirements_ansible_local.txt:
|
|||||||
--dest=requirements/vendor 2>/dev/null | sed -n 's/^\s*Saved\s*//p' > $@
|
--dest=requirements/vendor 2>/dev/null | sed -n 's/^\s*Saved\s*//p' > $@
|
||||||
|
|
||||||
rpm-build/$(RPM_NVR).src.rpm: /etc/mock/$(MOCK_CFG).cfg
|
rpm-build/$(RPM_NVR).src.rpm: /etc/mock/$(MOCK_CFG).cfg
|
||||||
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --buildsrpm --spec rpm-build/$(NAME).spec --sources rpm-build \
|
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --buildsrpm --spec rpm-build/$(NAME).spec --sources rpm-build
|
||||||
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)
|
|
||||||
|
|
||||||
mock-srpm: rpmtar rpm-build/$(RPM_NVR).src.rpm
|
mock-srpm: rpmtar rpm-build/$(RPM_NVR).src.rpm
|
||||||
@echo "#############################################"
|
@echo "#############################################"
|
||||||
@@ -776,8 +781,7 @@ mock-srpm: rpmtar rpm-build/$(RPM_NVR).src.rpm
|
|||||||
brew-srpm: brewrpmtar mock-srpm
|
brew-srpm: brewrpmtar mock-srpm
|
||||||
|
|
||||||
rpm-build/$(RPM_NVR).$(RPM_ARCH).rpm: rpm-build/$(RPM_NVR).src.rpm
|
rpm-build/$(RPM_NVR).$(RPM_ARCH).rpm: rpm-build/$(RPM_NVR).src.rpm
|
||||||
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --rebuild rpm-build/$(RPM_NVR).src.rpm \
|
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --rebuild rpm-build/$(RPM_NVR).src.rpm
|
||||||
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)
|
|
||||||
|
|
||||||
mock-rpm: rpmtar rpm-build/$(RPM_NVR).$(RPM_ARCH).rpm
|
mock-rpm: rpmtar rpm-build/$(RPM_NVR).$(RPM_ARCH).rpm
|
||||||
@echo "#############################################"
|
@echo "#############################################"
|
||||||
|
|||||||
@@ -234,7 +234,8 @@ class ApiV1PingView(APIView):
|
|||||||
|
|
||||||
response['instances'] = []
|
response['instances'] = []
|
||||||
for instance in Instance.objects.all():
|
for instance in Instance.objects.all():
|
||||||
response['instances'].append(dict(node=instance.hostname, heartbeat=instance.modified, capacity=instance.capacity))
|
response['instances'].append(dict(node=instance.hostname, heartbeat=instance.modified,
|
||||||
|
capacity=instance.capacity, version=instance.version))
|
||||||
response['instances'].sort()
|
response['instances'].sort()
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ class CharField(CharField):
|
|||||||
return super(CharField, self).to_representation(value)
|
return super(CharField, self).to_representation(value)
|
||||||
|
|
||||||
|
|
||||||
|
class IntegerField(IntegerField):
|
||||||
|
|
||||||
|
def get_value(self, dictionary):
|
||||||
|
ret = super(IntegerField, self).get_value(dictionary)
|
||||||
|
# Handle UI corner case
|
||||||
|
if ret == '' and self.allow_null and not getattr(self, 'allow_blank', False):
|
||||||
|
return None
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class StringListField(ListField):
|
class StringListField(ListField):
|
||||||
|
|
||||||
child = CharField()
|
child = CharField()
|
||||||
|
|||||||
@@ -86,7 +86,19 @@ def executor(tmpdir_factory, request):
|
|||||||
- name: Hello Message
|
- name: Hello Message
|
||||||
debug:
|
debug:
|
||||||
msg: "Hello World!"
|
msg: "Hello World!"
|
||||||
'''} # noqa
|
'''}, # noqa
|
||||||
|
{'results_included.yml': '''
|
||||||
|
- name: Run module which generates results list
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
vars:
|
||||||
|
results: ['foo', 'bar']
|
||||||
|
tasks:
|
||||||
|
- name: Generate results list
|
||||||
|
debug:
|
||||||
|
var: results
|
||||||
|
'''}, # noqa
|
||||||
])
|
])
|
||||||
def test_callback_plugin_receives_events(executor, cache, event, playbook):
|
def test_callback_plugin_receives_events(executor, cache, event, playbook):
|
||||||
executor.run()
|
executor.run()
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ class BaseCallbackModule(CallbackBase):
|
|||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def capture_event_data(self, event, **event_data):
|
def capture_event_data(self, event, **event_data):
|
||||||
|
|
||||||
event_data.setdefault('uuid', str(uuid.uuid4()))
|
event_data.setdefault('uuid', str(uuid.uuid4()))
|
||||||
|
|
||||||
if event not in self.EVENTS_WITHOUT_TASK:
|
if event not in self.EVENTS_WITHOUT_TASK:
|
||||||
@@ -75,7 +74,7 @@ class BaseCallbackModule(CallbackBase):
|
|||||||
if event_data['res'].get('_ansible_no_log', False):
|
if event_data['res'].get('_ansible_no_log', False):
|
||||||
event_data['res'] = {'censored': CENSORED}
|
event_data['res'] = {'censored': CENSORED}
|
||||||
for i, item in enumerate(event_data['res'].get('results', [])):
|
for i, item in enumerate(event_data['res'].get('results', [])):
|
||||||
if event_data['res']['results'][i].get('_ansible_no_log', False):
|
if isinstance(item, dict) and item.get('_ansible_no_log', False):
|
||||||
event_data['res']['results'][i] = {'censored': CENSORED}
|
event_data['res']['results'][i] = {'censored': CENSORED}
|
||||||
|
|
||||||
with event_context.display_lock:
|
with event_context.display_lock:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -61,12 +61,15 @@ class FactBrokerWorker(ConsumerMixin):
|
|||||||
host_obj = Host.objects.get(name=hostname, inventory__id=inventory_id)
|
host_obj = Host.objects.get(name=hostname, inventory__id=inventory_id)
|
||||||
except Fact.DoesNotExist:
|
except Fact.DoesNotExist:
|
||||||
logger.warn('Failed to intake fact. Host does not exist <hostname, inventory_id> <%s, %s>' % (hostname, inventory_id))
|
logger.warn('Failed to intake fact. Host does not exist <hostname, inventory_id> <%s, %s>' % (hostname, inventory_id))
|
||||||
|
message.ack()
|
||||||
return
|
return
|
||||||
except Fact.MultipleObjectsReturned:
|
except Fact.MultipleObjectsReturned:
|
||||||
logger.warn('Database inconsistent. Multiple Hosts found for <hostname, inventory_id> <%s, %s>.' % (hostname, inventory_id))
|
logger.warn('Database inconsistent. Multiple Hosts found for <hostname, inventory_id> <%s, %s>.' % (hostname, inventory_id))
|
||||||
|
message.ack()
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Exception communicating with Fact Cache Database: %s" % str(e))
|
logger.error("Exception communicating with Fact Cache Database: %s" % str(e))
|
||||||
|
message.ack()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
(module_name, facts) = self.process_facts(facts_data)
|
(module_name, facts) = self.process_facts(facts_data)
|
||||||
@@ -84,6 +87,7 @@ class FactBrokerWorker(ConsumerMixin):
|
|||||||
logger.info('Created new fact <fact_id, module> <%s, %s>' % (fact_obj.id, module_name))
|
logger.info('Created new fact <fact_id, module> <%s, %s>' % (fact_obj.id, module_name))
|
||||||
analytics_logger.info('Received message with fact data', extra=dict(
|
analytics_logger.info('Received message with fact data', extra=dict(
|
||||||
module_name=module_name, facts_data=facts))
|
module_name=module_name, facts_data=facts))
|
||||||
|
message.ack()
|
||||||
return fact_obj
|
return fact_obj
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
19
awx/main/migrations/0037_v313_instance_version.py
Normal file
19
awx/main/migrations/0037_v313_instance_version.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0036_v311_insights'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='instance',
|
||||||
|
name='version',
|
||||||
|
field=models.CharField(max_length=24, blank=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -26,6 +26,7 @@ class Instance(models.Model):
|
|||||||
hostname = models.CharField(max_length=250, unique=True)
|
hostname = models.CharField(max_length=250, unique=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
version = models.CharField(max_length=24, blank=True)
|
||||||
capacity = models.PositiveIntegerField(
|
capacity = models.PositiveIntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
editable=False,
|
editable=False,
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin):
|
|||||||
playbook=self.playbook,
|
playbook=self.playbook,
|
||||||
credential=self.credential.name if self.credential else None,
|
credential=self.credential.name if self.credential else None,
|
||||||
limit=self.limit,
|
limit=self.limit,
|
||||||
extra_vars=self.extra_vars,
|
extra_vars=self.display_extra_vars(),
|
||||||
hosts=all_hosts))
|
hosts=all_hosts))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Python
|
# Python
|
||||||
import json
|
import json
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -28,9 +29,7 @@ class ResourceMixin(models.Model):
|
|||||||
'''
|
'''
|
||||||
Use instead of `MyModel.objects` when you want to only consider
|
Use instead of `MyModel.objects` when you want to only consider
|
||||||
resources that a user has specific permissions for. For example:
|
resources that a user has specific permissions for. For example:
|
||||||
|
|
||||||
MyModel.accessible_objects(user, 'read_role').filter(name__istartswith='bar');
|
MyModel.accessible_objects(user, 'read_role').filter(name__istartswith='bar');
|
||||||
|
|
||||||
NOTE: This should only be used for list type things. If you have a
|
NOTE: This should only be used for list type things. If you have a
|
||||||
specific resource you want to check permissions on, it is more
|
specific resource you want to check permissions on, it is more
|
||||||
performant to resolve the resource in question then call
|
performant to resolve the resource in question then call
|
||||||
@@ -159,12 +158,13 @@ class SurveyJobTemplateMixin(models.Model):
|
|||||||
errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']],
|
errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']],
|
||||||
survey_element['variable']))
|
survey_element['variable']))
|
||||||
return errors
|
return errors
|
||||||
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']):
|
if not data[survey_element['variable']] == '$encrypted$' and not survey_element['type'] == 'password':
|
||||||
errors.append("'%s' value %s is too small (length is %s must be at least %s)." %
|
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']):
|
||||||
(survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min']))
|
errors.append("'%s' value %s is too small (length is %s must be at least %s)." %
|
||||||
if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']):
|
(survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min']))
|
||||||
errors.append("'%s' value %s is too large (must be no more than %s)." %
|
if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']):
|
||||||
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
|
errors.append("'%s' value %s is too large (must be no more than %s)." %
|
||||||
|
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
|
||||||
elif survey_element['type'] == 'integer':
|
elif survey_element['type'] == 'integer':
|
||||||
if survey_element['variable'] in data:
|
if survey_element['variable'] in data:
|
||||||
if type(data[survey_element['variable']]) != int:
|
if type(data[survey_element['variable']]) != int:
|
||||||
@@ -196,16 +196,22 @@ class SurveyJobTemplateMixin(models.Model):
|
|||||||
if type(data[survey_element['variable']]) != list:
|
if type(data[survey_element['variable']]) != list:
|
||||||
errors.append("'%s' value is expected to be a list." % survey_element['variable'])
|
errors.append("'%s' value is expected to be a list." % survey_element['variable'])
|
||||||
else:
|
else:
|
||||||
|
choice_list = copy(survey_element['choices'])
|
||||||
|
if isinstance(choice_list, basestring):
|
||||||
|
choice_list = choice_list.split('\n')
|
||||||
for val in data[survey_element['variable']]:
|
for val in data[survey_element['variable']]:
|
||||||
if val not in survey_element['choices']:
|
if val not in choice_list:
|
||||||
errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'],
|
errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'],
|
||||||
survey_element['choices']))
|
choice_list))
|
||||||
elif survey_element['type'] == 'multiplechoice':
|
elif survey_element['type'] == 'multiplechoice':
|
||||||
|
choice_list = copy(survey_element['choices'])
|
||||||
|
if isinstance(choice_list, basestring):
|
||||||
|
choice_list = choice_list.split('\n')
|
||||||
if survey_element['variable'] in data:
|
if survey_element['variable'] in data:
|
||||||
if data[survey_element['variable']] not in survey_element['choices']:
|
if data[survey_element['variable']] not in choice_list:
|
||||||
errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']],
|
errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']],
|
||||||
survey_element['variable'],
|
survey_element['variable'],
|
||||||
survey_element['choices']))
|
choice_list))
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def survey_variable_validation(self, data):
|
def survey_variable_validation(self, data):
|
||||||
|
|||||||
@@ -204,6 +204,12 @@ class ProjectOptions(models.Model):
|
|||||||
break
|
break
|
||||||
return sorted(results, key=lambda x: smart_str(x).lower())
|
return sorted(results, key=lambda x: smart_str(x).lower())
|
||||||
|
|
||||||
|
def get_lock_file(self):
|
||||||
|
proj_path = self.get_project_path()
|
||||||
|
if not proj_path:
|
||||||
|
return None
|
||||||
|
return proj_path + '.lock'
|
||||||
|
|
||||||
|
|
||||||
class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
|
class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import traceback
|
|||||||
import urlparse
|
import urlparse
|
||||||
import uuid
|
import uuid
|
||||||
from distutils.version import LooseVersion as Version
|
from distutils.version import LooseVersion as Version
|
||||||
|
from datetime import timedelta
|
||||||
import yaml
|
import yaml
|
||||||
|
import fcntl
|
||||||
try:
|
try:
|
||||||
import psutil
|
import psutil
|
||||||
except:
|
except:
|
||||||
@@ -46,6 +48,7 @@ from django.core.cache import cache
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
|
from awx import __version__ as tower_application_version
|
||||||
from awx.main.constants import CLOUD_PROVIDERS
|
from awx.main.constants import CLOUD_PROVIDERS
|
||||||
from awx.main.models import * # noqa
|
from awx.main.models import * # noqa
|
||||||
from awx.main.models.unified_jobs import ACTIVE_STATES
|
from awx.main.models.unified_jobs import ACTIVE_STATES
|
||||||
@@ -54,7 +57,7 @@ from awx.main.task_engine import TaskEnhancer
|
|||||||
from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url,
|
from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url,
|
||||||
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot,
|
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot,
|
||||||
get_system_task_capacity, OutputEventFilter, parse_yaml_or_json)
|
get_system_task_capacity, OutputEventFilter, parse_yaml_or_json)
|
||||||
from awx.main.utils.reload import restart_local_services
|
from awx.main.utils.reload import restart_local_services, stop_local_services
|
||||||
from awx.main.utils.handlers import configure_external_logger
|
from awx.main.utils.handlers import configure_external_logger
|
||||||
from awx.main.consumers import emit_channel_notification
|
from awx.main.consumers import emit_channel_notification
|
||||||
|
|
||||||
@@ -175,13 +178,27 @@ def purge_old_stdout_files(self):
|
|||||||
@task(bind=True)
|
@task(bind=True)
|
||||||
def cluster_node_heartbeat(self):
|
def cluster_node_heartbeat(self):
|
||||||
logger.debug("Cluster node heartbeat task.")
|
logger.debug("Cluster node heartbeat task.")
|
||||||
|
nowtime = now()
|
||||||
inst = Instance.objects.filter(hostname=settings.CLUSTER_HOST_ID)
|
inst = Instance.objects.filter(hostname=settings.CLUSTER_HOST_ID)
|
||||||
if inst.exists():
|
if inst.exists():
|
||||||
inst = inst[0]
|
inst = inst[0]
|
||||||
inst.capacity = get_system_task_capacity()
|
inst.capacity = get_system_task_capacity()
|
||||||
|
inst.version = tower_application_version
|
||||||
inst.save()
|
inst.save()
|
||||||
return
|
else:
|
||||||
raise RuntimeError("Cluster Host Not Found: {}".format(settings.CLUSTER_HOST_ID))
|
raise RuntimeError("Cluster Host Not Found: {}".format(settings.CLUSTER_HOST_ID))
|
||||||
|
recent_inst = Instance.objects.filter(modified__gt=nowtime - timedelta(seconds=70)).exclude(hostname=settings.CLUSTER_HOST_ID)
|
||||||
|
# IFF any node has a greater version than we do, then we'll shutdown services
|
||||||
|
for other_inst in recent_inst:
|
||||||
|
if other_inst.version == "":
|
||||||
|
continue
|
||||||
|
if Version(other_inst.version) > Version(tower_application_version):
|
||||||
|
logger.error("Host {} reports Tower version {}, but this node {} is at {}, shutting down".format(other_inst.hostname,
|
||||||
|
other_inst.version,
|
||||||
|
inst.hostname,
|
||||||
|
inst.version))
|
||||||
|
stop_local_services(['uwsgi', 'celery', 'beat', 'callback', 'fact'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@task(bind=True, queue='default')
|
@task(bind=True, queue='default')
|
||||||
@@ -1365,7 +1382,45 @@ class RunProjectUpdate(BaseTask):
|
|||||||
logger.error('Encountered error updating project dependent inventory: {}'.format(e))
|
logger.error('Encountered error updating project dependent inventory: {}'.format(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
def release_lock(self, instance):
|
||||||
|
try:
|
||||||
|
fcntl.flock(self.lock_fd, fcntl.LOCK_UN)
|
||||||
|
except IOError as e:
|
||||||
|
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, instance.get_lock_file(), e.strerror))
|
||||||
|
os.close(self.lock_fd)
|
||||||
|
raise
|
||||||
|
|
||||||
|
os.close(self.lock_fd)
|
||||||
|
self.lock_fd = None
|
||||||
|
|
||||||
|
'''
|
||||||
|
Note: We don't support blocking=False
|
||||||
|
'''
|
||||||
|
def acquire_lock(self, instance, blocking=True):
|
||||||
|
lock_path = instance.get_lock_file()
|
||||||
|
if lock_path is None:
|
||||||
|
raise RuntimeError(u'Invalid lock file path')
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.lock_fd = os.open(lock_path, os.O_RDONLY | os.O_CREAT)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
fcntl.flock(self.lock_fd, fcntl.LOCK_EX)
|
||||||
|
except IOError as e:
|
||||||
|
os.close(self.lock_fd)
|
||||||
|
logger.error("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def pre_run_hook(self, instance, **kwargs):
|
||||||
|
if instance.launch_type == 'sync':
|
||||||
|
self.acquire_lock(instance)
|
||||||
|
|
||||||
def post_run_hook(self, instance, status, **kwargs):
|
def post_run_hook(self, instance, status, **kwargs):
|
||||||
|
if instance.launch_type == 'sync':
|
||||||
|
self.release_lock(instance)
|
||||||
p = instance.project
|
p = instance.project
|
||||||
dependent_inventory_sources = p.scm_inventory_sources.all()
|
dependent_inventory_sources = p.scm_inventory_sources.all()
|
||||||
if instance.job_type == 'check' and status not in ('failed', 'canceled',):
|
if instance.job_type == 'check' and status not in ('failed', 'canceled',):
|
||||||
|
|||||||
@@ -135,13 +135,15 @@ def create_survey_spec(variables=None, default_type='integer', required=True):
|
|||||||
argument specifying variable name(s)
|
argument specifying variable name(s)
|
||||||
'''
|
'''
|
||||||
if isinstance(variables, list):
|
if isinstance(variables, list):
|
||||||
name = "%s survey" % variables[0]
|
|
||||||
description = "A survey that starts with %s." % variables[0]
|
|
||||||
vars_list = variables
|
vars_list = variables
|
||||||
else:
|
else:
|
||||||
name = "%s survey" % variables
|
|
||||||
description = "A survey about %s." % variables
|
|
||||||
vars_list = [variables]
|
vars_list = [variables]
|
||||||
|
if isinstance(variables[0], basestring):
|
||||||
|
slogan = variables[0]
|
||||||
|
else:
|
||||||
|
slogan = variables[0].get('question_name', 'something')
|
||||||
|
name = "%s survey" % slogan
|
||||||
|
description = "A survey that asks about %s." % slogan
|
||||||
|
|
||||||
spec = []
|
spec = []
|
||||||
index = 0
|
index = 0
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from awx.main.models import Job, Instance
|
|||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_orphan_unified_job_creation(instance, inventory):
|
def test_orphan_unified_job_creation(instance, inventory):
|
||||||
@@ -22,3 +24,15 @@ def test_job_capacity_and_with_inactive_node():
|
|||||||
assert Instance.objects.total_capacity() == 100
|
assert Instance.objects.total_capacity() == 100
|
||||||
with override_settings(AWX_ACTIVE_NODE_TIME=0):
|
with override_settings(AWX_ACTIVE_NODE_TIME=0):
|
||||||
assert Instance.objects.total_capacity() < 100
|
assert Instance.objects.total_capacity() < 100
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_job_notification_data(inventory):
|
||||||
|
encrypted_str = "$encrypted$"
|
||||||
|
job = Job.objects.create(
|
||||||
|
job_template=None, inventory=inventory, name='hi world',
|
||||||
|
extra_vars=json.dumps({"SSN": "123-45-6789"}),
|
||||||
|
survey_passwords={"SSN": encrypted_str}
|
||||||
|
)
|
||||||
|
notification_data = job.notification_data(block=0)
|
||||||
|
assert json.loads(notification_data['extra_vars'])['SSN'] == encrypted_str
|
||||||
|
|||||||
@@ -91,6 +91,40 @@ def test_update_kwargs_survey_invalid_default(survey_spec_factory):
|
|||||||
assert json.loads(defaulted_extra_vars['extra_vars'])['var2'] == 2
|
assert json.loads(defaulted_extra_vars['extra_vars'])['var2'] == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("question_type,default,expect_use,expect_value", [
|
||||||
|
("multiplechoice", "", False, 'N/A'), # historical bug
|
||||||
|
("multiplechoice", "zeb", False, 'N/A'), # zeb not in choices
|
||||||
|
("multiplechoice", "coffee", True, 'coffee'),
|
||||||
|
("multiselect", None, False, 'N/A'), # NOTE: Behavior is arguable, value of [] may be prefered
|
||||||
|
("multiselect", "", False, 'N/A'),
|
||||||
|
("multiselect", ["zeb"], False, 'N/A'),
|
||||||
|
("multiselect", ["milk"], True, ["milk"]),
|
||||||
|
("multiselect", ["orange\nmilk"], False, 'N/A'), # historical bug
|
||||||
|
])
|
||||||
|
def test_optional_survey_question_defaults(
|
||||||
|
survey_spec_factory, question_type, default, expect_use, expect_value):
|
||||||
|
spec = survey_spec_factory([
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"default": default,
|
||||||
|
"choices": "orange\nmilk\nchocolate\ncoffee",
|
||||||
|
"variable": "c",
|
||||||
|
"type": question_type
|
||||||
|
},
|
||||||
|
])
|
||||||
|
jt = JobTemplate(name="test-jt", survey_spec=spec, survey_enabled=True)
|
||||||
|
defaulted_extra_vars = jt._update_unified_job_kwargs()
|
||||||
|
element = spec['spec'][0]
|
||||||
|
if expect_use:
|
||||||
|
assert jt._survey_element_validation(element, {element['variable']: element['default']}) == []
|
||||||
|
else:
|
||||||
|
assert jt._survey_element_validation(element, {element['variable']: element['default']})
|
||||||
|
if expect_use:
|
||||||
|
assert json.loads(defaulted_extra_vars['extra_vars'])['c'] == expect_value
|
||||||
|
else:
|
||||||
|
assert 'c' not in defaulted_extra_vars['extra_vars']
|
||||||
|
|
||||||
|
|
||||||
class TestWorkflowSurveys:
|
class TestWorkflowSurveys:
|
||||||
def test_update_kwargs_survey_defaults(self, survey_spec_factory):
|
def test_update_kwargs_survey_defaults(self, survey_spec_factory):
|
||||||
"Assure that the survey default over-rides a JT variable"
|
"Assure that the survey default over-rides a JT variable"
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import ConfigParser
|
|||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
import os
|
||||||
|
import fcntl
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
|
||||||
import mock
|
import mock
|
||||||
|
import yaml
|
||||||
|
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
Credential,
|
Credential,
|
||||||
@@ -1067,3 +1069,62 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
|
|
||||||
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
|
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
|
||||||
|
def test_os_open_oserror():
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
os.open('this_file_does_not_exist', os.O_RDONLY)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fcntl_ioerror():
|
||||||
|
with pytest.raises(IOError):
|
||||||
|
fcntl.flock(99999, fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('os.open')
|
||||||
|
@mock.patch('logging.getLogger')
|
||||||
|
def test_aquire_lock_open_fail_logged(logging_getLogger, os_open):
|
||||||
|
err = OSError()
|
||||||
|
err.errno = 3
|
||||||
|
err.strerror = 'dummy message'
|
||||||
|
|
||||||
|
instance = mock.Mock()
|
||||||
|
instance.get_lock_file.return_value = 'this_file_does_not_exist'
|
||||||
|
|
||||||
|
os_open.side_effect = err
|
||||||
|
|
||||||
|
logger = mock.Mock()
|
||||||
|
logging_getLogger.return_value = logger
|
||||||
|
|
||||||
|
ProjectUpdate = tasks.RunProjectUpdate()
|
||||||
|
|
||||||
|
with pytest.raises(OSError, errno=3, strerror='dummy message'):
|
||||||
|
ProjectUpdate.acquire_lock(instance)
|
||||||
|
assert logger.err.called_with("I/O error({0}) while trying to open lock file [{1}]: {2}".format(3, 'this_file_does_not_exist', 'dummy message'))
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('os.open')
|
||||||
|
@mock.patch('os.close')
|
||||||
|
@mock.patch('logging.getLogger')
|
||||||
|
@mock.patch('fcntl.flock')
|
||||||
|
def test_aquire_lock_acquisition_fail_logged(fcntl_flock, logging_getLogger, os_close, os_open):
|
||||||
|
err = IOError()
|
||||||
|
err.errno = 3
|
||||||
|
err.strerror = 'dummy message'
|
||||||
|
|
||||||
|
instance = mock.Mock()
|
||||||
|
instance.get_lock_file.return_value = 'this_file_does_not_exist'
|
||||||
|
|
||||||
|
os_open.return_value = 3
|
||||||
|
|
||||||
|
logger = mock.Mock()
|
||||||
|
logging_getLogger.return_value = logger
|
||||||
|
|
||||||
|
fcntl_flock.side_effect = err
|
||||||
|
|
||||||
|
ProjectUpdate = tasks.RunProjectUpdate()
|
||||||
|
|
||||||
|
with pytest.raises(IOError, errno=3, strerror='dummy message'):
|
||||||
|
ProjectUpdate.acquire_lock(instance)
|
||||||
|
os_close.assert_called_with(3)
|
||||||
|
assert logger.err.called_with("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(3, 'this_file_does_not_exist', 'dummy message'))
|
||||||
|
|||||||
37
awx/main/tests/unit/test_views.py
Normal file
37
awx/main/tests/unit/test_views.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import pytest
|
||||||
|
import mock
|
||||||
|
|
||||||
|
# Django REST Framework
|
||||||
|
from rest_framework import exceptions
|
||||||
|
|
||||||
|
# AWX
|
||||||
|
from awx.main.views import ApiErrorView
|
||||||
|
|
||||||
|
|
||||||
|
HTTP_METHOD_NAMES = [
|
||||||
|
'get',
|
||||||
|
'post',
|
||||||
|
'put',
|
||||||
|
'patch',
|
||||||
|
'delete',
|
||||||
|
'head',
|
||||||
|
'options',
|
||||||
|
'trace',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def api_view_obj_fixture():
|
||||||
|
return ApiErrorView()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method_name', HTTP_METHOD_NAMES)
|
||||||
|
def test_exception_view_allow_http_methods(method_name):
|
||||||
|
assert hasattr(ApiErrorView, method_name)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method_name', HTTP_METHOD_NAMES)
|
||||||
|
def test_exception_view_raises_exception(api_view_obj_fixture, method_name):
|
||||||
|
request_mock = mock.MagicMock()
|
||||||
|
with pytest.raises(exceptions.APIException):
|
||||||
|
getattr(api_view_obj_fixture, method_name)(request_mock)
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# Copyright (c) 2017 Ansible, Inc.
|
# Copyright (c) 2017 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
import pytest
|
||||||
|
|
||||||
from awx.conf.models import Setting
|
from awx.conf.models import Setting
|
||||||
from awx.main.utils import common
|
from awx.main.utils import common
|
||||||
@@ -52,3 +53,13 @@ def test_encrypt_field_with_ask():
|
|||||||
def test_encrypt_field_with_empty_value():
|
def test_encrypt_field_with_empty_value():
|
||||||
encrypted = common.encrypt_field(Setting(value=None), 'value')
|
encrypted = common.encrypt_field(Setting(value=None), 'value')
|
||||||
assert encrypted is None
|
assert encrypted is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('input_, output', [
|
||||||
|
({"foo": "bar"}, {"foo": "bar"}),
|
||||||
|
('{"foo": "bar"}', {"foo": "bar"}),
|
||||||
|
('---\nfoo: bar', {"foo": "bar"}),
|
||||||
|
(4399, {}),
|
||||||
|
])
|
||||||
|
def test_parse_yaml_or_json(input_, output):
|
||||||
|
assert common.parse_yaml_or_json(input_) == output
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ from awx.main.utils import reload
|
|||||||
|
|
||||||
|
|
||||||
def test_produce_supervisor_command(mocker):
|
def test_produce_supervisor_command(mocker):
|
||||||
with mocker.patch.object(reload.subprocess, 'Popen'):
|
communicate_mock = mocker.MagicMock(return_value=('Everything is fine', ''))
|
||||||
reload._supervisor_service_restart(['beat', 'callback', 'fact'])
|
mock_process = mocker.MagicMock()
|
||||||
|
mock_process.communicate = communicate_mock
|
||||||
|
Popen_mock = mocker.MagicMock(return_value=mock_process)
|
||||||
|
with mocker.patch.object(reload.subprocess, 'Popen', Popen_mock):
|
||||||
|
reload._supervisor_service_command(['beat', 'callback', 'fact'], "restart")
|
||||||
reload.subprocess.Popen.assert_called_once_with(
|
reload.subprocess.Popen.assert_called_once_with(
|
||||||
['supervisorctl', 'restart', 'tower-processes:receiver', 'tower-processes:factcacher'])
|
['supervisorctl', 'restart', 'tower-processes:receiver', 'tower-processes:factcacher'],
|
||||||
|
stderr=-1, stdin=-1, stdout=-1)
|
||||||
|
|
||||||
|
|
||||||
def test_routing_of_service_restarts_works(mocker):
|
def test_routing_of_service_restarts_works(mocker):
|
||||||
@@ -14,13 +19,13 @@ def test_routing_of_service_restarts_works(mocker):
|
|||||||
This tests that the parent restart method will call the appropriate
|
This tests that the parent restart method will call the appropriate
|
||||||
service restart methods, depending on which services are given in args
|
service restart methods, depending on which services are given in args
|
||||||
'''
|
'''
|
||||||
with mocker.patch.object(reload, '_uwsgi_reload'),\
|
with mocker.patch.object(reload, '_uwsgi_fifo_command'),\
|
||||||
mocker.patch.object(reload, '_reset_celery_thread_pool'),\
|
mocker.patch.object(reload, '_reset_celery_thread_pool'),\
|
||||||
mocker.patch.object(reload, '_supervisor_service_restart'):
|
mocker.patch.object(reload, '_supervisor_service_command'):
|
||||||
reload.restart_local_services(['uwsgi', 'celery', 'flower', 'daphne'])
|
reload.restart_local_services(['uwsgi', 'celery', 'flower', 'daphne'])
|
||||||
reload._uwsgi_reload.assert_called_once_with()
|
reload._uwsgi_fifo_command.assert_called_once_with(uwsgi_command="c")
|
||||||
reload._reset_celery_thread_pool.assert_called_once_with()
|
reload._reset_celery_thread_pool.assert_called_once_with()
|
||||||
reload._supervisor_service_restart.assert_called_once_with(['flower', 'daphne'])
|
reload._supervisor_service_command.assert_called_once_with(['flower', 'daphne'], command="restart")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -28,11 +33,11 @@ def test_routing_of_service_restarts_diables(mocker):
|
|||||||
'''
|
'''
|
||||||
Test that methods are not called if not in the args
|
Test that methods are not called if not in the args
|
||||||
'''
|
'''
|
||||||
with mocker.patch.object(reload, '_uwsgi_reload'),\
|
with mocker.patch.object(reload, '_uwsgi_fifo_command'),\
|
||||||
mocker.patch.object(reload, '_reset_celery_thread_pool'),\
|
mocker.patch.object(reload, '_reset_celery_thread_pool'),\
|
||||||
mocker.patch.object(reload, '_supervisor_service_restart'):
|
mocker.patch.object(reload, '_supervisor_service_command'):
|
||||||
reload.restart_local_services(['flower'])
|
reload.restart_local_services(['flower'])
|
||||||
reload._uwsgi_reload.assert_not_called()
|
reload._uwsgi_fifo_command.assert_not_called()
|
||||||
reload._reset_celery_thread_pool.assert_not_called()
|
reload._reset_celery_thread_pool.assert_not_called()
|
||||||
reload._supervisor_service_restart.assert_called_once_with(['flower'])
|
reload._supervisor_service_command.assert_called_once_with(['flower'], command="restart")
|
||||||
|
|
||||||
|
|||||||
@@ -854,8 +854,8 @@ class OutputEventFilter(object):
|
|||||||
|
|
||||||
def callback_filter_out_ansible_extra_vars(extra_vars):
|
def callback_filter_out_ansible_extra_vars(extra_vars):
|
||||||
extra_vars_redacted = {}
|
extra_vars_redacted = {}
|
||||||
|
extra_vars = parse_yaml_or_json(extra_vars)
|
||||||
for key, value in extra_vars.iteritems():
|
for key, value in extra_vars.iteritems():
|
||||||
if not key.startswith('ansible_'):
|
if not key.startswith('ansible_'):
|
||||||
extra_vars_redacted[key] = value
|
extra_vars_redacted[key] = value
|
||||||
return extra_vars_redacted
|
return extra_vars_redacted
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ PARAM_NAMES = {
|
|||||||
'indv_facts': 'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
|
'indv_facts': 'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
|
||||||
'enabled_flag': 'LOG_AGGREGATOR_ENABLED',
|
'enabled_flag': 'LOG_AGGREGATOR_ENABLED',
|
||||||
'tcp_timeout': 'LOG_AGGREGATOR_TCP_TIMEOUT',
|
'tcp_timeout': 'LOG_AGGREGATOR_TCP_TIMEOUT',
|
||||||
|
'verify_cert': 'LOG_AGGREGATOR_VERIFY_CERT'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -242,8 +243,11 @@ class BaseHTTPSHandler(BaseHandler):
|
|||||||
payload_str = json.dumps(payload_input)
|
payload_str = json.dumps(payload_input)
|
||||||
else:
|
else:
|
||||||
payload_str = payload_input
|
payload_str = payload_input
|
||||||
return dict(data=payload_str, background_callback=unused_callback,
|
kwargs = dict(data=payload_str, background_callback=unused_callback,
|
||||||
timeout=self.tcp_timeout)
|
timeout=self.tcp_timeout)
|
||||||
|
if self.verify_cert is False:
|
||||||
|
kwargs['verify'] = False
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
def _send(self, payload):
|
def _send(self, payload):
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ from celery import current_app
|
|||||||
logger = logging.getLogger('awx.main.utils.reload')
|
logger = logging.getLogger('awx.main.utils.reload')
|
||||||
|
|
||||||
|
|
||||||
def _uwsgi_reload():
|
def _uwsgi_fifo_command(uwsgi_command):
|
||||||
# http://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands
|
# http://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands
|
||||||
logger.warn('Initiating uWSGI chain reload of server')
|
logger.warn('Initiating uWSGI chain reload of server')
|
||||||
TRIGGER_CHAIN_RELOAD = 'c'
|
TRIGGER_COMMAND = uwsgi_command
|
||||||
with open(settings.UWSGI_FIFO_LOCATION, 'w') as awxfifo:
|
with open(settings.UWSGI_FIFO_LOCATION, 'w') as awxfifo:
|
||||||
awxfifo.write(TRIGGER_CHAIN_RELOAD)
|
awxfifo.write(TRIGGER_COMMAND)
|
||||||
|
|
||||||
|
|
||||||
def _reset_celery_thread_pool():
|
def _reset_celery_thread_pool():
|
||||||
@@ -29,7 +29,7 @@ def _reset_celery_thread_pool():
|
|||||||
destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False)
|
destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False)
|
||||||
|
|
||||||
|
|
||||||
def _supervisor_service_restart(service_internal_names):
|
def _supervisor_service_command(service_internal_names, command):
|
||||||
'''
|
'''
|
||||||
Service internal name options:
|
Service internal name options:
|
||||||
- beat - celery - callback - channels - uwsgi - daphne
|
- beat - celery - callback - channels - uwsgi - daphne
|
||||||
@@ -46,23 +46,35 @@ def _supervisor_service_restart(service_internal_names):
|
|||||||
for n in service_internal_names:
|
for n in service_internal_names:
|
||||||
if n in name_translation_dict:
|
if n in name_translation_dict:
|
||||||
programs.append('{}:{}'.format(group_name, name_translation_dict[n]))
|
programs.append('{}:{}'.format(group_name, name_translation_dict[n]))
|
||||||
args.extend(['restart'])
|
args.extend([command])
|
||||||
args.extend(programs)
|
args.extend(programs)
|
||||||
logger.debug('Issuing command to restart services, args={}'.format(args))
|
logger.debug('Issuing command to restart services, args={}'.format(args))
|
||||||
subprocess.Popen(args)
|
supervisor_process = subprocess.Popen(args, stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
restart_stdout, restart_err = supervisor_process.communicate()
|
||||||
|
restart_code = supervisor_process.returncode
|
||||||
|
if restart_code or restart_err:
|
||||||
|
logger.error('supervisorctl restart errored with exit code `{}`, stdout:\n{}stderr:\n{}'.format(
|
||||||
|
restart_code, restart_stdout.strip(), restart_err.strip()))
|
||||||
|
else:
|
||||||
|
logger.info('supervisorctl restart finished, stdout:\n{}'.format(restart_stdout.strip()))
|
||||||
|
|
||||||
|
|
||||||
def restart_local_services(service_internal_names):
|
def restart_local_services(service_internal_names):
|
||||||
logger.warn('Restarting services {} on this node in response to user action'.format(service_internal_names))
|
logger.warn('Restarting services {} on this node in response to user action'.format(service_internal_names))
|
||||||
if 'uwsgi' in service_internal_names:
|
if 'uwsgi' in service_internal_names:
|
||||||
_uwsgi_reload()
|
_uwsgi_fifo_command(uwsgi_command='c')
|
||||||
service_internal_names.remove('uwsgi')
|
service_internal_names.remove('uwsgi')
|
||||||
restart_celery = False
|
restart_celery = False
|
||||||
if 'celery' in service_internal_names:
|
if 'celery' in service_internal_names:
|
||||||
restart_celery = True
|
restart_celery = True
|
||||||
service_internal_names.remove('celery')
|
service_internal_names.remove('celery')
|
||||||
_supervisor_service_restart(service_internal_names)
|
_supervisor_service_command(service_internal_names, command='restart')
|
||||||
if restart_celery:
|
if restart_celery:
|
||||||
# Celery restarted last because this probably includes current process
|
# Celery restarted last because this probably includes current process
|
||||||
_reset_celery_thread_pool()
|
_reset_celery_thread_pool()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_local_services(service_internal_names):
|
||||||
|
logger.warn('Stopping services {} on this node in response to user action'.format(service_internal_names))
|
||||||
|
_supervisor_service_command(service_internal_names, command='stop')
|
||||||
|
|||||||
@@ -10,20 +10,32 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from rest_framework import exceptions, permissions, views
|
from rest_framework import exceptions, permissions, views
|
||||||
|
|
||||||
|
|
||||||
|
def _force_raising_exception(view_obj, request, format=None):
|
||||||
|
raise view_obj.exception_class()
|
||||||
|
|
||||||
|
|
||||||
class ApiErrorView(views.APIView):
|
class ApiErrorView(views.APIView):
|
||||||
|
|
||||||
authentication_classes = []
|
authentication_classes = []
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
metadata_class = None
|
metadata_class = None
|
||||||
allowed_methods = ('GET', 'HEAD')
|
|
||||||
exception_class = exceptions.APIException
|
exception_class = exceptions.APIException
|
||||||
view_name = _('API Error')
|
view_name = _('API Error')
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return self.view_name
|
return self.view_name
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
raise self.exception_class()
|
response = super(ApiErrorView, self).finalize_response(request, response, *args, **kwargs)
|
||||||
|
try:
|
||||||
|
del response['Allow']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
for method_name in ApiErrorView.http_method_names:
|
||||||
|
setattr(ApiErrorView, method_name, _force_raising_exception)
|
||||||
|
|
||||||
|
|
||||||
def handle_error(request, status=404, **kwargs):
|
def handle_error(request, status=404, **kwargs):
|
||||||
|
|||||||
@@ -128,8 +128,8 @@
|
|||||||
url_password: "{{scm_password}}"
|
url_password: "{{scm_password}}"
|
||||||
force_basic_auth: yes
|
force_basic_auth: yes
|
||||||
force: yes
|
force: yes
|
||||||
when: scm_type == 'insights' and item.name != None
|
when: scm_type == 'insights' and item.name != None and item.name != ""
|
||||||
with_items: "{{insights_output.json}}"
|
with_items: "{{ insights_output.json|default([]) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Fetch Insights Playbook
|
- name: Fetch Insights Playbook
|
||||||
@@ -140,8 +140,8 @@
|
|||||||
url_password: "{{scm_password}}"
|
url_password: "{{scm_password}}"
|
||||||
force_basic_auth: yes
|
force_basic_auth: yes
|
||||||
force: yes
|
force: yes
|
||||||
when: scm_type == 'insights' and item.name == None
|
when: scm_type == 'insights' and (item.name == None or item.name == "")
|
||||||
with_items: "{{insights_output.json}}"
|
with_items: "{{ insights_output.json|default([]) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: detect requirements.yml
|
- name: detect requirements.yml
|
||||||
|
|||||||
@@ -876,8 +876,10 @@ INSIGHTS_URL_BASE = "https://access.redhat.com"
|
|||||||
|
|
||||||
TOWER_SETTINGS_MANIFEST = {}
|
TOWER_SETTINGS_MANIFEST = {}
|
||||||
|
|
||||||
|
# Settings related to external logger configuration
|
||||||
LOG_AGGREGATOR_ENABLED = False
|
LOG_AGGREGATOR_ENABLED = False
|
||||||
LOG_AGGREGATOR_TCP_TIMEOUT = 5
|
LOG_AGGREGATOR_TCP_TIMEOUT = 5
|
||||||
|
LOG_AGGREGATOR_VERIFY_CERT = True
|
||||||
|
|
||||||
# The number of retry attempts for websocket session establishment
|
# The number of retry attempts for websocket session establishment
|
||||||
# If you're encountering issues establishing websockets in clustered Tower,
|
# If you're encountering issues establishing websockets in clustered Tower,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from django.dispatch import receiver
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.conf import settings as django_settings
|
from django.conf import settings as django_settings
|
||||||
from django.core.signals import setting_changed
|
from django.core.signals import setting_changed
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
|
|
||||||
# django-auth-ldap
|
# django-auth-ldap
|
||||||
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
||||||
@@ -93,8 +92,8 @@ class LDAPBackend(BaseLDAPBackend):
|
|||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return super(LDAPBackend, self).authenticate(username, password)
|
return super(LDAPBackend, self).authenticate(username, password)
|
||||||
except ImproperlyConfigured:
|
except Exception:
|
||||||
logger.error("Unable to authenticate, LDAP is improperly configured")
|
logger.exception("Encountered an error authenticating to LDAP")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_user(self, user_id):
|
def get_user(self, user_id):
|
||||||
|
|||||||
@@ -353,7 +353,6 @@
|
|||||||
|
|
||||||
.select2-results__option{
|
.select2-results__option{
|
||||||
color: @field-label !important;
|
color: @field-label !important;
|
||||||
height: 30px!important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--default .select2-results__option--highlighted[aria-selected]{
|
.select2-container--default .select2-results__option--highlighted[aria-selected]{
|
||||||
@@ -692,3 +691,15 @@ input[type='radio']:checked:before {
|
|||||||
.alert-info--noTextTransform {
|
.alert-info--noTextTransform {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select2-results__option {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option:hover {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<div class="Form-tab"
|
<div class="Form-tab"
|
||||||
ng-click="selectTab('workflow_templates')"
|
ng-click="selectTab('workflow_templates')"
|
||||||
ng-class="{'is-selected': tab.workflow_templates}"
|
ng-class="{'is-selected': tab.workflow_templates}"
|
||||||
|
ng-hide="resolve.workflowTemplatesDataset.status === 402"
|
||||||
translate>
|
translate>
|
||||||
Workflow Templates
|
Workflow Templates
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
<div id="AddPermissions-jobTemplates" class="AddPermissions-list" ng-show="tab.job_templates">
|
<div id="AddPermissions-jobTemplates" class="AddPermissions-list" ng-show="tab.job_templates">
|
||||||
<rbac-multiselect-list view="JobTemplates" all-selected="allSelected" dataset="resolve.jobTemplatesDataset"></rbac-multiselect-list>
|
<rbac-multiselect-list view="JobTemplates" all-selected="allSelected" dataset="resolve.jobTemplatesDataset"></rbac-multiselect-list>
|
||||||
</div>
|
</div>
|
||||||
<div id="AddPermissions-workflowTemplates" class="AddPermissions-list" ng-show="tab.workflow_templates">
|
<div ng-if="resolve.workflowTemplatesDataset.status !== 402" id="AddPermissions-workflowTemplates" class="AddPermissions-list" ng-show="tab.workflow_templates">
|
||||||
<rbac-multiselect-list view="WorkflowTemplates" all-selected="allSelected" dataset="resolve.workflowTemplatesDataset"></rbac-multiselect-list>
|
<rbac-multiselect-list view="WorkflowTemplates" all-selected="allSelected" dataset="resolve.workflowTemplatesDataset"></rbac-multiselect-list>
|
||||||
</div>
|
</div>
|
||||||
<div id="AddPermissions-projects" class="AddPermissions-list" ng-show="tab.projects">
|
<div id="AddPermissions-projects" class="AddPermissions-list" ng-show="tab.projects">
|
||||||
|
|||||||
@@ -26,8 +26,18 @@ export default ['i18n', function(i18n) {
|
|||||||
},
|
},
|
||||||
AUTH_LDAP_USER_SEARCH: {
|
AUTH_LDAP_USER_SEARCH: {
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
rows: 6,
|
||||||
|
codeMirror: true,
|
||||||
|
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
|
||||||
reset: 'AUTH_LDAP_USER_SEARCH'
|
reset: 'AUTH_LDAP_USER_SEARCH'
|
||||||
},
|
},
|
||||||
|
AUTH_LDAP_GROUP_SEARCH: {
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 6,
|
||||||
|
codeMirror: true,
|
||||||
|
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
|
||||||
|
reset: 'AUTH_LDAP_GROUP_SEARCH'
|
||||||
|
},
|
||||||
AUTH_LDAP_USER_DN_TEMPLATE: {
|
AUTH_LDAP_USER_DN_TEMPLATE: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
reset: 'AUTH_LDAP_USER_DN_TEMPLATE'
|
reset: 'AUTH_LDAP_USER_DN_TEMPLATE'
|
||||||
@@ -39,10 +49,6 @@ export default ['i18n', function(i18n) {
|
|||||||
codeMirror: true,
|
codeMirror: true,
|
||||||
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
|
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
|
||||||
},
|
},
|
||||||
AUTH_LDAP_GROUP_SEARCH: {
|
|
||||||
type: 'textarea',
|
|
||||||
reset: 'AUTH_LDAP_GROUP_SEARCH'
|
|
||||||
},
|
|
||||||
AUTH_LDAP_GROUP_TYPE: {
|
AUTH_LDAP_GROUP_TYPE: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
reset: 'AUTH_LDAP_GROUP_TYPE',
|
reset: 'AUTH_LDAP_GROUP_TYPE',
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export default [
|
|||||||
if(key === "AD_HOC_COMMANDS"){
|
if(key === "AD_HOC_COMMANDS"){
|
||||||
$scope[key] = data[key].toString();
|
$scope[key] = data[key].toString();
|
||||||
}
|
}
|
||||||
else{
|
else if(key === "AUTH_LDAP_USER_SEARCH" || key === "AUTH_LDAP_GROUP_SEARCH"){
|
||||||
|
$scope[key] = JSON.stringify(data[key]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
$scope[key] = ConfigurationUtils.arrayToList(data[key], key);
|
$scope[key] = ConfigurationUtils.arrayToList(data[key], key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,27 +356,38 @@ export default [
|
|||||||
clearApiErrors();
|
clearApiErrors();
|
||||||
_.each(keys, function(key) {
|
_.each(keys, function(key) {
|
||||||
if($scope.configDataResolve[key].type === 'choice' || multiselectDropdowns.indexOf(key) !== -1) {
|
if($scope.configDataResolve[key].type === 'choice' || multiselectDropdowns.indexOf(key) !== -1) {
|
||||||
|
|
||||||
|
// Handle AD_HOC_COMMANDS
|
||||||
|
if(multiselectDropdowns.indexOf(key) !== -1) {
|
||||||
|
let newModules = $("#configuration_jobs_template_AD_HOC_COMMANDS > option")
|
||||||
|
.filter("[data-select2-tag=true]")
|
||||||
|
.map((i, val) => ({value: $(val).text()}));
|
||||||
|
newModules.each(function(i, val) {
|
||||||
|
$scope[key].push(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
payload[key] = ConfigurationUtils.listToArray(_.map($scope[key], 'value').join(','));
|
||||||
|
}
|
||||||
|
|
||||||
//Parse dropdowns and dropdowns labeled as lists
|
//Parse dropdowns and dropdowns labeled as lists
|
||||||
if($scope[key] === null) {
|
else if($scope[key] === null) {
|
||||||
payload[key] = null;
|
payload[key] = null;
|
||||||
} else if($scope[key][0] && $scope[key][0].value !== undefined) {
|
} else if($scope[key][0] && $scope[key][0].value !== undefined) {
|
||||||
if(multiselectDropdowns.indexOf(key) !== -1) {
|
payload[key] = _.map($scope[key], 'value').join(',');
|
||||||
// Handle AD_HOC_COMMANDS
|
|
||||||
payload[key] = ConfigurationUtils.listToArray(_.map($scope[key], 'value').join(','));
|
|
||||||
} else {
|
|
||||||
payload[key] = _.map($scope[key], 'value').join(',');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if(multiselectDropdowns.indexOf(key) !== -1) {
|
payload[key] = $scope[key].value;
|
||||||
// Default AD_HOC_COMMANDS to an empty list
|
|
||||||
payload[key] = $scope[key].value || [];
|
|
||||||
} else {
|
|
||||||
payload[key] = $scope[key].value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if($scope.configDataResolve[key].type === 'list' && $scope[key] !== null) {
|
} else if($scope.configDataResolve[key].type === 'list' && $scope[key] !== null) {
|
||||||
// Parse lists
|
|
||||||
payload[key] = ConfigurationUtils.listToArray($scope[key], key);
|
if(key === "AUTH_LDAP_USER_SEARCH" || key === "AUTH_LDAP_GROUP_SEARCH"){
|
||||||
|
payload[key] = $scope[key] === "{}" ? [] : ToJSON($scope.parseType,
|
||||||
|
$scope[key]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Parse lists
|
||||||
|
payload[key] = ConfigurationUtils.listToArray($scope[key], key);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if($scope.configDataResolve[key].type === 'nested object') {
|
else if($scope.configDataResolve[key].type === 'nested object') {
|
||||||
if($scope[key] === '') {
|
if($scope[key] === '') {
|
||||||
|
|||||||
@@ -79,7 +79,18 @@ export default [
|
|||||||
function populateAdhocCommand(flag){
|
function populateAdhocCommand(flag){
|
||||||
$scope.$parent.AD_HOC_COMMANDS = $scope.$parent.AD_HOC_COMMANDS.toString();
|
$scope.$parent.AD_HOC_COMMANDS = $scope.$parent.AD_HOC_COMMANDS.toString();
|
||||||
var ad_hoc_commands = $scope.$parent.AD_HOC_COMMANDS.split(',');
|
var ad_hoc_commands = $scope.$parent.AD_HOC_COMMANDS.split(',');
|
||||||
$scope.$parent.AD_HOC_COMMANDS = _.map(ad_hoc_commands, (item) => _.find($scope.$parent.AD_HOC_COMMANDS_options, { value: item }));
|
$scope.$parent.AD_HOC_COMMANDS = _.map(ad_hoc_commands, function(item){
|
||||||
|
let option = _.find($scope.$parent.AD_HOC_COMMANDS_options, { value: item });
|
||||||
|
if(!option){
|
||||||
|
option = {
|
||||||
|
name: item,
|
||||||
|
value: item,
|
||||||
|
label: item
|
||||||
|
};
|
||||||
|
$scope.$parent.AD_HOC_COMMANDS_options.push(option);
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
|
||||||
if(flag !== undefined){
|
if(flag !== undefined){
|
||||||
dropdownRendered = flag;
|
dropdownRendered = flag;
|
||||||
@@ -90,6 +101,7 @@ export default [
|
|||||||
CreateSelect2({
|
CreateSelect2({
|
||||||
element: '#configuration_jobs_template_AD_HOC_COMMANDS',
|
element: '#configuration_jobs_template_AD_HOC_COMMANDS',
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
addNew: true,
|
||||||
placeholder: i18n._('Select commands')
|
placeholder: i18n._('Select commands')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,9 @@
|
|||||||
|
|
||||||
$scope.item = group;
|
$scope.item = group;
|
||||||
$scope.submitMode = $stateParams.groups === undefined ? 'move' : 'copy';
|
$scope.submitMode = $stateParams.groups === undefined ? 'move' : 'copy';
|
||||||
$scope.toggle_row = function(id){
|
|
||||||
// toggle off anything else currently selected
|
$scope.updateSelected = function(selectedGroup) {
|
||||||
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
$scope.selected = angular.copy(selectedGroup);
|
||||||
// yoink the currently selected thing
|
|
||||||
$scope.selected = _.find($scope.groups, (item) => {return item.id === id;});
|
|
||||||
};
|
};
|
||||||
$scope.formCancel = function(){
|
$scope.formCancel = function(){
|
||||||
$state.go('^');
|
$state.go('^');
|
||||||
|
|||||||
@@ -11,11 +11,9 @@
|
|||||||
|
|
||||||
$scope.item = host;
|
$scope.item = host;
|
||||||
$scope.submitMode = 'copy';
|
$scope.submitMode = 'copy';
|
||||||
$scope.toggle_row = function(id){
|
|
||||||
// toggle off anything else currently selected
|
$scope.updateSelected = function(selectedGroup) {
|
||||||
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
$scope.selected = angular.copy(selectedGroup);
|
||||||
// yoink the currently selected thing
|
|
||||||
$scope.selected = _.find($scope.groups, (item) => {return item.id === id;});
|
|
||||||
};
|
};
|
||||||
$scope.formCancel = function(){
|
$scope.formCancel = function(){
|
||||||
$state.go('^');
|
$state.go('^');
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
['$scope', 'CopyMoveGroupList', 'Dataset',
|
||||||
|
function($scope, list, Dataset){
|
||||||
|
init();
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
$scope.list = list;
|
||||||
|
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||||
|
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||||
|
|
||||||
|
$scope.$watch('groups', function(){
|
||||||
|
if($scope.selectedGroup){
|
||||||
|
$scope.groups.forEach(function(row, i) {
|
||||||
|
if(row.id === $scope.selectedGroup.id) {
|
||||||
|
$scope.groups[i].checked = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.groups[i].checked = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.toggle_row = function(id) {
|
||||||
|
// toggle off anything else currently selected
|
||||||
|
_.forEach($scope.groups, (item) => {
|
||||||
|
if(item.id === id) {
|
||||||
|
item.checked = 1;
|
||||||
|
$scope.selectedGroup = item;
|
||||||
|
$scope.updateSelected(item);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item.checked = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}];
|
||||||
@@ -8,6 +8,7 @@ import { N_ } from '../../../i18n';
|
|||||||
|
|
||||||
import CopyMoveGroupsController from './copy-move-groups.controller';
|
import CopyMoveGroupsController from './copy-move-groups.controller';
|
||||||
import CopyMoveHostsController from './copy-move-hosts.controller';
|
import CopyMoveHostsController from './copy-move-hosts.controller';
|
||||||
|
import CopyMoveListController from './copy-move-list.controller';
|
||||||
|
|
||||||
var copyMoveGroupRoute = {
|
var copyMoveGroupRoute = {
|
||||||
name: 'inventoryManage.copyMoveGroup',
|
name: 'inventoryManage.copyMoveGroup',
|
||||||
@@ -53,7 +54,8 @@ var copyMoveGroupRoute = {
|
|||||||
input_type: 'radio'
|
input_type: 'radio'
|
||||||
});
|
});
|
||||||
return html;
|
return html;
|
||||||
}
|
},
|
||||||
|
controller: CopyMoveListController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -90,7 +92,8 @@ var copyMoveHostRoute = {
|
|||||||
input_type: 'radio'
|
input_type: 'radio'
|
||||||
});
|
});
|
||||||
return html;
|
return html;
|
||||||
}
|
},
|
||||||
|
controller: CopyMoveListController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -258,11 +258,7 @@ export default ['$log', 'moment', function($log, moment){
|
|||||||
.split("\r\n");
|
.split("\r\n");
|
||||||
|
|
||||||
if (lineNums.length > lines.length) {
|
if (lineNums.length > lines.length) {
|
||||||
let padBy = lineNums.length - lines.length;
|
lineNums = lineNums.slice(0, lines.length);
|
||||||
|
|
||||||
for (let i = 0; i <= padBy; i++) {
|
|
||||||
lines.push("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = this.distributeColors(lines);
|
lines = this.distributeColors(lines);
|
||||||
@@ -293,7 +289,7 @@ export default ['$log', 'moment', function($log, moment){
|
|||||||
return `
|
return `
|
||||||
<div class="JobResultsStdOut-aLineOfStdOut${this.getLineClasses(event, lineArr[1], lineArr[0])}">
|
<div class="JobResultsStdOut-aLineOfStdOut${this.getLineClasses(event, lineArr[1], lineArr[0])}">
|
||||||
<div class="JobResultsStdOut-lineNumberColumn">${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}</div>
|
<div class="JobResultsStdOut-lineNumberColumn">${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}</div>
|
||||||
<div class="JobResultsStdOut-stdoutColumn${this.getAnchorTags(event)}>${this.prettify(lineArr[1])} ${this.getStartTimeBadge(event, lineArr[1])}</div>
|
<div class="JobResultsStdOut-stdoutColumn${this.getAnchorTags(event)}><span ng-non-bindable>${this.prettify(lineArr[1])}</span>${this.getStartTimeBadge(event, lineArr[1])}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2017 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
/* jshint unused: vars */
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ 'Empty',
|
||||||
|
function(Empty) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
require: 'ngModel',
|
||||||
|
link: function(scope, elem, attr, ctrl) {
|
||||||
|
ctrl.$parsers.unshift(function(viewValue) {
|
||||||
|
var max = (attr.awPasswordMax) ? scope.$eval(attr.awPasswordMax) : Infinity;
|
||||||
|
if (!Empty(max) && !Empty(viewValue) && viewValue.length > max && viewValue !== '$encrypted$') {
|
||||||
|
ctrl.$setValidity('awPasswordMax', false);
|
||||||
|
return viewValue;
|
||||||
|
} else {
|
||||||
|
ctrl.$setValidity('awPasswordMax', true);
|
||||||
|
return viewValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2017 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ 'Empty',
|
||||||
|
function(Empty) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
require: 'ngModel',
|
||||||
|
link: function(scope, elem, attr, ctrl) {
|
||||||
|
ctrl.$parsers.unshift(function(viewValue) {
|
||||||
|
var min = (attr.awPasswordMin) ? scope.$eval(attr.awPasswordMin) : -Infinity;
|
||||||
|
if (!Empty(min) && !Empty(viewValue) && viewValue.length < min && viewValue !== '$encrypted$') {
|
||||||
|
ctrl.$setValidity('awPasswordMin', false);
|
||||||
|
return viewValue;
|
||||||
|
} else {
|
||||||
|
ctrl.$setValidity('awPasswordMin', true);
|
||||||
|
return viewValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -42,6 +42,13 @@ export default
|
|||||||
else if(question.type === "multiplechoice") {
|
else if(question.type === "multiplechoice") {
|
||||||
question.model = question.default ? angular.copy(question.default) : "";
|
question.model = question.default ? angular.copy(question.default) : "";
|
||||||
question.choices = question.choices.split(/\n/);
|
question.choices = question.choices.split(/\n/);
|
||||||
|
|
||||||
|
// Add a default empty string option to the choices array. If this choice is
|
||||||
|
// selected then the extra var will not be sent when we POST to the launch
|
||||||
|
// endpoint
|
||||||
|
if(!question.required) {
|
||||||
|
question.choices.unshift('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(question.type === "float"){
|
else if(question.type === "float"){
|
||||||
question.model = (!Empty(question.default)) ? angular.copy(question.default) : (!Empty(question.default_float)) ? angular.copy(question.default_float) : "";
|
question.model = (!Empty(question.default)) ? angular.copy(question.default) : (!Empty(question.default_float)) ? angular.copy(question.default_float) : "";
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ export default
|
|||||||
// for optional select lists, if they are left blank make sure we submit
|
// for optional select lists, if they are left blank make sure we submit
|
||||||
// a value that the API will consider "empty"
|
// a value that the API will consider "empty"
|
||||||
//
|
//
|
||||||
case "multiplechoice":
|
// ISSUE: I don't think this logic ever actually fires
|
||||||
job_launch_data.extra_vars[fld] = "";
|
// When I tested this, we don't pass this extra var back
|
||||||
break;
|
// through the api when the mutliselect is optional and empty
|
||||||
case "multiselect":
|
case "multiselect":
|
||||||
job_launch_data.extra_vars[fld] = [];
|
job_launch_data.extra_vars[fld] = [];
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -186,10 +186,11 @@
|
|||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" class="btn btn-default show_input_button JobSubmission-passwordButton" id="survey_question_{{$index}}_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="togglePassword('#survey_question_' + $index)" data-original-title="" title="">Show</button>
|
<button type="button" class="btn btn-default show_input_button JobSubmission-passwordButton" id="survey_question_{{$index}}_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="togglePassword('#survey_question_' + $index)" data-original-title="" title="">Show</button>
|
||||||
</span>
|
</span>
|
||||||
<input id="survey_question_{{$index}}" type="password" ng-model="question.model" name="survey_question_{{$index}}" ng-required="question.required" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control Form-textInput" autocomplete="false">
|
<input id="survey_question_{{$index}}" ng-if="!question.default" type="password" ng-model="question.model" name="survey_question_{{$index}}" ng-required="question.required" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control Form-textInput" autocomplete="false">
|
||||||
|
<input id="survey_question_{{$index}}" ng-if="question.default" type="password" ng-model="question.model" name="survey_question_{{$index}}" ng-required="question.required" aw-password-min="question.minlength" aw-password-max="question.maxlength" class="form-control Form-textInput" autocomplete="false">
|
||||||
</div>
|
</div>
|
||||||
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">Please enter an answer.</div>
|
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">Please enter an answer.</div>
|
||||||
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength">Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.</div>
|
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.awPasswordMin || forms.survey.survey_question_{{$index}}.$error.awPasswordMax || forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength">Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="question.type === 'integer'">
|
<div ng-if="question.type === 'integer'">
|
||||||
<input type="number" id="survey_question_{{$index}}" ng-model="question.model" class="form-control Form-textInput" name="survey_question_{{$index}}" ng-required="question.required" integer aw-min="question.minValue" aw-max="question.maxValue"/>
|
<input type="number" id="survey_question_{{$index}}" ng-model="question.model" class="form-control Form-textInput" name="survey_question_{{$index}}" ng-required="question.required" integer aw-min="question.minValue" aw-max="question.maxValue"/>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import PromptForPasswords from './job-submission-factories/prompt-for-passwords.
|
|||||||
import submitJob from './job-submission.directive';
|
import submitJob from './job-submission.directive';
|
||||||
import credentialList from './lists/credential/job-sub-cred-list.directive';
|
import credentialList from './lists/credential/job-sub-cred-list.directive';
|
||||||
import inventoryList from './lists/inventory/job-sub-inv-list.directive';
|
import inventoryList from './lists/inventory/job-sub-inv-list.directive';
|
||||||
|
import awPasswordMin from './job-submission-directives/aw-password-min.directive';
|
||||||
|
import awPasswordMax from './job-submission-directives/aw-password-max.directive';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('jobSubmission', [])
|
angular.module('jobSubmission', [])
|
||||||
@@ -30,4 +32,6 @@ export default
|
|||||||
.factory('PromptForPasswords', PromptForPasswords)
|
.factory('PromptForPasswords', PromptForPasswords)
|
||||||
.directive('submitJob', submitJob)
|
.directive('submitJob', submitJob)
|
||||||
.directive('jobSubCredList', credentialList)
|
.directive('jobSubCredList', credentialList)
|
||||||
.directive('jobSubInvList', inventoryList);
|
.directive('jobSubInvList', inventoryList)
|
||||||
|
.directive('awPasswordMin', awPasswordMin)
|
||||||
|
.directive('awPasswordMax', awPasswordMax);
|
||||||
|
|||||||
@@ -1387,7 +1387,7 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return false;
|
return (!value || value === "") ? false : true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,10 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
|
|||||||
.catch(function(response) {
|
.catch(function(response) {
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
|
|
||||||
|
if (/^\/api\/v[0-9]+\/workflow_job_templates\/$/.test(endpoint) && response.status === 402) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
this.error(response.data, response.status);
|
this.error(response.data, response.status);
|
||||||
|
|
||||||
throw response;
|
throw response;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1351
awx/ui/po/es.po
1351
awx/ui/po/es.po
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,7 @@ describe('parseStdoutService', () => {
|
|||||||
end_line: 11,
|
end_line: 11,
|
||||||
stdout: "a\r\nb\r\nc..."
|
stdout: "a\r\nb\r\nc..."
|
||||||
};
|
};
|
||||||
let expectedReturn = [[8, "a"],[9, "b"], [10,"c..."], [11, ""]];
|
let expectedReturn = [[8, "a"],[9, "b"], [10,"c..."]];
|
||||||
|
|
||||||
let returnedEvent = parseStdoutService.getLineArr(mockEvent);
|
let returnedEvent = parseStdoutService.getLineArr(mockEvent);
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ describe('parseStdoutService', () => {
|
|||||||
var expectedString = `
|
var expectedString = `
|
||||||
<div class="JobResultsStdOut-aLineOfStdOutline_classes">
|
<div class="JobResultsStdOut-aLineOfStdOutline_classes">
|
||||||
<div class="JobResultsStdOut-lineNumberColumn">collapse_icon_dom13</div>
|
<div class="JobResultsStdOut-lineNumberColumn">collapse_icon_dom13</div>
|
||||||
<div class="JobResultsStdOut-stdoutColumn" anchor_tag_dom>prettified_line </div>
|
<div class="JobResultsStdOut-stdoutColumn" anchor_tag_dom><span ng-non-bindable>prettified_line</span></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
expect(returnedString).toBe(expectedString);
|
expect(returnedString).toBe(expectedString);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user