Compare commits

..

18 Commits

Author SHA1 Message Date
Alex Corey
bea8b1a754 Adds an Instance Group component that renders IGs as a PF Label 2022-10-26 13:36:14 -04:00
Alex Corey
95dba81a9d Merge pull request #13110 from AlexSCorey/fixBrokenHostEventModalTest
Fixes a broken Host event modal unit test
2022-10-26 11:04:03 -04:00
Alex Corey
4b308d313a Fixes a broken Host event modal unit test 2022-10-25 15:12:47 -04:00
Alex Corey
d80db763bc Merge pull request #12942 from Tioborto/fix/ui-host-vent-modal
fix: UI host event modal when stdout is an array
2022-10-25 10:12:02 -04:00
Seth Foster
ddd09461fb Merge pull request #13093 from fosterseth/nginx_static_location
Make nginx conf consistent with settings.STATIC_ROOT
2022-10-24 11:02:17 -04:00
Seth Foster
6d192927ae Make nginx conf consistent with settings.STATIC_ROOT 2022-10-21 23:10:06 -04:00
Sarah Akus
e655e1dbc2 Merge pull request #13068 from AlexSCorey/11555-CalculateElapsedTimeonJob
Allows job output to calculate elapsed time
2022-10-21 11:51:47 -04:00
Sarabraj Singh
e41f20320a removed hostname check when editing hostname on existing host (#13057) 2022-10-21 10:28:40 -03:00
Alan Rominger
192f45bbd0 Make canceling view non-atomic to fix 500 errors with job bursts (#13072)
* Make canceling view non-atomic to fix 500 errors with job bursts

* Update test calls for cancel method changes
2022-10-20 15:02:54 -04:00
Alan Rominger
e013d25e2d Merge pull request #13073 from AlanCoding/max_conn_deadlock
Fix dispatcher connection deadlock w scheduler and cleanup
2022-10-19 14:06:12 -04:00
Alan Rominger
cba780a8f8 Fix dispatcher connection deadlock w scheduler and cleanup 2022-10-19 12:12:15 -04:00
Alan Rominger
3fc67dc76c Merge pull request #13081 from AlanCoding/raw_string
Use raw string to satisfy linter rules
2022-10-19 12:08:02 -04:00
Alan Rominger
6f85aef5fe Use raw string to satisfy linter rules 2022-10-19 11:07:43 -04:00
Alan Rominger
4d9b8400da Merge pull request #12887 from AlanCoding/more_rules
[tech debt] Add new flake8 rules to do some meaningful corrections
2022-10-18 20:27:00 -04:00
Alan Rominger
d3eb2c1975 Add new flak8 rules to do some meaningful corrections 2022-09-27 20:36:42 -04:00
Alexandre Bortoluzzi
5551874352 fix: HostEventModel test 2022-09-27 10:23:14 +02:00
Alexandre Bortoluzzi
8e2003a36b chore:add comment in test 2022-09-22 16:23:23 +02:00
Alexandre Bortoluzzi
4f52343cd9 fix: host modal stdout when stdout is an array 2022-09-22 16:03:11 +02:00
40 changed files with 246 additions and 268 deletions

View File

@@ -13,7 +13,7 @@ from django.contrib.auth import views as auth_views
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.core.exceptions import FieldDoesNotExist
from django.db import connection
from django.db import connection, transaction
from django.db.models.fields.related import OneToOneRel
from django.http import QueryDict
from django.shortcuts import get_object_or_404
@@ -64,6 +64,7 @@ __all__ = [
'ParentMixin',
'SubListAttachDetachAPIView',
'CopyAPIView',
'GenericCancelView',
'BaseUsersList',
]
@@ -985,6 +986,23 @@ class CopyAPIView(GenericAPIView):
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class GenericCancelView(RetrieveAPIView):
# In subclass set model, serializer_class
obj_permission_type = 'cancel'
@transaction.non_atomic_requests
def dispatch(self, *args, **kwargs):
return super(GenericCancelView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class BaseUsersList(SubListCreateAttachDetachAPIView):
def post(self, request, *args, **kwargs):
ret = super(BaseUsersList, self).post(request, *args, **kwargs)

View File

@@ -24,7 +24,6 @@ __all__ = [
'InventoryInventorySourcesUpdatePermission',
'UserPermission',
'IsSystemAdminOrAuditor',
'InstanceGroupTowerPermission',
'WorkflowApprovalPermission',
]

View File

@@ -4934,7 +4934,7 @@ class InstanceSerializer(BaseSerializer):
MaxLengthValidator(limit_value=250),
validators.UniqueValidator(queryset=Instance.objects.all()),
RegexValidator(
regex='^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$',
regex=r'^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$',
flags=re.IGNORECASE,
inverse_match=True,
message="hostname cannot be localhost or 127.0.0.1",

View File

@@ -69,6 +69,7 @@ from awx.api.generics import (
APIView,
BaseUsersList,
CopyAPIView,
GenericCancelView,
GenericAPIView,
ListAPIView,
ListCreateAPIView,
@@ -976,20 +977,11 @@ class SystemJobEventsList(SubListAPIView):
return job.get_event_queryset()
class ProjectUpdateCancel(RetrieveAPIView):
class ProjectUpdateCancel(GenericCancelView):
model = models.ProjectUpdate
obj_permission_type = 'cancel'
serializer_class = serializers.ProjectUpdateCancelSerializer
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class ProjectUpdateNotificationsList(SubListAPIView):
@@ -2262,20 +2254,11 @@ class InventoryUpdateCredentialsList(SubListAPIView):
relationship = 'credentials'
class InventoryUpdateCancel(RetrieveAPIView):
class InventoryUpdateCancel(GenericCancelView):
model = models.InventoryUpdate
obj_permission_type = 'cancel'
serializer_class = serializers.InventoryUpdateCancelSerializer
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class InventoryUpdateNotificationsList(SubListAPIView):
@@ -3050,8 +3033,7 @@ class WorkflowJobNodeChildrenBaseList(SubListAPIView):
search_fields = ('unified_job_template__name', 'unified_job_template__description')
#
# Limit the set of WorkflowJobeNodes to the related nodes of specified by
#'relationship'
# Limit the set of WorkflowJobNodes to the related nodes of specified by self.relationship
#
def get_queryset(self):
parent = self.get_parent_object()
@@ -3353,20 +3335,15 @@ class WorkflowJobWorkflowNodesList(SubListAPIView):
return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id')
class WorkflowJobCancel(RetrieveAPIView):
class WorkflowJobCancel(GenericCancelView):
model = models.WorkflowJob
obj_permission_type = 'cancel'
serializer_class = serializers.WorkflowJobCancelSerializer
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
ScheduleWorkflowManager().schedule()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
r = super().post(request, *args, **kwargs)
ScheduleWorkflowManager().schedule()
return r
class WorkflowJobNotificationsList(SubListAPIView):
@@ -3522,20 +3499,11 @@ class JobActivityStreamList(SubListAPIView):
search_fields = ('changes',)
class JobCancel(RetrieveAPIView):
class JobCancel(GenericCancelView):
model = models.Job
obj_permission_type = 'cancel'
serializer_class = serializers.JobCancelSerializer
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class JobRelaunch(RetrieveAPIView):
@@ -4006,20 +3974,11 @@ class AdHocCommandDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
serializer_class = serializers.AdHocCommandDetailSerializer
class AdHocCommandCancel(RetrieveAPIView):
class AdHocCommandCancel(GenericCancelView):
model = models.AdHocCommand
obj_permission_type = 'cancel'
serializer_class = serializers.AdHocCommandCancelSerializer
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class AdHocCommandRelaunch(GenericAPIView):
@@ -4154,20 +4113,11 @@ class SystemJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
serializer_class = serializers.SystemJobSerializer
class SystemJobCancel(RetrieveAPIView):
class SystemJobCancel(GenericCancelView):
model = models.SystemJob
obj_permission_type = 'cancel'
serializer_class = serializers.SystemJobCancelSerializer
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class SystemJobNotificationsList(SubListAPIView):

View File

@@ -993,9 +993,6 @@ class HostAccess(BaseAccess):
if data and 'name' in data:
self.check_license(add_host_name=data['name'])
# Check the per-org limit
self.check_org_host_limit({'inventory': obj.inventory}, add_host_name=data['name'])
# Checks for admin or change permission on inventory, controls whether
# the user can edit variable data.
return obj and self.user in obj.inventory.admin_role

View File

@@ -3,6 +3,7 @@ import uuid
import json
from django.conf import settings
from django.db import connection
import redis
from awx.main.dispatch import get_local_queuename
@@ -49,7 +50,10 @@ class Control(object):
reply_queue = Control.generate_reply_queue_name()
self.result = None
with pg_bus_conn(new_connection=True) as conn:
if not connection.get_autocommit():
raise RuntimeError('Control-with-reply messages can only be done in autocommit mode')
with pg_bus_conn() as conn:
conn.listen(reply_queue)
send_data = {'control': command, 'reply_to': reply_queue}
if extra_data:

View File

@@ -387,6 +387,8 @@ class AutoscalePool(WorkerPool):
reaper.reap_job(j, 'failed')
except Exception:
logger.exception('failed to reap job UUID {}'.format(w.current_task['uuid']))
else:
logger.warning(f'Worker was told to quit but has not, pid={w.pid}')
orphaned.extend(w.orphaned_tasks)
self.workers.remove(w)
elif w.idle and len(self.workers) > self.min_workers:
@@ -450,9 +452,6 @@ class AutoscalePool(WorkerPool):
try:
if isinstance(body, dict) and body.get('bind_kwargs'):
self.add_bind_kwargs(body)
# when the cluster heartbeat occurs, clean up internally
if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']:
self.cleanup()
if self.should_grow:
self.up()
# we don't care about "preferred queue" round robin distribution, just

View File

@@ -114,7 +114,6 @@ class AWXConsumerBase(object):
queue = 0
self.pool.write(queue, body)
self.total_messages += 1
self.record_statistics()
@log_excess_runtime(logger)
def record_statistics(self):
@@ -156,6 +155,16 @@ class AWXConsumerPG(AWXConsumerBase):
# if no successful loops have ran since startup, then we should fail right away
self.pg_is_down = True # set so that we fail if we get database errors on startup
self.pg_down_time = time.time() - self.pg_max_wait # allow no grace period
self.last_cleanup = time.time()
def run_periodic_tasks(self):
self.record_statistics() # maintains time buffer in method
if time.time() - self.last_cleanup > 60: # same as cluster_node_heartbeat
# NOTE: if we run out of database connections, it is important to still run cleanup
# so that we scale down workers and free up connections
self.pool.cleanup()
self.last_cleanup = time.time()
def run(self, *args, **kwargs):
super(AWXConsumerPG, self).run(*args, **kwargs)
@@ -171,8 +180,10 @@ class AWXConsumerPG(AWXConsumerBase):
if init is False:
self.worker.on_start()
init = True
for e in conn.events():
self.process_task(json.loads(e.payload))
for e in conn.events(yield_timeouts=True):
if e is not None:
self.process_task(json.loads(e.payload))
self.run_periodic_tasks()
self.pg_is_down = False
if self.should_stop:
return
@@ -229,6 +240,8 @@ class BaseWorker(object):
# so we can establish a new connection
conn.close_if_unusable_or_obsolete()
self.perform_work(body, *args)
except Exception:
logger.exception(f'Unhandled exception in perform_work in worker pid={os.getpid()}')
finally:
if 'uuid' in body:
uuid = body['uuid']

View File

@@ -25,7 +25,7 @@ class Command(BaseCommand):
with connection.cursor() as cursor:
cursor.execute(
f'''
SELECT
SELECT
b.id, b.job_id, b.host_name, b.created - a.created delta,
b.task task,
b.event_data::json->'task_action' task_action,

View File

@@ -4,7 +4,7 @@ from django.utils.timezone import now
logger = logging.getLogger('awx.main.migrations')
__all__ = ['create_collection_jt', 'create_clearsessions_jt', 'create_cleartokens_jt']
__all__ = ['create_clearsessions_jt', 'create_cleartokens_jt']
'''
These methods are called by migrations to create various system job templates

View File

@@ -44,7 +44,7 @@ def migrate_galaxy_settings(apps, schema_editor):
credential_type=galaxy_type,
inputs={'url': 'https://galaxy.ansible.com/'},
)
except:
except Exception:
# Needed for new migrations, tests
public_galaxy_credential = Credential(
created=now(), modified=now(), name='Ansible Galaxy', managed=True, credential_type=galaxy_type, inputs={'url': 'https://galaxy.ansible.com/'}

View File

@@ -153,7 +153,7 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
#
# Find the DTSTART rule or raise an error, its usually the first rule but that is not strictly enforced
start_date_rule = re.sub('^.*(DTSTART[^\s]+)\s.*$', r'\1', rrule)
start_date_rule = re.sub(r'^.*(DTSTART[^\s]+)\s.*$', r'\1', rrule)
if not start_date_rule:
raise ValueError('A DTSTART field needs to be in the rrule')

View File

@@ -1467,23 +1467,23 @@ class UnifiedJob(
self.job_explanation = job_explanation
cancel_fields.append('job_explanation')
# Important to save here before sending cancel signal to dispatcher to cancel because
# the job control process will use the cancel_flag to distinguish a shutdown from a cancel
self.save(update_fields=cancel_fields)
controller_notified = False
if self.celery_task_id:
controller_notified = self.cancel_dispatcher_process()
else:
# Avoid race condition where we have stale model from pending state but job has already started,
# its checking signal but not cancel_flag, so re-send signal after this database commit
connection.on_commit(self.fallback_cancel)
# If a SIGTERM signal was sent to the control process, and acked by the dispatcher
# then we want to let its own cleanup change status, otherwise change status now
if not controller_notified:
if self.status != 'canceled':
self.status = 'canceled'
cancel_fields.append('status')
self.save(update_fields=cancel_fields)
self.save(update_fields=['status'])
# Avoid race condition where we have stale model from pending state but job has already started,
# its checking signal but not cancel_flag, so re-send signal after updating cancel fields
self.fallback_cancel()
return self.cancel_flag

View File

@@ -216,7 +216,7 @@ def test_instance_attach_to_instance_group(post, instance_group, node_type_insta
count = ActivityStream.objects.count()
url = reverse(f'api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
url = reverse('api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
post(url, {'associate': True, 'id': instance.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
@@ -240,7 +240,7 @@ def test_instance_unattach_from_instance_group(post, instance_group, node_type_i
count = ActivityStream.objects.count()
url = reverse(f'api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
url = reverse('api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
post(url, {'disassociate': True, 'id': instance.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
@@ -263,7 +263,7 @@ def test_instance_group_attach_to_instance(post, instance_group, node_type_insta
count = ActivityStream.objects.count()
url = reverse(f'api:instance_instance_groups_list', kwargs={'pk': instance.pk})
url = reverse('api:instance_instance_groups_list', kwargs={'pk': instance.pk})
post(url, {'associate': True, 'id': instance_group.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
@@ -287,7 +287,7 @@ def test_instance_group_unattach_from_instance(post, instance_group, node_type_i
count = ActivityStream.objects.count()
url = reverse(f'api:instance_instance_groups_list', kwargs={'pk': instance.pk})
url = reverse('api:instance_instance_groups_list', kwargs={'pk': instance.pk})
post(url, {'disassociate': True, 'id': instance_group.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
@@ -314,4 +314,4 @@ def test_cannot_remove_controlplane_hybrid_instances(post, controlplane_instance
url = reverse('api:instance_instance_groups_list', kwargs={'pk': instance.pk})
r = post(url, {'disassociate': True, 'id': controlplane_instance_group.id}, admin_user, expect=400)
assert f'Cannot disassociate hybrid instance' in str(r.data)
assert 'Cannot disassociate hybrid instance' in str(r.data)

View File

@@ -147,19 +147,19 @@ def test_survey_password_default(post, patch, admin_user, project, inventory, su
("DTSTART:20030925T104941Z RRULE:FREQ=DAILY;INTERVAL=10;COUNT=500;UNTIL=20040925T104941Z", "RRULE may not contain both COUNT and UNTIL"), # noqa
("DTSTART:20300308T050000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=2000", "COUNT > 999 is unsupported"), # noqa
# Individual rule test with multiple rules
## Bad Rule: RRULE:NONSENSE
# Bad Rule: RRULE:NONSENSE
("DTSTART:20300308T050000Z RRULE:NONSENSE RRULE:INTERVAL=1;FREQ=DAILY EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU", "INTERVAL required in rrule"),
## Bad Rule: RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=5MO
# Bad Rule: RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=5MO
(
"DTSTART:20300308T050000Z RRULE:INTERVAL=1;FREQ=DAILY EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=5MO",
"BYDAY with numeric prefix not supported",
), # noqa
## Bad Rule: RRULE:FREQ=DAILY;INTERVAL=10;COUNT=500;UNTIL=20040925T104941Z
# Bad Rule: RRULE:FREQ=DAILY;INTERVAL=10;COUNT=500;UNTIL=20040925T104941Z
(
"DTSTART:20030925T104941Z RRULE:INTERVAL=1;FREQ=DAILY EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU RRULE:FREQ=DAILY;INTERVAL=10;COUNT=500;UNTIL=20040925T104941Z",
"RRULE may not contain both COUNT and UNTIL",
), # noqa
## Bad Rule: RRULE:FREQ=DAILY;INTERVAL=1;COUNT=2000
# Bad Rule: RRULE:FREQ=DAILY;INTERVAL=1;COUNT=2000
(
"DTSTART:20300308T050000Z RRULE:INTERVAL=1;FREQ=DAILY EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU RRULE:FREQ=DAILY;INTERVAL=1;COUNT=2000",
"COUNT > 999 is unsupported",

View File

@@ -24,7 +24,7 @@ class TestApiRootView:
endpoints = [
'ping',
'config',
#'settings',
# 'settings',
'me',
'dashboard',
'organizations',

View File

@@ -50,7 +50,10 @@ def test_cancel(unified_job):
# Some more thought may want to go into only emitting canceled if/when the job record
# status is changed to canceled. Unlike, currently, where it's emitted unconditionally.
unified_job.websocket_emit_status.assert_called_with("canceled")
unified_job.save.assert_called_with(update_fields=['cancel_flag', 'start_args', 'status'])
assert [(args, kwargs) for args, kwargs in unified_job.save.call_args_list] == [
((), {'update_fields': ['cancel_flag', 'start_args']}),
((), {'update_fields': ['status']}),
]
def test_cancel_job_explanation(unified_job):
@@ -60,7 +63,10 @@ def test_cancel_job_explanation(unified_job):
unified_job.cancel(job_explanation=job_explanation)
assert unified_job.job_explanation == job_explanation
unified_job.save.assert_called_with(update_fields=['cancel_flag', 'start_args', 'job_explanation', 'status'])
assert [(args, kwargs) for args, kwargs in unified_job.save.call_args_list] == [
((), {'update_fields': ['cancel_flag', 'start_args', 'job_explanation']}),
((), {'update_fields': ['status']}),
]
def test_organization_copy_to_jobs():

View File

@@ -5,7 +5,6 @@ __metaclass__ = type
import gnupg
import os
import tempfile
from ansible.module_utils.basic import *
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
@@ -15,7 +14,7 @@ from ansible_sign.checksum import (
InvalidChecksumLine,
)
from ansible_sign.checksum.differ import DistlibManifestChecksumFileExistenceDiffer
from ansible_sign.signing import *
from ansible_sign.signing import GPGVerifier
display = Display()

View File

@@ -360,7 +360,7 @@ REST_FRAMEWORK = {
# For swagger schema generation
# see https://github.com/encode/django-rest-framework/pull/6532
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',
#'URL_FORMAT_OVERRIDE': None,
# 'URL_FORMAT_OVERRIDE': None,
}
AUTHENTICATION_BACKENDS = (

View File

@@ -101,5 +101,5 @@ except IOError:
# The below runs AFTER all of the custom settings are imported.
DATABASES.setdefault('default', dict()).setdefault('OPTIONS', dict()).setdefault(
'application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63]
'application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63] # NOQA
) # noqa

View File

@@ -53,7 +53,7 @@ SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT = _(
'''\
Mapping to organization admins/users from social auth accounts. This setting
controls which users are placed into which organizations based on their
username and email address. Configuration details are available in the
username and email address. Configuration details are available in the
documentation.\
'''
)

View File

@@ -6,7 +6,7 @@ _values_to_change = ['is_superuser_value', 'is_superuser_role', 'is_system_audit
def _get_setting():
with connection.cursor() as cursor:
cursor.execute(f'SELECT value FROM conf_setting WHERE key= %s', ['SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
cursor.execute('SELECT value FROM conf_setting WHERE key= %s', ['SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
row = cursor.fetchone()
if row == None:
return {}
@@ -24,7 +24,7 @@ def _get_setting():
def _set_setting(value):
with connection.cursor() as cursor:
cursor.execute(f'UPDATE conf_setting SET value = %s WHERE key = %s', [json.dumps(value), 'SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
cursor.execute('UPDATE conf_setting SET value = %s WHERE key = %s', [json.dumps(value), 'SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
def forwards(app, schema_editor):

View File

@@ -163,9 +163,9 @@ class TestSAMLAttr:
'PersonImmutableID': [],
},
},
#'social': <UserSocialAuth: cmeyers@redhat.com>,
# 'social': <UserSocialAuth: cmeyers@redhat.com>,
'social': None,
#'strategy': <awx.sso.strategies.django_strategy.AWXDjangoStrategy object at 0x8523a10>,
# 'strategy': <awx.sso.strategies.django_strategy.AWXDjangoStrategy object at 0x8523a10>,
'strategy': None,
'new_association': False,
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { arrayOf, bool, number, shape, string } from 'prop-types';
import { Label, LabelGroup } from '@patternfly/react-core';
import { Link } from 'react-router-dom';
function InstanceGroupLabels({ labels, isLinkable }) {
const buildLinkURL = (isContainerGroup) =>
isContainerGroup
? '/instance_groups/container_group/'
: '/instance_groups/';
return (
<LabelGroup numLabels={5}>
{labels.map(({ id, name, is_container_group }) =>
isLinkable ? (
<Label
color="blue"
key={id}
render={({ className, content, componentRef }) => (
<Link
className={className}
innerRef={componentRef}
to={`${buildLinkURL(is_container_group)}${id}/details`}
>
{content}
</Link>
)}
>
{name}
</Label>
) : (
<Label color="blue" key={id}>
{name}
</Label>
)
)}
</LabelGroup>
);
}
InstanceGroupLabels.propTypes = {
labels: arrayOf(shape({ id: number.isRequired, name: string.isRequired }))
.isRequired,
isLinkable: bool,
};
InstanceGroupLabels.defaultProps = { isLinkable: false };
export default InstanceGroupLabels;

View File

@@ -0,0 +1 @@
export { default } from './InstanceGroupLabels';

View File

@@ -6,6 +6,7 @@ import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Chip, Divider, Title } from '@patternfly/react-core';
import { toTitleCase } from 'util/strings';
import InstanceGroupLabels from 'components/InstanceGroupLabels';
import CredentialChip from '../CredentialChip';
import ChipGroup from '../ChipGroup';
import { DetailList, Detail, UserDateDetail } from '../DetailList';
@@ -227,21 +228,7 @@ function PromptDetail({
label={t`Instance Groups`}
rows={4}
value={
<ChipGroup
numChips={5}
totalChips={overrides.instance_groups.length}
ouiaId="prompt-instance-groups-chips"
>
{overrides.instance_groups.map((instance_group) => (
<Chip
key={instance_group.id}
ouiaId={`instance-group-${instance_group.id}-chip`}
isReadOnly
>
{instance_group.name}
</Chip>
))}
</ChipGroup>
<InstanceGroupLabels labels={overrides.instance_groups} />
}
/>
)}

View File

@@ -10,6 +10,7 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
import { JobTemplatesAPI, SchedulesAPI, WorkflowJobTemplatesAPI } from 'api';
import { parseVariableField, jsonToYaml } from 'util/yaml';
import { useConfig } from 'contexts/Config';
import InstanceGroupLabels from 'components/InstanceGroupLabels';
import parseRuleObj from '../shared/parseRuleObj';
import FrequencyDetails from './FrequencyDetails';
import AlertModal from '../../AlertModal';
@@ -27,11 +28,6 @@ import { VariablesDetail } from '../../CodeEditor';
import { VERBOSITY } from '../../VerbositySelectField';
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
const buildLinkURL = (instance) =>
instance.is_container_group
? '/instance_groups/container_group/'
: '/instance_groups/';
const PromptDivider = styled(Divider)`
margin-top: var(--pf-global--spacer--lg);
margin-bottom: var(--pf-global--spacer--lg);
@@ -498,26 +494,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
fullWidth
label={t`Instance Groups`}
value={
<ChipGroup
numChips={5}
totalChips={instanceGroups.length}
ouiaId="instance-group-chips"
>
{instanceGroups.map((ig) => (
<Link
to={`${buildLinkURL(ig)}${ig.id}/details`}
key={ig.id}
>
<Chip
key={ig.id}
ouiaId={`instance-group-${ig.id}-chip`}
isReadOnly
>
{ig.name}
</Chip>
</Link>
))}
</ChipGroup>
<InstanceGroupLabels labels={instanceGroups} isLinkable />
}
isEmpty={instanceGroups.length === 0}
/>

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Link, useHistory, useParams } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import { t, Plural } from '@lingui/macro';
import {
Button,
@@ -11,7 +11,6 @@ import {
CodeBlockCode,
Tooltip,
Slider,
Label,
} from '@patternfly/react-core';
import { DownloadIcon, OutlinedClockIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
@@ -34,6 +33,7 @@ import useRequest, {
useDismissableError,
} from 'hooks/useRequest';
import HealthCheckAlert from 'components/HealthCheckAlert';
import InstanceGroupLabels from 'components/InstanceGroupLabels';
import RemoveInstanceButton from '../Shared/RemoveInstanceButton';
const Unavailable = styled.span`
@@ -156,11 +156,6 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
</>
);
const buildLinkURL = (inst) =>
inst.is_container_group
? '/instance_groups/container_group/'
: '/instance_groups/';
const { error, dismissError } = useDismissableError(
updateInstanceError || healthCheckError
);
@@ -225,25 +220,9 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
label={t`Instance Groups`}
dataCy="instance-groups"
helpText={t`The Instance Groups to which this instance belongs.`}
value={instanceGroups.map((ig) => (
<React.Fragment key={ig.id}>
<Label
color="blue"
isTruncated
render={({ className, content, componentRef }) => (
<Link
to={`${buildLinkURL(ig)}${ig.id}/details`}
className={className}
innerRef={componentRef}
>
{content}
</Link>
)}
>
{ig.name}
</Label>{' '}
</React.Fragment>
))}
value={
<InstanceGroupLabels labels={instanceGroups} isLinkable />
}
isEmpty={instanceGroups.length === 0}
/>
)}

View File

@@ -23,6 +23,7 @@ import { InventoriesAPI } from 'api';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import { Inventory } from 'types';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import InstanceGroupLabels from 'components/InstanceGroupLabels';
import getHelpText from '../shared/Inventory.helptext';
function InventoryDetail({ inventory }) {
@@ -105,23 +106,7 @@ function InventoryDetail({ inventory }) {
<Detail
fullWidth
label={t`Instance Groups`}
value={
<ChipGroup
numChips={5}
totalChips={instanceGroups?.length}
ouiaId="instance-group-chips"
>
{instanceGroups?.map((ig) => (
<Chip
key={ig.id}
isReadOnly
ouiaId={`instance-group-${ig.id}-chip`}
>
{ig.name}
</Chip>
))}
</ChipGroup>
}
value={<InstanceGroupLabels labels={instanceGroups} isLinkable />}
isEmpty={instanceGroups.length === 0}
/>
)}

View File

@@ -131,9 +131,8 @@ describe('<InventoryDetail />', () => {
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith(
mockInventory.id
);
const chip = wrapper.find('Chip').at(0);
expect(chip.prop('isReadOnly')).toEqual(true);
expect(chip.prop('children')).toEqual('Foo');
const label = wrapper.find('Label').at(0);
expect(label.prop('children')).toEqual('Foo');
});
test('should not load instance groups', async () => {

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { t } from '@lingui/macro';
import { Button, Chip, Label } from '@patternfly/react-core';
import { Button, Label } from '@patternfly/react-core';
import { Inventory } from 'types';
import { InventoriesAPI, UnifiedJobsAPI } from 'api';
@@ -10,7 +10,6 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
import AlertModal from 'components/AlertModal';
import { CardBody, CardActionsRow } from 'components/Card';
import ChipGroup from 'components/ChipGroup';
import { VariablesDetail } from 'components/CodeEditor';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
@@ -18,6 +17,7 @@ import DeleteButton from 'components/DeleteButton';
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
import ErrorDetail from 'components/ErrorDetail';
import Sparkline from 'components/Sparkline';
import InstanceGroupLabels from 'components/InstanceGroupLabels';
function SmartInventoryDetail({ inventory }) {
const history = useHistory();
@@ -120,23 +120,7 @@ function SmartInventoryDetail({ inventory }) {
<Detail
fullWidth
label={t`Instance groups`}
value={
<ChipGroup
numChips={5}
totalChips={instanceGroups.length}
ouiaId="instance-group-chips"
>
{instanceGroups.map((ig) => (
<Chip
key={ig.id}
isReadOnly
ouiaId={`instance-group-${ig.id}-chip`}
>
{ig.name}
</Chip>
))}
</ChipGroup>
}
value={<InstanceGroupLabels labels={instanceGroups} />}
isEmpty={instanceGroups.length === 0}
/>
<VariablesDetail

View File

@@ -40,6 +40,8 @@ const processCodeEditorValue = (value) => {
codeEditorValue = '';
} else if (typeof value === 'string') {
codeEditorValue = encode(value);
} else if (Array.isArray(value)) {
codeEditorValue = encode(value.join(' '));
} else {
codeEditorValue = value;
}
@@ -60,7 +62,7 @@ const getStdOutValue = (hostEvent) => {
) {
stdOut = res.results.join('\n');
} else if (res?.stdout) {
stdOut = res.stdout;
stdOut = Array.isArray(res.stdout) ? res.stdout.join(' ') : res.stdout;
}
return stdOut;
};

View File

@@ -52,6 +52,60 @@ const hostEvent = {
},
};
/*
Some libraries return a list of string in stdout
Example: https://github.com/ansible-collections/cisco.ios/blob/main/plugins/modules/ios_command.py#L124-L128
*/
const hostEventWithArray = {
changed: true,
event: 'runner_on_ok',
event_data: {
host: 'foo',
play: 'all',
playbook: 'run_command.yml',
res: {
ansible_loop_var: 'item',
changed: true,
item: '1',
msg: 'This is a debug message: 1',
stdout: [
' total used free shared buff/cache available\nMem: 7973 3005 960 30 4007 4582\nSwap: 1023 0 1023',
],
stderr: 'problems',
cmd: ['free', '-m'],
stderr_lines: [],
stdout_lines: [
' total used free shared buff/cache available',
'Mem: 7973 3005 960 30 4007 4582',
'Swap: 1023 0 1023',
],
},
task: 'command',
task_action: 'command',
},
event_display: 'Host OK',
event_level: 3,
failed: false,
host: 1,
host_name: 'foo',
id: 123,
job: 4,
play: 'all',
playbook: 'run_command.yml',
stdout: `stdout: "changed: [localhost] => {"changed": true, "cmd": ["free", "-m"], "delta": "0:00:01.479609", "end": "2019-09-10 14:21:45.469533", "rc": 0, "start": "2019-09-10 14:21:43.989924", "stderr": "", "stderr_lines": [], "stdout": " total used free shared buff/cache available\nMem: 7973 3005 960 30 4007 4582\nSwap: 1023 0 1023", "stdout_lines": [" total used free shared buff/cache available", "Mem: 7973 3005 960 30 4007 4582", "Swap: 1023 0 1023"]}"
`,
task: 'command',
type: 'job_event',
url: '/api/v2/job_events/123/',
summary_fields: {
host: {
id: 1,
name: 'foo',
description: 'Bar',
},
},
};
/* eslint-disable no-useless-escape */
const jsonValue = `{
\"ansible_loop_var\": \"item\",
@@ -281,4 +335,25 @@ describe('HostEventModal', () => {
expect(codeEditor.prop('readOnly')).toBe(true);
expect(codeEditor.prop('value')).toEqual('baz\nbar');
});
test('should display Standard Out array stdout content', () => {
const wrapper = shallow(
<HostEventModal
hostEvent={hostEventWithArray}
onClose={() => {}}
isOpen
/>
);
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
handleTabClick(null, 2);
wrapper.update();
const codeEditor = wrapper.find('Tab[eventKey=2] CodeEditor');
expect(codeEditor.prop('mode')).toBe('javascript');
expect(codeEditor.prop('readOnly')).toBe(true);
expect(codeEditor.prop('value')).toEqual(
hostEventWithArray.event_data.res.stdout.join(' ')
);
});
});

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro';
import { Button, Chip } from '@patternfly/react-core';
import { Button } from '@patternfly/react-core';
import { OrganizationsAPI } from 'api';
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
import { CardBody, CardActionsRow } from 'components/Card';
@@ -16,6 +16,7 @@ import ErrorDetail from 'components/ErrorDetail';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import { useConfig } from 'contexts/Config';
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
import InstanceGroupLabels from 'components/InstanceGroupLabels';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
function OrganizationDetail({ organization }) {
@@ -79,11 +80,6 @@ function OrganizationDetail({ organization }) {
return <ContentError error={contentError} />;
}
const buildLinkURL = (instance) =>
instance.is_container_group
? '/instance_groups/container_group/'
: '/instance_groups/';
return (
<CardBody>
<DetailList>
@@ -126,25 +122,7 @@ function OrganizationDetail({ organization }) {
fullWidth
label={t`Instance Groups`}
helpText={t`The Instance Groups for this Organization to run on.`}
value={
<ChipGroup
numChips={5}
totalChips={instanceGroups.length}
ouiaId="instance-group-chips"
>
{instanceGroups.map((ig) => (
<Link to={`${buildLinkURL(ig)}${ig.id}/details`} key={ig.id}>
<Chip
key={ig.id}
isReadOnly
ouiaId={`instance-group-${ig.id}-chip`}
>
{ig.name}
</Chip>
</Link>
))}
</ChipGroup>
}
value={<InstanceGroupLabels labels={instanceGroups} isLinkable />}
isEmpty={instanceGroups.length === 0}
/>
)}

View File

@@ -90,7 +90,7 @@ describe('<OrganizationDetail />', () => {
await waitForElement(component, 'ContentLoading', (el) => el.length === 0);
expect(
component
.find('Chip')
.find('Label')
.findWhere((el) => el.text() === 'One')
.exists()
).toBe(true);

View File

@@ -34,6 +34,7 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
import useBrandName from 'hooks/useBrandName';
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import InstanceGroupLabels from 'components/InstanceGroupLabels';
import getHelpText from '../shared/JobTemplate.helptext';
function JobTemplateDetail({ template }) {
@@ -167,11 +168,6 @@ function JobTemplateDetail({ template }) {
);
};
const buildLinkURL = (instance) =>
instance.is_container_group
? '/instance_groups/container_group/'
: '/instance_groups/';
if (instanceGroupsError) {
return <ContentError error={instanceGroupsError} />;
}
@@ -422,25 +418,7 @@ function JobTemplateDetail({ template }) {
label={t`Instance Groups`}
dataCy="jt-detail-instance-groups"
helpText={helpText.instanceGroups}
value={
<ChipGroup
numChips={5}
totalChips={instanceGroups.length}
ouiaId="instance-group-chips"
>
{instanceGroups.map((ig) => (
<Link to={`${buildLinkURL(ig)}${ig.id}/details`} key={ig.id}>
<Chip
key={ig.id}
ouiaId={`instance-group-${ig.id}-chip`}
isReadOnly
>
{ig.name}
</Chip>
</Link>
))}
</ChipGroup>
}
value={<InstanceGroupLabels labels={instanceGroups} isLinkable />}
isEmpty={instanceGroups.length === 0}
/>
{job_tags && (

View File

@@ -172,7 +172,7 @@ options:
signature_validation_credential:
description:
- Name of the credential to use for signature validation.
- If signature validation credential is provided, signature validation will be enabled.
- If signature validation credential is provided, signature validation will be enabled.
type: str
extends_documentation_fragment: awx.awx.auth

View File

@@ -514,7 +514,7 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id):
# Lookup Job Template ID
if workflow_node['unified_job_template']['name']:
if workflow_node['unified_job_template']['type'] is None:
module.fail_json(msg='Could not find unified job template type in workflow_nodes {1}'.format(workflow_node))
module.fail_json(msg='Could not find unified job template type in workflow_nodes {0}'.format(workflow_node))
search_fields['type'] = workflow_node['unified_job_template']['type']
if workflow_node['unified_job_template']['type'] == 'inventory_source':
if 'inventory' in workflow_node['unified_job_template']:

View File

@@ -85,8 +85,7 @@ server {
add_header X-Content-Type-Options nosniff;
location /static/ {
root /awx_devel;
try_files /awx/ui/$uri /awx/$uri /awx/public/$uri =404;
alias /var/lib/awx/public/static/;
access_log off;
sendfile off;
}

View File

@@ -16,5 +16,5 @@ commands =
yamllint -s .
[flake8]
select = F401,F402,F821,F823,F841,F811
select = F401,F402,F821,F823,F841,F811,E265,E266,F541,W605,E722,F822,F523,W291,F405
exclude = awx/ui/node_modules,awx/ui/node_modules,env,awx_collection_build