Compare commits

..

38 Commits

Author SHA1 Message Date
Rick Elrod
1bedf32baf Fix traceback on timeout with slicing + facts (#13139)
Slicing a QS with a step parameter forces the QS and returns a list.

Fixes #13131

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-11-01 09:11:20 -05:00
Jeff Bradberry
c5cf39abb7 Merge pull request #13132 from jbradberry/import-project-schedules
Expand the dependencies of the Schedule page type
2022-10-31 16:29:04 -04:00
Seth Foster
6b315f39de Merge pull request #12963 from fosterseth/minikube_cg
Make installing minikube optional in dev setup
2022-10-31 14:53:36 -04:00
Seth Foster
529a936d0a Make installing minikube optional in dev setup 2022-10-31 13:52:48 -04:00
kialam
e40824bded Fix Schedules Form date validation same day different time scenario (#13062)
* Format datetime and convert to ms to compare which date is larger.

* Add supporting unit test.
2022-10-28 19:59:54 -03:00
Alan Rominger
ed318ea784 Merge pull request #13082 from AlanCoding/health_check_stdout
Include stdout from health check if it is not nothing
2022-10-28 13:59:21 -04:00
Alex Corey
d2b69e05f6 Merge pull request #13041 from ansible/12966-DisableManualHealthCheck
Allows health checks on only execution nodes
2022-10-28 13:26:11 -04:00
Jeff Bradberry
b57ae592ed Expand the dependencies of the Schedule page type
Really these could get any of the unified job template types, not just
system job templates, so importing e.g. a project with a schedule was
doing them in the wrong order.

Also, bump the timeout of the project update and make sure that we
stash it in the page cache even if it doesn't finish in 5 minutes.
2022-10-28 12:56:35 -04:00
John Westcott IV
e22f887765 Merge pull request #13129 from john-westcott-iv/update_pr_body_check
Fix pr_body_check
2022-10-27 16:05:03 -04:00
John Westcott IV
fc838ba44b Fix pr_body_check 2022-10-27 15:39:18 -04:00
Darshan
b19aa4a88d Fixes confusing Error when trying to sync project set to scm_type Manual (#13080)
Signed-off-by: darshanip <darshancoding@gmail.com>
Co-authored-by: Rick Elrod <rick@elrod.me>
2022-10-27 15:04:26 -04:00
Hao Liu
eba24db74c Merge pull request #13103 from saito-hideki/pr/add_arm64_arch_to_fact_list
Add arm64 architecture mapping to image_architecture for m1mac
2022-10-27 13:23:59 -04:00
Jeff Bradberry
153a197fad Merge pull request #13125 from jbradberry/improve-staticfile-management
UI static files are now directly copied to the proper static dir
2022-10-27 09:25:09 -04:00
Jeff Bradberry
8f4c329c2a UI static files are now directly copied to the proper static dir
when running `make ui-devel`.  Previously they were going to
/awx_devel/awx/public/static, but that directory is no longer being
served up by nginx, which forced us to have to run `make
collectstatic` (or equivalent) to get the files to the right place.
2022-10-26 17:51:29 -04:00
Seth Foster
368eb46f5b Merge pull request #13097 from fosterseth/cyan_log_lifecyle
make job lifecycle Cyan again
2022-10-26 16:57:03 -04:00
Alan Rominger
d6fea77082 Include stdout from health check if it is not nothing 2022-10-26 16:26:59 -04:00
Alex Corey
aaf6f5f17e Merge pull request #13104 from rooftopcellist/translations_updated_2022-10-25_09_13_58
Pushing updated strings for localization
2022-10-26 11:23:41 -04:00
Christian Adams
3303f7bfcf Pushing updated strings for localization 2022-10-26 11:05:00 -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
Alex Corey
41fd6ea37f Prevents health checks on all node types except for Execution nodes 2022-10-25 10:11:45 -04:00
Hideki Saito
4808a0053f Add arm64 architecture mapping to image_architecture for m1mac
* Addresses "make docker-compose-build" failure due to missing architecture mapping.

Signed-off-by: Hideki Saito <saito@fgrep.org>
2022-10-25 11:44:59 +09:00
Seth Foster
de41601f27 make job lifecycle Cyan again 2022-10-24 13:50:42 -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
70 changed files with 204035 additions and 180 deletions

View File

@@ -13,21 +13,13 @@ jobs:
packages: write
contents: read
steps:
- name: Write PR body to a file
run: |
cat >> pr.body << __SOME_RANDOM_PR_EOF__
${{ github.event.pull_request.body }}
__SOME_RANDOM_PR_EOF__
- name: Display the received body for troubleshooting
run: cat pr.body
# We want to write these out individually just incase the options were joined on a single line
- name: Check for each of the lines
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
grep "Bug, Docs Fix or other nominal change" pr.body > Z
grep "New or Enhanced Feature" pr.body > Y
grep "Breaking Change" pr.body > X
echo $PR_BODY | grep "Bug, Docs Fix or other nominal change" > Z
echo $PR_BODY | grep "New or Enhanced Feature" > Y
echo $PR_BODY | grep "Breaking Change" > X
exit 0
# We exit 0 and set the shell to prevent the returns from the greps from failing this step
# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference

View File

@@ -85,6 +85,7 @@ clean: clean-ui clean-api clean-awxkit clean-dist
clean-api:
rm -rf build $(NAME)-$(VERSION) *.egg-info
rm -rf .tox
find . -type f -regex ".*\.py[co]$$" -delete
find . -type d -name "__pycache__" -delete
rm -f awx/awx_test.sqlite3*
@@ -181,7 +182,7 @@ collectstatic:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
fi; \
mkdir -p awx/public/static && $(PYTHON) manage.py collectstatic --clear --noinput > /dev/null 2>&1
$(PYTHON) manage.py collectstatic --clear --noinput > /dev/null 2>&1
DEV_RELOAD_COMMAND ?= supervisorctl restart tower-processes:*
@@ -377,6 +378,8 @@ clean-ui:
rm -rf awx/ui/build
rm -rf awx/ui/src/locales/_build
rm -rf $(UI_BUILD_FLAG_FILE)
# the collectstatic command doesn't like it if this dir doesn't exist.
mkdir -p awx/ui/build/static
awx/ui/node_modules:
NODE_OPTIONS=--max-old-space-size=6144 $(NPM_BIN) --prefix awx/ui --loglevel warn --force ci
@@ -386,16 +389,14 @@ $(UI_BUILD_FLAG_FILE):
$(PYTHON) tools/scripts/compilemessages.py
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
mkdir -p awx/public/static/css
mkdir -p awx/public/static/js
mkdir -p awx/public/static/media
cp -r awx/ui/build/static/css/* awx/public/static/css
cp -r awx/ui/build/static/js/* awx/public/static/js
cp -r awx/ui/build/static/media/* awx/public/static/media
mkdir -p /var/lib/awx/public/static/css
mkdir -p /var/lib/awx/public/static/js
mkdir -p /var/lib/awx/public/static/media
cp -r awx/ui/build/static/css/* /var/lib/awx/public/static/css
cp -r awx/ui/build/static/js/* /var/lib/awx/public/static/js
cp -r awx/ui/build/static/media/* /var/lib/awx/public/static/media
touch $@
ui-release: $(UI_BUILD_FLAG_FILE)
ui-devel: awx/ui/node_modules
@@ -453,6 +454,7 @@ COMPOSE_OPTS ?=
CONTROL_PLANE_NODE_COUNT ?= 1
EXECUTION_NODE_COUNT ?= 2
MINIKUBE_CONTAINER_GROUP ?= false
MINIKUBE_SETUP ?= false # if false, run minikube separately
EXTRA_SOURCES_ANSIBLE_OPTS ?=
ifneq ($(ADMIN_PASSWORD),)
@@ -461,7 +463,7 @@ endif
docker-compose-sources: .git/hooks/pre-commit
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose-minikube/deploy.yml; \
ansible-playbook -i tools/docker-compose/inventory -e minikube_setup=$(MINIKUBE_SETUP) tools/docker-compose-minikube/deploy.yml; \
fi;
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
@@ -635,4 +637,4 @@ help/generate:
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u
@printf "\n"
@printf "\n"

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",
@@ -4952,7 +4952,7 @@ class InstanceSerializer(BaseSerializer):
res['install_bundle'] = self.reverse('api:instance_install_bundle', kwargs={'pk': obj.pk})
res['peers'] = self.reverse('api:instance_peers_list', kwargs={"pk": obj.pk})
if self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
if obj.node_type != 'hop':
if obj.node_type == 'execution':
res['health_check'] = self.reverse('api:instance_health_check', kwargs={'pk': obj.pk})
return res

View File

@@ -69,6 +69,7 @@ from awx.api.generics import (
APIView,
BaseUsersList,
CopyAPIView,
GenericCancelView,
GenericAPIView,
ListAPIView,
ListCreateAPIView,
@@ -391,8 +392,8 @@ class InstanceHealthCheck(GenericAPIView):
permission_classes = (IsSystemAdminOrAuditor,)
def get_queryset(self):
return super().get_queryset().filter(node_type='execution')
# FIXME: For now, we don't have a good way of checking the health of a hop node.
return super().get_queryset().exclude(node_type='hop')
def get(self, request, *args, **kwargs):
obj = self.get_object()
@@ -412,9 +413,10 @@ class InstanceHealthCheck(GenericAPIView):
execution_node_health_check.apply_async([obj.hostname])
else:
from awx.main.tasks.system import cluster_node_health_check
cluster_node_health_check.apply_async([obj.hostname], queue=obj.hostname)
return Response(
{"error": f"Cannot run a health check on instances of type {obj.node_type}. Health checks can only be run on execution nodes."},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({'msg': f"Health check is running for {obj.hostname}."}, status=status.HTTP_200_OK)
@@ -976,20 +978,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 +2255,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 +3034,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 +3336,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 +3500,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 +3975,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 +4114,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):

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

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

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

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

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

@@ -247,6 +247,19 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
return (number, step)
def get_sliced_hosts(self, host_queryset, slice_number, slice_count):
"""
Returns a slice of Hosts given a slice number and total slice count, or
the original queryset if slicing is not requested.
NOTE: If slicing is performed, this will return a List[Host] with the
resulting slice. If slicing is not performed it will return the
original queryset (not evaluating it or forcing it to a list). This
puts the burden on the caller to check the resulting type. This is
non-ideal because it's easy to get wrong, but I think the only way
around it is to force the queryset which has memory implications for
large inventories.
"""
if slice_count > 1 and slice_number > 0:
offset = slice_number - 1
host_queryset = host_queryset[offset::slice_count]

View File

@@ -15,6 +15,7 @@ from urllib.parse import urljoin
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.query import QuerySet
# from django.core.cache import cache
from django.utils.encoding import smart_str
@@ -844,22 +845,30 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
def get_notification_friendly_name(self):
return "Job"
def _get_inventory_hosts(self, only=['name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id']):
def _get_inventory_hosts(self, only=('name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id'), **filters):
"""Return value is an iterable for the relevant hosts for this job"""
if not self.inventory:
return []
host_queryset = self.inventory.hosts.only(*only)
return self.inventory.get_sliced_hosts(host_queryset, self.job_slice_number, self.job_slice_count)
if filters:
host_queryset = host_queryset.filter(**filters)
host_queryset = self.inventory.get_sliced_hosts(host_queryset, self.job_slice_number, self.job_slice_count)
if isinstance(host_queryset, QuerySet):
return host_queryset.iterator()
return host_queryset
def start_job_fact_cache(self, destination, modification_times, timeout=None):
self.log_lifecycle("start_job_fact_cache")
os.makedirs(destination, mode=0o700)
hosts = self._get_inventory_hosts()
if timeout is None:
timeout = settings.ANSIBLE_FACT_CACHE_TIMEOUT
if timeout > 0:
# exclude hosts with fact data older than `settings.ANSIBLE_FACT_CACHE_TIMEOUT seconds`
timeout = now() - datetime.timedelta(seconds=timeout)
hosts = hosts.filter(ansible_facts_modified__gte=timeout)
hosts = self._get_inventory_hosts(ansible_facts_modified__gte=timeout)
else:
hosts = self._get_inventory_hosts()
for host in hosts:
filepath = os.sep.join(map(str, [destination, host.name]))
if not os.path.realpath(filepath).startswith(destination):

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

@@ -208,7 +208,10 @@ def run_until_complete(node, timing_data=None, **kwargs):
if state_name.lower() == 'failed':
work_detail = status.get('Detail', '')
if work_detail:
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}')
if stdout:
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}\nstdout:\n{stdout}')
else:
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}')
else:
raise RemoteJobError(f'Unknown ansible-runner error on node {node}, stdout:\n{stdout}')

View File

@@ -7,7 +7,7 @@ from awx.main.models.ha import Instance
from django.test.utils import override_settings
INSTANCE_KWARGS = dict(hostname='example-host', cpu=6, memory=36000000000, cpu_capacity=6, mem_capacity=42)
INSTANCE_KWARGS = dict(hostname='example-host', cpu=6, node_type='execution', memory=36000000000, cpu_capacity=6, mem_capacity=42)
@pytest.mark.django_db

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

@@ -110,7 +110,7 @@ if settings.COLOR_LOGS is True:
# logs rendered with cyan text
previous_level_map = self.level_map.copy()
if record.name == "awx.analytics.job_lifecycle":
self.level_map[logging.DEBUG] = (None, 'cyan', True)
self.level_map[logging.INFO] = (None, 'cyan', True)
msg = super(ColorHandler, self).colorize(line, record)
self.level_map = previous_level_map
return msg

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

@@ -101,7 +101,7 @@ USE_L10N = True
USE_TZ = True
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'ui', 'build', 'static'), os.path.join(BASE_DIR, 'static'))
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'ui', 'build', 'static'), os.path.join(BASE_DIR, 'static')]
# Absolute filesystem path to the directory where static file are collected via
# the collectstatic command.
@@ -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

@@ -416,8 +416,14 @@ function ScheduleForm({
if (options.end === 'onDate') {
if (
DateTime.fromISO(values.startDate) >=
DateTime.fromISO(options.endDate)
DateTime.fromFormat(
`${values.startDate} ${values.startTime}`,
'yyyy-LL-dd h:mm a'
).toMillis() >=
DateTime.fromFormat(
`${options.endDate} ${options.endTime}`,
'yyyy-LL-dd h:mm a'
).toMillis()
) {
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
}

View File

@@ -900,6 +900,36 @@ describe('<ScheduleForm />', () => {
);
});
test('should create schedule with the same start and end date provided that the end date is at a later time', async () => {
const today = DateTime.now().toFormat('yyyy-LL-dd');
const laterTime = DateTime.now().plus({ hours: 1 }).toFormat('h:mm a');
await act(async () => {
wrapper.find('DatePicker[aria-label="End date"]').prop('onChange')(
today,
new Date(today)
);
});
wrapper.update();
expect(
wrapper
.find('FormGroup[data-cy="schedule-End date/time"]')
.prop('helperTextInvalid')
).toBe(
'Please select an end date/time that comes after the start date/time.'
);
await act(async () => {
wrapper.find('TimePicker[aria-label="End time"]').prop('onChange')(
laterTime
);
});
wrapper.update();
expect(
wrapper
.find('FormGroup[data-cy="schedule-End date/time"]')
.prop('helperTextInvalid')
).toBe(undefined);
});
test('error shown when on day number is not between 1 and 31', async () => {
await act(async () => {
wrapper.find('FrequencySelect#schedule-frequency').invoke('onChange')([

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

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

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

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

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

@@ -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

@@ -114,7 +114,12 @@ def main():
# Update the project
result = module.post_endpoint(project['related']['update'])
if result['status_code'] != 202:
if result['status_code'] == 405:
module.fail_json(
msg="Unable to trigger a project update because the project scm_type ({0}) does not support it.".format(project['scm_type']),
response=result
)
elif result['status_code'] != 202:
module.fail_json(msg="Failed to update project, see response for details", response=result)
module.json_output['changed'] = True

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

@@ -275,7 +275,13 @@ class ApiV2(base.Base):
# When creating a project, we need to wait for its
# first project update to finish so that associated
# JTs have valid options for playbook names
_page.wait_until_completed()
try:
_page.wait_until_completed(timeout=300)
except AssertionError:
# If the project update times out, try to
# carry on in the hopes that it will
# finish before it is needed.
pass
else:
# If we are an existing project and our scm_tpye is not changing don't try and import the local_path setting
if asset['natural_key']['type'] == 'project' and 'local_path' in post_data and _page['scm_type'] == post_data['scm_type']:

View File

@@ -1,6 +1,7 @@
from contextlib import suppress
from awxkit.api.pages import SystemJobTemplate
from awxkit.api.pages import JobTemplate, SystemJobTemplate, Project, InventorySource
from awxkit.api.pages.workflow_job_templates import WorkflowJobTemplate
from awxkit.api.mixins import HasCreate
from awxkit.api.resources import resources
from awxkit.config import config
@@ -11,7 +12,7 @@ from . import base
class Schedule(HasCreate, base.Base):
dependencies = [SystemJobTemplate]
dependencies = [JobTemplate, SystemJobTemplate, Project, InventorySource, WorkflowJobTemplate]
NATURAL_KEY = ('unified_job_template', 'name')
def silent_delete(self):

View File

@@ -9,4 +9,4 @@ template_dest: '_build'
receptor_image: quay.io/ansible/receptor:devel
# Helper vars to construct the proper download URL for the current architecture
image_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm", "ppc64le": "ppc64le" }[ansible_facts.architecture] }}'
image_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm", "arm64": "arm64", "ppc64le": "ppc64le" }[ansible_facts.architecture] }}'

View File

@@ -9,8 +9,8 @@ addons:
minikube_url_linux: 'https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64'
minikube_url_macos: 'https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64'
kubectl_url_linux: 'https://dl.k8s.io/release/v1.21.0/bin/linux/amd64/kubectl'
kubectl_url_macos: 'https://dl.k8s.io/release/v1.21.0/bin/darwin/amd64/kubectl'
kubectl_url_linux: 'https://dl.k8s.io/release/v1.25.0/bin/linux/amd64/kubectl'
kubectl_url_macos: 'https://dl.k8s.io/release/v1.25.0/bin/darwin/amd64/kubectl'
# Service Account Name
minikube_service_account_name: 'awx-devel'

View File

@@ -8,6 +8,10 @@
state: 'directory'
mode: '0700'
- name: debug minikube_setup
debug:
var: minikube_setup
# Linux block
- block:
- name: Download Minikube
@@ -24,6 +28,7 @@
when:
- ansible_architecture == "x86_64"
- ansible_system == "Linux"
- minikube_setup | default(False) | bool
# MacOS block
- block:
@@ -41,25 +46,29 @@
when:
- ansible_architecture == "x86_64"
- ansible_system == "Darwin"
- minikube_setup | default(False) | bool
- name: Starting Minikube
shell: "{{ sources_dest }}/minikube start --driver={{ driver }} --install-addons=true --addons={{ addons | join(',') }}"
register: minikube_stdout
- block:
- name: Starting Minikube
shell: "{{ sources_dest }}/minikube start --driver={{ driver }} --install-addons=true --addons={{ addons | join(',') }}"
register: minikube_stdout
- name: Enable Ingress Controller on Minikube
shell: "{{ sources_dest }}/minikube addons enable ingress"
- name: Enable Ingress Controller on Minikube
shell: "{{ sources_dest }}/minikube addons enable ingress"
when:
- minikube_stdout.rc == 0
register: _minikube_ingress
ignore_errors: true
- name: Show Minikube Ingress known-issue 7332 warning
pause:
seconds: 5
prompt: "The Minikube Ingress addon has been disabled since it looks like you are hitting https://github.com/kubernetes/minikube/issues/7332"
when:
- '"minikube/issues/7332" in _minikube_ingress.stderr'
- ansible_system == "Darwin"
when:
- minikube_stdout.rc == 0
register: _minikube_ingress
ignore_errors: true
- name: Show Minikube Ingress known-issue 7332 warning
pause:
seconds: 5
prompt: "The Minikube Ingress addon has been disabled since it looks like you are hitting https://github.com/kubernetes/minikube/issues/7332"
when:
- '"minikube/issues/7332" in _minikube_ingress.stderr'
- ansible_system == "Darwin"
- minikube_setup | default(False) | bool
- name: Create ServiceAccount and clusterRoleBinding
k8s:

View File

@@ -301,11 +301,19 @@ Note that you may see multiple messages of the form `2021-03-04 20:11:47,666 WAR
To bring up a 1 node AWX + minikube that is accessible from AWX run the following.
Start minikube
```bash
(host)$minikube start --cpus=4 --memory=8g --addons=ingress`
```
Start AWX
```bash
(host)$ make docker-compose-container-group
```
Alternatively, you can set the env var `MINIKUBE_CONTAINER_GROUP=true` to use the default dev env bring up. his way you can use other env flags like the cluster node count.
Alternatively, you can set the env var `MINIKUBE_CONTAINER_GROUP=true` to use the default dev env bring up. his way you can use other env flags like the cluster node count. Set `MINIKUBE_SETUP=true` to make the roles download, install and run minikube for you, but if you run into issues with this just start minikube yourself.
```bash
(host)$ MINIKUBE_CONTAINER_GROUP=true make docker-compose

View File

@@ -19,6 +19,9 @@ else
wait-for-migrations
fi
# Make sure that the UI static file directory exists, Django complains otherwise.
mkdir -p /awx_devel/awx/ui/build/static
if output=$(awx-manage createsuperuser --noinput --username=admin --email=admin@localhost 2> /dev/null); then
echo $output
fi
@@ -27,10 +30,6 @@ echo "Admin password: ${DJANGO_SUPERUSER_PASSWORD}"
awx-manage create_preload_data
awx-manage register_default_execution_environments
mkdir -p /awx_devel/awx/public/static
mkdir -p /awx_devel/awx/ui/static
mkdir -p /awx_devel/awx/ui/build/static
awx-manage provision_instance --hostname="$(hostname)" --node_type="$MAIN_NODE_TYPE"
awx-manage register_queue --queuename=controlplane --instance_percent=100
awx-manage register_queue --queuename=default --instance_percent=100

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