mirror of
https://github.com/ansible/awx.git
synced 2026-02-11 06:34:42 -03:30
Compare commits
1 Commits
21.8.0
...
12640-Refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d87c091eea |
20
.github/workflows/pr_body_check.yml
vendored
20
.github/workflows/pr_body_check.yml
vendored
@@ -13,13 +13,21 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Check for each of the lines
|
- name: Write PR body to a file
|
||||||
env:
|
|
||||||
PR_BODY: ${{ github.event.pull_request.body }}
|
|
||||||
run: |
|
run: |
|
||||||
echo $PR_BODY | grep "Bug, Docs Fix or other nominal change" > Z
|
cat >> pr.body << __SOME_RANDOM_PR_EOF__
|
||||||
echo $PR_BODY | grep "New or Enhanced Feature" > Y
|
${{ github.event.pull_request.body }}
|
||||||
echo $PR_BODY | grep "Breaking Change" > X
|
__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
|
||||||
|
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
|
||||||
exit 0
|
exit 0
|
||||||
# We exit 0 and set the shell to prevent the returns from the greps from failing this step
|
# 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
|
# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
||||||
|
|||||||
24
Makefile
24
Makefile
@@ -85,7 +85,6 @@ clean: clean-ui clean-api clean-awxkit clean-dist
|
|||||||
|
|
||||||
clean-api:
|
clean-api:
|
||||||
rm -rf build $(NAME)-$(VERSION) *.egg-info
|
rm -rf build $(NAME)-$(VERSION) *.egg-info
|
||||||
rm -rf .tox
|
|
||||||
find . -type f -regex ".*\.py[co]$$" -delete
|
find . -type f -regex ".*\.py[co]$$" -delete
|
||||||
find . -type d -name "__pycache__" -delete
|
find . -type d -name "__pycache__" -delete
|
||||||
rm -f awx/awx_test.sqlite3*
|
rm -f awx/awx_test.sqlite3*
|
||||||
@@ -182,7 +181,7 @@ collectstatic:
|
|||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
$(PYTHON) manage.py collectstatic --clear --noinput > /dev/null 2>&1
|
mkdir -p awx/public/static && $(PYTHON) manage.py collectstatic --clear --noinput > /dev/null 2>&1
|
||||||
|
|
||||||
DEV_RELOAD_COMMAND ?= supervisorctl restart tower-processes:*
|
DEV_RELOAD_COMMAND ?= supervisorctl restart tower-processes:*
|
||||||
|
|
||||||
@@ -378,8 +377,6 @@ clean-ui:
|
|||||||
rm -rf awx/ui/build
|
rm -rf awx/ui/build
|
||||||
rm -rf awx/ui/src/locales/_build
|
rm -rf awx/ui/src/locales/_build
|
||||||
rm -rf $(UI_BUILD_FLAG_FILE)
|
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:
|
awx/ui/node_modules:
|
||||||
NODE_OPTIONS=--max-old-space-size=6144 $(NPM_BIN) --prefix awx/ui --loglevel warn --force ci
|
NODE_OPTIONS=--max-old-space-size=6144 $(NPM_BIN) --prefix awx/ui --loglevel warn --force ci
|
||||||
@@ -389,14 +386,16 @@ $(UI_BUILD_FLAG_FILE):
|
|||||||
$(PYTHON) tools/scripts/compilemessages.py
|
$(PYTHON) tools/scripts/compilemessages.py
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
|
||||||
mkdir -p /var/lib/awx/public/static/css
|
mkdir -p awx/public/static/css
|
||||||
mkdir -p /var/lib/awx/public/static/js
|
mkdir -p awx/public/static/js
|
||||||
mkdir -p /var/lib/awx/public/static/media
|
mkdir -p awx/public/static/media
|
||||||
cp -r awx/ui/build/static/css/* /var/lib/awx/public/static/css
|
cp -r awx/ui/build/static/css/* awx/public/static/css
|
||||||
cp -r awx/ui/build/static/js/* /var/lib/awx/public/static/js
|
cp -r awx/ui/build/static/js/* awx/public/static/js
|
||||||
cp -r awx/ui/build/static/media/* /var/lib/awx/public/static/media
|
cp -r awx/ui/build/static/media/* awx/public/static/media
|
||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ui-release: $(UI_BUILD_FLAG_FILE)
|
ui-release: $(UI_BUILD_FLAG_FILE)
|
||||||
|
|
||||||
ui-devel: awx/ui/node_modules
|
ui-devel: awx/ui/node_modules
|
||||||
@@ -454,7 +453,6 @@ COMPOSE_OPTS ?=
|
|||||||
CONTROL_PLANE_NODE_COUNT ?= 1
|
CONTROL_PLANE_NODE_COUNT ?= 1
|
||||||
EXECUTION_NODE_COUNT ?= 2
|
EXECUTION_NODE_COUNT ?= 2
|
||||||
MINIKUBE_CONTAINER_GROUP ?= false
|
MINIKUBE_CONTAINER_GROUP ?= false
|
||||||
MINIKUBE_SETUP ?= false # if false, run minikube separately
|
|
||||||
EXTRA_SOURCES_ANSIBLE_OPTS ?=
|
EXTRA_SOURCES_ANSIBLE_OPTS ?=
|
||||||
|
|
||||||
ifneq ($(ADMIN_PASSWORD),)
|
ifneq ($(ADMIN_PASSWORD),)
|
||||||
@@ -463,7 +461,7 @@ endif
|
|||||||
|
|
||||||
docker-compose-sources: .git/hooks/pre-commit
|
docker-compose-sources: .git/hooks/pre-commit
|
||||||
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
|
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
|
||||||
ansible-playbook -i tools/docker-compose/inventory -e minikube_setup=$(MINIKUBE_SETUP) tools/docker-compose-minikube/deploy.yml; \
|
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose-minikube/deploy.yml; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
||||||
@@ -637,4 +635,4 @@ help/generate:
|
|||||||
} \
|
} \
|
||||||
} \
|
} \
|
||||||
{ lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u
|
{ lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u
|
||||||
@printf "\n"
|
@printf "\n"
|
||||||
@@ -6,6 +6,7 @@ import inspect
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -13,7 +14,7 @@ from django.contrib.auth import views as auth_views
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db import connection, transaction
|
from django.db import connection
|
||||||
from django.db.models.fields.related import OneToOneRel
|
from django.db.models.fields.related import OneToOneRel
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@@ -29,7 +30,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
from rest_framework.renderers import StaticHTMLRenderer, JSONRenderer
|
||||||
from rest_framework.negotiation import DefaultContentNegotiation
|
from rest_framework.negotiation import DefaultContentNegotiation
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
@@ -40,7 +41,7 @@ from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd,
|
|||||||
from awx.main.utils.db import get_all_field_names
|
from awx.main.utils.db import get_all_field_names
|
||||||
from awx.main.utils.licensing import server_product_name
|
from awx.main.utils.licensing import server_product_name
|
||||||
from awx.main.views import ApiErrorView
|
from awx.main.views import ApiErrorView
|
||||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer
|
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
||||||
from awx.api.versioning import URLPathVersioning
|
from awx.api.versioning import URLPathVersioning
|
||||||
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
||||||
from awx.conf import settings_registry
|
from awx.conf import settings_registry
|
||||||
@@ -64,7 +65,6 @@ __all__ = [
|
|||||||
'ParentMixin',
|
'ParentMixin',
|
||||||
'SubListAttachDetachAPIView',
|
'SubListAttachDetachAPIView',
|
||||||
'CopyAPIView',
|
'CopyAPIView',
|
||||||
'GenericCancelView',
|
|
||||||
'BaseUsersList',
|
'BaseUsersList',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -90,9 +90,13 @@ class LoggedLoginView(auth_views.LoginView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
|
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
|
||||||
|
current_user = getattr(request, 'user', None)
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None))))
|
logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None))))
|
||||||
ret.set_cookie('userLoggedIn', 'true')
|
ret.set_cookie('userLoggedIn', 'true')
|
||||||
|
current_user = UserSerializer(self.request.user)
|
||||||
|
current_user = smart_str(JSONRenderer().render(current_user.data))
|
||||||
|
current_user = urllib.parse.quote('%s' % current_user, '')
|
||||||
ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@@ -249,7 +253,7 @@ class APIView(views.APIView):
|
|||||||
response['X-API-Query-Time'] = '%0.3fs' % sum(q_times)
|
response['X-API-Query-Time'] = '%0.3fs' % sum(q_times)
|
||||||
|
|
||||||
if getattr(self, 'deprecated', False):
|
if getattr(self, 'deprecated', False):
|
||||||
response['Warning'] = '299 awx "This resource has been deprecated and will be removed in a future release."'
|
response['Warning'] = '299 awx "This resource has been deprecated and will be removed in a future release."' # noqa
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -986,23 +990,6 @@ class CopyAPIView(GenericAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
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):
|
class BaseUsersList(SubListCreateAttachDetachAPIView):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
ret = super(BaseUsersList, self).post(request, *args, **kwargs)
|
ret = super(BaseUsersList, self).post(request, *args, **kwargs)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ __all__ = [
|
|||||||
'InventoryInventorySourcesUpdatePermission',
|
'InventoryInventorySourcesUpdatePermission',
|
||||||
'UserPermission',
|
'UserPermission',
|
||||||
'IsSystemAdminOrAuditor',
|
'IsSystemAdminOrAuditor',
|
||||||
|
'InstanceGroupTowerPermission',
|
||||||
'WorkflowApprovalPermission',
|
'WorkflowApprovalPermission',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework.exceptions import ValidationError, PermissionDenied
|
from rest_framework.exceptions import ValidationError, PermissionDenied
|
||||||
@@ -121,9 +120,6 @@ from awx.main.validators import vars_validate_or_raise
|
|||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, VerbatimField, DeprecatedCredentialField
|
from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, VerbatimField, DeprecatedCredentialField
|
||||||
|
|
||||||
# AWX Utils
|
|
||||||
from awx.api.validators import HostnameRegexValidator
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.serializers')
|
logger = logging.getLogger('awx.api.serializers')
|
||||||
|
|
||||||
# Fields that should be summarized regardless of object type.
|
# Fields that should be summarized regardless of object type.
|
||||||
@@ -3750,11 +3746,7 @@ class LaunchConfigurationBaseSerializer(BaseSerializer):
|
|||||||
|
|
||||||
# Build unsaved version of this config, use it to detect prompts errors
|
# Build unsaved version of this config, use it to detect prompts errors
|
||||||
mock_obj = self._build_mock_obj(attrs)
|
mock_obj = self._build_mock_obj(attrs)
|
||||||
if set(list(ujt.get_ask_mapping().keys()) + ['extra_data']) & set(attrs.keys()):
|
accepted, rejected, errors = ujt._accept_or_ignore_job_kwargs(_exclude_errors=self.exclude_errors, **mock_obj.prompts_dict())
|
||||||
accepted, rejected, errors = ujt._accept_or_ignore_job_kwargs(_exclude_errors=self.exclude_errors, **mock_obj.prompts_dict())
|
|
||||||
else:
|
|
||||||
# Only perform validation of prompts if prompts fields are provided
|
|
||||||
errors = {}
|
|
||||||
|
|
||||||
# Remove all unprocessed $encrypted$ strings, indicating default usage
|
# Remove all unprocessed $encrypted$ strings, indicating default usage
|
||||||
if 'extra_data' in attrs and password_dict:
|
if 'extra_data' in attrs and password_dict:
|
||||||
@@ -4929,19 +4921,6 @@ class InstanceSerializer(BaseSerializer):
|
|||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'node_type': {'initial': Instance.Types.EXECUTION, 'default': Instance.Types.EXECUTION},
|
'node_type': {'initial': Instance.Types.EXECUTION, 'default': Instance.Types.EXECUTION},
|
||||||
'node_state': {'initial': Instance.States.INSTALLED, 'default': Instance.States.INSTALLED},
|
'node_state': {'initial': Instance.States.INSTALLED, 'default': Instance.States.INSTALLED},
|
||||||
'hostname': {
|
|
||||||
'validators': [
|
|
||||||
MaxLengthValidator(limit_value=250),
|
|
||||||
validators.UniqueValidator(queryset=Instance.objects.all()),
|
|
||||||
RegexValidator(
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
HostnameRegexValidator(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
@@ -4952,7 +4931,7 @@ class InstanceSerializer(BaseSerializer):
|
|||||||
res['install_bundle'] = self.reverse('api:instance_install_bundle', kwargs={'pk': obj.pk})
|
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})
|
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 self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
|
||||||
if obj.node_type == 'execution':
|
if obj.node_type != 'hop':
|
||||||
res['health_check'] = self.reverse('api:instance_health_check', kwargs={'pk': obj.pk})
|
res['health_check'] = self.reverse('api:instance_health_check', kwargs={'pk': obj.pk})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -5012,10 +4991,6 @@ class InstanceSerializer(BaseSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_hostname(self, value):
|
def validate_hostname(self, value):
|
||||||
"""
|
|
||||||
- Hostname cannot be "localhost" - but can be something like localhost.domain
|
|
||||||
- Cannot change the hostname of an-already instantiated & initialized Instance object
|
|
||||||
"""
|
|
||||||
if self.instance and self.instance.hostname != value:
|
if self.instance and self.instance.hostname != value:
|
||||||
raise serializers.ValidationError("Cannot change hostname.")
|
raise serializers.ValidationError("Cannot change hostname.")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
receptor_user: awx
|
|
||||||
receptor_group: awx
|
|
||||||
receptor_verify: true
|
receptor_verify: true
|
||||||
receptor_tls: true
|
receptor_tls: true
|
||||||
receptor_work_commands:
|
receptor_work_commands:
|
||||||
@@ -12,12 +10,12 @@ custom_worksign_public_keyfile: receptor/work-public-key.pem
|
|||||||
custom_tls_certfile: receptor/tls/receptor.crt
|
custom_tls_certfile: receptor/tls/receptor.crt
|
||||||
custom_tls_keyfile: receptor/tls/receptor.key
|
custom_tls_keyfile: receptor/tls/receptor.key
|
||||||
custom_ca_certfile: receptor/tls/ca/receptor-ca.crt
|
custom_ca_certfile: receptor/tls/ca/receptor-ca.crt
|
||||||
|
receptor_user: awx
|
||||||
|
receptor_group: awx
|
||||||
receptor_protocol: 'tcp'
|
receptor_protocol: 'tcp'
|
||||||
receptor_listener: true
|
receptor_listener: true
|
||||||
receptor_port: {{ instance.listener_port }}
|
receptor_port: {{ instance.listener_port }}
|
||||||
receptor_dependencies:
|
receptor_dependencies:
|
||||||
|
- podman
|
||||||
|
- crun
|
||||||
- python39-pip
|
- python39-pip
|
||||||
{% verbatim %}
|
|
||||||
podman_user: "{{ receptor_user }}"
|
|
||||||
podman_group: "{{ receptor_group }}"
|
|
||||||
{% endverbatim %}
|
|
||||||
|
|||||||
@@ -9,12 +9,10 @@
|
|||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
- name: Enable Copr repo for Receptor
|
- name: Enable Copr repo for Receptor
|
||||||
command: dnf copr enable ansible-awx/receptor -y
|
command: dnf copr enable ansible-awx/receptor -y
|
||||||
- import_role:
|
|
||||||
name: ansible.receptor.podman
|
|
||||||
- import_role:
|
- import_role:
|
||||||
name: ansible.receptor.setup
|
name: ansible.receptor.setup
|
||||||
- name: Install ansible-runner
|
- name: Install ansible-runner
|
||||||
pip:
|
pip:
|
||||||
name: ansible-runner
|
name: ansible-runner
|
||||||
executable: pip3.9
|
executable: pip3.9
|
||||||
{% endverbatim %}
|
{% endverbatim %}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
collections:
|
collections:
|
||||||
- name: ansible.receptor
|
- name: ansible.receptor
|
||||||
version: 1.1.0
|
source: https://github.com/ansible/receptor-collection/
|
||||||
|
type: git
|
||||||
|
version: 0.1.1
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from awx.api.views import (
|
|||||||
InstanceUnifiedJobsList,
|
InstanceUnifiedJobsList,
|
||||||
InstanceInstanceGroupsList,
|
InstanceInstanceGroupsList,
|
||||||
InstanceHealthCheck,
|
InstanceHealthCheck,
|
||||||
|
InstanceInstallBundle,
|
||||||
InstancePeersList,
|
InstancePeersList,
|
||||||
)
|
)
|
||||||
from awx.api.views.instance_install_bundle import InstanceInstallBundle
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,28 +3,26 @@
|
|||||||
|
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from awx.api.views.inventory import (
|
from awx.api.views import (
|
||||||
InventoryList,
|
InventoryList,
|
||||||
InventoryDetail,
|
InventoryDetail,
|
||||||
|
InventoryHostsList,
|
||||||
|
InventoryGroupsList,
|
||||||
|
InventoryRootGroupsList,
|
||||||
|
InventoryVariableData,
|
||||||
|
InventoryScriptView,
|
||||||
|
InventoryTreeView,
|
||||||
|
InventoryInventorySourcesList,
|
||||||
|
InventoryInventorySourcesUpdate,
|
||||||
InventoryActivityStreamList,
|
InventoryActivityStreamList,
|
||||||
InventoryJobTemplateList,
|
InventoryJobTemplateList,
|
||||||
|
InventoryAdHocCommandsList,
|
||||||
InventoryAccessList,
|
InventoryAccessList,
|
||||||
InventoryObjectRolesList,
|
InventoryObjectRolesList,
|
||||||
InventoryInstanceGroupsList,
|
InventoryInstanceGroupsList,
|
||||||
InventoryLabelList,
|
InventoryLabelList,
|
||||||
InventoryCopy,
|
InventoryCopy,
|
||||||
)
|
)
|
||||||
from awx.api.views import (
|
|
||||||
InventoryHostsList,
|
|
||||||
InventoryGroupsList,
|
|
||||||
InventoryInventorySourcesList,
|
|
||||||
InventoryInventorySourcesUpdate,
|
|
||||||
InventoryAdHocCommandsList,
|
|
||||||
InventoryRootGroupsList,
|
|
||||||
InventoryScriptView,
|
|
||||||
InventoryTreeView,
|
|
||||||
InventoryVariableData,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
|
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from awx.api.views.inventory import (
|
|
||||||
InventoryUpdateEventsList,
|
|
||||||
)
|
|
||||||
from awx.api.views import (
|
from awx.api.views import (
|
||||||
InventoryUpdateList,
|
InventoryUpdateList,
|
||||||
InventoryUpdateDetail,
|
InventoryUpdateDetail,
|
||||||
@@ -13,6 +10,7 @@ from awx.api.views import (
|
|||||||
InventoryUpdateStdout,
|
InventoryUpdateStdout,
|
||||||
InventoryUpdateNotificationsList,
|
InventoryUpdateNotificationsList,
|
||||||
InventoryUpdateCredentialsList,
|
InventoryUpdateCredentialsList,
|
||||||
|
InventoryUpdateEventsList,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from oauthlib import oauth2
|
|||||||
from oauth2_provider import views
|
from oauth2_provider import views
|
||||||
|
|
||||||
from awx.main.models import RefreshToken
|
from awx.main.models import RefreshToken
|
||||||
from awx.api.views.root import ApiOAuthAuthorizationRootView
|
from awx.api.views import ApiOAuthAuthorizationRootView
|
||||||
|
|
||||||
|
|
||||||
class TokenView(views.TokenView):
|
class TokenView(views.TokenView):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from awx.api.views.organization import (
|
from awx.api.views import (
|
||||||
OrganizationList,
|
OrganizationList,
|
||||||
OrganizationDetail,
|
OrganizationDetail,
|
||||||
OrganizationUsersList,
|
OrganizationUsersList,
|
||||||
@@ -14,6 +14,7 @@ from awx.api.views.organization import (
|
|||||||
OrganizationJobTemplatesList,
|
OrganizationJobTemplatesList,
|
||||||
OrganizationWorkflowJobTemplatesList,
|
OrganizationWorkflowJobTemplatesList,
|
||||||
OrganizationTeamsList,
|
OrganizationTeamsList,
|
||||||
|
OrganizationCredentialList,
|
||||||
OrganizationActivityStreamList,
|
OrganizationActivityStreamList,
|
||||||
OrganizationNotificationTemplatesList,
|
OrganizationNotificationTemplatesList,
|
||||||
OrganizationNotificationTemplatesErrorList,
|
OrganizationNotificationTemplatesErrorList,
|
||||||
@@ -24,8 +25,8 @@ from awx.api.views.organization import (
|
|||||||
OrganizationGalaxyCredentialsList,
|
OrganizationGalaxyCredentialsList,
|
||||||
OrganizationObjectRolesList,
|
OrganizationObjectRolesList,
|
||||||
OrganizationAccessList,
|
OrganizationAccessList,
|
||||||
|
OrganizationApplicationList,
|
||||||
)
|
)
|
||||||
from awx.api.views import OrganizationCredentialList, OrganizationApplicationList
|
|
||||||
|
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ from django.urls import include, re_path
|
|||||||
|
|
||||||
from awx import MODE
|
from awx import MODE
|
||||||
from awx.api.generics import LoggedLoginView, LoggedLogoutView
|
from awx.api.generics import LoggedLoginView, LoggedLogoutView
|
||||||
from awx.api.views.root import (
|
from awx.api.views import (
|
||||||
ApiRootView,
|
ApiRootView,
|
||||||
ApiV2RootView,
|
ApiV2RootView,
|
||||||
ApiV2PingView,
|
ApiV2PingView,
|
||||||
ApiV2ConfigView,
|
ApiV2ConfigView,
|
||||||
ApiV2SubscriptionView,
|
ApiV2SubscriptionView,
|
||||||
ApiV2AttachView,
|
ApiV2AttachView,
|
||||||
)
|
|
||||||
from awx.api.views import (
|
|
||||||
AuthView,
|
AuthView,
|
||||||
UserMeList,
|
UserMeList,
|
||||||
DashboardView,
|
DashboardView,
|
||||||
@@ -30,8 +28,8 @@ from awx.api.views import (
|
|||||||
OAuth2TokenList,
|
OAuth2TokenList,
|
||||||
ApplicationOAuth2TokenList,
|
ApplicationOAuth2TokenList,
|
||||||
OAuth2ApplicationDetail,
|
OAuth2ApplicationDetail,
|
||||||
|
MeshVisualizer,
|
||||||
)
|
)
|
||||||
from awx.api.views.mesh_visualizer import MeshVisualizer
|
|
||||||
|
|
||||||
from awx.api.views.metrics import MetricsView
|
from awx.api.views.metrics import MetricsView
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from awx.api.views.webhooks import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver
|
from awx.api.views import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from django.core.validators import RegexValidator, validate_ipv46_address
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
|
||||||
class HostnameRegexValidator(RegexValidator):
|
|
||||||
"""
|
|
||||||
Fully validates a domain name that is compliant with norms in Linux/RHEL
|
|
||||||
- Cannot start with a hyphen
|
|
||||||
- Cannot begin with, or end with a "."
|
|
||||||
- Cannot contain any whitespaces
|
|
||||||
- Entire hostname is max 255 chars (including dots)
|
|
||||||
- Each domain/label is between 1 and 63 characters, except top level domain, which must be at least 2 characters
|
|
||||||
- Supports ipv4, ipv6, simple hostnames and FQDNs
|
|
||||||
- Follows RFC 9210 (modern RFC 1123, 1178) requirements
|
|
||||||
|
|
||||||
Accepts an IP Address or Hostname as the argument
|
|
||||||
"""
|
|
||||||
|
|
||||||
regex = '^[a-z0-9][-a-z0-9]*$|^([a-z0-9][-a-z0-9]{0,62}[.])*[a-z0-9][-a-z0-9]{1,62}$'
|
|
||||||
flags = re.IGNORECASE
|
|
||||||
|
|
||||||
def __call__(self, value):
|
|
||||||
regex_matches, err = self.__validate(value)
|
|
||||||
invalid_input = regex_matches if self.inverse_match else not regex_matches
|
|
||||||
if invalid_input:
|
|
||||||
if err is None:
|
|
||||||
err = ValidationError(self.message, code=self.code, params={"value": value})
|
|
||||||
raise err
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"regex={self.regex}, message={self.message}, code={self.code}, inverse_match={self.inverse_match}, flags={self.flags}"
|
|
||||||
|
|
||||||
def __validate(self, value):
|
|
||||||
|
|
||||||
if ' ' in value:
|
|
||||||
return False, ValidationError("whitespaces in hostnames are illegal")
|
|
||||||
|
|
||||||
"""
|
|
||||||
If we have an IP address, try and validate it.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
validate_ipv46_address(value)
|
|
||||||
return True, None
|
|
||||||
except ValidationError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
"""
|
|
||||||
By this point in the code, we probably have a simple hostname, FQDN or a strange hostname like "192.localhost.domain.101"
|
|
||||||
"""
|
|
||||||
if not self.regex.match(value):
|
|
||||||
return False, ValidationError(f"illegal characters detected in hostname={value}. Please verify.")
|
|
||||||
|
|
||||||
return True, None
|
|
||||||
@@ -69,7 +69,6 @@ from awx.api.generics import (
|
|||||||
APIView,
|
APIView,
|
||||||
BaseUsersList,
|
BaseUsersList,
|
||||||
CopyAPIView,
|
CopyAPIView,
|
||||||
GenericCancelView,
|
|
||||||
GenericAPIView,
|
GenericAPIView,
|
||||||
ListAPIView,
|
ListAPIView,
|
||||||
ListCreateAPIView,
|
ListCreateAPIView,
|
||||||
@@ -123,6 +122,56 @@ from awx.api.views.mixin import (
|
|||||||
UnifiedJobDeletionMixin,
|
UnifiedJobDeletionMixin,
|
||||||
NoTruncateMixin,
|
NoTruncateMixin,
|
||||||
)
|
)
|
||||||
|
from awx.api.views.instance_install_bundle import InstanceInstallBundle # noqa
|
||||||
|
from awx.api.views.inventory import ( # noqa
|
||||||
|
InventoryList,
|
||||||
|
InventoryDetail,
|
||||||
|
InventoryUpdateEventsList,
|
||||||
|
InventoryList,
|
||||||
|
InventoryDetail,
|
||||||
|
InventoryActivityStreamList,
|
||||||
|
InventoryInstanceGroupsList,
|
||||||
|
InventoryAccessList,
|
||||||
|
InventoryObjectRolesList,
|
||||||
|
InventoryJobTemplateList,
|
||||||
|
InventoryLabelList,
|
||||||
|
InventoryCopy,
|
||||||
|
)
|
||||||
|
from awx.api.views.mesh_visualizer import MeshVisualizer # noqa
|
||||||
|
from awx.api.views.organization import ( # noqa
|
||||||
|
OrganizationList,
|
||||||
|
OrganizationDetail,
|
||||||
|
OrganizationInventoriesList,
|
||||||
|
OrganizationUsersList,
|
||||||
|
OrganizationAdminsList,
|
||||||
|
OrganizationExecutionEnvironmentsList,
|
||||||
|
OrganizationProjectsList,
|
||||||
|
OrganizationJobTemplatesList,
|
||||||
|
OrganizationWorkflowJobTemplatesList,
|
||||||
|
OrganizationTeamsList,
|
||||||
|
OrganizationActivityStreamList,
|
||||||
|
OrganizationNotificationTemplatesList,
|
||||||
|
OrganizationNotificationTemplatesAnyList,
|
||||||
|
OrganizationNotificationTemplatesErrorList,
|
||||||
|
OrganizationNotificationTemplatesStartedList,
|
||||||
|
OrganizationNotificationTemplatesSuccessList,
|
||||||
|
OrganizationNotificationTemplatesApprovalList,
|
||||||
|
OrganizationInstanceGroupsList,
|
||||||
|
OrganizationGalaxyCredentialsList,
|
||||||
|
OrganizationAccessList,
|
||||||
|
OrganizationObjectRolesList,
|
||||||
|
)
|
||||||
|
from awx.api.views.root import ( # noqa
|
||||||
|
ApiRootView,
|
||||||
|
ApiOAuthAuthorizationRootView,
|
||||||
|
ApiVersionRootView,
|
||||||
|
ApiV2RootView,
|
||||||
|
ApiV2PingView,
|
||||||
|
ApiV2ConfigView,
|
||||||
|
ApiV2SubscriptionView,
|
||||||
|
ApiV2AttachView,
|
||||||
|
)
|
||||||
|
from awx.api.views.webhooks import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver # noqa
|
||||||
from awx.api.pagination import UnifiedJobEventPagination
|
from awx.api.pagination import UnifiedJobEventPagination
|
||||||
from awx.main.utils import set_environ
|
from awx.main.utils import set_environ
|
||||||
|
|
||||||
@@ -392,8 +441,8 @@ class InstanceHealthCheck(GenericAPIView):
|
|||||||
permission_classes = (IsSystemAdminOrAuditor,)
|
permission_classes = (IsSystemAdminOrAuditor,)
|
||||||
|
|
||||||
def get_queryset(self):
|
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.
|
# 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):
|
def get(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
@@ -413,10 +462,9 @@ class InstanceHealthCheck(GenericAPIView):
|
|||||||
|
|
||||||
execution_node_health_check.apply_async([obj.hostname])
|
execution_node_health_check.apply_async([obj.hostname])
|
||||||
else:
|
else:
|
||||||
return Response(
|
from awx.main.tasks.system import cluster_node_health_check
|
||||||
{"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,
|
cluster_node_health_check.apply_async([obj.hostname], queue=obj.hostname)
|
||||||
)
|
|
||||||
return Response({'msg': f"Health check is running for {obj.hostname}."}, status=status.HTTP_200_OK)
|
return Response({'msg': f"Health check is running for {obj.hostname}."}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@@ -978,11 +1026,20 @@ class SystemJobEventsList(SubListAPIView):
|
|||||||
return job.get_event_queryset()
|
return job.get_event_queryset()
|
||||||
|
|
||||||
|
|
||||||
class ProjectUpdateCancel(GenericCancelView):
|
class ProjectUpdateCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = models.ProjectUpdate
|
model = models.ProjectUpdate
|
||||||
|
obj_permission_type = 'cancel'
|
||||||
serializer_class = serializers.ProjectUpdateCancelSerializer
|
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):
|
class ProjectUpdateNotificationsList(SubListAPIView):
|
||||||
|
|
||||||
@@ -2255,11 +2312,20 @@ class InventoryUpdateCredentialsList(SubListAPIView):
|
|||||||
relationship = 'credentials'
|
relationship = 'credentials'
|
||||||
|
|
||||||
|
|
||||||
class InventoryUpdateCancel(GenericCancelView):
|
class InventoryUpdateCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = models.InventoryUpdate
|
model = models.InventoryUpdate
|
||||||
|
obj_permission_type = 'cancel'
|
||||||
serializer_class = serializers.InventoryUpdateCancelSerializer
|
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):
|
class InventoryUpdateNotificationsList(SubListAPIView):
|
||||||
|
|
||||||
@@ -3034,7 +3100,8 @@ class WorkflowJobNodeChildrenBaseList(SubListAPIView):
|
|||||||
search_fields = ('unified_job_template__name', 'unified_job_template__description')
|
search_fields = ('unified_job_template__name', 'unified_job_template__description')
|
||||||
|
|
||||||
#
|
#
|
||||||
# Limit the set of WorkflowJobNodes to the related nodes of specified by self.relationship
|
# Limit the set of WorkflowJobeNodes to the related nodes of specified by
|
||||||
|
#'relationship'
|
||||||
#
|
#
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
parent = self.get_parent_object()
|
parent = self.get_parent_object()
|
||||||
@@ -3336,15 +3403,20 @@ class WorkflowJobWorkflowNodesList(SubListAPIView):
|
|||||||
return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id')
|
return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id')
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobCancel(GenericCancelView):
|
class WorkflowJobCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = models.WorkflowJob
|
model = models.WorkflowJob
|
||||||
|
obj_permission_type = 'cancel'
|
||||||
serializer_class = serializers.WorkflowJobCancelSerializer
|
serializer_class = serializers.WorkflowJobCancelSerializer
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
r = super().post(request, *args, **kwargs)
|
obj = self.get_object()
|
||||||
ScheduleWorkflowManager().schedule()
|
if obj.can_cancel:
|
||||||
return r
|
obj.cancel()
|
||||||
|
ScheduleWorkflowManager().schedule()
|
||||||
|
return Response(status=status.HTTP_202_ACCEPTED)
|
||||||
|
else:
|
||||||
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobNotificationsList(SubListAPIView):
|
class WorkflowJobNotificationsList(SubListAPIView):
|
||||||
@@ -3500,11 +3572,20 @@ class JobActivityStreamList(SubListAPIView):
|
|||||||
search_fields = ('changes',)
|
search_fields = ('changes',)
|
||||||
|
|
||||||
|
|
||||||
class JobCancel(GenericCancelView):
|
class JobCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = models.Job
|
model = models.Job
|
||||||
|
obj_permission_type = 'cancel'
|
||||||
serializer_class = serializers.JobCancelSerializer
|
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):
|
class JobRelaunch(RetrieveAPIView):
|
||||||
|
|
||||||
@@ -3975,11 +4056,20 @@ class AdHocCommandDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|||||||
serializer_class = serializers.AdHocCommandDetailSerializer
|
serializer_class = serializers.AdHocCommandDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
class AdHocCommandCancel(GenericCancelView):
|
class AdHocCommandCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = models.AdHocCommand
|
model = models.AdHocCommand
|
||||||
|
obj_permission_type = 'cancel'
|
||||||
serializer_class = serializers.AdHocCommandCancelSerializer
|
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):
|
class AdHocCommandRelaunch(GenericAPIView):
|
||||||
|
|
||||||
@@ -4114,11 +4204,20 @@ class SystemJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|||||||
serializer_class = serializers.SystemJobSerializer
|
serializer_class = serializers.SystemJobSerializer
|
||||||
|
|
||||||
|
|
||||||
class SystemJobCancel(GenericCancelView):
|
class SystemJobCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = models.SystemJob
|
model = models.SystemJob
|
||||||
|
obj_permission_type = 'cancel'
|
||||||
serializer_class = serializers.SystemJobCancelSerializer
|
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):
|
class SystemJobNotificationsList(SubListAPIView):
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ def generate_receptor_tls(instance_obj):
|
|||||||
.public_key(csr.public_key())
|
.public_key(csr.public_key())
|
||||||
.serial_number(x509.random_serial_number())
|
.serial_number(x509.random_serial_number())
|
||||||
.not_valid_before(datetime.datetime.utcnow())
|
.not_valid_before(datetime.datetime.utcnow())
|
||||||
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650))
|
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=10))
|
||||||
.add_extension(
|
.add_extension(
|
||||||
csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value,
|
csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value,
|
||||||
critical=csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).critical,
|
critical=csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).critical,
|
||||||
|
|||||||
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
@@ -993,6 +993,9 @@ class HostAccess(BaseAccess):
|
|||||||
if data and 'name' in data:
|
if data and 'name' in data:
|
||||||
self.check_license(add_host_name=data['name'])
|
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
|
# Checks for admin or change permission on inventory, controls whether
|
||||||
# the user can edit variable data.
|
# the user can edit variable data.
|
||||||
return obj and self.user in obj.inventory.admin_role
|
return obj and self.user in obj.inventory.admin_role
|
||||||
|
|||||||
@@ -166,7 +166,11 @@ class Metrics:
|
|||||||
elif settings.IS_TESTING():
|
elif settings.IS_TESTING():
|
||||||
self.instance_name = "awx_testing"
|
self.instance_name = "awx_testing"
|
||||||
else:
|
else:
|
||||||
self.instance_name = Instance.objects.my_hostname()
|
try:
|
||||||
|
self.instance_name = Instance.objects.me().hostname
|
||||||
|
except Exception as e:
|
||||||
|
self.instance_name = settings.CLUSTER_HOST_ID
|
||||||
|
logger.info(f'Instance {self.instance_name} seems to be unregistered, error: {e}')
|
||||||
|
|
||||||
# metric name, help_text
|
# metric name, help_text
|
||||||
METRICSLIST = [
|
METRICSLIST = [
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import uuid
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connection
|
|
||||||
import redis
|
import redis
|
||||||
|
|
||||||
from awx.main.dispatch import get_local_queuename
|
from awx.main.dispatch import get_local_queuename
|
||||||
@@ -50,10 +49,7 @@ class Control(object):
|
|||||||
reply_queue = Control.generate_reply_queue_name()
|
reply_queue = Control.generate_reply_queue_name()
|
||||||
self.result = None
|
self.result = None
|
||||||
|
|
||||||
if not connection.get_autocommit():
|
with pg_bus_conn(new_connection=True) as conn:
|
||||||
raise RuntimeError('Control-with-reply messages can only be done in autocommit mode')
|
|
||||||
|
|
||||||
with pg_bus_conn() as conn:
|
|
||||||
conn.listen(reply_queue)
|
conn.listen(reply_queue)
|
||||||
send_data = {'control': command, 'reply_to': reply_queue}
|
send_data = {'control': command, 'reply_to': reply_queue}
|
||||||
if extra_data:
|
if extra_data:
|
||||||
|
|||||||
@@ -387,8 +387,6 @@ class AutoscalePool(WorkerPool):
|
|||||||
reaper.reap_job(j, 'failed')
|
reaper.reap_job(j, 'failed')
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('failed to reap job UUID {}'.format(w.current_task['uuid']))
|
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)
|
orphaned.extend(w.orphaned_tasks)
|
||||||
self.workers.remove(w)
|
self.workers.remove(w)
|
||||||
elif w.idle and len(self.workers) > self.min_workers:
|
elif w.idle and len(self.workers) > self.min_workers:
|
||||||
@@ -452,6 +450,9 @@ class AutoscalePool(WorkerPool):
|
|||||||
try:
|
try:
|
||||||
if isinstance(body, dict) and body.get('bind_kwargs'):
|
if isinstance(body, dict) and body.get('bind_kwargs'):
|
||||||
self.add_bind_kwargs(body)
|
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:
|
if self.should_grow:
|
||||||
self.up()
|
self.up()
|
||||||
# we don't care about "preferred queue" round robin distribution, just
|
# we don't care about "preferred queue" round robin distribution, just
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ def startup_reaping():
|
|||||||
If this particular instance is starting, then we know that any running jobs are invalid
|
If this particular instance is starting, then we know that any running jobs are invalid
|
||||||
so we will reap those jobs as a special action here
|
so we will reap those jobs as a special action here
|
||||||
"""
|
"""
|
||||||
jobs = UnifiedJob.objects.filter(status='running', controller_node=Instance.objects.my_hostname())
|
try:
|
||||||
|
me = Instance.objects.me()
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.warning(f'Local instance is not registered, not running startup reaper: {e}')
|
||||||
|
return
|
||||||
|
jobs = UnifiedJob.objects.filter(status='running', controller_node=me.hostname)
|
||||||
job_ids = []
|
job_ids = []
|
||||||
for j in jobs:
|
for j in jobs:
|
||||||
job_ids.append(j.id)
|
job_ids.append(j.id)
|
||||||
@@ -57,13 +62,16 @@ def reap_waiting(instance=None, status='failed', job_explanation=None, grace_per
|
|||||||
if grace_period is None:
|
if grace_period is None:
|
||||||
grace_period = settings.JOB_WAITING_GRACE_PERIOD + settings.TASK_MANAGER_TIMEOUT
|
grace_period = settings.JOB_WAITING_GRACE_PERIOD + settings.TASK_MANAGER_TIMEOUT
|
||||||
|
|
||||||
if instance is None:
|
me = instance
|
||||||
hostname = Instance.objects.my_hostname()
|
if me is None:
|
||||||
else:
|
try:
|
||||||
hostname = instance.hostname
|
me = Instance.objects.me()
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.warning(f'Local instance is not registered, not running reaper: {e}')
|
||||||
|
return
|
||||||
if ref_time is None:
|
if ref_time is None:
|
||||||
ref_time = tz_now()
|
ref_time = tz_now()
|
||||||
jobs = UnifiedJob.objects.filter(status='waiting', modified__lte=ref_time - timedelta(seconds=grace_period), controller_node=hostname)
|
jobs = UnifiedJob.objects.filter(status='waiting', modified__lte=ref_time - timedelta(seconds=grace_period), controller_node=me.hostname)
|
||||||
if excluded_uuids:
|
if excluded_uuids:
|
||||||
jobs = jobs.exclude(celery_task_id__in=excluded_uuids)
|
jobs = jobs.exclude(celery_task_id__in=excluded_uuids)
|
||||||
for j in jobs:
|
for j in jobs:
|
||||||
@@ -74,13 +82,16 @@ def reap(instance=None, status='failed', job_explanation=None, excluded_uuids=No
|
|||||||
"""
|
"""
|
||||||
Reap all jobs in running for this instance.
|
Reap all jobs in running for this instance.
|
||||||
"""
|
"""
|
||||||
if instance is None:
|
me = instance
|
||||||
hostname = Instance.objects.my_hostname()
|
if me is None:
|
||||||
else:
|
try:
|
||||||
hostname = instance.hostname
|
me = Instance.objects.me()
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.warning(f'Local instance is not registered, not running reaper: {e}')
|
||||||
|
return
|
||||||
workflow_ctype_id = ContentType.objects.get_for_model(WorkflowJob).id
|
workflow_ctype_id = ContentType.objects.get_for_model(WorkflowJob).id
|
||||||
jobs = UnifiedJob.objects.filter(
|
jobs = UnifiedJob.objects.filter(
|
||||||
Q(status='running') & (Q(execution_node=hostname) | Q(controller_node=hostname)) & ~Q(polymorphic_ctype_id=workflow_ctype_id)
|
Q(status='running') & (Q(execution_node=me.hostname) | Q(controller_node=me.hostname)) & ~Q(polymorphic_ctype_id=workflow_ctype_id)
|
||||||
)
|
)
|
||||||
if excluded_uuids:
|
if excluded_uuids:
|
||||||
jobs = jobs.exclude(celery_task_id__in=excluded_uuids)
|
jobs = jobs.exclude(celery_task_id__in=excluded_uuids)
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class AWXConsumerBase(object):
|
|||||||
queue = 0
|
queue = 0
|
||||||
self.pool.write(queue, body)
|
self.pool.write(queue, body)
|
||||||
self.total_messages += 1
|
self.total_messages += 1
|
||||||
|
self.record_statistics()
|
||||||
|
|
||||||
@log_excess_runtime(logger)
|
@log_excess_runtime(logger)
|
||||||
def record_statistics(self):
|
def record_statistics(self):
|
||||||
@@ -155,16 +156,6 @@ class AWXConsumerPG(AWXConsumerBase):
|
|||||||
# if no successful loops have ran since startup, then we should fail right away
|
# 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_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.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):
|
def run(self, *args, **kwargs):
|
||||||
super(AWXConsumerPG, self).run(*args, **kwargs)
|
super(AWXConsumerPG, self).run(*args, **kwargs)
|
||||||
@@ -180,10 +171,8 @@ class AWXConsumerPG(AWXConsumerBase):
|
|||||||
if init is False:
|
if init is False:
|
||||||
self.worker.on_start()
|
self.worker.on_start()
|
||||||
init = True
|
init = True
|
||||||
for e in conn.events(yield_timeouts=True):
|
for e in conn.events():
|
||||||
if e is not None:
|
self.process_task(json.loads(e.payload))
|
||||||
self.process_task(json.loads(e.payload))
|
|
||||||
self.run_periodic_tasks()
|
|
||||||
self.pg_is_down = False
|
self.pg_is_down = False
|
||||||
if self.should_stop:
|
if self.should_stop:
|
||||||
return
|
return
|
||||||
@@ -240,8 +229,6 @@ class BaseWorker(object):
|
|||||||
# so we can establish a new connection
|
# so we can establish a new connection
|
||||||
conn.close_if_unusable_or_obsolete()
|
conn.close_if_unusable_or_obsolete()
|
||||||
self.perform_work(body, *args)
|
self.perform_work(body, *args)
|
||||||
except Exception:
|
|
||||||
logger.exception(f'Unhandled exception in perform_work in worker pid={os.getpid()}')
|
|
||||||
finally:
|
finally:
|
||||||
if 'uuid' in body:
|
if 'uuid' in body:
|
||||||
uuid = body['uuid']
|
uuid = body['uuid']
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class Command(BaseCommand):
|
|||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
f'''
|
f'''
|
||||||
SELECT
|
SELECT
|
||||||
b.id, b.job_id, b.host_name, b.created - a.created delta,
|
b.id, b.job_id, b.host_name, b.created - a.created delta,
|
||||||
b.task task,
|
b.task task,
|
||||||
b.event_data::json->'task_action' task_action,
|
b.event_data::json->'task_action' task_action,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class Command(BaseCommand):
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_connection_status(cls, hostnames, data):
|
def get_connection_status(cls, me, hostnames, data):
|
||||||
host_stats = [('hostname', 'state', 'start time', 'duration (sec)')]
|
host_stats = [('hostname', 'state', 'start time', 'duration (sec)')]
|
||||||
for h in hostnames:
|
for h in hostnames:
|
||||||
connection_color = '91' # red
|
connection_color = '91' # red
|
||||||
@@ -78,7 +78,7 @@ class Command(BaseCommand):
|
|||||||
return host_stats
|
return host_stats
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_connection_stats(cls, hostnames, data):
|
def get_connection_stats(cls, me, hostnames, data):
|
||||||
host_stats = [('hostname', 'total', 'per minute')]
|
host_stats = [('hostname', 'total', 'per minute')]
|
||||||
for h in hostnames:
|
for h in hostnames:
|
||||||
h_safe = safe_name(h)
|
h_safe = safe_name(h)
|
||||||
@@ -119,8 +119,8 @@ class Command(BaseCommand):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
my_hostname = Instance.objects.my_hostname()
|
me = Instance.objects.me()
|
||||||
logger.info('Active instance with hostname {} is registered.'.format(my_hostname))
|
logger.info('Active instance with hostname {} is registered.'.format(me.hostname))
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
# the CLUSTER_HOST_ID in the task, and web instance must match and
|
# the CLUSTER_HOST_ID in the task, and web instance must match and
|
||||||
# ensure network connectivity between the task and web instance
|
# ensure network connectivity between the task and web instance
|
||||||
@@ -145,19 +145,19 @@ class Command(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
data[family.name] = family.samples[0].value
|
data[family.name] = family.samples[0].value
|
||||||
|
|
||||||
my_hostname = Instance.objects.my_hostname()
|
me = Instance.objects.me()
|
||||||
hostnames = [i.hostname for i in Instance.objects.exclude(hostname=my_hostname)]
|
hostnames = [i.hostname for i in Instance.objects.exclude(hostname=me.hostname)]
|
||||||
|
|
||||||
host_stats = Command.get_connection_status(hostnames, data)
|
host_stats = Command.get_connection_status(me, hostnames, data)
|
||||||
lines = Command._format_lines(host_stats)
|
lines = Command._format_lines(host_stats)
|
||||||
|
|
||||||
print(f'Broadcast websocket connection status from "{my_hostname}" to:')
|
print(f'Broadcast websocket connection status from "{me.hostname}" to:')
|
||||||
print('\n'.join(lines))
|
print('\n'.join(lines))
|
||||||
|
|
||||||
host_stats = Command.get_connection_stats(hostnames, data)
|
host_stats = Command.get_connection_stats(me, hostnames, data)
|
||||||
lines = Command._format_lines(host_stats)
|
lines = Command._format_lines(host_stats)
|
||||||
|
|
||||||
print(f'\nBroadcast websocket connection stats from "{my_hostname}" to:')
|
print(f'\nBroadcast websocket connection stats from "{me.hostname}" to:')
|
||||||
print('\n'.join(lines))
|
print('\n'.join(lines))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -99,12 +99,9 @@ class InstanceManager(models.Manager):
|
|||||||
instance or role.
|
instance or role.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def my_hostname(self):
|
|
||||||
return settings.CLUSTER_HOST_ID
|
|
||||||
|
|
||||||
def me(self):
|
def me(self):
|
||||||
"""Return the currently active instance."""
|
"""Return the currently active instance."""
|
||||||
node = self.filter(hostname=self.my_hostname())
|
node = self.filter(hostname=settings.CLUSTER_HOST_ID)
|
||||||
if node.exists():
|
if node.exists():
|
||||||
return node[0]
|
return node[0]
|
||||||
raise RuntimeError("No instance found with the current cluster host id")
|
raise RuntimeError("No instance found with the current cluster host id")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.utils.timezone import now
|
|||||||
|
|
||||||
logger = logging.getLogger('awx.main.migrations')
|
logger = logging.getLogger('awx.main.migrations')
|
||||||
|
|
||||||
__all__ = ['create_clearsessions_jt', 'create_cleartokens_jt']
|
__all__ = ['create_collection_jt', 'create_clearsessions_jt', 'create_cleartokens_jt']
|
||||||
|
|
||||||
'''
|
'''
|
||||||
These methods are called by migrations to create various system job templates
|
These methods are called by migrations to create various system job templates
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def migrate_galaxy_settings(apps, schema_editor):
|
|||||||
credential_type=galaxy_type,
|
credential_type=galaxy_type,
|
||||||
inputs={'url': 'https://galaxy.ansible.com/'},
|
inputs={'url': 'https://galaxy.ansible.com/'},
|
||||||
)
|
)
|
||||||
except Exception:
|
except:
|
||||||
# Needed for new migrations, tests
|
# Needed for new migrations, tests
|
||||||
public_galaxy_credential = Credential(
|
public_galaxy_credential = Credential(
|
||||||
created=now(), modified=now(), name='Ansible Galaxy', managed=True, credential_type=galaxy_type, inputs={'url': 'https://galaxy.ansible.com/'}
|
created=now(), modified=now(), name='Ansible Galaxy', managed=True, credential_type=galaxy_type, inputs={'url': 'https://galaxy.ansible.com/'}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
return field['default']
|
return field['default']
|
||||||
if 'default' in kwargs:
|
if 'default' in kwargs:
|
||||||
return kwargs['default']
|
return kwargs['default']
|
||||||
raise AttributeError(field_name)
|
raise AttributeError
|
||||||
if field_name in self.inputs:
|
if field_name in self.inputs:
|
||||||
return self.inputs[field_name]
|
return self.inputs[field_name]
|
||||||
if 'default' in kwargs:
|
if 'default' in kwargs:
|
||||||
|
|||||||
@@ -247,19 +247,6 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
|
|||||||
return (number, step)
|
return (number, step)
|
||||||
|
|
||||||
def get_sliced_hosts(self, host_queryset, slice_number, slice_count):
|
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:
|
if slice_count > 1 and slice_number > 0:
|
||||||
offset = slice_number - 1
|
offset = slice_number - 1
|
||||||
host_queryset = host_queryset[offset::slice_count]
|
host_queryset = host_queryset[offset::slice_count]
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from urllib.parse import urljoin
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
|
|
||||||
# from django.core.cache import cache
|
# from django.core.cache import cache
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
@@ -845,30 +844,22 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
|||||||
def get_notification_friendly_name(self):
|
def get_notification_friendly_name(self):
|
||||||
return "Job"
|
return "Job"
|
||||||
|
|
||||||
def _get_inventory_hosts(self, only=('name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id'), **filters):
|
def _get_inventory_hosts(self, only=['name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id']):
|
||||||
"""Return value is an iterable for the relevant hosts for this job"""
|
|
||||||
if not self.inventory:
|
if not self.inventory:
|
||||||
return []
|
return []
|
||||||
host_queryset = self.inventory.hosts.only(*only)
|
host_queryset = self.inventory.hosts.only(*only)
|
||||||
if filters:
|
return self.inventory.get_sliced_hosts(host_queryset, self.job_slice_number, self.job_slice_count)
|
||||||
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):
|
def start_job_fact_cache(self, destination, modification_times, timeout=None):
|
||||||
self.log_lifecycle("start_job_fact_cache")
|
self.log_lifecycle("start_job_fact_cache")
|
||||||
os.makedirs(destination, mode=0o700)
|
os.makedirs(destination, mode=0o700)
|
||||||
|
hosts = self._get_inventory_hosts()
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
timeout = settings.ANSIBLE_FACT_CACHE_TIMEOUT
|
timeout = settings.ANSIBLE_FACT_CACHE_TIMEOUT
|
||||||
if timeout > 0:
|
if timeout > 0:
|
||||||
# exclude hosts with fact data older than `settings.ANSIBLE_FACT_CACHE_TIMEOUT seconds`
|
# exclude hosts with fact data older than `settings.ANSIBLE_FACT_CACHE_TIMEOUT seconds`
|
||||||
timeout = now() - datetime.timedelta(seconds=timeout)
|
timeout = now() - datetime.timedelta(seconds=timeout)
|
||||||
hosts = self._get_inventory_hosts(ansible_facts_modified__gte=timeout)
|
hosts = hosts.filter(ansible_facts_modified__gte=timeout)
|
||||||
else:
|
|
||||||
hosts = self._get_inventory_hosts()
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
filepath = os.sep.join(map(str, [destination, host.name]))
|
filepath = os.sep.join(map(str, [destination, host.name]))
|
||||||
if not os.path.realpath(filepath).startswith(destination):
|
if not os.path.realpath(filepath).startswith(destination):
|
||||||
|
|||||||
@@ -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
|
# Find the DTSTART rule or raise an error, its usually the first rule but that is not strictly enforced
|
||||||
start_date_rule = re.sub(r'^.*(DTSTART[^\s]+)\s.*$', r'\1', rrule)
|
start_date_rule = re.sub('^.*(DTSTART[^\s]+)\s.*$', r'\1', rrule)
|
||||||
if not start_date_rule:
|
if not start_date_rule:
|
||||||
raise ValueError('A DTSTART field needs to be in the rrule')
|
raise ValueError('A DTSTART field needs to be in the rrule')
|
||||||
|
|
||||||
|
|||||||
@@ -1305,8 +1305,6 @@ class UnifiedJob(
|
|||||||
status_data['instance_group_name'] = None
|
status_data['instance_group_name'] = None
|
||||||
elif status in ['successful', 'failed', 'canceled'] and self.finished:
|
elif status in ['successful', 'failed', 'canceled'] and self.finished:
|
||||||
status_data['finished'] = datetime.datetime.strftime(self.finished, "%Y-%m-%dT%H:%M:%S.%fZ")
|
status_data['finished'] = datetime.datetime.strftime(self.finished, "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||||
elif status == 'running':
|
|
||||||
status_data['started'] = datetime.datetime.strftime(self.finished, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
||||||
status_data.update(self.websocket_emit_data())
|
status_data.update(self.websocket_emit_data())
|
||||||
status_data['group_name'] = 'jobs'
|
status_data['group_name'] = 'jobs'
|
||||||
if getattr(self, 'unified_job_template_id', None):
|
if getattr(self, 'unified_job_template_id', None):
|
||||||
@@ -1467,23 +1465,23 @@ class UnifiedJob(
|
|||||||
self.job_explanation = job_explanation
|
self.job_explanation = job_explanation
|
||||||
cancel_fields.append('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
|
controller_notified = False
|
||||||
if self.celery_task_id:
|
if self.celery_task_id:
|
||||||
controller_notified = self.cancel_dispatcher_process()
|
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
|
# 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
|
# then we want to let its own cleanup change status, otherwise change status now
|
||||||
if not controller_notified:
|
if not controller_notified:
|
||||||
if self.status != 'canceled':
|
if self.status != 'canceled':
|
||||||
self.status = 'canceled'
|
self.status = 'canceled'
|
||||||
self.save(update_fields=['status'])
|
cancel_fields.append('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.save(update_fields=cancel_fields)
|
||||||
self.fallback_cancel()
|
|
||||||
|
|
||||||
return self.cancel_flag
|
return self.cancel_flag
|
||||||
|
|
||||||
|
|||||||
@@ -700,7 +700,7 @@ class SourceControlMixin(BaseTask):
|
|||||||
|
|
||||||
def spawn_project_sync(self, project, sync_needs, scm_branch=None):
|
def spawn_project_sync(self, project, sync_needs, scm_branch=None):
|
||||||
pu_ig = self.instance.instance_group
|
pu_ig = self.instance.instance_group
|
||||||
pu_en = Instance.objects.my_hostname()
|
pu_en = Instance.objects.me().hostname
|
||||||
|
|
||||||
sync_metafields = dict(
|
sync_metafields = dict(
|
||||||
launch_type="sync",
|
launch_type="sync",
|
||||||
|
|||||||
@@ -208,10 +208,7 @@ def run_until_complete(node, timing_data=None, **kwargs):
|
|||||||
if state_name.lower() == 'failed':
|
if state_name.lower() == 'failed':
|
||||||
work_detail = status.get('Detail', '')
|
work_detail = status.get('Detail', '')
|
||||||
if work_detail:
|
if work_detail:
|
||||||
if stdout:
|
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}')
|
||||||
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:
|
else:
|
||||||
raise RemoteJobError(f'Unknown ansible-runner error on node {node}, stdout:\n{stdout}')
|
raise RemoteJobError(f'Unknown ansible-runner error on node {node}, stdout:\n{stdout}')
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ from awx.api.versioning import reverse
|
|||||||
from awx.main.models.activity_stream import ActivityStream
|
from awx.main.models.activity_stream import ActivityStream
|
||||||
from awx.main.models.ha import Instance
|
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
|
@pytest.mark.django_db
|
||||||
@@ -56,33 +54,3 @@ def test_health_check_usage(get, post, admin_user):
|
|||||||
get(url=url, user=admin_user, expect=200)
|
get(url=url, user=admin_user, expect=200)
|
||||||
r = post(url=url, user=admin_user, expect=200)
|
r = post(url=url, user=admin_user, expect=200)
|
||||||
assert r.data['msg'] == f"Health check is running for {instance.hostname}."
|
assert r.data['msg'] == f"Health check is running for {instance.hostname}."
|
||||||
|
|
||||||
|
|
||||||
def test_custom_hostname_regex(post, admin_user):
|
|
||||||
url = reverse('api:instance_list')
|
|
||||||
with override_settings(IS_K8S=True):
|
|
||||||
for value in [
|
|
||||||
("foo.bar.baz", 201),
|
|
||||||
("f.bar.bz", 201),
|
|
||||||
("foo.bar.b", 400),
|
|
||||||
("a.b.c", 400),
|
|
||||||
("localhost", 400),
|
|
||||||
("127.0.0.1", 400),
|
|
||||||
("192.168.56.101", 201),
|
|
||||||
("2001:0db8:85a3:0000:0000:8a2e:0370:7334", 201),
|
|
||||||
("foobar", 201),
|
|
||||||
("--yoooo", 400),
|
|
||||||
("$3$@foobar@#($!@#*$", 400),
|
|
||||||
("999.999.999.999", 201),
|
|
||||||
("0000:0000:0000:0000:0000:0000:0000:0001", 400),
|
|
||||||
("whitespaces are bad for hostnames", 400),
|
|
||||||
("0:0:0:0:0:0:0:1", 400),
|
|
||||||
("192.localhost.domain.101", 201),
|
|
||||||
("F@$%(@#$H%^(I@#^HCTQEWRFG", 400),
|
|
||||||
]:
|
|
||||||
data = {
|
|
||||||
"hostname": value[0],
|
|
||||||
"node_type": "execution",
|
|
||||||
"node_state": "installed",
|
|
||||||
}
|
|
||||||
post(url=url, user=admin_user, data=data, expect=value[1])
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ def test_instance_attach_to_instance_group(post, instance_group, node_type_insta
|
|||||||
|
|
||||||
count = ActivityStream.objects.count()
|
count = ActivityStream.objects.count()
|
||||||
|
|
||||||
url = reverse('api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
|
url = reverse(f'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)
|
post(url, {'associate': True, 'id': instance.id}, admin, expect=204 if node_type != 'control' else 400)
|
||||||
|
|
||||||
new_activity = ActivityStream.objects.all()[count:]
|
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()
|
count = ActivityStream.objects.count()
|
||||||
|
|
||||||
url = reverse('api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
|
url = reverse(f'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)
|
post(url, {'disassociate': True, 'id': instance.id}, admin, expect=204 if node_type != 'control' else 400)
|
||||||
|
|
||||||
new_activity = ActivityStream.objects.all()[count:]
|
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()
|
count = ActivityStream.objects.count()
|
||||||
|
|
||||||
url = reverse('api:instance_instance_groups_list', kwargs={'pk': instance.pk})
|
url = reverse(f'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)
|
post(url, {'associate': True, 'id': instance_group.id}, admin, expect=204 if node_type != 'control' else 400)
|
||||||
|
|
||||||
new_activity = ActivityStream.objects.all()[count:]
|
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()
|
count = ActivityStream.objects.count()
|
||||||
|
|
||||||
url = reverse('api:instance_instance_groups_list', kwargs={'pk': instance.pk})
|
url = reverse(f'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)
|
post(url, {'disassociate': True, 'id': instance_group.id}, admin, expect=204 if node_type != 'control' else 400)
|
||||||
|
|
||||||
new_activity = ActivityStream.objects.all()[count:]
|
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})
|
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)
|
r = post(url, {'disassociate': True, 'id': controlplane_instance_group.id}, admin_user, expect=400)
|
||||||
assert 'Cannot disassociate hybrid instance' in str(r.data)
|
assert f'Cannot disassociate hybrid instance' in str(r.data)
|
||||||
|
|||||||
@@ -105,30 +105,6 @@ def test_encrypted_survey_answer(post, patch, admin_user, project, inventory, su
|
|||||||
assert decrypt_value(get_encryption_key('value', pk=None), schedule.extra_data['var1']) == 'bar'
|
assert decrypt_value(get_encryption_key('value', pk=None), schedule.extra_data['var1']) == 'bar'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_survey_password_default(post, patch, admin_user, project, inventory, survey_spec_factory):
|
|
||||||
job_template = JobTemplate.objects.create(
|
|
||||||
name='test-jt',
|
|
||||||
project=project,
|
|
||||||
playbook='helloworld.yml',
|
|
||||||
inventory=inventory,
|
|
||||||
ask_variables_on_launch=False,
|
|
||||||
survey_enabled=True,
|
|
||||||
survey_spec=survey_spec_factory([{'variable': 'var1', 'question_name': 'Q1', 'type': 'password', 'required': True, 'default': 'foobar'}]),
|
|
||||||
)
|
|
||||||
|
|
||||||
# test removal of $encrypted$
|
|
||||||
url = reverse('api:job_template_schedules_list', kwargs={'pk': job_template.id})
|
|
||||||
r = post(url, {'name': 'test sch', 'rrule': RRULE_EXAMPLE, 'extra_data': '{"var1": "$encrypted$"}'}, admin_user, expect=201)
|
|
||||||
schedule = Schedule.objects.get(pk=r.data['id'])
|
|
||||||
assert schedule.extra_data == {}
|
|
||||||
assert schedule.enabled is True
|
|
||||||
|
|
||||||
# test an unrelated change
|
|
||||||
patch(schedule.get_absolute_url(), data={'enabled': False}, user=admin_user, expect=200)
|
|
||||||
patch(schedule.get_absolute_url(), data={'enabled': True}, user=admin_user, expect=200)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'rrule, error',
|
'rrule, error',
|
||||||
@@ -147,19 +123,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: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
|
("DTSTART:20300308T050000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=2000", "COUNT > 999 is unsupported"), # noqa
|
||||||
# Individual rule test with multiple rules
|
# 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"),
|
("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",
|
"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",
|
"BYDAY with numeric prefix not supported",
|
||||||
), # noqa
|
), # 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",
|
"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",
|
"RRULE may not contain both COUNT and UNTIL",
|
||||||
), # noqa
|
), # 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",
|
"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",
|
"COUNT > 999 is unsupported",
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ from unittest import mock
|
|||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from awx.api.views.root import ApiVersionRootView
|
from awx.api.views import ApiVersionRootView, JobTemplateLabelList, InventoryInventorySourcesUpdate, JobTemplateSurveySpec
|
||||||
from awx.api.views import JobTemplateLabelList, InventoryInventorySourcesUpdate, JobTemplateSurveySpec
|
|
||||||
|
|
||||||
from awx.main.views import handle_error
|
from awx.main.views import handle_error
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ class TestApiRootView:
|
|||||||
endpoints = [
|
endpoints = [
|
||||||
'ping',
|
'ping',
|
||||||
'config',
|
'config',
|
||||||
# 'settings',
|
#'settings',
|
||||||
'me',
|
'me',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'organizations',
|
'organizations',
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ def test_cancel(unified_job):
|
|||||||
# Some more thought may want to go into only emitting canceled if/when the job record
|
# 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.
|
# status is changed to canceled. Unlike, currently, where it's emitted unconditionally.
|
||||||
unified_job.websocket_emit_status.assert_called_with("canceled")
|
unified_job.websocket_emit_status.assert_called_with("canceled")
|
||||||
assert [(args, kwargs) for args, kwargs in unified_job.save.call_args_list] == [
|
unified_job.save.assert_called_with(update_fields=['cancel_flag', 'start_args', 'status'])
|
||||||
((), {'update_fields': ['cancel_flag', 'start_args']}),
|
|
||||||
((), {'update_fields': ['status']}),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_cancel_job_explanation(unified_job):
|
def test_cancel_job_explanation(unified_job):
|
||||||
@@ -63,10 +60,7 @@ def test_cancel_job_explanation(unified_job):
|
|||||||
unified_job.cancel(job_explanation=job_explanation)
|
unified_job.cancel(job_explanation=job_explanation)
|
||||||
|
|
||||||
assert unified_job.job_explanation == job_explanation
|
assert unified_job.job_explanation == job_explanation
|
||||||
assert [(args, kwargs) for args, kwargs in unified_job.save.call_args_list] == [
|
unified_job.save.assert_called_with(update_fields=['cancel_flag', 'start_args', 'job_explanation', 'status'])
|
||||||
((), {'update_fields': ['cancel_flag', 'start_args', 'job_explanation']}),
|
|
||||||
((), {'update_fields': ['status']}),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_organization_copy_to_jobs():
|
def test_organization_copy_to_jobs():
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
# Copyright (c) 2017 Ansible, Inc.
|
# Copyright (c) 2017 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import pytest
|
import pytest
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import json
|
import json
|
||||||
@@ -13,13 +12,9 @@ from unittest import mock
|
|||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
from awx.main.utils import common
|
from awx.main.utils import common
|
||||||
from awx.api.validators import HostnameRegexValidator
|
|
||||||
|
|
||||||
from awx.main.models import Job, AdHocCommand, InventoryUpdate, ProjectUpdate, SystemJob, WorkflowJob, Inventory, JobTemplate, UnifiedJobTemplate, UnifiedJob
|
from awx.main.models import Job, AdHocCommand, InventoryUpdate, ProjectUpdate, SystemJob, WorkflowJob, Inventory, JobTemplate, UnifiedJobTemplate, UnifiedJob
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.regex_helper import _lazy_re_compile
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'input_, output',
|
'input_, output',
|
||||||
@@ -199,136 +194,3 @@ def test_extract_ansible_vars():
|
|||||||
redacted, var_list = common.extract_ansible_vars(json.dumps(my_dict))
|
redacted, var_list = common.extract_ansible_vars(json.dumps(my_dict))
|
||||||
assert var_list == set(['ansible_connetion_setting'])
|
assert var_list == set(['ansible_connetion_setting'])
|
||||||
assert redacted == {"foobar": "baz"}
|
assert redacted == {"foobar": "baz"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
'scm_type, url, username, password, check_special_cases, scp_format, expected',
|
|
||||||
[
|
|
||||||
# General/random cases
|
|
||||||
('git', '', True, True, True, False, ''),
|
|
||||||
('git', 'git://example.com/foo.git', True, True, True, False, 'git://example.com/foo.git'),
|
|
||||||
('git', 'http://example.com/foo.git', True, True, True, False, 'http://example.com/foo.git'),
|
|
||||||
('git', 'example.com:bar.git', True, True, True, False, 'git+ssh://example.com/bar.git'),
|
|
||||||
('git', 'user@example.com:bar.git', True, True, True, False, 'git+ssh://user@example.com/bar.git'),
|
|
||||||
('git', '127.0.0.1:bar.git', True, True, True, False, 'git+ssh://127.0.0.1/bar.git'),
|
|
||||||
('git', 'git+ssh://127.0.0.1/bar.git', True, True, True, True, '127.0.0.1:bar.git'),
|
|
||||||
('git', 'ssh://127.0.0.1:22/bar.git', True, True, True, False, 'ssh://127.0.0.1:22/bar.git'),
|
|
||||||
('git', 'ssh://root@127.0.0.1:22/bar.git', True, True, True, False, 'ssh://root@127.0.0.1:22/bar.git'),
|
|
||||||
('git', 'some/path', True, True, True, False, 'file:///some/path'),
|
|
||||||
('git', '/some/path', True, True, True, False, 'file:///some/path'),
|
|
||||||
# Invalid URLs - ensure we error properly
|
|
||||||
('cvs', 'anything', True, True, True, False, ValueError('Unsupported SCM type "cvs"')),
|
|
||||||
('svn', 'anything-without-colon-slash-slash', True, True, True, False, ValueError('Invalid svn URL')),
|
|
||||||
('git', 'http://example.com:123invalidport/foo.git', True, True, True, False, ValueError('Invalid git URL')),
|
|
||||||
('git', 'git+ssh://127.0.0.1/bar.git', True, True, True, False, ValueError('Unsupported git URL')),
|
|
||||||
('git', 'git@example.com:3000:/git/repo.git', True, True, True, False, ValueError('Invalid git URL')),
|
|
||||||
('insights', 'git://example.com/foo.git', True, True, True, False, ValueError('Unsupported insights URL')),
|
|
||||||
('svn', 'file://example/path', True, True, True, False, ValueError('Unsupported host "example" for file:// URL')),
|
|
||||||
('svn', 'svn:///example', True, True, True, False, ValueError('Host is required for svn URL')),
|
|
||||||
# Username/password cases
|
|
||||||
('git', 'https://example@example.com/bar.git', False, True, True, False, 'https://example.com/bar.git'),
|
|
||||||
('git', 'https://example@example.com/bar.git', 'user', True, True, False, 'https://user@example.com/bar.git'),
|
|
||||||
('git', 'https://example@example.com/bar.git', 'user:pw', True, True, False, 'https://user%3Apw@example.com/bar.git'),
|
|
||||||
('git', 'https://example@example.com/bar.git', False, 'pw', True, False, 'https://example.com/bar.git'),
|
|
||||||
('git', 'https://some:example@example.com/bar.git', True, False, True, False, 'https://some@example.com/bar.git'),
|
|
||||||
('git', 'https://some:example@example.com/bar.git', False, False, True, False, 'https://example.com/bar.git'),
|
|
||||||
('git', 'https://example.com/bar.git', 'user', 'pw', True, False, 'https://user:pw@example.com/bar.git'),
|
|
||||||
('git', 'https://example@example.com/bar.git', False, 'something', True, False, 'https://example.com/bar.git'),
|
|
||||||
# Special github/bitbucket cases
|
|
||||||
('git', 'notgit@github.com:ansible/awx.git', True, True, True, False, ValueError('Username must be "git" for SSH access to github.com.')),
|
|
||||||
(
|
|
||||||
'git',
|
|
||||||
'notgit@bitbucket.org:does-not-exist/example.git',
|
|
||||||
True,
|
|
||||||
True,
|
|
||||||
True,
|
|
||||||
False,
|
|
||||||
ValueError('Username must be "git" for SSH access to bitbucket.org.'),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'git',
|
|
||||||
'notgit@altssh.bitbucket.org:does-not-exist/example.git',
|
|
||||||
True,
|
|
||||||
True,
|
|
||||||
True,
|
|
||||||
False,
|
|
||||||
ValueError('Username must be "git" for SSH access to altssh.bitbucket.org.'),
|
|
||||||
),
|
|
||||||
('git', 'git:password@github.com:ansible/awx.git', True, True, True, False, 'git+ssh://git@github.com/ansible/awx.git'),
|
|
||||||
# Disabling the special handling should not raise an error
|
|
||||||
('git', 'notgit@github.com:ansible/awx.git', True, True, False, False, 'git+ssh://notgit@github.com/ansible/awx.git'),
|
|
||||||
('git', 'notgit@bitbucket.org:does-not-exist/example.git', True, True, False, False, 'git+ssh://notgit@bitbucket.org/does-not-exist/example.git'),
|
|
||||||
(
|
|
||||||
'git',
|
|
||||||
'notgit@altssh.bitbucket.org:does-not-exist/example.git',
|
|
||||||
True,
|
|
||||||
True,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
'git+ssh://notgit@altssh.bitbucket.org/does-not-exist/example.git',
|
|
||||||
),
|
|
||||||
# awx#12992 - IPv6
|
|
||||||
('git', 'http://[fd00:1234:2345:6789::11]:3000/foo.git', True, True, True, False, 'http://[fd00:1234:2345:6789::11]:3000/foo.git'),
|
|
||||||
('git', 'http://foo:bar@[fd00:1234:2345:6789::11]:3000/foo.git', True, True, True, False, 'http://foo:bar@[fd00:1234:2345:6789::11]:3000/foo.git'),
|
|
||||||
('git', 'example@[fd00:1234:2345:6789::11]:example/foo.git', True, True, True, False, 'git+ssh://example@[fd00:1234:2345:6789::11]/example/foo.git'),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_update_scm_url(scm_type, url, username, password, check_special_cases, scp_format, expected):
|
|
||||||
if isinstance(expected, Exception):
|
|
||||||
with pytest.raises(type(expected)) as excinfo:
|
|
||||||
common.update_scm_url(scm_type, url, username, password, check_special_cases, scp_format)
|
|
||||||
assert str(excinfo.value) == str(expected)
|
|
||||||
else:
|
|
||||||
assert common.update_scm_url(scm_type, url, username, password, check_special_cases, scp_format) == expected
|
|
||||||
|
|
||||||
|
|
||||||
class TestHostnameRegexValidator:
|
|
||||||
@pytest.fixture
|
|
||||||
def regex_expr(self):
|
|
||||||
return '^[a-z0-9][-a-z0-9]*$|^([a-z0-9][-a-z0-9]{0,62}[.])*[a-z0-9][-a-z0-9]{1,62}$'
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def re_flags(self):
|
|
||||||
return re.IGNORECASE
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def custom_err_message(self):
|
|
||||||
return "foobar"
|
|
||||||
|
|
||||||
def test_hostame_regex_validator_constructor_with_args(self, regex_expr, re_flags, custom_err_message):
|
|
||||||
h = HostnameRegexValidator(regex=regex_expr, flags=re_flags, message=custom_err_message)
|
|
||||||
assert h.regex == _lazy_re_compile(regex_expr, re_flags)
|
|
||||||
assert h.message == 'foobar'
|
|
||||||
assert h.code == 'invalid'
|
|
||||||
assert h.inverse_match == False
|
|
||||||
assert h.flags == re_flags
|
|
||||||
|
|
||||||
def test_hostame_regex_validator_default_constructor(self, regex_expr, re_flags):
|
|
||||||
h = HostnameRegexValidator()
|
|
||||||
assert h.regex == _lazy_re_compile(regex_expr, re_flags)
|
|
||||||
assert h.message == 'Enter a valid value.'
|
|
||||||
assert h.code == 'invalid'
|
|
||||||
assert h.inverse_match == False
|
|
||||||
assert h.flags == re_flags
|
|
||||||
|
|
||||||
def test_good_call(self, regex_expr, re_flags):
|
|
||||||
h = HostnameRegexValidator(regex=regex_expr, flags=re_flags)
|
|
||||||
assert (h("192.168.56.101"), None)
|
|
||||||
|
|
||||||
def test_bad_call(self, regex_expr, re_flags):
|
|
||||||
h = HostnameRegexValidator(regex=regex_expr, flags=re_flags)
|
|
||||||
try:
|
|
||||||
h("@#$%)$#(TUFAS_DG")
|
|
||||||
except ValidationError as e:
|
|
||||||
assert e.message is not None
|
|
||||||
|
|
||||||
def test_good_call_with_inverse(self, regex_expr, re_flags, inverse_match=True):
|
|
||||||
h = HostnameRegexValidator(regex=regex_expr, flags=re_flags, inverse_match=inverse_match)
|
|
||||||
try:
|
|
||||||
h("1.2.3.4")
|
|
||||||
except ValidationError as e:
|
|
||||||
assert e.message is not None
|
|
||||||
|
|
||||||
def test_bad_call_with_inverse(self, regex_expr, re_flags, inverse_match=True):
|
|
||||||
h = HostnameRegexValidator(regex=regex_expr, flags=re_flags, inverse_match=inverse_match)
|
|
||||||
assert (h("@#$%)$#(TUFAS_DG"), None)
|
|
||||||
|
|||||||
@@ -264,15 +264,9 @@ def update_scm_url(scm_type, url, username=True, password=True, check_special_ca
|
|||||||
userpass, hostpath = url.split('@', 1)
|
userpass, hostpath = url.split('@', 1)
|
||||||
else:
|
else:
|
||||||
userpass, hostpath = '', url
|
userpass, hostpath = '', url
|
||||||
# Handle IPv6 here. In this case, we might have hostpath of:
|
if hostpath.count(':') > 1:
|
||||||
# [fd00:1234:2345:6789::11]:example/foo.git
|
|
||||||
if hostpath.startswith('[') and ']:' in hostpath:
|
|
||||||
host, path = hostpath.split(']:', 1)
|
|
||||||
host = host + ']'
|
|
||||||
elif hostpath.count(':') > 1:
|
|
||||||
raise ValueError(_('Invalid %s URL') % scm_type)
|
raise ValueError(_('Invalid %s URL') % scm_type)
|
||||||
else:
|
host, path = hostpath.split(':', 1)
|
||||||
host, path = hostpath.split(':', 1)
|
|
||||||
# if not path.startswith('/') and not path.startswith('~/'):
|
# if not path.startswith('/') and not path.startswith('~/'):
|
||||||
# path = '~/%s' % path
|
# path = '~/%s' % path
|
||||||
# if path.startswith('/'):
|
# if path.startswith('/'):
|
||||||
@@ -331,11 +325,7 @@ def update_scm_url(scm_type, url, username=True, password=True, check_special_ca
|
|||||||
netloc = u':'.join([urllib.parse.quote(x, safe='') for x in (netloc_username, netloc_password) if x])
|
netloc = u':'.join([urllib.parse.quote(x, safe='') for x in (netloc_username, netloc_password) if x])
|
||||||
else:
|
else:
|
||||||
netloc = u''
|
netloc = u''
|
||||||
# urllib.parse strips brackets from IPv6 addresses, so we need to add them back in
|
netloc = u'@'.join(filter(None, [netloc, parts.hostname]))
|
||||||
hostname = parts.hostname
|
|
||||||
if hostname and ':' in hostname and '[' in url and ']' in url:
|
|
||||||
hostname = f'[{hostname}]'
|
|
||||||
netloc = u'@'.join(filter(None, [netloc, hostname]))
|
|
||||||
if parts.port:
|
if parts.port:
|
||||||
netloc = u':'.join([netloc, str(parts.port)])
|
netloc = u':'.join([netloc, str(parts.port)])
|
||||||
new_url = urllib.parse.urlunsplit([parts.scheme, netloc, parts.path, parts.query, parts.fragment])
|
new_url = urllib.parse.urlunsplit([parts.scheme, netloc, parts.path, parts.query, parts.fragment])
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ if settings.COLOR_LOGS is True:
|
|||||||
# logs rendered with cyan text
|
# logs rendered with cyan text
|
||||||
previous_level_map = self.level_map.copy()
|
previous_level_map = self.level_map.copy()
|
||||||
if record.name == "awx.analytics.job_lifecycle":
|
if record.name == "awx.analytics.job_lifecycle":
|
||||||
self.level_map[logging.INFO] = (None, 'cyan', True)
|
self.level_map[logging.DEBUG] = (None, 'cyan', True)
|
||||||
msg = super(ColorHandler, self).colorize(line, record)
|
msg = super(ColorHandler, self).colorize(line, record)
|
||||||
self.level_map = previous_level_map
|
self.level_map = previous_level_map
|
||||||
return msg
|
return msg
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def unwrap_broadcast_msg(payload: dict):
|
|||||||
def get_broadcast_hosts():
|
def get_broadcast_hosts():
|
||||||
Instance = apps.get_model('main', 'Instance')
|
Instance = apps.get_model('main', 'Instance')
|
||||||
instances = (
|
instances = (
|
||||||
Instance.objects.exclude(hostname=Instance.objects.my_hostname())
|
Instance.objects.exclude(hostname=Instance.objects.me().hostname)
|
||||||
.exclude(node_type='execution')
|
.exclude(node_type='execution')
|
||||||
.exclude(node_type='hop')
|
.exclude(node_type='hop')
|
||||||
.order_by('hostname')
|
.order_by('hostname')
|
||||||
@@ -47,7 +47,7 @@ def get_broadcast_hosts():
|
|||||||
|
|
||||||
def get_local_host():
|
def get_local_host():
|
||||||
Instance = apps.get_model('main', 'Instance')
|
Instance = apps.get_model('main', 'Instance')
|
||||||
return Instance.objects.my_hostname()
|
return Instance.objects.me().hostname
|
||||||
|
|
||||||
|
|
||||||
class WebsocketTask:
|
class WebsocketTask:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ __metaclass__ = type
|
|||||||
import gnupg
|
import gnupg
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
from ansible.plugins.action import ActionBase
|
from ansible.plugins.action import ActionBase
|
||||||
from ansible.utils.display import Display
|
from ansible.utils.display import Display
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ from ansible_sign.checksum import (
|
|||||||
InvalidChecksumLine,
|
InvalidChecksumLine,
|
||||||
)
|
)
|
||||||
from ansible_sign.checksum.differ import DistlibManifestChecksumFileExistenceDiffer
|
from ansible_sign.checksum.differ import DistlibManifestChecksumFileExistenceDiffer
|
||||||
from ansible_sign.signing import GPGVerifier
|
from ansible_sign.signing import *
|
||||||
|
|
||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = 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
|
# Absolute filesystem path to the directory where static file are collected via
|
||||||
# the collectstatic command.
|
# the collectstatic command.
|
||||||
@@ -360,7 +360,7 @@ REST_FRAMEWORK = {
|
|||||||
# For swagger schema generation
|
# For swagger schema generation
|
||||||
# see https://github.com/encode/django-rest-framework/pull/6532
|
# see https://github.com/encode/django-rest-framework/pull/6532
|
||||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',
|
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',
|
||||||
# 'URL_FORMAT_OVERRIDE': None,
|
#'URL_FORMAT_OVERRIDE': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
|||||||
@@ -101,5 +101,5 @@ except IOError:
|
|||||||
# The below runs AFTER all of the custom settings are imported.
|
# The below runs AFTER all of the custom settings are imported.
|
||||||
|
|
||||||
DATABASES.setdefault('default', dict()).setdefault('OPTIONS', dict()).setdefault(
|
DATABASES.setdefault('default', dict()).setdefault('OPTIONS', dict()).setdefault(
|
||||||
'application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63] # NOQA
|
'application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63]
|
||||||
) # noqa
|
) # noqa
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ import ldap
|
|||||||
# Django
|
# Django
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
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.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
|
|
||||||
# django-auth-ldap
|
# django-auth-ldap
|
||||||
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
||||||
@@ -329,32 +327,31 @@ class SAMLAuth(BaseSAMLAuth):
|
|||||||
return super(SAMLAuth, self).get_user(user_id)
|
return super(SAMLAuth, self).get_user(user_id)
|
||||||
|
|
||||||
|
|
||||||
def _update_m2m_from_groups(ldap_user, opts, remove=True):
|
def _update_m2m_from_groups(user, ldap_user, related, opts, remove=True):
|
||||||
"""
|
"""
|
||||||
Hepler function to evaluate the LDAP team/org options to determine if LDAP user should
|
Hepler function to update m2m relationship based on LDAP group membership.
|
||||||
be a member of the team/org based on their ldap group dns.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True - User should be added
|
|
||||||
False - User should be removed
|
|
||||||
None - Users membership should not be changed
|
|
||||||
"""
|
"""
|
||||||
|
should_add = False
|
||||||
if opts is None:
|
if opts is None:
|
||||||
return None
|
return
|
||||||
elif not opts:
|
elif not opts:
|
||||||
pass
|
pass
|
||||||
elif isinstance(opts, bool) and opts is True:
|
elif opts is True:
|
||||||
return True
|
should_add = True
|
||||||
else:
|
else:
|
||||||
if isinstance(opts, str):
|
if isinstance(opts, str):
|
||||||
opts = [opts]
|
opts = [opts]
|
||||||
# If any of the users groups matches any of the list options
|
|
||||||
for group_dn in opts:
|
for group_dn in opts:
|
||||||
if not isinstance(group_dn, str):
|
if not isinstance(group_dn, str):
|
||||||
continue
|
continue
|
||||||
if ldap_user._get_groups().is_member_of(group_dn):
|
if ldap_user._get_groups().is_member_of(group_dn):
|
||||||
return True
|
should_add = True
|
||||||
return False
|
if should_add:
|
||||||
|
user.save()
|
||||||
|
related.add(user)
|
||||||
|
elif remove and user in related.all():
|
||||||
|
user.save()
|
||||||
|
related.remove(user)
|
||||||
|
|
||||||
|
|
||||||
@receiver(populate_user, dispatch_uid='populate-ldap-user')
|
@receiver(populate_user, dispatch_uid='populate-ldap-user')
|
||||||
@@ -386,73 +383,31 @@ def on_populate_user(sender, **kwargs):
|
|||||||
force_user_update = True
|
force_user_update = True
|
||||||
logger.warning('LDAP user {} has {} > max {} characters'.format(user.username, field, max_len))
|
logger.warning('LDAP user {} has {} > max {} characters'.format(user.username, field, max_len))
|
||||||
|
|
||||||
|
# Update organization membership based on group memberships.
|
||||||
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
|
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
|
||||||
team_map = getattr(backend.settings, 'TEAM_MAP', {})
|
|
||||||
|
|
||||||
# Move this junk into save of the settings for performance later, there is no need to do that here
|
|
||||||
# with maybe the exception of someone defining this in settings before the server is started?
|
|
||||||
# ==============================================================================================================
|
|
||||||
|
|
||||||
# Get all of the IDs and names of orgs in the DB and create any new org defined in LDAP that does not exist in the DB
|
|
||||||
existing_orgs = {}
|
|
||||||
for (org_id, org_name) in Organization.objects.all().values_list('id', 'name'):
|
|
||||||
existing_orgs[org_name] = org_id
|
|
||||||
|
|
||||||
# Create any orgs (if needed) for all entries in the org and team maps
|
|
||||||
for org_name in set(list(org_map.keys()) + [item.get('organization', None) for item in team_map.values()]):
|
|
||||||
if org_name and org_name not in existing_orgs:
|
|
||||||
logger.info("LDAP adapter is creating org {}".format(org_name))
|
|
||||||
try:
|
|
||||||
new_org = Organization.objects.create(name=org_name)
|
|
||||||
except IntegrityError:
|
|
||||||
# Another thread must have created this org before we did so now we need to get it
|
|
||||||
new_org = Organization.objects.get(name=org_name)
|
|
||||||
# Add the org name to the existing orgs since we created it and we may need it to build the teams below
|
|
||||||
existing_orgs[org_name] = new_org.id
|
|
||||||
|
|
||||||
# Do the same for teams
|
|
||||||
existing_team_names = list(Team.objects.all().values_list('name', flat=True))
|
|
||||||
for team_name, team_opts in team_map.items():
|
|
||||||
if not team_opts.get('organization', None):
|
|
||||||
# You can't save the LDAP config in the UI w/o an org (or '' or null as the org) so if we somehow got this condition its an error
|
|
||||||
logger.error("Team named {} in LDAP team map settings is invalid due to missing organization".format(team_name))
|
|
||||||
continue
|
|
||||||
if team_name not in existing_team_names:
|
|
||||||
try:
|
|
||||||
Team.objects.create(name=team_name, organization_id=existing_orgs[team_opts['organization']])
|
|
||||||
except IntegrityError:
|
|
||||||
# If another process got here before us that is ok because we don't need the ID from this team or anything
|
|
||||||
pass
|
|
||||||
# End move some day
|
|
||||||
# ==============================================================================================================
|
|
||||||
|
|
||||||
# Compute in memory what the state is of the different LDAP orgs
|
|
||||||
org_roles_and_ldap_attributes = {'admin_role': 'admins', 'auditor_role': 'auditors', 'member_role': 'users'}
|
|
||||||
desired_org_states = {}
|
|
||||||
for org_name, org_opts in org_map.items():
|
for org_name, org_opts in org_map.items():
|
||||||
|
org, created = Organization.objects.get_or_create(name=org_name)
|
||||||
remove = bool(org_opts.get('remove', True))
|
remove = bool(org_opts.get('remove', True))
|
||||||
desired_org_states[org_name] = {}
|
admins_opts = org_opts.get('admins', None)
|
||||||
for org_role_name in org_roles_and_ldap_attributes.keys():
|
remove_admins = bool(org_opts.get('remove_admins', remove))
|
||||||
ldap_name = org_roles_and_ldap_attributes[org_role_name]
|
_update_m2m_from_groups(user, ldap_user, org.admin_role.members, admins_opts, remove_admins)
|
||||||
opts = org_opts.get(ldap_name, None)
|
auditors_opts = org_opts.get('auditors', None)
|
||||||
remove = bool(org_opts.get('remove_{}'.format(ldap_name), remove))
|
remove_auditors = bool(org_opts.get('remove_auditors', remove))
|
||||||
desired_org_states[org_name][org_role_name] = _update_m2m_from_groups(ldap_user, opts, remove)
|
_update_m2m_from_groups(user, ldap_user, org.auditor_role.members, auditors_opts, remove_auditors)
|
||||||
|
users_opts = org_opts.get('users', None)
|
||||||
|
remove_users = bool(org_opts.get('remove_users', remove))
|
||||||
|
_update_m2m_from_groups(user, ldap_user, org.member_role.members, users_opts, remove_users)
|
||||||
|
|
||||||
# If everything returned None (because there was no configuration) we can remove this org from our map
|
# Update team membership based on group memberships.
|
||||||
# This will prevent us from loading the org in the next query
|
team_map = getattr(backend.settings, 'TEAM_MAP', {})
|
||||||
if all(desired_org_states[org_name][org_role_name] is None for org_role_name in org_roles_and_ldap_attributes.keys()):
|
|
||||||
del desired_org_states[org_name]
|
|
||||||
|
|
||||||
# Compute in memory what the state is of the different LDAP teams
|
|
||||||
desired_team_states = {}
|
|
||||||
for team_name, team_opts in team_map.items():
|
for team_name, team_opts in team_map.items():
|
||||||
if 'organization' not in team_opts:
|
if 'organization' not in team_opts:
|
||||||
continue
|
continue
|
||||||
|
org, created = Organization.objects.get_or_create(name=team_opts['organization'])
|
||||||
|
team, created = Team.objects.get_or_create(name=team_name, organization=org)
|
||||||
users_opts = team_opts.get('users', None)
|
users_opts = team_opts.get('users', None)
|
||||||
remove = bool(team_opts.get('remove', True))
|
remove = bool(team_opts.get('remove', True))
|
||||||
state = _update_m2m_from_groups(ldap_user, users_opts, remove)
|
_update_m2m_from_groups(user, ldap_user, team.member_role.members, users_opts, remove)
|
||||||
if state is not None:
|
|
||||||
desired_team_states[team_name] = {'member_role': state}
|
|
||||||
|
|
||||||
# Check if user.profile is available, otherwise force user.save()
|
# Check if user.profile is available, otherwise force user.save()
|
||||||
try:
|
try:
|
||||||
@@ -468,62 +423,3 @@ def on_populate_user(sender, **kwargs):
|
|||||||
if profile.ldap_dn != ldap_user.dn:
|
if profile.ldap_dn != ldap_user.dn:
|
||||||
profile.ldap_dn = ldap_user.dn
|
profile.ldap_dn = ldap_user.dn
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, 'LDAP')
|
|
||||||
|
|
||||||
|
|
||||||
def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, source):
|
|
||||||
from awx.main.models import Organization, Team
|
|
||||||
|
|
||||||
content_types = []
|
|
||||||
reconcile_items = []
|
|
||||||
if desired_org_states:
|
|
||||||
content_types.append(ContentType.objects.get_for_model(Organization))
|
|
||||||
reconcile_items.append(('organization', desired_org_states, Organization))
|
|
||||||
if desired_team_states:
|
|
||||||
content_types.append(ContentType.objects.get_for_model(Team))
|
|
||||||
reconcile_items.append(('team', desired_team_states, Team))
|
|
||||||
|
|
||||||
if not content_types:
|
|
||||||
# If both desired states were empty we can simply return because there is nothing to reconcile
|
|
||||||
return
|
|
||||||
|
|
||||||
# users_roles is a flat set of IDs
|
|
||||||
users_roles = set(user.roles.filter(content_type__in=content_types).values_list('pk', flat=True))
|
|
||||||
|
|
||||||
for object_type, desired_states, model in reconcile_items:
|
|
||||||
# Get all of the roles in the desired states for efficient DB extraction
|
|
||||||
roles = []
|
|
||||||
for sub_dict in desired_states.values():
|
|
||||||
for role_name in sub_dict:
|
|
||||||
if sub_dict[role_name] is None:
|
|
||||||
continue
|
|
||||||
if role_name not in roles:
|
|
||||||
roles.append(role_name)
|
|
||||||
|
|
||||||
# Get a set of named tuples for the org/team name plus all of the roles we got above
|
|
||||||
model_roles = model.objects.filter(name__in=desired_states.keys()).values_list('name', *roles, named=True)
|
|
||||||
for row in model_roles:
|
|
||||||
for role_name in roles:
|
|
||||||
desired_state = desired_states.get(row.name, {})
|
|
||||||
if desired_state[role_name] is None:
|
|
||||||
# The mapping was not defined for this [org/team]/role so we can just pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
# If somehow the auth adapter knows about an items role but that role is not defined in the DB we are going to print a pretty error
|
|
||||||
# This is your classic safety net that we should never hit; but here you are reading this comment... good luck and Godspeed.
|
|
||||||
role_id = getattr(row, role_name, None)
|
|
||||||
if role_id is None:
|
|
||||||
logger.error("{} adapter wanted to manage role {} of {} {} but that role is not defined".format(source, role_name, object_type, row.name))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if desired_state[role_name]:
|
|
||||||
# The desired state was the user mapped into the object_type, if the user was not mapped in map them in
|
|
||||||
if role_id not in users_roles:
|
|
||||||
logger.debug("{} adapter adding user {} to {} {} as {}".format(source, user.username, object_type, row.name, role_name))
|
|
||||||
user.roles.add(role_id)
|
|
||||||
else:
|
|
||||||
# The desired state was the user was not mapped into the org, if the user has the permission remove it
|
|
||||||
if role_id in users_roles:
|
|
||||||
logger.debug("{} adapter removing user {} permission of {} from {} {}".format(source, user.username, role_name, object_type, row.name))
|
|
||||||
user.roles.remove(role_id)
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT = _(
|
|||||||
'''\
|
'''\
|
||||||
Mapping to organization admins/users from social auth accounts. This setting
|
Mapping to organization admins/users from social auth accounts. This setting
|
||||||
controls which users are placed into which organizations based on their
|
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.\
|
documentation.\
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ _values_to_change = ['is_superuser_value', 'is_superuser_role', 'is_system_audit
|
|||||||
|
|
||||||
def _get_setting():
|
def _get_setting():
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('SELECT value FROM conf_setting WHERE key= %s', ['SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
|
cursor.execute(f'SELECT value FROM conf_setting WHERE key= %s', ['SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row == None:
|
if row == None:
|
||||||
return {}
|
return {}
|
||||||
@@ -24,7 +24,7 @@ def _get_setting():
|
|||||||
|
|
||||||
def _set_setting(value):
|
def _set_setting(value):
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('UPDATE conf_setting SET value = %s WHERE key = %s', [json.dumps(value), 'SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
|
cursor.execute(f'UPDATE conf_setting SET value = %s WHERE key = %s', [json.dumps(value), 'SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR'])
|
||||||
|
|
||||||
|
|
||||||
def forwards(app, schema_editor):
|
def forwards(app, schema_editor):
|
||||||
|
|||||||
@@ -163,9 +163,9 @@ class TestSAMLAttr:
|
|||||||
'PersonImmutableID': [],
|
'PersonImmutableID': [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# 'social': <UserSocialAuth: cmeyers@redhat.com>,
|
#'social': <UserSocialAuth: cmeyers@redhat.com>,
|
||||||
'social': None,
|
'social': None,
|
||||||
# 'strategy': <awx.sso.strategies.django_strategy.AWXDjangoStrategy object at 0x8523a10>,
|
#'strategy': <awx.sso.strategies.django_strategy.AWXDjangoStrategy object at 0x8523a10>,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'new_association': False,
|
'new_association': False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from django.http import HttpResponse
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.views.generic.base import RedirectView
|
from django.views.generic.base import RedirectView
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
|
from awx.api.serializers import UserSerializer
|
||||||
|
from rest_framework.renderers import JSONRenderer
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
logger = logging.getLogger('awx.sso.views')
|
logger = logging.getLogger('awx.sso.views')
|
||||||
@@ -40,6 +42,9 @@ class CompleteView(BaseRedirectView):
|
|||||||
if self.request.user and self.request.user.is_authenticated:
|
if self.request.user and self.request.user.is_authenticated:
|
||||||
logger.info(smart_str(u"User {} logged in".format(self.request.user.username)))
|
logger.info(smart_str(u"User {} logged in".format(self.request.user.username)))
|
||||||
response.set_cookie('userLoggedIn', 'true')
|
response.set_cookie('userLoggedIn', 'true')
|
||||||
|
current_user = UserSerializer(self.request.user)
|
||||||
|
current_user = smart_str(JSONRenderer().render(current_user.data))
|
||||||
|
current_user = urllib.parse.quote('%s' % current_user, '')
|
||||||
response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
184
awx/ui/package-lock.json
generated
184
awx/ui/package-lock.json
generated
@@ -8,14 +8,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "3.14.0",
|
"@lingui/react": "3.14.0",
|
||||||
"@patternfly/patternfly": "4.210.2",
|
"@patternfly/patternfly": "4.210.2",
|
||||||
"@patternfly/react-core": "^4.239.0",
|
"@patternfly/react-core": "^4.221.3",
|
||||||
"@patternfly/react-icons": "4.90.0",
|
"@patternfly/react-icons": "4.75.1",
|
||||||
"@patternfly/react-table": "4.108.0",
|
"@patternfly/react-table": "4.100.8",
|
||||||
"ace-builds": "^1.10.1",
|
"ace-builds": "^1.10.1",
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"d3": "7.6.1",
|
"d3": "7.4.4",
|
||||||
"dagre": "^0.8.4",
|
"dagre": "^0.8.4",
|
||||||
"dompurify": "2.4.0",
|
"dompurify": "2.4.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"react-router-dom": "^5.3.3",
|
"react-router-dom": "^5.3.3",
|
||||||
"react-virtualized": "^9.21.1",
|
"react-virtualized": "^9.21.1",
|
||||||
"rrule": "2.7.1",
|
"rrule": "2.7.1",
|
||||||
"styled-components": "5.3.6"
|
"styled-components": "5.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.10",
|
"@babel/core": "^7.16.10",
|
||||||
@@ -3752,13 +3752,13 @@
|
|||||||
"integrity": "sha512-aZiW24Bxi6uVmk5RyNTp+6q6ThtlJZotNRJfWVeGuwu1UlbBuV4DFa1bpjA6jfTZpfEpX2YL5+R+4ZVSCFAVdw=="
|
"integrity": "sha512-aZiW24Bxi6uVmk5RyNTp+6q6ThtlJZotNRJfWVeGuwu1UlbBuV4DFa1bpjA6jfTZpfEpX2YL5+R+4ZVSCFAVdw=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-core": {
|
"node_modules/@patternfly/react-core": {
|
||||||
"version": "4.239.0",
|
"version": "4.231.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.239.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.231.8.tgz",
|
||||||
"integrity": "sha512-6CmYABCJLUXTlzCk6C3WouMNZpS0BCT+aHU8CvYpFQ/NrpYp3MJaDsYbqgCRWV42rmIO5iXun/4WhXBJzJEoQg==",
|
"integrity": "sha512-2ClqlYCvSADppMfVfkUGIA/8XlO6jX8batoClXLxZDwqGoOfr61XyUgQ6SSlE4w60czoNeX4Nf6cfQKUH4RIKw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/react-icons": "^4.90.0",
|
"@patternfly/react-icons": "^4.82.8",
|
||||||
"@patternfly/react-styles": "^4.89.0",
|
"@patternfly/react-styles": "^4.81.8",
|
||||||
"@patternfly/react-tokens": "^4.91.0",
|
"@patternfly/react-tokens": "^4.83.8",
|
||||||
"focus-trap": "6.9.2",
|
"focus-trap": "6.9.2",
|
||||||
"react-dropzone": "9.0.0",
|
"react-dropzone": "9.0.0",
|
||||||
"tippy.js": "5.1.2",
|
"tippy.js": "5.1.2",
|
||||||
@@ -3769,34 +3769,43 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0"
|
"react-dom": "^16.8.0 || ^17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@patternfly/react-core/node_modules/@patternfly/react-icons": {
|
||||||
|
"version": "4.82.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
|
||||||
|
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@patternfly/react-core/node_modules/tslib": {
|
"node_modules/@patternfly/react-core/node_modules/tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-icons": {
|
"node_modules/@patternfly/react-icons": {
|
||||||
"version": "4.90.0",
|
"version": "4.75.1",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.90.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.75.1.tgz",
|
||||||
"integrity": "sha512-qEnQKbxbUgyiosiKSkeKEBwmhgJwWEqniIAFyoxj+kpzAdeu7ueWe5iBbqo06mvDOedecFiM5mIE1N0MXwk8Yw==",
|
"integrity": "sha512-1ly8SVi/kcc0zkiViOjUd8D5BEr7GeqWGmDPuDSBtD60l1dYf3hZc44IWFVkRM/oHZML/musdrJkLfh4MDqX9w==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0",
|
"react": "^16.8.0 || ^17.0.0",
|
||||||
"react-dom": "^16.8.0 || ^17.0.0"
|
"react-dom": "^16.8.0 || ^17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-styles": {
|
"node_modules/@patternfly/react-styles": {
|
||||||
"version": "4.89.0",
|
"version": "4.81.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.89.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.81.8.tgz",
|
||||||
"integrity": "sha512-SkT+qx3Xqu70T5s+i/AUT2hI2sKAPDX4ffeiJIUDu/oyWiFdk+/9DEivnLSyJMruroXXN33zKibvzb5rH7DKTQ=="
|
"integrity": "sha512-Q5FiureSSCMIuz+KLMcEm1317TzbXcwmg2q5iNDRKyf/K+5CT6tJp0Wbtk3FlfRvzli4u/7YfXipahia5TL+tA=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-table": {
|
"node_modules/@patternfly/react-table": {
|
||||||
"version": "4.108.0",
|
"version": "4.100.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.108.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.100.8.tgz",
|
||||||
"integrity": "sha512-EUvd3rlkE1UXobAm7L6JHgNE3TW8IYTaVwwH/px4Mkn5mBayDO6f+w6QM3OeoDQVZcXK6IYFe7QQaYd/vWIJCQ==",
|
"integrity": "sha512-80XZCZzoYN9gsoufNdXUB/dk33SuWF9lUnOJs7ilezD6noTSD7ARqO1h532eaEPIbPBp4uIVkEUdfGSHd0HJtg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/react-core": "^4.239.0",
|
"@patternfly/react-core": "^4.231.8",
|
||||||
"@patternfly/react-icons": "^4.90.0",
|
"@patternfly/react-icons": "^4.82.8",
|
||||||
"@patternfly/react-styles": "^4.89.0",
|
"@patternfly/react-styles": "^4.81.8",
|
||||||
"@patternfly/react-tokens": "^4.91.0",
|
"@patternfly/react-tokens": "^4.83.8",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -3805,15 +3814,24 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0"
|
"react-dom": "^16.8.0 || ^17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@patternfly/react-table/node_modules/@patternfly/react-icons": {
|
||||||
|
"version": "4.82.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
|
||||||
|
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@patternfly/react-table/node_modules/tslib": {
|
"node_modules/@patternfly/react-table/node_modules/tslib": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-tokens": {
|
"node_modules/@patternfly/react-tokens": {
|
||||||
"version": "4.91.0",
|
"version": "4.83.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.91.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.83.8.tgz",
|
||||||
"integrity": "sha512-QeQCy8o8E/16fAr8mxqXIYRmpTsjCHJXi5p5jmgEDFmYMesN6Pqfv6N5D0FHb+CIaNOZWRps7GkWvlIMIE81sw=="
|
"integrity": "sha512-Z/MHXNY8PQOuBFGUar2yzPVbz3BNJuhB+Dnk5RJcc/iIn3S+VlSru7g6v5jqoV/+a5wLqZtLGEBp8uhCZ7Xkig=="
|
||||||
},
|
},
|
||||||
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
@@ -7464,16 +7482,16 @@
|
|||||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
||||||
},
|
},
|
||||||
"node_modules/d3": {
|
"node_modules/d3": {
|
||||||
"version": "7.6.1",
|
"version": "7.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3/-/d3-7.4.4.tgz",
|
||||||
"integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==",
|
"integrity": "sha512-97FE+MYdAlV3R9P74+R3Uar7wUKkIFu89UWMjEaDhiJ9VxKvqaMxauImy8PC2DdBkdM2BxJOIoLxPrcZUyrKoQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-array": "3",
|
"d3-array": "3",
|
||||||
"d3-axis": "3",
|
"d3-axis": "3",
|
||||||
"d3-brush": "3",
|
"d3-brush": "3",
|
||||||
"d3-chord": "3",
|
"d3-chord": "3",
|
||||||
"d3-color": "3",
|
"d3-color": "3",
|
||||||
"d3-contour": "4",
|
"d3-contour": "3",
|
||||||
"d3-delaunay": "6",
|
"d3-delaunay": "6",
|
||||||
"d3-dispatch": "3",
|
"d3-dispatch": "3",
|
||||||
"d3-drag": "3",
|
"d3-drag": "3",
|
||||||
@@ -7504,9 +7522,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
"version": "3.2.0",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.1.1.tgz",
|
||||||
"integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
|
"integrity": "sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"internmap": "1 - 2"
|
"internmap": "1 - 2"
|
||||||
},
|
},
|
||||||
@@ -7557,11 +7575,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-contour": {
|
"node_modules/d3-contour": {
|
||||||
"version": "4.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-3.0.1.tgz",
|
||||||
"integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==",
|
"integrity": "sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-array": "^3.2.0"
|
"d3-array": "2 - 3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -20216,9 +20234,9 @@
|
|||||||
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
|
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
|
||||||
},
|
},
|
||||||
"node_modules/styled-components": {
|
"node_modules/styled-components": {
|
||||||
"version": "5.3.6",
|
"version": "5.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
|
||||||
"integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==",
|
"integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
@@ -25094,19 +25112,25 @@
|
|||||||
"integrity": "sha512-aZiW24Bxi6uVmk5RyNTp+6q6ThtlJZotNRJfWVeGuwu1UlbBuV4DFa1bpjA6jfTZpfEpX2YL5+R+4ZVSCFAVdw=="
|
"integrity": "sha512-aZiW24Bxi6uVmk5RyNTp+6q6ThtlJZotNRJfWVeGuwu1UlbBuV4DFa1bpjA6jfTZpfEpX2YL5+R+4ZVSCFAVdw=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-core": {
|
"@patternfly/react-core": {
|
||||||
"version": "4.239.0",
|
"version": "4.231.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.239.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.231.8.tgz",
|
||||||
"integrity": "sha512-6CmYABCJLUXTlzCk6C3WouMNZpS0BCT+aHU8CvYpFQ/NrpYp3MJaDsYbqgCRWV42rmIO5iXun/4WhXBJzJEoQg==",
|
"integrity": "sha512-2ClqlYCvSADppMfVfkUGIA/8XlO6jX8batoClXLxZDwqGoOfr61XyUgQ6SSlE4w60czoNeX4Nf6cfQKUH4RIKw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@patternfly/react-icons": "^4.90.0",
|
"@patternfly/react-icons": "^4.82.8",
|
||||||
"@patternfly/react-styles": "^4.89.0",
|
"@patternfly/react-styles": "^4.81.8",
|
||||||
"@patternfly/react-tokens": "^4.91.0",
|
"@patternfly/react-tokens": "^4.83.8",
|
||||||
"focus-trap": "6.9.2",
|
"focus-trap": "6.9.2",
|
||||||
"react-dropzone": "9.0.0",
|
"react-dropzone": "9.0.0",
|
||||||
"tippy.js": "5.1.2",
|
"tippy.js": "5.1.2",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@patternfly/react-icons": {
|
||||||
|
"version": "4.82.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
|
||||||
|
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
@@ -25115,29 +25139,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@patternfly/react-icons": {
|
"@patternfly/react-icons": {
|
||||||
"version": "4.90.0",
|
"version": "4.75.1",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.90.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.75.1.tgz",
|
||||||
"integrity": "sha512-qEnQKbxbUgyiosiKSkeKEBwmhgJwWEqniIAFyoxj+kpzAdeu7ueWe5iBbqo06mvDOedecFiM5mIE1N0MXwk8Yw==",
|
"integrity": "sha512-1ly8SVi/kcc0zkiViOjUd8D5BEr7GeqWGmDPuDSBtD60l1dYf3hZc44IWFVkRM/oHZML/musdrJkLfh4MDqX9w==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@patternfly/react-styles": {
|
"@patternfly/react-styles": {
|
||||||
"version": "4.89.0",
|
"version": "4.81.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.89.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.81.8.tgz",
|
||||||
"integrity": "sha512-SkT+qx3Xqu70T5s+i/AUT2hI2sKAPDX4ffeiJIUDu/oyWiFdk+/9DEivnLSyJMruroXXN33zKibvzb5rH7DKTQ=="
|
"integrity": "sha512-Q5FiureSSCMIuz+KLMcEm1317TzbXcwmg2q5iNDRKyf/K+5CT6tJp0Wbtk3FlfRvzli4u/7YfXipahia5TL+tA=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-table": {
|
"@patternfly/react-table": {
|
||||||
"version": "4.108.0",
|
"version": "4.100.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.108.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.100.8.tgz",
|
||||||
"integrity": "sha512-EUvd3rlkE1UXobAm7L6JHgNE3TW8IYTaVwwH/px4Mkn5mBayDO6f+w6QM3OeoDQVZcXK6IYFe7QQaYd/vWIJCQ==",
|
"integrity": "sha512-80XZCZzoYN9gsoufNdXUB/dk33SuWF9lUnOJs7ilezD6noTSD7ARqO1h532eaEPIbPBp4uIVkEUdfGSHd0HJtg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@patternfly/react-core": "^4.239.0",
|
"@patternfly/react-core": "^4.231.8",
|
||||||
"@patternfly/react-icons": "^4.90.0",
|
"@patternfly/react-icons": "^4.82.8",
|
||||||
"@patternfly/react-styles": "^4.89.0",
|
"@patternfly/react-styles": "^4.81.8",
|
||||||
"@patternfly/react-tokens": "^4.91.0",
|
"@patternfly/react-tokens": "^4.83.8",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@patternfly/react-icons": {
|
||||||
|
"version": "4.82.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
|
||||||
|
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||||
@@ -25146,9 +25176,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@patternfly/react-tokens": {
|
"@patternfly/react-tokens": {
|
||||||
"version": "4.91.0",
|
"version": "4.83.8",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.91.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.83.8.tgz",
|
||||||
"integrity": "sha512-QeQCy8o8E/16fAr8mxqXIYRmpTsjCHJXi5p5jmgEDFmYMesN6Pqfv6N5D0FHb+CIaNOZWRps7GkWvlIMIE81sw=="
|
"integrity": "sha512-Z/MHXNY8PQOuBFGUar2yzPVbz3BNJuhB+Dnk5RJcc/iIn3S+VlSru7g6v5jqoV/+a5wLqZtLGEBp8uhCZ7Xkig=="
|
||||||
},
|
},
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": {
|
"@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
@@ -28052,16 +28082,16 @@
|
|||||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
||||||
},
|
},
|
||||||
"d3": {
|
"d3": {
|
||||||
"version": "7.6.1",
|
"version": "7.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3/-/d3-7.4.4.tgz",
|
||||||
"integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==",
|
"integrity": "sha512-97FE+MYdAlV3R9P74+R3Uar7wUKkIFu89UWMjEaDhiJ9VxKvqaMxauImy8PC2DdBkdM2BxJOIoLxPrcZUyrKoQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"d3-array": "3",
|
"d3-array": "3",
|
||||||
"d3-axis": "3",
|
"d3-axis": "3",
|
||||||
"d3-brush": "3",
|
"d3-brush": "3",
|
||||||
"d3-chord": "3",
|
"d3-chord": "3",
|
||||||
"d3-color": "3",
|
"d3-color": "3",
|
||||||
"d3-contour": "4",
|
"d3-contour": "3",
|
||||||
"d3-delaunay": "6",
|
"d3-delaunay": "6",
|
||||||
"d3-dispatch": "3",
|
"d3-dispatch": "3",
|
||||||
"d3-drag": "3",
|
"d3-drag": "3",
|
||||||
@@ -28089,9 +28119,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"d3-array": {
|
"d3-array": {
|
||||||
"version": "3.2.0",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.1.1.tgz",
|
||||||
"integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
|
"integrity": "sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"internmap": "1 - 2"
|
"internmap": "1 - 2"
|
||||||
}
|
}
|
||||||
@@ -28127,11 +28157,11 @@
|
|||||||
"integrity": "sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw=="
|
"integrity": "sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw=="
|
||||||
},
|
},
|
||||||
"d3-contour": {
|
"d3-contour": {
|
||||||
"version": "4.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-3.0.1.tgz",
|
||||||
"integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==",
|
"integrity": "sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"d3-array": "^3.2.0"
|
"d3-array": "2 - 3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"d3-delaunay": {
|
"d3-delaunay": {
|
||||||
@@ -37675,9 +37705,9 @@
|
|||||||
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
|
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
|
||||||
},
|
},
|
||||||
"styled-components": {
|
"styled-components": {
|
||||||
"version": "5.3.6",
|
"version": "5.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
|
||||||
"integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==",
|
"integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
"@babel/traverse": "^7.4.5",
|
"@babel/traverse": "^7.4.5",
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "3.14.0",
|
"@lingui/react": "3.14.0",
|
||||||
"@patternfly/patternfly": "4.210.2",
|
"@patternfly/patternfly": "4.210.2",
|
||||||
"@patternfly/react-core": "^4.239.0",
|
"@patternfly/react-core": "^4.221.3",
|
||||||
"@patternfly/react-icons": "4.90.0",
|
"@patternfly/react-icons": "4.75.1",
|
||||||
"@patternfly/react-table": "4.108.0",
|
"@patternfly/react-table": "4.100.8",
|
||||||
"ace-builds": "^1.10.1",
|
"ace-builds": "^1.10.1",
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"d3": "7.6.1",
|
"d3": "7.4.4",
|
||||||
"dagre": "^0.8.4",
|
"dagre": "^0.8.4",
|
||||||
"dompurify": "2.4.0",
|
"dompurify": "2.4.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"react-router-dom": "^5.3.3",
|
"react-router-dom": "^5.3.3",
|
||||||
"react-virtualized": "^9.21.1",
|
"react-virtualized": "^9.21.1",
|
||||||
"rrule": "2.7.1",
|
"rrule": "2.7.1",
|
||||||
"styled-components": "5.3.6"
|
"styled-components": "5.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.10",
|
"@babel/core": "^7.16.10",
|
||||||
|
|||||||
@@ -3,12 +3,7 @@ import { Plural, t } from '@lingui/macro';
|
|||||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||||
import { useKebabifiedMenu } from 'contexts/Kebabified';
|
import { useKebabifiedMenu } from 'contexts/Kebabified';
|
||||||
|
|
||||||
function HealthCheckButton({
|
function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
|
||||||
isDisabled,
|
|
||||||
onClick,
|
|
||||||
selectedItems,
|
|
||||||
healthCheckPending,
|
|
||||||
}) {
|
|
||||||
const { isKebabified } = useKebabifiedMenu();
|
const { isKebabified } = useKebabifiedMenu();
|
||||||
|
|
||||||
const selectedItemsCount = selectedItems.length;
|
const selectedItemsCount = selectedItems.length;
|
||||||
@@ -33,10 +28,8 @@ function HealthCheckButton({
|
|||||||
component="button"
|
component="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
ouiaId="health-check"
|
ouiaId="health-check"
|
||||||
isLoading={healthCheckPending}
|
|
||||||
spinnerAriaLabel={t`Running health check`}
|
|
||||||
>
|
>
|
||||||
{healthCheckPending ? t`Running health check` : t`Run health check`}
|
{t`Run health check`}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -49,11 +42,7 @@ function HealthCheckButton({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
ouiaId="health-check"
|
ouiaId="health-check"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
isLoading={healthCheckPending}
|
>{t`Run health check`}</Button>
|
||||||
spinnerAriaLabel={t`Running health check`}
|
|
||||||
>
|
|
||||||
{healthCheckPending ? t`Running health check` : t`Run health check`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -107,17 +107,6 @@ function LaunchButton({ resource, children }) {
|
|||||||
jobPromise = JobsAPI.relaunch(resource.id, params || {});
|
jobPromise = JobsAPI.relaunch(resource.id, params || {});
|
||||||
} else if (resource.type === 'workflow_job') {
|
} else if (resource.type === 'workflow_job') {
|
||||||
jobPromise = WorkflowJobsAPI.relaunch(resource.id, params || {});
|
jobPromise = WorkflowJobsAPI.relaunch(resource.id, params || {});
|
||||||
} else if (resource.type === 'ad_hoc_command') {
|
|
||||||
if (params?.credential_passwords) {
|
|
||||||
// The api expects the passwords at the top level of the object instead of nested
|
|
||||||
// in credential_passwords like the other relaunch endpoints
|
|
||||||
Object.keys(params.credential_passwords).forEach((key) => {
|
|
||||||
params[key] = params.credential_passwords[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
delete params.credential_passwords;
|
|
||||||
}
|
|
||||||
jobPromise = AdHocCommandsAPI.relaunch(resource.id, params || {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: job } = await jobPromise;
|
const { data: job } = await jobPromise;
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ function PromptModalForm({
|
|||||||
}}
|
}}
|
||||||
title={t`Launch | ${resource.name}`}
|
title={t`Launch | ${resource.name}`}
|
||||||
description={
|
description={
|
||||||
resource.description?.length > 512 ? (
|
resource.description.length > 512 ? (
|
||||||
<ExpandableSection
|
<ExpandableSection
|
||||||
toggleText={
|
toggleText={
|
||||||
showDescription ? t`Hide description` : t`Show description`
|
showDescription ? t`Hide description` : t`Show description`
|
||||||
|
|||||||
@@ -67,14 +67,14 @@ function ScheduleForm({
|
|||||||
if (schedule.id) {
|
if (schedule.id) {
|
||||||
if (
|
if (
|
||||||
resource.type === 'job_template' &&
|
resource.type === 'job_template' &&
|
||||||
launchConfig?.ask_credential_on_launch
|
launchConfig.ask_credential_on_launch
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
} = await SchedulesAPI.readCredentials(schedule.id);
|
} = await SchedulesAPI.readCredentials(schedule.id);
|
||||||
creds = results;
|
creds = results;
|
||||||
}
|
}
|
||||||
if (launchConfig?.ask_labels_on_launch) {
|
if (launchConfig.ask_labels_on_launch) {
|
||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
} = await SchedulesAPI.readAllLabels(schedule.id);
|
} = await SchedulesAPI.readAllLabels(schedule.id);
|
||||||
@@ -82,7 +82,7 @@ function ScheduleForm({
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
resource.type === 'job_template' &&
|
resource.type === 'job_template' &&
|
||||||
launchConfig?.ask_instance_groups_on_launch
|
launchConfig.ask_instance_groups_on_launch
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
@@ -91,7 +91,7 @@ function ScheduleForm({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (resource.type === 'job_template') {
|
if (resource.type === 'job_template') {
|
||||||
if (launchConfig?.ask_labels_on_launch) {
|
if (launchConfig.ask_labels_on_launch) {
|
||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
} = await JobTemplatesAPI.readAllLabels(resource.id);
|
} = await JobTemplatesAPI.readAllLabels(resource.id);
|
||||||
@@ -100,7 +100,7 @@ function ScheduleForm({
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
resource.type === 'workflow_job_template' &&
|
resource.type === 'workflow_job_template' &&
|
||||||
launchConfig?.ask_labels_on_launch
|
launchConfig.ask_labels_on_launch
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
@@ -123,7 +123,14 @@ function ScheduleForm({
|
|||||||
zoneLinks: data.links,
|
zoneLinks: data.links,
|
||||||
credentials: creds,
|
credentials: creds,
|
||||||
};
|
};
|
||||||
}, [schedule, resource.id, resource.type, launchConfig]),
|
}, [
|
||||||
|
schedule,
|
||||||
|
resource.id,
|
||||||
|
resource.type,
|
||||||
|
launchConfig.ask_labels_on_launch,
|
||||||
|
launchConfig.ask_instance_groups_on_launch,
|
||||||
|
launchConfig.ask_credential_on_launch,
|
||||||
|
]),
|
||||||
{
|
{
|
||||||
zonesOptions: [],
|
zonesOptions: [],
|
||||||
zoneLinks: {},
|
zoneLinks: {},
|
||||||
@@ -139,7 +146,7 @@ function ScheduleForm({
|
|||||||
const missingRequiredInventory = useCallback(() => {
|
const missingRequiredInventory = useCallback(() => {
|
||||||
let missingInventory = false;
|
let missingInventory = false;
|
||||||
if (
|
if (
|
||||||
launchConfig?.inventory_needed_to_start &&
|
launchConfig.inventory_needed_to_start &&
|
||||||
!schedule?.summary_fields?.inventory?.id
|
!schedule?.summary_fields?.inventory?.id
|
||||||
) {
|
) {
|
||||||
missingInventory = true;
|
missingInventory = true;
|
||||||
@@ -416,14 +423,8 @@ function ScheduleForm({
|
|||||||
|
|
||||||
if (options.end === 'onDate') {
|
if (options.end === 'onDate') {
|
||||||
if (
|
if (
|
||||||
DateTime.fromFormat(
|
DateTime.fromISO(values.startDate) >=
|
||||||
`${values.startDate} ${values.startTime}`,
|
DateTime.fromISO(options.endDate)
|
||||||
'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.`;
|
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -900,36 +900,6 @@ 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 () => {
|
test('error shown when on day number is not between 1 and 31', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('FrequencySelect#schedule-frequency').invoke('onChange')([
|
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
@@ -12,7 +12,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Slider,
|
Slider,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { CaretLeftIcon, OutlinedClockIcon } from '@patternfly/react-icons';
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { useConfig } from 'contexts/Config';
|
import { useConfig } from 'contexts/Config';
|
||||||
@@ -23,7 +23,6 @@ import ErrorDetail from 'components/ErrorDetail';
|
|||||||
import DisassociateButton from 'components/DisassociateButton';
|
import DisassociateButton from 'components/DisassociateButton';
|
||||||
import InstanceToggle from 'components/InstanceToggle';
|
import InstanceToggle from 'components/InstanceToggle';
|
||||||
import { CardBody, CardActionsRow } from 'components/Card';
|
import { CardBody, CardActionsRow } from 'components/Card';
|
||||||
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import RoutedTabs from 'components/RoutedTabs';
|
import RoutedTabs from 'components/RoutedTabs';
|
||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
@@ -63,7 +62,7 @@ function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
||||||
const config = useConfig();
|
const { me = {} } = useConfig();
|
||||||
const { id, instanceId } = useParams();
|
const { id, instanceId } = useParams();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -116,9 +115,15 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDetails();
|
fetchDetails();
|
||||||
}, [fetchDetails]);
|
}, [fetchDetails]);
|
||||||
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
|
const {
|
||||||
|
error: healthCheckError,
|
||||||
|
isLoading: isRunningHealthCheck,
|
||||||
|
request: fetchHealthCheck,
|
||||||
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const { status } = await InstancesAPI.healthCheck(instanceId);
|
const { status } = await InstancesAPI.healthCheck(instanceId);
|
||||||
|
const { data } = await InstancesAPI.readHealthCheckDetail(instanceId);
|
||||||
|
setHealthCheck(data);
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
setShowHealthCheckAlert(true);
|
setShowHealthCheckAlert(true);
|
||||||
}
|
}
|
||||||
@@ -156,18 +161,6 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatHealthCheckTimeStamp = (last) => (
|
|
||||||
<>
|
|
||||||
{formatDateString(last)}
|
|
||||||
{instance.health_check_pending ? (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<OutlinedClockIcon />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(
|
const { error, dismissError } = useDismissableError(
|
||||||
disassociateError || updateInstanceError || healthCheckError
|
disassociateError || updateInstanceError || healthCheckError
|
||||||
);
|
);
|
||||||
@@ -196,8 +189,6 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExecutionNode = instance.node_type === 'execution';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RoutedTabs tabsArray={tabsArray} />
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
@@ -227,22 +218,7 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
|
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
|
||||||
<Detail
|
<Detail
|
||||||
label={t`Last Health Check`}
|
label={t`Last Health Check`}
|
||||||
helpText={
|
value={formatDateString(healthCheck?.last_health_check)}
|
||||||
<>
|
|
||||||
{t`Health checks are asynchronous tasks. See the`}{' '}
|
|
||||||
<a
|
|
||||||
href={`${getDocsBaseUrl(
|
|
||||||
config
|
|
||||||
)}/html/administration/instances.html#health-check`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t`documentation`}
|
|
||||||
</a>{' '}
|
|
||||||
{t`for more info.`}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
value={formatHealthCheckTimeStamp(instance.last_health_check)}
|
|
||||||
/>
|
/>
|
||||||
<Detail label={t`Node Type`} value={instance.node_type} />
|
<Detail label={t`Node Type`} value={instance.node_type} />
|
||||||
<Detail
|
<Detail
|
||||||
@@ -261,7 +237,7 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={!config?.me?.is_superuser || !instance.enabled}
|
isDisabled={!me?.is_superuser || !instance.enabled}
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -298,25 +274,19 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
)}
|
)}
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
{isExecutionNode && (
|
<Tooltip content={t`Run a health check on the instance`}>
|
||||||
<Tooltip content={t`Run a health check on the instance`}>
|
<Button
|
||||||
<Button
|
isDisabled={!me.is_superuser || isRunningHealthCheck}
|
||||||
isDisabled={
|
variant="primary"
|
||||||
!config?.me?.is_superuser || instance.health_check_pending
|
ouiaId="health-check-button"
|
||||||
}
|
onClick={fetchHealthCheck}
|
||||||
variant="primary"
|
isLoading={isRunningHealthCheck}
|
||||||
ouiaId="health-check-button"
|
spinnerAriaLabel={t`Running health check`}
|
||||||
onClick={fetchHealthCheck}
|
>
|
||||||
isLoading={instance.health_check_pending}
|
{t`Run health check`}
|
||||||
spinnerAriaLabel={t`Running health check`}
|
</Button>
|
||||||
>
|
</Tooltip>
|
||||||
{instance.health_check_pending
|
{me.is_superuser && instance.node_type !== 'control' && (
|
||||||
? t`Running health check`
|
|
||||||
: t`Run health check`}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{config?.me?.is_superuser && instance.node_type !== 'control' && (
|
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
verifyCannotDisassociate={instanceGroup.name === 'controlplane'}
|
verifyCannotDisassociate={instanceGroup.name === 'controlplane'}
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
|
|||||||
@@ -87,9 +87,8 @@ describe('<InstanceDetails/>', () => {
|
|||||||
mem_capacity: 38,
|
mem_capacity: 38,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
managed_by_policy: true,
|
managed_by_policy: true,
|
||||||
node_type: 'execution',
|
node_type: 'hybrid',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
health_check_pending: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InstancesAPI.readHealthCheckDetail.mockResolvedValue({
|
InstancesAPI.readHealthCheckDetail.mockResolvedValue({
|
||||||
@@ -348,67 +347,6 @@ describe('<InstanceDetails/>', () => {
|
|||||||
expect(wrapper.find('ErrorDetail')).toHaveLength(1);
|
expect(wrapper.find('ErrorDetail')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([
|
|
||||||
[1, 'hybrid', 0],
|
|
||||||
[2, 'hop', 0],
|
|
||||||
[3, 'control', 0],
|
|
||||||
])(
|
|
||||||
'hide health check button for non-execution type nodes',
|
|
||||||
async (a, b, expected) => {
|
|
||||||
InstancesAPI.readDetail.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
id: a,
|
|
||||||
type: 'instance',
|
|
||||||
url: '/api/v2/instances/1/',
|
|
||||||
related: {
|
|
||||||
named_url: '/api/v2/instances/awx_1/',
|
|
||||||
jobs: '/api/v2/instances/1/jobs/',
|
|
||||||
instance_groups: '/api/v2/instances/1/instance_groups/',
|
|
||||||
health_check: '/api/v2/instances/1/health_check/',
|
|
||||||
},
|
|
||||||
uuid: '00000000-0000-0000-0000-000000000000',
|
|
||||||
hostname: 'awx_1',
|
|
||||||
created: '2021-09-08T17:10:34.484569Z',
|
|
||||||
modified: '2021-09-09T13:55:44.219900Z',
|
|
||||||
last_seen: '2021-09-09T20:20:31.623148Z',
|
|
||||||
last_health_check: '2021-09-09T20:20:31.623148Z',
|
|
||||||
errors: '',
|
|
||||||
capacity_adjustment: '1.00',
|
|
||||||
version: '19.1.0',
|
|
||||||
capacity: 38,
|
|
||||||
consumed_capacity: 0,
|
|
||||||
percent_capacity_remaining: 100.0,
|
|
||||||
jobs_running: 0,
|
|
||||||
jobs_total: 0,
|
|
||||||
cpu: 8,
|
|
||||||
memory: 6232231936,
|
|
||||||
cpu_capacity: 32,
|
|
||||||
mem_capacity: 38,
|
|
||||||
enabled: true,
|
|
||||||
managed_by_policy: true,
|
|
||||||
node_type: b,
|
|
||||||
node_state: 'ready',
|
|
||||||
health_check_pending: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
jest.spyOn(ConfigContext, 'useConfig').mockImplementation(() => ({
|
|
||||||
me: { is_superuser: true },
|
|
||||||
}));
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<InstanceDetails
|
|
||||||
instanceGroup={instanceGroup}
|
|
||||||
setBreadcrumb={() => {}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
|
||||||
expect(wrapper.find("Button[ouiaId='health-check-button']")).toHaveLength(
|
|
||||||
expected
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
test('Should call disassociate', async () => {
|
test('Should call disassociate', async () => {
|
||||||
InstanceGroupsAPI.readInstances.mockResolvedValue({
|
InstanceGroupsAPI.readInstances.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ const QS_CONFIG = getQSConfig('instance', {
|
|||||||
function InstanceList({ instanceGroup }) {
|
function InstanceList({ instanceGroup }) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
|
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
|
||||||
const [pendingHealthCheck, setPendingHealthCheck] = useState(false);
|
|
||||||
const [canRunHealthCheck, setCanRunHealthCheck] = useState(true);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { id: instanceGroupId } = useParams();
|
const { id: instanceGroupId } = useParams();
|
||||||
|
|
||||||
@@ -58,10 +56,6 @@ function InstanceList({ instanceGroup }) {
|
|||||||
InstanceGroupsAPI.readInstances(instanceGroupId, params),
|
InstanceGroupsAPI.readInstances(instanceGroupId, params),
|
||||||
InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
|
InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
|
||||||
]);
|
]);
|
||||||
const isPending = response.data.results.some(
|
|
||||||
(i) => i.health_check_pending === true
|
|
||||||
);
|
|
||||||
setPendingHealthCheck(isPending);
|
|
||||||
return {
|
return {
|
||||||
instances: response.data.results,
|
instances: response.data.results,
|
||||||
count: response.data.count,
|
count: response.data.count,
|
||||||
@@ -96,7 +90,7 @@ function InstanceList({ instanceGroup }) {
|
|||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [...response] = await Promise.all(
|
const [...response] = await Promise.all(
|
||||||
selected
|
selected
|
||||||
.filter(({ node_type }) => node_type === 'execution')
|
.filter(({ node_type }) => node_type !== 'hop')
|
||||||
.map(({ id }) => InstancesAPI.healthCheck(id))
|
.map(({ id }) => InstancesAPI.healthCheck(id))
|
||||||
);
|
);
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -105,18 +99,6 @@ function InstanceList({ instanceGroup }) {
|
|||||||
}, [selected])
|
}, [selected])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selected) {
|
|
||||||
selected.forEach((i) => {
|
|
||||||
if (i.node_type === 'execution') {
|
|
||||||
setCanRunHealthCheck(true);
|
|
||||||
} else {
|
|
||||||
setCanRunHealthCheck(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selected]);
|
|
||||||
|
|
||||||
const handleHealthCheck = async () => {
|
const handleHealthCheck = async () => {
|
||||||
await fetchHealthCheck();
|
await fetchHealthCheck();
|
||||||
clearSelected();
|
clearSelected();
|
||||||
@@ -264,10 +246,9 @@ function InstanceList({ instanceGroup }) {
|
|||||||
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
|
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
|
||||||
/>,
|
/>,
|
||||||
<HealthCheckButton
|
<HealthCheckButton
|
||||||
isDisabled={!canAdd || !canRunHealthCheck}
|
isDisabled={!canAdd}
|
||||||
onClick={handleHealthCheck}
|
onClick={handleHealthCheck}
|
||||||
selectedItems={selected}
|
selectedItems={selected}
|
||||||
healthCheckPending={pendingHealthCheck}
|
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
emptyStateControls={
|
emptyStateControls={
|
||||||
@@ -282,10 +263,7 @@ function InstanceList({ instanceGroup }) {
|
|||||||
)}
|
)}
|
||||||
headerRow={
|
headerRow={
|
||||||
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
||||||
<HeaderCell
|
<HeaderCell sortKey="hostname">{t`Name`}</HeaderCell>
|
||||||
tooltip={t`Health checks can only be run on execution nodes.`}
|
|
||||||
sortKey="hostname"
|
|
||||||
>{t`Name`}</HeaderCell>
|
|
||||||
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
|
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
|
||||||
<HeaderCell sortKey="node_type">{t`Node Type`}</HeaderCell>
|
<HeaderCell sortKey="node_type">{t`Node Type`}</HeaderCell>
|
||||||
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
|
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ describe('<InstanceList/>', () => {
|
|||||||
await act(async () =>
|
await act(async () =>
|
||||||
wrapper.find('Button[ouiaId="health-check"]').prop('onClick')()
|
wrapper.find('Button[ouiaId="health-check"]').prop('onClick')()
|
||||||
);
|
);
|
||||||
expect(InstancesAPI.healthCheck).toBeCalledTimes(1);
|
expect(InstancesAPI.healthCheck).toBeCalledTimes(3);
|
||||||
});
|
});
|
||||||
test('should render health check error', async () => {
|
test('should render health check error', async () => {
|
||||||
InstancesAPI.healthCheck.mockRejectedValue(
|
InstancesAPI.healthCheck.mockRejectedValue(
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { OutlinedClockIcon } from '@patternfly/react-icons';
|
|
||||||
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||||
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
||||||
import InstanceToggle from 'components/InstanceToggle';
|
import InstanceToggle from 'components/InstanceToggle';
|
||||||
@@ -54,7 +52,7 @@ function InstanceListItem({
|
|||||||
fetchInstances,
|
fetchInstances,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
}) {
|
}) {
|
||||||
const config = useConfig();
|
const { me = {} } = useConfig();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [forks, setForks] = useState(
|
const [forks, setForks] = useState(
|
||||||
computeForks(
|
computeForks(
|
||||||
@@ -102,18 +100,6 @@ function InstanceListItem({
|
|||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatHealthCheckTimeStamp = (last) => (
|
|
||||||
<>
|
|
||||||
{formatDateString(last)}
|
|
||||||
{instance.health_check_pending ? (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<OutlinedClockIcon />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
@@ -168,7 +154,7 @@ function InstanceListItem({
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={!config?.me?.is_superuser || !instance.enabled}
|
isDisabled={!me?.is_superuser || !instance.enabled}
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -220,22 +206,7 @@ function InstanceListItem({
|
|||||||
<Detail
|
<Detail
|
||||||
data-cy="last-health-check"
|
data-cy="last-health-check"
|
||||||
label={t`Last Health Check`}
|
label={t`Last Health Check`}
|
||||||
helpText={
|
value={formatDateString(instance.last_health_check)}
|
||||||
<>
|
|
||||||
{t`Health checks are asynchronous tasks. See the`}{' '}
|
|
||||||
<a
|
|
||||||
href={`${getDocsBaseUrl(
|
|
||||||
config
|
|
||||||
)}/html/administration/instances.html#health-check`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t`documentation`}
|
|
||||||
</a>{' '}
|
|
||||||
{t`for more info.`}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
value={formatHealthCheckTimeStamp(instance.last_health_check)}
|
|
||||||
/>
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
</ExpandableRowContent>
|
</ExpandableRowContent>
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ describe('<InstanceListItem/>', () => {
|
|||||||
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
||||||
'Auto'
|
'Auto'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('Detail[label="Last Health Check"]').text()).toBe(
|
expect(
|
||||||
'Last Health Check9/15/2021, 6:02:07 PM'
|
wrapper.find('Detail[label="Last Health Check"]').prop('value')
|
||||||
);
|
).toBe('9/15/2021, 6:02:07 PM');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Label,
|
Label,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { DownloadIcon, OutlinedClockIcon } from '@patternfly/react-icons';
|
import { DownloadIcon } from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { useConfig } from 'contexts/Config';
|
import { useConfig } from 'contexts/Config';
|
||||||
@@ -23,7 +23,6 @@ import AlertModal from 'components/AlertModal';
|
|||||||
import ErrorDetail from 'components/ErrorDetail';
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
import InstanceToggle from 'components/InstanceToggle';
|
import InstanceToggle from 'components/InstanceToggle';
|
||||||
import { CardBody, CardActionsRow } from 'components/Card';
|
import { CardBody, CardActionsRow } from 'components/Card';
|
||||||
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
import ContentLoading from 'components/ContentLoading';
|
import ContentLoading from 'components/ContentLoading';
|
||||||
@@ -63,8 +62,7 @@ function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InstanceDetail({ setBreadcrumb, isK8s }) {
|
function InstanceDetail({ setBreadcrumb, isK8s }) {
|
||||||
const config = useConfig();
|
const { me = {} } = useConfig();
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [forks, setForks] = useState();
|
const [forks, setForks] = useState();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -87,7 +85,8 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
InstancesAPI.readDetail(id),
|
InstancesAPI.readDetail(id),
|
||||||
InstancesAPI.readInstanceGroup(id),
|
InstancesAPI.readInstanceGroup(id),
|
||||||
]);
|
]);
|
||||||
if (details.node_type === 'execution') {
|
|
||||||
|
if (details.node_type !== 'hop') {
|
||||||
const { data: healthCheckData } =
|
const { data: healthCheckData } =
|
||||||
await InstancesAPI.readHealthCheckDetail(id);
|
await InstancesAPI.readHealthCheckDetail(id);
|
||||||
setHealthCheck(healthCheckData);
|
setHealthCheck(healthCheckData);
|
||||||
@@ -116,9 +115,15 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
setBreadcrumb(instance);
|
setBreadcrumb(instance);
|
||||||
}
|
}
|
||||||
}, [instance, setBreadcrumb]);
|
}, [instance, setBreadcrumb]);
|
||||||
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
|
const {
|
||||||
|
error: healthCheckError,
|
||||||
|
isLoading: isRunningHealthCheck,
|
||||||
|
request: fetchHealthCheck,
|
||||||
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const { status } = await InstancesAPI.healthCheck(id);
|
const { status } = await InstancesAPI.healthCheck(id);
|
||||||
|
const { data } = await InstancesAPI.readHealthCheckDetail(id);
|
||||||
|
setHealthCheck(data);
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
setShowHealthCheckAlert(true);
|
setShowHealthCheckAlert(true);
|
||||||
}
|
}
|
||||||
@@ -144,18 +149,6 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatHealthCheckTimeStamp = (last) => (
|
|
||||||
<>
|
|
||||||
{formatDateString(last)}
|
|
||||||
{instance.health_check_pending ? (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<OutlinedClockIcon />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildLinkURL = (inst) =>
|
const buildLinkURL = (inst) =>
|
||||||
inst.is_container_group
|
inst.is_container_group
|
||||||
? '/instance_groups/container_group/'
|
? '/instance_groups/container_group/'
|
||||||
@@ -186,7 +179,6 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
const isHopNode = instance.node_type === 'hop';
|
const isHopNode = instance.node_type === 'hop';
|
||||||
const isExecutionNode = instance.node_type === 'execution';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -250,22 +242,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
<Detail
|
<Detail
|
||||||
label={t`Last Health Check`}
|
label={t`Last Health Check`}
|
||||||
dataCy="last-health-check"
|
dataCy="last-health-check"
|
||||||
helpText={
|
value={formatDateString(healthCheck?.last_health_check)}
|
||||||
<>
|
|
||||||
{t`Health checks are asynchronous tasks. See the`}{' '}
|
|
||||||
<a
|
|
||||||
href={`${getDocsBaseUrl(
|
|
||||||
config
|
|
||||||
)}/html/administration/instances.html#health-check`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t`documentation`}
|
|
||||||
</a>{' '}
|
|
||||||
{t`for more info.`}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
value={formatHealthCheckTimeStamp(instance.last_health_check)}
|
|
||||||
/>
|
/>
|
||||||
{instance.related?.install_bundle && (
|
{instance.related?.install_bundle && (
|
||||||
<Detail
|
<Detail
|
||||||
@@ -303,9 +280,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={
|
isDisabled={!me?.is_superuser || !instance.enabled}
|
||||||
!config?.me?.is_superuser || !instance.enabled
|
|
||||||
}
|
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -349,7 +324,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
</DetailList>
|
</DetailList>
|
||||||
{!isHopNode && (
|
{!isHopNode && (
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
{config?.me?.is_superuser && isK8s && isExecutionNode && (
|
{me.is_superuser && isK8s && instance.node_type === 'execution' && (
|
||||||
<RemoveInstanceButton
|
<RemoveInstanceButton
|
||||||
dataCy="remove-instance-button"
|
dataCy="remove-instance-button"
|
||||||
itemsToRemove={[instance]}
|
itemsToRemove={[instance]}
|
||||||
@@ -357,24 +332,18 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
onRemove={removeInstances}
|
onRemove={removeInstances}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isExecutionNode && (
|
<Tooltip content={t`Run a health check on the instance`}>
|
||||||
<Tooltip content={t`Run a health check on the instance`}>
|
<Button
|
||||||
<Button
|
isDisabled={!me.is_superuser || isRunningHealthCheck}
|
||||||
isDisabled={
|
variant="primary"
|
||||||
!config?.me?.is_superuser || instance.health_check_pending
|
ouiaId="health-check-button"
|
||||||
}
|
onClick={fetchHealthCheck}
|
||||||
variant="primary"
|
isLoading={isRunningHealthCheck}
|
||||||
ouiaId="health-check-button"
|
spinnerAriaLabel={t`Running health check`}
|
||||||
onClick={fetchHealthCheck}
|
>
|
||||||
isLoading={instance.health_check_pending}
|
{t`Run health check`}
|
||||||
spinnerAriaLabel={t`Running health check`}
|
</Button>
|
||||||
>
|
</Tooltip>
|
||||||
{instance.health_check_pending
|
|
||||||
? t`Running health check`
|
|
||||||
: t`Run health check`}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<InstanceToggle
|
<InstanceToggle
|
||||||
css="display: inline-flex;"
|
css="display: inline-flex;"
|
||||||
fetchInstances={fetchDetails}
|
fetchInstances={fetchDetails}
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ describe('<InstanceDetail/>', () => {
|
|||||||
mem_capacity: 38,
|
mem_capacity: 38,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
managed_by_policy: true,
|
managed_by_policy: true,
|
||||||
node_type: 'execution',
|
node_type: 'hybrid',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
health_check_pending: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InstancesAPI.readInstanceGroup.mockResolvedValue({
|
InstancesAPI.readInstanceGroup.mockResolvedValue({
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ function InstanceList() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
|
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
|
||||||
const [pendingHealthCheck, setPendingHealthCheck] = useState(false);
|
|
||||||
const [canRunHealthCheck, setCanRunHealthCheck] = useState(true);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { instances, count, relatedSearchableKeys, searchableKeys, isK8s },
|
result: { instances, count, relatedSearchableKeys, searchableKeys, isK8s },
|
||||||
@@ -53,10 +51,6 @@ function InstanceList() {
|
|||||||
InstancesAPI.readOptions(),
|
InstancesAPI.readOptions(),
|
||||||
SettingsAPI.readCategory('system'),
|
SettingsAPI.readCategory('system'),
|
||||||
]);
|
]);
|
||||||
const isPending = response.data.results.some(
|
|
||||||
(i) => i.health_check_pending === true
|
|
||||||
);
|
|
||||||
setPendingHealthCheck(isPending);
|
|
||||||
return {
|
return {
|
||||||
instances: response.data.results,
|
instances: response.data.results,
|
||||||
isK8s: sysSettings.data.IS_K8S,
|
isK8s: sysSettings.data.IS_K8S,
|
||||||
@@ -93,7 +87,7 @@ function InstanceList() {
|
|||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [...response] = await Promise.all(
|
const [...response] = await Promise.all(
|
||||||
selected
|
selected
|
||||||
.filter(({ node_type }) => node_type === 'execution')
|
.filter(({ node_type }) => node_type !== 'hop')
|
||||||
.map(({ id }) => InstancesAPI.healthCheck(id))
|
.map(({ id }) => InstancesAPI.healthCheck(id))
|
||||||
);
|
);
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -102,18 +96,6 @@ function InstanceList() {
|
|||||||
}, [selected])
|
}, [selected])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selected) {
|
|
||||||
selected.forEach((i) => {
|
|
||||||
if (i.node_type === 'execution') {
|
|
||||||
setCanRunHealthCheck(true);
|
|
||||||
} else {
|
|
||||||
setCanRunHealthCheck(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selected]);
|
|
||||||
|
|
||||||
const handleHealthCheck = async () => {
|
const handleHealthCheck = async () => {
|
||||||
await fetchHealthCheck();
|
await fetchHealthCheck();
|
||||||
clearSelected();
|
clearSelected();
|
||||||
@@ -207,8 +189,6 @@ function InstanceList() {
|
|||||||
onClick={handleHealthCheck}
|
onClick={handleHealthCheck}
|
||||||
key="healthCheck"
|
key="healthCheck"
|
||||||
selectedItems={selected}
|
selectedItems={selected}
|
||||||
healthCheckPending={pendingHealthCheck}
|
|
||||||
isDisabled={!canRunHealthCheck}
|
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -216,7 +196,7 @@ function InstanceList() {
|
|||||||
headerRow={
|
headerRow={
|
||||||
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
||||||
<HeaderCell
|
<HeaderCell
|
||||||
tooltip={t`Health checks can only be run on execution nodes.`}
|
tooltip={t`Cannot run health check on hop nodes.`}
|
||||||
sortKey="hostname"
|
sortKey="hostname"
|
||||||
>{t`Name`}</HeaderCell>
|
>{t`Name`}</HeaderCell>
|
||||||
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
|
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const instances = [
|
|||||||
jobs_running: 0,
|
jobs_running: 0,
|
||||||
jobs_total: 68,
|
jobs_total: 68,
|
||||||
cpu: 6,
|
cpu: 6,
|
||||||
node_type: 'execution',
|
node_type: 'control',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
memory: 2087469056,
|
memory: 2087469056,
|
||||||
cpu_capacity: 24,
|
cpu_capacity: 24,
|
||||||
@@ -52,7 +52,7 @@ const instances = [
|
|||||||
jobs_running: 0,
|
jobs_running: 0,
|
||||||
jobs_total: 68,
|
jobs_total: 68,
|
||||||
cpu: 6,
|
cpu: 6,
|
||||||
node_type: 'execution',
|
node_type: 'hybrid',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
memory: 2087469056,
|
memory: 2087469056,
|
||||||
cpu_capacity: 24,
|
cpu_capacity: 24,
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { OutlinedClockIcon } from '@patternfly/react-icons';
|
|
||||||
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||||
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import computeForks from 'util/computeForks';
|
import computeForks from 'util/computeForks';
|
||||||
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
||||||
@@ -54,7 +52,7 @@ function InstanceListItem({
|
|||||||
fetchInstances,
|
fetchInstances,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
}) {
|
}) {
|
||||||
const config = useConfig();
|
const { me = {} } = useConfig();
|
||||||
const [forks, setForks] = useState(
|
const [forks, setForks] = useState(
|
||||||
computeForks(
|
computeForks(
|
||||||
instance.mem_capacity,
|
instance.mem_capacity,
|
||||||
@@ -100,21 +98,7 @@ function InstanceListItem({
|
|||||||
);
|
);
|
||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatHealthCheckTimeStamp = (last) => (
|
|
||||||
<>
|
|
||||||
{formatDateString(last)}
|
|
||||||
{instance.health_check_pending ? (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<OutlinedClockIcon />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const isHopNode = instance.node_type === 'hop';
|
const isHopNode = instance.node_type === 'hop';
|
||||||
const isExecutionNode = instance.node_type === 'execution';
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
@@ -137,7 +121,7 @@ function InstanceListItem({
|
|||||||
rowIndex,
|
rowIndex,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
disable: !isExecutionNode,
|
disable: isHopNode,
|
||||||
}}
|
}}
|
||||||
dataLabel={t`Selected`}
|
dataLabel={t`Selected`}
|
||||||
/>
|
/>
|
||||||
@@ -180,7 +164,7 @@ function InstanceListItem({
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={!config?.me?.is_superuser || !instance.enabled}
|
isDisabled={!me?.is_superuser || !instance.enabled}
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -237,22 +221,7 @@ function InstanceListItem({
|
|||||||
<Detail
|
<Detail
|
||||||
data-cy="last-health-check"
|
data-cy="last-health-check"
|
||||||
label={t`Last Health Check`}
|
label={t`Last Health Check`}
|
||||||
helpText={
|
value={formatDateString(instance.last_health_check)}
|
||||||
<>
|
|
||||||
{t`Health checks are asynchronous tasks. See the`}{' '}
|
|
||||||
<a
|
|
||||||
href={`${getDocsBaseUrl(
|
|
||||||
config
|
|
||||||
)}/html/administration/instances.html#health-check`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t`documentation`}
|
|
||||||
</a>{' '}
|
|
||||||
{t`for more info.`}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
value={formatHealthCheckTimeStamp(instance.last_health_check)}
|
|
||||||
/>
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
</ExpandableRowContent>
|
</ExpandableRowContent>
|
||||||
|
|||||||
@@ -272,9 +272,9 @@ describe('<InstanceListItem/>', () => {
|
|||||||
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
||||||
'Auto'
|
'Auto'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('Detail[label="Last Health Check"]').text()).toBe(
|
expect(
|
||||||
'Last Health Check9/15/2021, 6:02:07 PM'
|
wrapper.find('Detail[label="Last Health Check"]').prop('value')
|
||||||
);
|
).toBe('9/15/2021, 6:02:07 PM');
|
||||||
});
|
});
|
||||||
test('Hop should not render some things', async () => {
|
test('Hop should not render some things', async () => {
|
||||||
const onSelect = jest.fn();
|
const onSelect = jest.fn();
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ const SmartInventoryFormFields = ({ inventory }) => {
|
|||||||
id="variables"
|
id="variables"
|
||||||
name="variables"
|
name="variables"
|
||||||
label={t`Variables`}
|
label={t`Variables`}
|
||||||
tooltip={t`Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Controller documentation for example syntax.`}
|
tooltip={t`Enter inventory variables using either JSON or YAML syntax.
|
||||||
|
Use the radio button to toggle between the two. Refer to the
|
||||||
|
Ansible Controller documentation for example syntax.`}
|
||||||
/>
|
/>
|
||||||
</FormFullWidthLayout>
|
</FormFullWidthLayout>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ const processCodeEditorValue = (value) => {
|
|||||||
codeEditorValue = '';
|
codeEditorValue = '';
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
codeEditorValue = encode(value);
|
codeEditorValue = encode(value);
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
codeEditorValue = encode(value.join(' '));
|
|
||||||
} else {
|
} else {
|
||||||
codeEditorValue = value;
|
codeEditorValue = value;
|
||||||
}
|
}
|
||||||
@@ -62,7 +60,7 @@ const getStdOutValue = (hostEvent) => {
|
|||||||
) {
|
) {
|
||||||
stdOut = res.results.join('\n');
|
stdOut = res.results.join('\n');
|
||||||
} else if (res?.stdout) {
|
} else if (res?.stdout) {
|
||||||
stdOut = Array.isArray(res.stdout) ? res.stdout.join(' ') : res.stdout;
|
stdOut = res.stdout;
|
||||||
}
|
}
|
||||||
return stdOut;
|
return stdOut;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,60 +52,6 @@ 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: "[0;33mchanged: [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"]}[0m"
|
|
||||||
`,
|
|
||||||
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 */
|
/* eslint-disable no-useless-escape */
|
||||||
const jsonValue = `{
|
const jsonValue = `{
|
||||||
\"ansible_loop_var\": \"item\",
|
\"ansible_loop_var\": \"item\",
|
||||||
@@ -335,25 +281,4 @@ describe('HostEventModal', () => {
|
|||||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||||
expect(codeEditor.prop('value')).toEqual('baz\nbar');
|
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(' ')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { DateTime, Duration } from 'luxon';
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { bool, shape, func } from 'prop-types';
|
import { bool, shape, func } from 'prop-types';
|
||||||
import {
|
import {
|
||||||
@@ -41,18 +41,18 @@ const Wrapper = styled.div`
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`;
|
`;
|
||||||
const calculateElapsed = (started) => {
|
|
||||||
const now = DateTime.now();
|
|
||||||
const duration = now
|
|
||||||
.diff(DateTime.fromISO(`${started}`), [
|
|
||||||
'milliseconds',
|
|
||||||
'seconds',
|
|
||||||
'minutes',
|
|
||||||
'hours',
|
|
||||||
])
|
|
||||||
.toObject();
|
|
||||||
|
|
||||||
return Duration.fromObject({ ...duration }).toFormat('hh:mm:ss');
|
const toHHMMSS = (elapsed) => {
|
||||||
|
const sec_num = parseInt(elapsed, 10);
|
||||||
|
const hours = Math.floor(sec_num / 3600);
|
||||||
|
const minutes = Math.floor(sec_num / 60) % 60;
|
||||||
|
const seconds = sec_num % 60;
|
||||||
|
|
||||||
|
const stampHours = hours < 10 ? `0${hours}` : hours;
|
||||||
|
const stampMinutes = minutes < 10 ? `0${minutes}` : minutes;
|
||||||
|
const stampSeconds = seconds < 10 ? `0${seconds}` : seconds;
|
||||||
|
|
||||||
|
return `${stampHours}:${stampMinutes}:${stampSeconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OUTPUT_NO_COUNT_JOB_TYPES = [
|
const OUTPUT_NO_COUNT_JOB_TYPES = [
|
||||||
@@ -62,7 +62,6 @@ const OUTPUT_NO_COUNT_JOB_TYPES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
|
const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
|
||||||
const [activeJobElapsedTime, setActiveJobElapsedTime] = useState('00:00:00');
|
|
||||||
const hideCounts = OUTPUT_NO_COUNT_JOB_TYPES.includes(job.type);
|
const hideCounts = OUTPUT_NO_COUNT_JOB_TYPES.includes(job.type);
|
||||||
|
|
||||||
const playCount = job?.playbook_counts?.play_count;
|
const playCount = job?.playbook_counts?.play_count;
|
||||||
@@ -77,20 +76,6 @@ const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
|
|||||||
: 0;
|
: 0;
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let secTimer;
|
|
||||||
if (job.finished) {
|
|
||||||
return () => clearInterval(secTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
secTimer = setInterval(() => {
|
|
||||||
const elapsedTime = calculateElapsed(job.started);
|
|
||||||
setActiveJobElapsedTime(elapsedTime);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(secTimer);
|
|
||||||
}, [job.started, job.finished]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{!hideCounts && (
|
{!hideCounts && (
|
||||||
@@ -139,13 +124,7 @@ const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
|
|||||||
<BadgeGroup aria-label={t`Elapsed Time`}>
|
<BadgeGroup aria-label={t`Elapsed Time`}>
|
||||||
<div>{t`Elapsed`}</div>
|
<div>{t`Elapsed`}</div>
|
||||||
<Tooltip content={t`Elapsed time that the job ran`}>
|
<Tooltip content={t`Elapsed time that the job ran`}>
|
||||||
<Badge isRead>
|
<Badge isRead>{toHHMMSS(job.elapsed)}</Badge>
|
||||||
{job.finished
|
|
||||||
? Duration.fromObject({ seconds: job.elapsed }).toFormat(
|
|
||||||
'hh:mm:ss'
|
|
||||||
)
|
|
||||||
: activeJobElapsedTime}
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</BadgeGroup>
|
</BadgeGroup>
|
||||||
{['pending', 'waiting', 'running'].includes(jobStatus) &&
|
{['pending', 'waiting', 'running'].includes(jobStatus) &&
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user