mirror of
https://github.com/ansible/awx.git
synced 2026-03-30 07:15:11 -02:30
Compare commits
61 Commits
21.10.1
...
13089-Sche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26a947ed31 | ||
|
|
b99a434dee | ||
|
|
6cee99a9f9 | ||
|
|
ee509aea56 | ||
|
|
b5452a48f8 | ||
|
|
0c980fa7d5 | ||
|
|
e34ce8c795 | ||
|
|
3543644e0e | ||
|
|
36c0d07b30 | ||
|
|
239827a9cf | ||
|
|
ac9871b36f | ||
|
|
f739908ccf | ||
|
|
cf1ec07eab | ||
|
|
d968b648de | ||
|
|
5dd0eab806 | ||
|
|
41f3f381ec | ||
|
|
ac8cff75ce | ||
|
|
94b34b801c | ||
|
|
8f6849fc22 | ||
|
|
821b1701bf | ||
|
|
b7f2825909 | ||
|
|
e87e041a2a | ||
|
|
cc336e791c | ||
|
|
c2a3c3b285 | ||
|
|
7b8dcc98e7 | ||
|
|
d5011492bf | ||
|
|
e363ddf470 | ||
|
|
987709cdb3 | ||
|
|
f04ac3c798 | ||
|
|
71a6baccdb | ||
|
|
d07076b686 | ||
|
|
7129f3e8cd | ||
|
|
df61a5cea1 | ||
|
|
a4b950f79b | ||
|
|
8be739d255 | ||
|
|
ca54195099 | ||
|
|
f0fcfdde39 | ||
|
|
80b1ba4a35 | ||
|
|
51f8e362dc | ||
|
|
737d6d8c8b | ||
|
|
beaf6b6058 | ||
|
|
aad1fbcef8 | ||
|
|
0b96d617ac | ||
|
|
fe768a159b | ||
|
|
c1ebea858b | ||
|
|
7b2938f515 | ||
|
|
e524d3df3e | ||
|
|
cec2d2dfb9 | ||
|
|
15b7ad3570 | ||
|
|
36ff9cbc6d | ||
|
|
ed74d80ecb | ||
|
|
4a7f4d0ed4 | ||
|
|
6e08c3567f | ||
|
|
58734a33c4 | ||
|
|
2832f28014 | ||
|
|
d34f6af830 | ||
|
|
f9bb26ad33 | ||
|
|
878035c13b | ||
|
|
2cc971a43f | ||
|
|
9d77c54612 | ||
|
|
ef651a3a21 |
10
.github/triage_replies.md
vendored
10
.github/triage_replies.md
vendored
@@ -53,6 +53,16 @@ https://github.com/ansible/awx/#get-involved \
|
|||||||
Thank you once again for this and your interest in AWX!
|
Thank you once again for this and your interest in AWX!
|
||||||
|
|
||||||
|
|
||||||
|
### Red Hat Support Team
|
||||||
|
- Hi! \
|
||||||
|
\
|
||||||
|
It appears that you are using an RPM build for RHEL. Please reach out to the Red Hat support team and submit a ticket. \
|
||||||
|
\
|
||||||
|
Here is the link to do so: \
|
||||||
|
\
|
||||||
|
https://access.redhat.com/support \
|
||||||
|
\
|
||||||
|
Thank you for your submission and for supporting AWX!
|
||||||
|
|
||||||
|
|
||||||
## Common
|
## Common
|
||||||
|
|||||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -145,3 +145,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
AWX_TEST_IMAGE: awx
|
AWX_TEST_IMAGE: awx
|
||||||
AWX_TEST_VERSION: ci
|
AWX_TEST_VERSION: ci
|
||||||
|
|
||||||
|
collection-sanity:
|
||||||
|
name: awx_collection sanity
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# The containers that GitHub Actions use have Ansible installed, so upgrade to make sure we have the latest version.
|
||||||
|
- name: Upgrade ansible-core
|
||||||
|
run: python3 -m pip install --upgrade ansible-core
|
||||||
|
|
||||||
|
- name: Run sanity tests
|
||||||
|
run: make test_collection_sanity
|
||||||
|
env:
|
||||||
|
# needed due to cgroupsv2. This is fixed, but a stable release
|
||||||
|
# with the fix has not been made yet.
|
||||||
|
ANSIBLE_TEST_PREFER_PODMAN: 1
|
||||||
|
|||||||
4
.github/workflows/e2e_test.yml
vendored
4
.github/workflows/e2e_test.yml
vendored
@@ -6,7 +6,7 @@ env:
|
|||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
jobs:
|
jobs:
|
||||||
e2e-test:
|
e2e-test:
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'qe:e2e')
|
if: contains(github.event.pull_request.labels.*.name, 'qe:e2e')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -107,5 +107,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: AWX-logs-${{ matrix.job }}
|
name: AWX-logs-${{ matrix.job }}
|
||||||
path: make-docker-compose-output.log
|
path: make-docker-compose-output.log
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
34
Makefile
34
Makefile
@@ -6,7 +6,20 @@ CHROMIUM_BIN=/tmp/chrome-linux/chrome
|
|||||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
MANAGEMENT_COMMAND ?= awx-manage
|
MANAGEMENT_COMMAND ?= awx-manage
|
||||||
VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
|
VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
|
||||||
COLLECTION_VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
|
|
||||||
|
# ansible-test requires semver compatable version, so we allow overrides to hack it
|
||||||
|
COLLECTION_VERSION ?= $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
|
||||||
|
# args for the ansible-test sanity command
|
||||||
|
COLLECTION_SANITY_ARGS ?= --docker
|
||||||
|
# collection unit testing directories
|
||||||
|
COLLECTION_TEST_DIRS ?= awx_collection/test/awx
|
||||||
|
# collection integration test directories (defaults to all)
|
||||||
|
COLLECTION_TEST_TARGET ?=
|
||||||
|
# args for collection install
|
||||||
|
COLLECTION_PACKAGE ?= awx
|
||||||
|
COLLECTION_NAMESPACE ?= awx
|
||||||
|
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
|
||||||
|
COLLECTION_TEMPLATE_VERSION ?= false
|
||||||
|
|
||||||
# NOTE: This defaults the container image version to the branch that's active
|
# NOTE: This defaults the container image version to the branch that's active
|
||||||
COMPOSE_TAG ?= $(GIT_BRANCH)
|
COMPOSE_TAG ?= $(GIT_BRANCH)
|
||||||
@@ -288,19 +301,13 @@ test:
|
|||||||
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
|
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
|
||||||
awx-manage check_migrations --dry-run --check -n 'missing_migration_file'
|
awx-manage check_migrations --dry-run --check -n 'missing_migration_file'
|
||||||
|
|
||||||
COLLECTION_TEST_DIRS ?= awx_collection/test/awx
|
|
||||||
COLLECTION_TEST_TARGET ?=
|
|
||||||
COLLECTION_PACKAGE ?= awx
|
|
||||||
COLLECTION_NAMESPACE ?= awx
|
|
||||||
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
|
|
||||||
COLLECTION_TEMPLATE_VERSION ?= false
|
|
||||||
|
|
||||||
test_collection:
|
test_collection:
|
||||||
rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt
|
rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt
|
||||||
if [ "$(VENV_BASE)" ]; then \
|
if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi && \
|
fi && \
|
||||||
pip install ansible-core && \
|
if ! [ -x "$(shell command -v ansible-playbook)" ]; then pip install ansible-core; fi
|
||||||
|
ansible --version
|
||||||
py.test $(COLLECTION_TEST_DIRS) -v
|
py.test $(COLLECTION_TEST_DIRS) -v
|
||||||
# The python path needs to be modified so that the tests can find Ansible within the container
|
# The python path needs to be modified so that the tests can find Ansible within the container
|
||||||
# First we will use anything expility set as PYTHONPATH
|
# First we will use anything expility set as PYTHONPATH
|
||||||
@@ -330,8 +337,13 @@ install_collection: build_collection
|
|||||||
rm -rf $(COLLECTION_INSTALL)
|
rm -rf $(COLLECTION_INSTALL)
|
||||||
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(COLLECTION_VERSION).tar.gz
|
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(COLLECTION_VERSION).tar.gz
|
||||||
|
|
||||||
test_collection_sanity: install_collection
|
test_collection_sanity:
|
||||||
cd $(COLLECTION_INSTALL) && ansible-test sanity
|
rm -rf awx_collection_build/
|
||||||
|
rm -rf $(COLLECTION_INSTALL)
|
||||||
|
if ! [ -x "$(shell command -v ansible-test)" ]; then pip install ansible-core; fi
|
||||||
|
ansible --version
|
||||||
|
COLLECTION_VERSION=1.0.0 make install_collection
|
||||||
|
cd $(COLLECTION_INSTALL) && ansible-test sanity $(COLLECTION_SANITY_ARGS)
|
||||||
|
|
||||||
test_collection_integration: install_collection
|
test_collection_integration: install_collection
|
||||||
cd $(COLLECTION_INSTALL) && ansible-test integration $(COLLECTION_TEST_TARGET)
|
cd $(COLLECTION_INSTALL) && ansible-test integration $(COLLECTION_TEST_TARGET)
|
||||||
|
|||||||
@@ -344,6 +344,13 @@ class InstanceDetail(RetrieveUpdateAPIView):
|
|||||||
model = models.Instance
|
model = models.Instance
|
||||||
serializer_class = serializers.InstanceSerializer
|
serializer_class = serializers.InstanceSerializer
|
||||||
|
|
||||||
|
def update_raw_data(self, data):
|
||||||
|
# these fields are only valid on creation of an instance, so they unwanted on detail view
|
||||||
|
data.pop('listener_port', None)
|
||||||
|
data.pop('node_type', None)
|
||||||
|
data.pop('hostname', None)
|
||||||
|
return super(InstanceDetail, self).update_raw_data(data)
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
r = super(InstanceDetail, self).update(request, *args, **kwargs)
|
r = super(InstanceDetail, self).update(request, *args, **kwargs)
|
||||||
if status.is_success(r.status_code):
|
if status.is_success(r.status_code):
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from awx.main.constants import ACTIVE_STATES
|
from awx.main.constants import ACTIVE_STATES
|
||||||
from awx.main.utils import get_object_or_400
|
from awx.main.utils import get_object_or_400
|
||||||
from awx.main.models.ha import Instance, InstanceGroup
|
from awx.main.models.ha import Instance, InstanceGroup, schedule_policy_task
|
||||||
from awx.main.models.organization import Team
|
from awx.main.models.organization import Team
|
||||||
from awx.main.models.projects import Project
|
from awx.main.models.projects import Project
|
||||||
from awx.main.models.inventory import Inventory
|
from awx.main.models.inventory import Inventory
|
||||||
@@ -107,6 +107,11 @@ class InstanceGroupMembershipMixin(object):
|
|||||||
if inst_name in ig_obj.policy_instance_list:
|
if inst_name in ig_obj.policy_instance_list:
|
||||||
ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name))
|
ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name))
|
||||||
ig_obj.save(update_fields=['policy_instance_list'])
|
ig_obj.save(update_fields=['policy_instance_list'])
|
||||||
|
|
||||||
|
# sometimes removing an instance has a non-obvious consequence
|
||||||
|
# this is almost always true if policy_instance_percentage or _minimum is non-zero
|
||||||
|
# after removing a single instance, the other memberships need to be re-balanced
|
||||||
|
schedule_policy_task()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2697,46 +2697,66 @@ class ActivityStreamAccess(BaseAccess):
|
|||||||
# 'job_template', 'job', 'project', 'project_update', 'workflow_job',
|
# 'job_template', 'job', 'project', 'project_update', 'workflow_job',
|
||||||
# 'inventory_source', 'workflow_job_template'
|
# 'inventory_source', 'workflow_job_template'
|
||||||
|
|
||||||
inventory_set = Inventory.accessible_objects(self.user, 'read_role')
|
q = Q(user=self.user)
|
||||||
credential_set = Credential.accessible_objects(self.user, 'read_role')
|
inventory_set = Inventory.accessible_pk_qs(self.user, 'read_role')
|
||||||
|
if inventory_set:
|
||||||
|
q |= (
|
||||||
|
Q(ad_hoc_command__inventory__in=inventory_set)
|
||||||
|
| Q(inventory__in=inventory_set)
|
||||||
|
| Q(host__inventory__in=inventory_set)
|
||||||
|
| Q(group__inventory__in=inventory_set)
|
||||||
|
| Q(inventory_source__inventory__in=inventory_set)
|
||||||
|
| Q(inventory_update__inventory_source__inventory__in=inventory_set)
|
||||||
|
)
|
||||||
|
|
||||||
|
credential_set = Credential.accessible_pk_qs(self.user, 'read_role')
|
||||||
|
if credential_set:
|
||||||
|
q |= Q(credential__in=credential_set)
|
||||||
|
|
||||||
auditing_orgs = (
|
auditing_orgs = (
|
||||||
(Organization.accessible_objects(self.user, 'admin_role') | Organization.accessible_objects(self.user, 'auditor_role'))
|
(Organization.accessible_objects(self.user, 'admin_role') | Organization.accessible_objects(self.user, 'auditor_role'))
|
||||||
.distinct()
|
.distinct()
|
||||||
.values_list('id', flat=True)
|
.values_list('id', flat=True)
|
||||||
)
|
)
|
||||||
project_set = Project.accessible_objects(self.user, 'read_role')
|
if auditing_orgs:
|
||||||
jt_set = JobTemplate.accessible_objects(self.user, 'read_role')
|
q |= (
|
||||||
team_set = Team.accessible_objects(self.user, 'read_role')
|
Q(user__in=auditing_orgs.values('member_role__members'))
|
||||||
wfjt_set = WorkflowJobTemplate.accessible_objects(self.user, 'read_role')
|
| Q(organization__in=auditing_orgs)
|
||||||
app_set = OAuth2ApplicationAccess(self.user).filtered_queryset()
|
| Q(notification_template__organization__in=auditing_orgs)
|
||||||
token_set = OAuth2TokenAccess(self.user).filtered_queryset()
|
| Q(notification__notification_template__organization__in=auditing_orgs)
|
||||||
|
| Q(label__organization__in=auditing_orgs)
|
||||||
|
| Q(role__in=Role.objects.filter(ancestors__in=self.user.roles.all()) if auditing_orgs else [])
|
||||||
|
)
|
||||||
|
|
||||||
return qs.filter(
|
project_set = Project.accessible_pk_qs(self.user, 'read_role')
|
||||||
Q(ad_hoc_command__inventory__in=inventory_set)
|
if project_set:
|
||||||
| Q(o_auth2_application__in=app_set)
|
q |= Q(project__in=project_set) | Q(project_update__project__in=project_set)
|
||||||
| Q(o_auth2_access_token__in=token_set)
|
|
||||||
| Q(user__in=auditing_orgs.values('member_role__members'))
|
jt_set = JobTemplate.accessible_pk_qs(self.user, 'read_role')
|
||||||
| Q(user=self.user)
|
if jt_set:
|
||||||
| Q(organization__in=auditing_orgs)
|
q |= Q(job_template__in=jt_set) | Q(job__job_template__in=jt_set)
|
||||||
| Q(inventory__in=inventory_set)
|
|
||||||
| Q(host__inventory__in=inventory_set)
|
wfjt_set = WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role')
|
||||||
| Q(group__inventory__in=inventory_set)
|
if wfjt_set:
|
||||||
| Q(inventory_source__inventory__in=inventory_set)
|
q |= (
|
||||||
| Q(inventory_update__inventory_source__inventory__in=inventory_set)
|
Q(workflow_job_template__in=wfjt_set)
|
||||||
| Q(credential__in=credential_set)
|
| Q(workflow_job_template_node__workflow_job_template__in=wfjt_set)
|
||||||
| Q(team__in=team_set)
|
| Q(workflow_job__workflow_job_template__in=wfjt_set)
|
||||||
| Q(project__in=project_set)
|
)
|
||||||
| Q(project_update__project__in=project_set)
|
|
||||||
| Q(job_template__in=jt_set)
|
team_set = Team.accessible_pk_qs(self.user, 'read_role')
|
||||||
| Q(job__job_template__in=jt_set)
|
if team_set:
|
||||||
| Q(workflow_job_template__in=wfjt_set)
|
q |= Q(team__in=team_set)
|
||||||
| Q(workflow_job_template_node__workflow_job_template__in=wfjt_set)
|
|
||||||
| Q(workflow_job__workflow_job_template__in=wfjt_set)
|
app_set = OAuth2ApplicationAccess(self.user).filtered_queryset()
|
||||||
| Q(notification_template__organization__in=auditing_orgs)
|
if app_set:
|
||||||
| Q(notification__notification_template__organization__in=auditing_orgs)
|
q |= Q(o_auth2_application__in=app_set)
|
||||||
| Q(label__organization__in=auditing_orgs)
|
|
||||||
| Q(role__in=Role.objects.filter(ancestors__in=self.user.roles.all()) if auditing_orgs else [])
|
token_set = OAuth2TokenAccess(self.user).filtered_queryset()
|
||||||
).distinct()
|
if token_set:
|
||||||
|
q |= Q(o_auth2_access_token__in=token_set)
|
||||||
|
|
||||||
|
return qs.filter(q).distinct()
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ aim_inputs = {
|
|||||||
'fields': [
|
'fields': [
|
||||||
{
|
{
|
||||||
'id': 'url',
|
'id': 'url',
|
||||||
'label': _('CyberArk AIM URL'),
|
'label': _('CyberArk CCP URL'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'format': 'url',
|
'format': 'url',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'id': 'webservice_id',
|
||||||
|
'label': _('Web Service ID'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('The CCP Web Service ID. Leave blank to default to AIMWebService.'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'id': 'app_id',
|
'id': 'app_id',
|
||||||
'label': _('Application ID'),
|
'label': _('Application ID'),
|
||||||
@@ -64,10 +70,13 @@ def aim_backend(**kwargs):
|
|||||||
client_cert = kwargs.get('client_cert', None)
|
client_cert = kwargs.get('client_cert', None)
|
||||||
client_key = kwargs.get('client_key', None)
|
client_key = kwargs.get('client_key', None)
|
||||||
verify = kwargs['verify']
|
verify = kwargs['verify']
|
||||||
|
webservice_id = kwargs['webservice_id']
|
||||||
app_id = kwargs['app_id']
|
app_id = kwargs['app_id']
|
||||||
object_query = kwargs['object_query']
|
object_query = kwargs['object_query']
|
||||||
object_query_format = kwargs['object_query_format']
|
object_query_format = kwargs['object_query_format']
|
||||||
reason = kwargs.get('reason', None)
|
reason = kwargs.get('reason', None)
|
||||||
|
if webservice_id == '':
|
||||||
|
webservice_id = 'AIMWebService'
|
||||||
|
|
||||||
query_params = {
|
query_params = {
|
||||||
'AppId': app_id,
|
'AppId': app_id,
|
||||||
@@ -78,7 +87,7 @@ def aim_backend(**kwargs):
|
|||||||
query_params['reason'] = reason
|
query_params['reason'] = reason
|
||||||
|
|
||||||
request_qs = '?' + urlencode(query_params, quote_via=quote)
|
request_qs = '?' + urlencode(query_params, quote_via=quote)
|
||||||
request_url = urljoin(url, '/'.join(['AIMWebService', 'api', 'Accounts']))
|
request_url = urljoin(url, '/'.join([webservice_id, 'api', 'Accounts']))
|
||||||
|
|
||||||
with CertFiles(client_cert, client_key) as cert:
|
with CertFiles(client_cert, client_key) as cert:
|
||||||
res = requests.get(
|
res = requests.get(
|
||||||
@@ -92,4 +101,4 @@ def aim_backend(**kwargs):
|
|||||||
return res.json()['Content']
|
return res.json()['Content']
|
||||||
|
|
||||||
|
|
||||||
aim_plugin = CredentialPlugin('CyberArk AIM Central Credential Provider Lookup', inputs=aim_inputs, backend=aim_backend)
|
aim_plugin = CredentialPlugin('CyberArk Central Credential Provider Lookup', inputs=aim_inputs, backend=aim_backend)
|
||||||
|
|||||||
@@ -32,8 +32,14 @@ class Command(BaseCommand):
|
|||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
self.old_key = settings.SECRET_KEY
|
self.old_key = settings.SECRET_KEY
|
||||||
custom_key = os.environ.get("TOWER_SECRET_KEY")
|
custom_key = os.environ.get("TOWER_SECRET_KEY")
|
||||||
if options.get("use_custom_key") and custom_key:
|
if options.get("use_custom_key"):
|
||||||
self.new_key = custom_key
|
if custom_key:
|
||||||
|
self.new_key = custom_key
|
||||||
|
else:
|
||||||
|
print("Use custom key was specified but the env var TOWER_SECRET_KEY was not available")
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
self.new_key = base64.encodebytes(os.urandom(33)).decode().rstrip()
|
self.new_key = base64.encodebytes(os.urandom(33)).decode().rstrip()
|
||||||
self._notification_templates()
|
self._notification_templates()
|
||||||
|
|||||||
@@ -158,7 +158,11 @@ class InstanceManager(models.Manager):
|
|||||||
return (False, instance)
|
return (False, instance)
|
||||||
|
|
||||||
# Create new instance, and fill in default values
|
# Create new instance, and fill in default values
|
||||||
create_defaults = {'node_state': Instance.States.INSTALLED, 'capacity': 0}
|
create_defaults = {
|
||||||
|
'node_state': Instance.States.INSTALLED,
|
||||||
|
'capacity': 0,
|
||||||
|
'listener_port': 27199,
|
||||||
|
}
|
||||||
if defaults is not None:
|
if defaults is not None:
|
||||||
create_defaults.update(defaults)
|
create_defaults.update(defaults)
|
||||||
uuid_option = {}
|
uuid_option = {}
|
||||||
|
|||||||
18
awx/main/migrations/0174_ensure_org_ee_admin_roles.py
Normal file
18
awx/main/migrations/0174_ensure_org_ee_admin_roles.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2022-12-07 21:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from awx.main.migrations import _rbac as rbac
|
||||||
|
from awx.main.migrations import _migration_utils as migration_utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0173_instancegroup_max_limits'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||||
|
migrations.RunPython(rbac.create_roles),
|
||||||
|
]
|
||||||
@@ -15,6 +15,7 @@ def aws(cred, env, private_data_dir):
|
|||||||
|
|
||||||
if cred.has_input('security_token'):
|
if cred.has_input('security_token'):
|
||||||
env['AWS_SECURITY_TOKEN'] = cred.get_input('security_token', default='')
|
env['AWS_SECURITY_TOKEN'] = cred.get_input('security_token', default='')
|
||||||
|
env['AWS_SESSION_TOKEN'] = env['AWS_SECURITY_TOKEN']
|
||||||
|
|
||||||
|
|
||||||
def gce(cred, env, private_data_dir):
|
def gce(cred, env, private_data_dir):
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ class TaskManager(TaskBase):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@timeit
|
@timeit
|
||||||
def start_task(self, task, instance_group, dependent_tasks=None, instance=None):
|
def start_task(self, task, instance_group, instance=None):
|
||||||
# Just like for process_running_tasks, add the job to the dependency graph and
|
# Just like for process_running_tasks, add the job to the dependency graph and
|
||||||
# ask the TaskManagerInstanceGroups object to update consumed capacity on all
|
# ask the TaskManagerInstanceGroups object to update consumed capacity on all
|
||||||
# implicated instances and container groups.
|
# implicated instances and container groups.
|
||||||
@@ -524,14 +524,6 @@ class TaskManager(TaskBase):
|
|||||||
ScheduleTaskManager().schedule()
|
ScheduleTaskManager().schedule()
|
||||||
from awx.main.tasks.system import handle_work_error, handle_work_success
|
from awx.main.tasks.system import handle_work_error, handle_work_success
|
||||||
|
|
||||||
dependent_tasks = dependent_tasks or []
|
|
||||||
|
|
||||||
task_actual = {
|
|
||||||
'type': get_type_for_model(type(task)),
|
|
||||||
'id': task.id,
|
|
||||||
}
|
|
||||||
dependencies = [{'type': get_type_for_model(type(t)), 'id': t.id} for t in dependent_tasks]
|
|
||||||
|
|
||||||
task.status = 'waiting'
|
task.status = 'waiting'
|
||||||
|
|
||||||
(start_status, opts) = task.pre_start()
|
(start_status, opts) = task.pre_start()
|
||||||
@@ -563,6 +555,7 @@ class TaskManager(TaskBase):
|
|||||||
# apply_async does a NOTIFY to the channel dispatcher is listening to
|
# apply_async does a NOTIFY to the channel dispatcher is listening to
|
||||||
# postgres will treat this as part of the transaction, which is what we want
|
# postgres will treat this as part of the transaction, which is what we want
|
||||||
if task.status != 'failed' and type(task) is not WorkflowJob:
|
if task.status != 'failed' and type(task) is not WorkflowJob:
|
||||||
|
task_actual = {'type': get_type_for_model(type(task)), 'id': task.id}
|
||||||
task_cls = task._get_task_class()
|
task_cls = task._get_task_class()
|
||||||
task_cls.apply_async(
|
task_cls.apply_async(
|
||||||
[task.pk],
|
[task.pk],
|
||||||
@@ -570,7 +563,7 @@ class TaskManager(TaskBase):
|
|||||||
queue=task.get_queue_name(),
|
queue=task.get_queue_name(),
|
||||||
uuid=task.celery_task_id,
|
uuid=task.celery_task_id,
|
||||||
callbacks=[{'task': handle_work_success.name, 'kwargs': {'task_actual': task_actual}}],
|
callbacks=[{'task': handle_work_success.name, 'kwargs': {'task_actual': task_actual}}],
|
||||||
errbacks=[{'task': handle_work_error.name, 'args': [task.celery_task_id], 'kwargs': {'subtasks': [task_actual] + dependencies}}],
|
errbacks=[{'task': handle_work_error.name, 'kwargs': {'task_actual': task_actual}}],
|
||||||
)
|
)
|
||||||
|
|
||||||
# In exception cases, like a job failing pre-start checks, we send the websocket status message
|
# In exception cases, like a job failing pre-start checks, we send the websocket status message
|
||||||
@@ -609,7 +602,7 @@ class TaskManager(TaskBase):
|
|||||||
if isinstance(task, WorkflowJob):
|
if isinstance(task, WorkflowJob):
|
||||||
# Previously we were tracking allow_simultaneous blocking both here and in DependencyGraph.
|
# Previously we were tracking allow_simultaneous blocking both here and in DependencyGraph.
|
||||||
# Double check that using just the DependencyGraph works for Workflows and Sliced Jobs.
|
# Double check that using just the DependencyGraph works for Workflows and Sliced Jobs.
|
||||||
self.start_task(task, None, task.get_jobs_fail_chain(), None)
|
self.start_task(task, None, None)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
found_acceptable_queue = False
|
found_acceptable_queue = False
|
||||||
@@ -637,7 +630,7 @@ class TaskManager(TaskBase):
|
|||||||
execution_instance = self.tm_models.instances[control_instance.hostname].obj
|
execution_instance = self.tm_models.instances[control_instance.hostname].obj
|
||||||
task.log_lifecycle("controller_node_chosen")
|
task.log_lifecycle("controller_node_chosen")
|
||||||
task.log_lifecycle("execution_node_chosen")
|
task.log_lifecycle("execution_node_chosen")
|
||||||
self.start_task(task, self.controlplane_ig, task.get_jobs_fail_chain(), execution_instance)
|
self.start_task(task, self.controlplane_ig, execution_instance)
|
||||||
found_acceptable_queue = True
|
found_acceptable_queue = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -645,7 +638,7 @@ class TaskManager(TaskBase):
|
|||||||
if not self.tm_models.instance_groups[instance_group.name].has_remaining_capacity(task):
|
if not self.tm_models.instance_groups[instance_group.name].has_remaining_capacity(task):
|
||||||
continue
|
continue
|
||||||
if instance_group.is_container_group:
|
if instance_group.is_container_group:
|
||||||
self.start_task(task, instance_group, task.get_jobs_fail_chain(), None)
|
self.start_task(task, instance_group, None)
|
||||||
found_acceptable_queue = True
|
found_acceptable_queue = True
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -670,7 +663,7 @@ class TaskManager(TaskBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
execution_instance = self.tm_models.instances[execution_instance.hostname].obj
|
execution_instance = self.tm_models.instances[execution_instance.hostname].obj
|
||||||
self.start_task(task, instance_group, task.get_jobs_fail_chain(), execution_instance)
|
self.start_task(task, instance_group, execution_instance)
|
||||||
found_acceptable_queue = True
|
found_acceptable_queue = True
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -411,9 +411,11 @@ class AWXReceptorJob:
|
|||||||
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
|
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
|
||||||
detail = unit_status.get('Detail', None)
|
detail = unit_status.get('Detail', None)
|
||||||
state_name = unit_status.get('StateName', None)
|
state_name = unit_status.get('StateName', None)
|
||||||
|
stdout_size = unit_status.get('StdoutSize', 0)
|
||||||
except Exception:
|
except Exception:
|
||||||
detail = ''
|
detail = ''
|
||||||
state_name = ''
|
state_name = ''
|
||||||
|
stdout_size = 0
|
||||||
logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}')
|
logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}')
|
||||||
|
|
||||||
if 'exceeded quota' in detail:
|
if 'exceeded quota' in detail:
|
||||||
@@ -424,9 +426,16 @@ class AWXReceptorJob:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resultsock = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True)
|
receptor_output = ''
|
||||||
lines = resultsock.readlines()
|
if state_name == 'Failed' and self.task.runner_callback.event_ct == 0:
|
||||||
receptor_output = b"".join(lines).decode()
|
# if receptor work unit failed and no events were emitted, work results may
|
||||||
|
# contain useful information about why the job failed. In case stdout is
|
||||||
|
# massive, only ask for last 1000 bytes
|
||||||
|
startpos = max(stdout_size - 1000, 0)
|
||||||
|
resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, startpos=startpos, return_socket=True, return_sockfile=True)
|
||||||
|
resultsock.setblocking(False) # this makes resultfile reads non blocking
|
||||||
|
lines = resultfile.readlines()
|
||||||
|
receptor_output = b"".join(lines).decode()
|
||||||
if receptor_output:
|
if receptor_output:
|
||||||
self.task.runner_callback.delay_update(result_traceback=receptor_output)
|
self.task.runner_callback.delay_update(result_traceback=receptor_output)
|
||||||
elif detail:
|
elif detail:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from awx.main.constants import ACTIVE_STATES
|
|||||||
from awx.main.dispatch.publish import task
|
from awx.main.dispatch.publish import task
|
||||||
from awx.main.dispatch import get_local_queuename, reaper
|
from awx.main.dispatch import get_local_queuename, reaper
|
||||||
from awx.main.utils.common import (
|
from awx.main.utils.common import (
|
||||||
|
get_type_for_model,
|
||||||
ignore_inventory_computed_fields,
|
ignore_inventory_computed_fields,
|
||||||
ignore_inventory_group_removal,
|
ignore_inventory_group_removal,
|
||||||
ScheduleWorkflowManager,
|
ScheduleWorkflowManager,
|
||||||
@@ -720,45 +721,43 @@ def handle_work_success(task_actual):
|
|||||||
|
|
||||||
|
|
||||||
@task(queue=get_local_queuename)
|
@task(queue=get_local_queuename)
|
||||||
def handle_work_error(task_id, *args, **kwargs):
|
def handle_work_error(task_actual):
|
||||||
subtasks = kwargs.get('subtasks', None)
|
try:
|
||||||
logger.debug('Executing error task id %s, subtasks: %s' % (task_id, str(subtasks)))
|
instance = UnifiedJob.get_instance_by_type(task_actual['type'], task_actual['id'])
|
||||||
first_instance = None
|
except ObjectDoesNotExist:
|
||||||
first_instance_type = ''
|
logger.warning('Missing {} `{}` in error callback.'.format(task_actual['type'], task_actual['id']))
|
||||||
if subtasks is not None:
|
return
|
||||||
for each_task in subtasks:
|
if not instance:
|
||||||
try:
|
return
|
||||||
instance = UnifiedJob.get_instance_by_type(each_task['type'], each_task['id'])
|
|
||||||
if not instance:
|
|
||||||
# Unknown task type
|
|
||||||
logger.warning("Unknown task type: {}".format(each_task['type']))
|
|
||||||
continue
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
logger.warning('Missing {} `{}` in error callback.'.format(each_task['type'], each_task['id']))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if first_instance is None:
|
subtasks = instance.get_jobs_fail_chain() # reverse of dependent_jobs mostly
|
||||||
first_instance = instance
|
logger.debug(f'Executing error task id {task_actual["id"]}, subtasks: {[subtask.id for subtask in subtasks]}')
|
||||||
first_instance_type = each_task['type']
|
|
||||||
|
|
||||||
if instance.celery_task_id != task_id and not instance.cancel_flag and not instance.status in ('successful', 'failed'):
|
deps_of_deps = {}
|
||||||
instance.status = 'failed'
|
|
||||||
instance.failed = True
|
for subtask in subtasks:
|
||||||
if not instance.job_explanation:
|
if subtask.celery_task_id != instance.celery_task_id and not subtask.cancel_flag and not subtask.status in ('successful', 'failed'):
|
||||||
instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (
|
# If there are multiple in the dependency chain, A->B->C, and this was called for A, blame B for clarity
|
||||||
first_instance_type,
|
blame_job = deps_of_deps.get(subtask.id, instance)
|
||||||
first_instance.name,
|
subtask.status = 'failed'
|
||||||
first_instance.id,
|
subtask.failed = True
|
||||||
)
|
if not subtask.job_explanation:
|
||||||
instance.save()
|
subtask.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (
|
||||||
instance.websocket_emit_status("failed")
|
get_type_for_model(type(blame_job)),
|
||||||
|
blame_job.name,
|
||||||
|
blame_job.id,
|
||||||
|
)
|
||||||
|
subtask.save()
|
||||||
|
subtask.websocket_emit_status("failed")
|
||||||
|
|
||||||
|
for sub_subtask in subtask.get_jobs_fail_chain():
|
||||||
|
deps_of_deps[sub_subtask.id] = subtask
|
||||||
|
|
||||||
# We only send 1 job complete message since all the job completion message
|
# We only send 1 job complete message since all the job completion message
|
||||||
# handling does is trigger the scheduler. If we extend the functionality of
|
# handling does is trigger the scheduler. If we extend the functionality of
|
||||||
# what the job complete message handler does then we may want to send a
|
# what the job complete message handler does then we may want to send a
|
||||||
# completion event for each job here.
|
# completion event for each job here.
|
||||||
if first_instance:
|
schedule_manager_success_or_error(instance)
|
||||||
schedule_manager_success_or_error(first_instance)
|
|
||||||
|
|
||||||
|
|
||||||
@task(queue=get_local_queuename)
|
@task(queue=get_local_queuename)
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||||
"AWS_ACCESS_KEY_ID": "fooo",
|
"AWS_ACCESS_KEY_ID": "fooo",
|
||||||
"AWS_SECRET_ACCESS_KEY": "fooo",
|
"AWS_SECRET_ACCESS_KEY": "fooo",
|
||||||
"AWS_SECURITY_TOKEN": "fooo"
|
"AWS_SECURITY_TOKEN": "fooo",
|
||||||
|
"AWS_SESSION_TOKEN": "fooo"
|
||||||
}
|
}
|
||||||
@@ -171,13 +171,17 @@ class TestKeyRegeneration:
|
|||||||
|
|
||||||
def test_use_custom_key_with_empty_tower_secret_key_env_var(self):
|
def test_use_custom_key_with_empty_tower_secret_key_env_var(self):
|
||||||
os.environ['TOWER_SECRET_KEY'] = ''
|
os.environ['TOWER_SECRET_KEY'] = ''
|
||||||
new_key = call_command('regenerate_secret_key', '--use-custom-key')
|
with pytest.raises(SystemExit) as e:
|
||||||
assert settings.SECRET_KEY != new_key
|
call_command('regenerate_secret_key', '--use-custom-key')
|
||||||
|
assert e.type == SystemExit
|
||||||
|
assert e.value.code == 1
|
||||||
|
|
||||||
def test_use_custom_key_with_no_tower_secret_key_env_var(self):
|
def test_use_custom_key_with_no_tower_secret_key_env_var(self):
|
||||||
os.environ.pop('TOWER_SECRET_KEY', None)
|
os.environ.pop('TOWER_SECRET_KEY', None)
|
||||||
new_key = call_command('regenerate_secret_key', '--use-custom-key')
|
with pytest.raises(SystemExit) as e:
|
||||||
assert settings.SECRET_KEY != new_key
|
call_command('regenerate_secret_key', '--use-custom-key')
|
||||||
|
assert e.type == SystemExit
|
||||||
|
assert e.value.code == 1
|
||||||
|
|
||||||
def test_with_tower_secret_key_env_var(self):
|
def test_with_tower_secret_key_env_var(self):
|
||||||
custom_key = 'MXSq9uqcwezBOChl/UfmbW1k4op+bC+FQtwPqgJ1u9XV'
|
custom_key = 'MXSq9uqcwezBOChl/UfmbW1k4op+bC+FQtwPqgJ1u9XV'
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def test_multi_group_basic_job_launch(instance_factory, controlplane_instance_gr
|
|||||||
mock_task_impact.return_value = 500
|
mock_task_impact.return_value = 500
|
||||||
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_has_calls([mock.call(j1, ig1, [], i1), mock.call(j2, ig2, [], i2)])
|
TaskManager.start_task.assert_has_calls([mock.call(j1, ig1, i1), mock.call(j2, ig2, i2)])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -54,7 +54,7 @@ def test_multi_group_with_shared_dependency(instance_factory, controlplane_insta
|
|||||||
DependencyManager().schedule()
|
DependencyManager().schedule()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
pu = p.project_updates.first()
|
pu = p.project_updates.first()
|
||||||
TaskManager.start_task.assert_called_once_with(pu, controlplane_instance_group, [j1, j2], controlplane_instance_group.instances.all()[0])
|
TaskManager.start_task.assert_called_once_with(pu, controlplane_instance_group, controlplane_instance_group.instances.all()[0])
|
||||||
pu.finished = pu.created + timedelta(seconds=1)
|
pu.finished = pu.created + timedelta(seconds=1)
|
||||||
pu.status = "successful"
|
pu.status = "successful"
|
||||||
pu.save()
|
pu.save()
|
||||||
@@ -62,8 +62,8 @@ def test_multi_group_with_shared_dependency(instance_factory, controlplane_insta
|
|||||||
DependencyManager().schedule()
|
DependencyManager().schedule()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
|
|
||||||
TaskManager.start_task.assert_any_call(j1, ig1, [], i1)
|
TaskManager.start_task.assert_any_call(j1, ig1, i1)
|
||||||
TaskManager.start_task.assert_any_call(j2, ig2, [], i2)
|
TaskManager.start_task.assert_any_call(j2, ig2, i2)
|
||||||
assert TaskManager.start_task.call_count == 2
|
assert TaskManager.start_task.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ def test_workflow_job_no_instancegroup(workflow_job_template_factory, controlpla
|
|||||||
wfj.save()
|
wfj.save()
|
||||||
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(wfj, None, [], None)
|
TaskManager.start_task.assert_called_once_with(wfj, None, None)
|
||||||
assert wfj.instance_group is None
|
assert wfj.instance_group is None
|
||||||
|
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ def test_failover_group_run(instance_factory, controlplane_instance_group, mocke
|
|||||||
mock_task_impact.return_value = 500
|
mock_task_impact.return_value = 500
|
||||||
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
||||||
tm.schedule()
|
tm.schedule()
|
||||||
mock_job.assert_has_calls([mock.call(j1, ig1, [], i1), mock.call(j1_1, ig2, [], i2)])
|
mock_job.assert_has_calls([mock.call(j1, ig1, i1), mock.call(j1_1, ig2, i2)])
|
||||||
assert mock_job.call_count == 2
|
assert mock_job.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def test_single_job_scheduler_launch(hybrid_instance, controlplane_instance_grou
|
|||||||
j = create_job(objects.job_template)
|
j = create_job(objects.job_template)
|
||||||
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -240,12 +240,12 @@ def test_multi_jt_capacity_blocking(hybrid_instance, job_template_factory, mocke
|
|||||||
mock_task_impact.return_value = 505
|
mock_task_impact.return_value = 505
|
||||||
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
||||||
tm.schedule()
|
tm.schedule()
|
||||||
mock_job.assert_called_once_with(j1, controlplane_instance_group, [], instance)
|
mock_job.assert_called_once_with(j1, controlplane_instance_group, instance)
|
||||||
j1.status = "successful"
|
j1.status = "successful"
|
||||||
j1.save()
|
j1.save()
|
||||||
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
||||||
tm.schedule()
|
tm.schedule()
|
||||||
mock_job.assert_called_once_with(j2, controlplane_instance_group, [], instance)
|
mock_job.assert_called_once_with(j2, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -337,12 +337,12 @@ def test_single_job_dependencies_project_launch(controlplane_instance_group, job
|
|||||||
pu = [x for x in p.project_updates.all()]
|
pu = [x for x in p.project_updates.all()]
|
||||||
assert len(pu) == 1
|
assert len(pu) == 1
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(pu[0], controlplane_instance_group, [j], instance)
|
TaskManager.start_task.assert_called_once_with(pu[0], controlplane_instance_group, instance)
|
||||||
pu[0].status = "successful"
|
pu[0].status = "successful"
|
||||||
pu[0].save()
|
pu[0].save()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -365,12 +365,12 @@ def test_single_job_dependencies_inventory_update_launch(controlplane_instance_g
|
|||||||
iu = [x for x in ii.inventory_updates.all()]
|
iu = [x for x in ii.inventory_updates.all()]
|
||||||
assert len(iu) == 1
|
assert len(iu) == 1
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(iu[0], controlplane_instance_group, [j], instance)
|
TaskManager.start_task.assert_called_once_with(iu[0], controlplane_instance_group, instance)
|
||||||
iu[0].status = "successful"
|
iu[0].status = "successful"
|
||||||
iu[0].save()
|
iu[0].save()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -412,7 +412,7 @@ def test_job_dependency_with_already_updated(controlplane_instance_group, job_te
|
|||||||
mock_iu.assert_not_called()
|
mock_iu.assert_not_called()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -442,9 +442,7 @@ def test_shared_dependencies_launch(controlplane_instance_group, job_template_fa
|
|||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
pu = p.project_updates.first()
|
pu = p.project_updates.first()
|
||||||
iu = ii.inventory_updates.first()
|
iu = ii.inventory_updates.first()
|
||||||
TaskManager.start_task.assert_has_calls(
|
TaskManager.start_task.assert_has_calls([mock.call(iu, controlplane_instance_group, instance), mock.call(pu, controlplane_instance_group, instance)])
|
||||||
[mock.call(iu, controlplane_instance_group, [j1, j2], instance), mock.call(pu, controlplane_instance_group, [j1, j2], instance)]
|
|
||||||
)
|
|
||||||
pu.status = "successful"
|
pu.status = "successful"
|
||||||
pu.finished = pu.created + timedelta(seconds=1)
|
pu.finished = pu.created + timedelta(seconds=1)
|
||||||
pu.save()
|
pu.save()
|
||||||
@@ -453,9 +451,7 @@ def test_shared_dependencies_launch(controlplane_instance_group, job_template_fa
|
|||||||
iu.save()
|
iu.save()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_has_calls(
|
TaskManager.start_task.assert_has_calls([mock.call(j1, controlplane_instance_group, instance), mock.call(j2, controlplane_instance_group, instance)])
|
||||||
[mock.call(j1, controlplane_instance_group, [], instance), mock.call(j2, controlplane_instance_group, [], instance)]
|
|
||||||
)
|
|
||||||
pu = [x for x in p.project_updates.all()]
|
pu = [x for x in p.project_updates.all()]
|
||||||
iu = [x for x in ii.inventory_updates.all()]
|
iu = [x for x in ii.inventory_updates.all()]
|
||||||
assert len(pu) == 1
|
assert len(pu) == 1
|
||||||
@@ -479,7 +475,7 @@ def test_job_not_blocking_project_update(controlplane_instance_group, job_templa
|
|||||||
project_update.status = "pending"
|
project_update.status = "pending"
|
||||||
project_update.save()
|
project_update.save()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(project_update, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(project_update, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -503,7 +499,7 @@ def test_job_not_blocking_inventory_update(controlplane_instance_group, job_temp
|
|||||||
|
|
||||||
DependencyManager().schedule()
|
DependencyManager().schedule()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(inventory_update, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(inventory_update, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import tempfile
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from awx.main.tasks.jobs import RunJob
|
from awx.main.tasks.jobs import RunJob
|
||||||
from awx.main.tasks.system import execution_node_health_check, _cleanup_images_and_files
|
from awx.main.tasks.system import execution_node_health_check, _cleanup_images_and_files, handle_work_error
|
||||||
from awx.main.models import Instance, Job
|
from awx.main.models import Instance, Job, InventoryUpdate, ProjectUpdate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -74,3 +74,17 @@ def test_does_not_run_reaped_job(mocker, mock_me):
|
|||||||
job.refresh_from_db()
|
job.refresh_from_db()
|
||||||
assert job.status == 'failed'
|
assert job.status == 'failed'
|
||||||
mock_run.assert_not_called()
|
mock_run.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_handle_work_error_nested(project, inventory_source):
|
||||||
|
pu = ProjectUpdate.objects.create(status='failed', project=project, celery_task_id='1234')
|
||||||
|
iu = InventoryUpdate.objects.create(status='pending', inventory_source=inventory_source, source='scm')
|
||||||
|
job = Job.objects.create(status='pending')
|
||||||
|
iu.dependent_jobs.add(pu)
|
||||||
|
job.dependent_jobs.add(pu, iu)
|
||||||
|
handle_work_error({'type': 'project_update', 'id': pu.id})
|
||||||
|
iu.refresh_from_db()
|
||||||
|
job.refresh_from_db()
|
||||||
|
assert iu.job_explanation == f'Previous Task Failed: {{"job_type": "project_update", "job_name": "", "job_id": "{pu.id}"}}'
|
||||||
|
assert job.job_explanation == f'Previous Task Failed: {{"job_type": "inventory_update", "job_name": "", "job_id": "{iu.id}"}}'
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ if 'sqlite3' not in DATABASES['default']['ENGINE']: # noqa
|
|||||||
# this needs to stay at the bottom of this file
|
# this needs to stay at the bottom of this file
|
||||||
try:
|
try:
|
||||||
if os.getenv('AWX_KUBE_DEVEL', False):
|
if os.getenv('AWX_KUBE_DEVEL', False):
|
||||||
include(optional('minikube.py'), scope=locals())
|
include(optional('development_kube.py'), scope=locals())
|
||||||
else:
|
else:
|
||||||
include(optional('local_*.py'), scope=locals())
|
include(optional('local_*.py'), scope=locals())
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖'
|
BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖'
|
||||||
BROADCAST_WEBSOCKET_PORT = 8013
|
BROADCAST_WEBSOCKET_PORT = 8052
|
||||||
BROADCAST_WEBSOCKET_VERIFY_CERT = False
|
BROADCAST_WEBSOCKET_VERIFY_CERT = False
|
||||||
BROADCAST_WEBSOCKET_PROTOCOL = 'http'
|
BROADCAST_WEBSOCKET_PROTOCOL = 'http'
|
||||||
79
awx/ui/package-lock.json
generated
79
awx/ui/package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "3.14.0",
|
"@lingui/react": "3.14.0",
|
||||||
"@patternfly/patternfly": "4.217.1",
|
"@patternfly/patternfly": "4.217.1",
|
||||||
"@patternfly/react-core": "^4.250.1",
|
"@patternfly/react-core": "^4.264.0",
|
||||||
"@patternfly/react-icons": "4.92.10",
|
"@patternfly/react-icons": "4.92.10",
|
||||||
"@patternfly/react-table": "4.108.0",
|
"@patternfly/react-table": "4.108.0",
|
||||||
"ace-builds": "^1.10.1",
|
"ace-builds": "^1.10.1",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"has-ansi": "5.0.1",
|
"has-ansi": "5.0.1",
|
||||||
"html-entities": "2.3.2",
|
"html-entities": "2.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
@@ -3752,13 +3752,13 @@
|
|||||||
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-core": {
|
"node_modules/@patternfly/react-core": {
|
||||||
"version": "4.250.1",
|
"version": "4.264.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.250.1.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.264.0.tgz",
|
||||||
"integrity": "sha512-vAOZPQdZzYXl/vkHnHMIt1eC3nrPDdsuuErPatkNPwmSvilXuXmWP5wxoJ36FbSNRRURkprFwx52zMmWS3iHJA==",
|
"integrity": "sha512-tK0BMWxw8nhukev40HZ6q6d02pDnjX7oyA91vHa18aakJUKBWMaerqpG4NZVMoh0tPKX3aLNj+zyCwDALFAZZw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/react-icons": "^4.92.6",
|
"@patternfly/react-icons": "^4.93.0",
|
||||||
"@patternfly/react-styles": "^4.91.6",
|
"@patternfly/react-styles": "^4.92.0",
|
||||||
"@patternfly/react-tokens": "^4.93.6",
|
"@patternfly/react-tokens": "^4.94.0",
|
||||||
"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,6 +3769,15 @@
|
|||||||
"react-dom": "^16.8 || ^17 || ^18"
|
"react-dom": "^16.8 || ^17 || ^18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@patternfly/react-core/node_modules/@patternfly/react-icons": {
|
||||||
|
"version": "4.93.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.93.0.tgz",
|
||||||
|
"integrity": "sha512-OH0vORVioL+HLWMEog8/3u8jsiMCeJ0pFpvRKRhy5Uk4CdAe40k1SOBvXJP6opr+O8TLbz0q3bm8Jsh/bPaCuQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8 || ^17 || ^18",
|
||||||
|
"react-dom": "^16.8 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -3784,9 +3793,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-styles": {
|
"node_modules/@patternfly/react-styles": {
|
||||||
"version": "4.91.10",
|
"version": "4.92.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.91.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.92.0.tgz",
|
||||||
"integrity": "sha512-fAG4Vjp63ohiR92F4e/Gkw5q1DSSckHKqdnEF75KUpSSBORzYP0EKMpupSd6ItpQFJw3iWs3MJi3/KIAAfU1Jw=="
|
"integrity": "sha512-B/f6iyu8UEN1+wRxdC4sLIhvJeyL8SqInDXZmwOIqK8uPJ8Lze7qrbVhkkVzbMF37/oDPVa6dZH8qZFq062LEA=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-table": {
|
"node_modules/@patternfly/react-table": {
|
||||||
"version": "4.108.0",
|
"version": "4.108.0",
|
||||||
@@ -3811,9 +3820,9 @@
|
|||||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-tokens": {
|
"node_modules/@patternfly/react-tokens": {
|
||||||
"version": "4.93.10",
|
"version": "4.94.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.93.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.94.0.tgz",
|
||||||
"integrity": "sha512-F+j1irDc9M6zvY6qNtDryhbpnHz3R8ymHRdGelNHQzPTIK88YSWEnT1c9iUI+uM/iuZol7sJmO5STtg2aPIDRQ=="
|
"integrity": "sha512-fYXxUJZnzpn89K2zzHF0cSncZZVGKrohdb5f5T1wzxwU2NZPVGpvr88xhm+V2Y/fSrrTPwXcP3IIdtNOOtJdZw=="
|
||||||
},
|
},
|
||||||
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
@@ -15468,9 +15477,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/luxon": {
|
"node_modules/luxon": {
|
||||||
"version": "3.0.3",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz",
|
||||||
"integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w==",
|
"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -25094,19 +25103,25 @@
|
|||||||
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-core": {
|
"@patternfly/react-core": {
|
||||||
"version": "4.250.1",
|
"version": "4.264.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.250.1.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.264.0.tgz",
|
||||||
"integrity": "sha512-vAOZPQdZzYXl/vkHnHMIt1eC3nrPDdsuuErPatkNPwmSvilXuXmWP5wxoJ36FbSNRRURkprFwx52zMmWS3iHJA==",
|
"integrity": "sha512-tK0BMWxw8nhukev40HZ6q6d02pDnjX7oyA91vHa18aakJUKBWMaerqpG4NZVMoh0tPKX3aLNj+zyCwDALFAZZw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@patternfly/react-icons": "^4.92.6",
|
"@patternfly/react-icons": "^4.93.0",
|
||||||
"@patternfly/react-styles": "^4.91.6",
|
"@patternfly/react-styles": "^4.92.0",
|
||||||
"@patternfly/react-tokens": "^4.93.6",
|
"@patternfly/react-tokens": "^4.94.0",
|
||||||
"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.93.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.93.0.tgz",
|
||||||
|
"integrity": "sha512-OH0vORVioL+HLWMEog8/3u8jsiMCeJ0pFpvRKRhy5Uk4CdAe40k1SOBvXJP6opr+O8TLbz0q3bm8Jsh/bPaCuQ==",
|
||||||
|
"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",
|
||||||
@@ -25121,9 +25136,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@patternfly/react-styles": {
|
"@patternfly/react-styles": {
|
||||||
"version": "4.91.10",
|
"version": "4.92.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.91.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.92.0.tgz",
|
||||||
"integrity": "sha512-fAG4Vjp63ohiR92F4e/Gkw5q1DSSckHKqdnEF75KUpSSBORzYP0EKMpupSd6ItpQFJw3iWs3MJi3/KIAAfU1Jw=="
|
"integrity": "sha512-B/f6iyu8UEN1+wRxdC4sLIhvJeyL8SqInDXZmwOIqK8uPJ8Lze7qrbVhkkVzbMF37/oDPVa6dZH8qZFq062LEA=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-table": {
|
"@patternfly/react-table": {
|
||||||
"version": "4.108.0",
|
"version": "4.108.0",
|
||||||
@@ -25146,9 +25161,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@patternfly/react-tokens": {
|
"@patternfly/react-tokens": {
|
||||||
"version": "4.93.10",
|
"version": "4.94.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.93.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.94.0.tgz",
|
||||||
"integrity": "sha512-F+j1irDc9M6zvY6qNtDryhbpnHz3R8ymHRdGelNHQzPTIK88YSWEnT1c9iUI+uM/iuZol7sJmO5STtg2aPIDRQ=="
|
"integrity": "sha512-fYXxUJZnzpn89K2zzHF0cSncZZVGKrohdb5f5T1wzxwU2NZPVGpvr88xhm+V2Y/fSrrTPwXcP3IIdtNOOtJdZw=="
|
||||||
},
|
},
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": {
|
"@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
@@ -34210,9 +34225,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"luxon": {
|
"luxon": {
|
||||||
"version": "3.0.3",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz",
|
||||||
"integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w=="
|
"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw=="
|
||||||
},
|
},
|
||||||
"lz-string": {
|
"lz-string": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "3.14.0",
|
"@lingui/react": "3.14.0",
|
||||||
"@patternfly/patternfly": "4.217.1",
|
"@patternfly/patternfly": "4.217.1",
|
||||||
"@patternfly/react-core": "^4.250.1",
|
"@patternfly/react-core": "^4.264.0",
|
||||||
"@patternfly/react-icons": "4.92.10",
|
"@patternfly/react-icons": "4.92.10",
|
||||||
"@patternfly/react-table": "4.108.0",
|
"@patternfly/react-table": "4.108.0",
|
||||||
"ace-builds": "^1.10.1",
|
"ace-builds": "^1.10.1",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"has-ansi": "5.0.1",
|
"has-ansi": "5.0.1",
|
||||||
"html-entities": "2.3.2",
|
"html-entities": "2.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ function CredentialsStep({
|
|||||||
}))}
|
}))}
|
||||||
value={selectedType && selectedType.id}
|
value={selectedType && selectedType.id}
|
||||||
onChange={(e, id) => {
|
onChange={(e, id) => {
|
||||||
|
// Reset query params when the category of credentials is changed
|
||||||
|
history.replace({
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
setSelectedType(types.find((o) => o.id === parseInt(id, 10)));
|
setSelectedType(types.find((o) => o.id === parseInt(id, 10)));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { act } from 'react-dom/test-utils';
|
|||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { CredentialsAPI, CredentialTypesAPI } from 'api';
|
import { CredentialsAPI, CredentialTypesAPI } from 'api';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import CredentialsStep from './CredentialsStep';
|
import CredentialsStep from './CredentialsStep';
|
||||||
|
|
||||||
jest.mock('../../../api/models/CredentialTypes');
|
jest.mock('../../../api/models/CredentialTypes');
|
||||||
@@ -164,6 +165,41 @@ describe('CredentialsStep', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reset query params (credential.page) when selected credential type is changed', async () => {
|
||||||
|
let wrapper;
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['?credential.page=2'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik>
|
||||||
|
<CredentialsStep allowCredentialsWithPasswords />
|
||||||
|
</Formik>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 1,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 2,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('AnsibleSelect').invoke('onChange')({}, 3);
|
||||||
|
});
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 3,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("error should be shown when a credential that prompts for passwords is selected on a step that doesn't allow it", async () => {
|
test("error should be shown when a credential that prompts for passwords is selected on a step that doesn't allow it", async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ function MultiCredentialsLookup({
|
|||||||
}))}
|
}))}
|
||||||
value={selectedType && selectedType.id}
|
value={selectedType && selectedType.id}
|
||||||
onChange={(e, id) => {
|
onChange={(e, id) => {
|
||||||
|
// Reset query params when the category of credentials is changed
|
||||||
|
history.replace({
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
setSelectedType(
|
setSelectedType(
|
||||||
credentialTypes.find((o) => o.id === parseInt(id, 10))
|
credentialTypes.find((o) => o.id === parseInt(id, 10))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import MultiCredentialsLookup from './MultiCredentialsLookup';
|
import MultiCredentialsLookup from './MultiCredentialsLookup';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
@@ -228,6 +229,53 @@ describe('<Formik><MultiCredentialsLookup /></Formik>', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reset query params (credentials.page) when selected credential type is changed', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['?credentials.page=2'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik>
|
||||||
|
<MultiCredentialsLookup
|
||||||
|
value={credentials}
|
||||||
|
tooltip="This is credentials look up"
|
||||||
|
onChange={() => {}}
|
||||||
|
onError={() => {}}
|
||||||
|
/>
|
||||||
|
</Formik>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const searchButton = await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Button[aria-label="Search"]'
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
searchButton.invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 400,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 2,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = await waitForElement(wrapper, 'AnsibleSelect');
|
||||||
|
await act(async () => {
|
||||||
|
select.invoke('onChange')({}, 500);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 500,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should only add 1 credential per credential type except vault(see below)', async () => {
|
test('should only add 1 credential per credential type except vault(see below)', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ function DateTimePicker({ dateFieldName, timeFieldName, label }) {
|
|||||||
onChange={onDateChange}
|
onChange={onDateChange}
|
||||||
/>
|
/>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
placeholder="hh:mm AM/PM"
|
|
||||||
stepMinutes={15}
|
stepMinutes={15}
|
||||||
aria-label={
|
aria-label={
|
||||||
timeFieldName.startsWith('start') ? t`Start time` : t`End time`
|
timeFieldName.startsWith('start') ? t`Start time` : t`End time`
|
||||||
|
|||||||
93
awx/ui/src/components/Schedule/shared/FrequenciesList.js
Normal file
93
awx/ui/src/components/Schedule/shared/FrequenciesList.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Switch,
|
||||||
|
Toolbar,
|
||||||
|
ToolbarContent,
|
||||||
|
ToolbarItem,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Tbody,
|
||||||
|
Thead,
|
||||||
|
Th,
|
||||||
|
Tr,
|
||||||
|
Td,
|
||||||
|
} from '@patternfly/react-table';
|
||||||
|
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import ContentEmpty from 'components/ContentEmpty';
|
||||||
|
|
||||||
|
function FrequenciesList({ openWizard }) {
|
||||||
|
const [isShowingRules, setIsShowingRules] = useState(true);
|
||||||
|
const [frequencies] = useField('frequencies');
|
||||||
|
const list = (freq) => (
|
||||||
|
<Tr key={freq.rrule}>
|
||||||
|
<Td>{freq.frequency}</Td>
|
||||||
|
<Td>{freq.rrule}</Td>
|
||||||
|
<Td>{t`End`}</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
aria-label={t`Click to toggle default value`}
|
||||||
|
ouiaId={freq ? `${freq}-button` : 'new-freq-button'}
|
||||||
|
onClick={() => {
|
||||||
|
openWizard(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarContent>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
openWizard(true);
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{isShowingRules ? t`Add RRules` : t`Add Exception`}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Switch
|
||||||
|
label={t`Occurances`}
|
||||||
|
labelOff={t`Exceptions`}
|
||||||
|
isChecked={isShowingRules}
|
||||||
|
onChange={(isChecked) => {
|
||||||
|
setIsShowingRules(isChecked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarContent>
|
||||||
|
</Toolbar>
|
||||||
|
<div css="overflow: auto">
|
||||||
|
{frequencies.value[0].frequency === '' &&
|
||||||
|
frequencies.value.length < 2 ? (
|
||||||
|
<ContentEmpty title={t`RRules`} message={t`Add RRules`} />
|
||||||
|
) : (
|
||||||
|
<TableComposable aria-label={t`RRules`} ouiaId="rrules-list">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t`Frequency`}</Th>
|
||||||
|
<Th>{t`RRule`}</Th>
|
||||||
|
<Th>{t`Ending`}</Th>
|
||||||
|
<Th>{t`Actions`}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>{frequencies.value.map((freq, i) => list(freq, i))}</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FrequenciesList;
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
import 'styled-components/macro';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { useField } from 'formik';
|
|
||||||
|
|
||||||
import { t, Trans, Plural } from '@lingui/macro';
|
|
||||||
import { RRule } from 'rrule';
|
|
||||||
import {
|
|
||||||
Checkbox as _Checkbox,
|
|
||||||
FormGroup,
|
|
||||||
Radio,
|
|
||||||
TextInput,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { required, requiredPositiveInteger } from 'util/validators';
|
|
||||||
import AnsibleSelect from '../../AnsibleSelect';
|
|
||||||
import FormField from '../../FormField';
|
|
||||||
import DateTimePicker from './DateTimePicker';
|
|
||||||
|
|
||||||
const RunOnRadio = styled(Radio)`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(:last-of-type) {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:not(:first-of-type) {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RunEveryLabel = styled.p`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Checkbox = styled(_Checkbox)`
|
|
||||||
:not(:last-of-type) {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FrequencyDetailSubform = ({ frequency, prefix, isException }) => {
|
|
||||||
const id = prefix.replace('.', '-');
|
|
||||||
const [runOnDayMonth] = useField({
|
|
||||||
name: `${prefix}.runOnDayMonth`,
|
|
||||||
});
|
|
||||||
const [runOnDayNumber] = useField({
|
|
||||||
name: `${prefix}.runOnDayNumber`,
|
|
||||||
});
|
|
||||||
const [runOnTheOccurrence] = useField({
|
|
||||||
name: `${prefix}.runOnTheOccurrence`,
|
|
||||||
});
|
|
||||||
const [runOnTheDay] = useField({
|
|
||||||
name: `${prefix}.runOnTheDay`,
|
|
||||||
});
|
|
||||||
const [runOnTheMonth] = useField({
|
|
||||||
name: `${prefix}.runOnTheMonth`,
|
|
||||||
});
|
|
||||||
const [startDate] = useField(`${prefix}.startDate`);
|
|
||||||
|
|
||||||
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({
|
|
||||||
name: `${prefix}.daysOfWeek`,
|
|
||||||
validate: (val) => {
|
|
||||||
if (frequency === 'week') {
|
|
||||||
return required(t`Select a value for this field`)(val?.length > 0);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [end, endMeta] = useField({
|
|
||||||
name: `${prefix}.end`,
|
|
||||||
validate: required(t`Select a value for this field`),
|
|
||||||
});
|
|
||||||
const [interval, intervalMeta] = useField({
|
|
||||||
name: `${prefix}.interval`,
|
|
||||||
validate: requiredPositiveInteger(),
|
|
||||||
});
|
|
||||||
const [runOn, runOnMeta] = useField({
|
|
||||||
name: `${prefix}.runOn`,
|
|
||||||
validate: (val) => {
|
|
||||||
if (frequency === 'month' || frequency === 'year') {
|
|
||||||
return required(t`Select a value for this field`)(val);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const monthOptions = [
|
|
||||||
{
|
|
||||||
key: 'january',
|
|
||||||
value: 1,
|
|
||||||
label: t`January`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'february',
|
|
||||||
value: 2,
|
|
||||||
label: t`February`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'march',
|
|
||||||
value: 3,
|
|
||||||
label: t`March`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'april',
|
|
||||||
value: 4,
|
|
||||||
label: t`April`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'may',
|
|
||||||
value: 5,
|
|
||||||
label: t`May`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'june',
|
|
||||||
value: 6,
|
|
||||||
label: t`June`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'july',
|
|
||||||
value: 7,
|
|
||||||
label: t`July`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'august',
|
|
||||||
value: 8,
|
|
||||||
label: t`August`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'september',
|
|
||||||
value: 9,
|
|
||||||
label: t`September`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'october',
|
|
||||||
value: 10,
|
|
||||||
label: t`October`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'november',
|
|
||||||
value: 11,
|
|
||||||
label: t`November`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'december',
|
|
||||||
value: 12,
|
|
||||||
label: t`December`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const updateDaysOfWeek = (day, checked) => {
|
|
||||||
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
|
|
||||||
daysOfWeekHelpers.setTouched(true);
|
|
||||||
if (checked) {
|
|
||||||
newDaysOfWeek.push(day);
|
|
||||||
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
|
||||||
} else {
|
|
||||||
daysOfWeekHelpers.setValue(
|
|
||||||
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPeriodLabel = () => {
|
|
||||||
switch (frequency) {
|
|
||||||
case 'minute':
|
|
||||||
return t`Minute`;
|
|
||||||
case 'hour':
|
|
||||||
return t`Hour`;
|
|
||||||
case 'day':
|
|
||||||
return t`Day`;
|
|
||||||
case 'week':
|
|
||||||
return t`Week`;
|
|
||||||
case 'month':
|
|
||||||
return t`Month`;
|
|
||||||
case 'year':
|
|
||||||
return t`Year`;
|
|
||||||
default:
|
|
||||||
throw new Error(t`Frequency did not match an expected value`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRunEveryLabel = () => {
|
|
||||||
const intervalValue = interval.value;
|
|
||||||
|
|
||||||
switch (frequency) {
|
|
||||||
case 'minute':
|
|
||||||
return <Plural value={intervalValue} one="minute" other="minutes" />;
|
|
||||||
case 'hour':
|
|
||||||
return <Plural value={intervalValue} one="hour" other="hours" />;
|
|
||||||
case 'day':
|
|
||||||
return <Plural value={intervalValue} one="day" other="days" />;
|
|
||||||
case 'week':
|
|
||||||
return <Plural value={intervalValue} one="week" other="weeks" />;
|
|
||||||
case 'month':
|
|
||||||
return <Plural value={intervalValue} one="month" other="months" />;
|
|
||||||
case 'year':
|
|
||||||
return <Plural value={intervalValue} one="year" other="years" />;
|
|
||||||
default:
|
|
||||||
throw new Error(t`Frequency did not match an expected value`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p css="grid-column: 1/-1">
|
|
||||||
<b>{getPeriodLabel()}</b>
|
|
||||||
</p>
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.interval`}
|
|
||||||
fieldId={`schedule-run-every-${id}`}
|
|
||||||
helperTextInvalid={intervalMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={
|
|
||||||
!intervalMeta.touched || !intervalMeta.error ? 'default' : 'error'
|
|
||||||
}
|
|
||||||
label={isException ? t`Skip every` : t`Run every`}
|
|
||||||
>
|
|
||||||
<div css="display: flex">
|
|
||||||
<TextInput
|
|
||||||
css="margin-right: 10px;"
|
|
||||||
id={`schedule-run-every-${id}`}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
{...interval}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
interval.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RunEveryLabel>{getRunEveryLabel()}</RunEveryLabel>
|
|
||||||
</div>
|
|
||||||
</FormGroup>
|
|
||||||
{frequency === 'week' && (
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
fieldId={`schedule-days-of-week-${id}`}
|
|
||||||
helperTextInvalid={daysOfWeekMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={
|
|
||||||
!daysOfWeekMeta.touched || !daysOfWeekMeta.error
|
|
||||||
? 'default'
|
|
||||||
: 'error'
|
|
||||||
}
|
|
||||||
label={t`On days`}
|
|
||||||
>
|
|
||||||
<div css="display: flex">
|
|
||||||
<Checkbox
|
|
||||||
label={t`Sun`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.SU)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.SU, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Sunday`}
|
|
||||||
id={`schedule-days-of-week-sun-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-sun-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Mon`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.MO)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.MO, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Monday`}
|
|
||||||
id={`schedule-days-of-week-mon-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-mon-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Tue`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.TU)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.TU, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Tuesday`}
|
|
||||||
id={`schedule-days-of-week-tue-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-tue-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Wed`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.WE)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.WE, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Wednesday`}
|
|
||||||
id={`schedule-days-of-week-wed-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-wed-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Thu`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.TH)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.TH, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Thursday`}
|
|
||||||
id={`schedule-days-of-week-thu-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-thu-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Fri`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.FR)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.FR, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Friday`}
|
|
||||||
id={`schedule-days-of-week-fri-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-fri-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Sat`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.SA)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.SA, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Saturday`}
|
|
||||||
id={`schedule-days-of-week-sat-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-sat-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{(frequency === 'month' || frequency === 'year') &&
|
|
||||||
!Number.isNaN(new Date(startDate.value)) && (
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.runOn`}
|
|
||||||
fieldId={`schedule-run-on-${id}`}
|
|
||||||
helperTextInvalid={runOnMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={
|
|
||||||
!runOnMeta.touched || !runOnMeta.error ? 'default' : 'error'
|
|
||||||
}
|
|
||||||
label={t`Run on`}
|
|
||||||
>
|
|
||||||
<RunOnRadio
|
|
||||||
id={`schedule-run-on-day-${id}`}
|
|
||||||
name={`${prefix}.runOn`}
|
|
||||||
label={
|
|
||||||
<div css="display: flex;align-items: center;">
|
|
||||||
{frequency === 'month' && (
|
|
||||||
<span
|
|
||||||
id="radio-schedule-run-on-day"
|
|
||||||
css="margin-right: 10px;"
|
|
||||||
>
|
|
||||||
<Trans>Day</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{frequency === 'year' && (
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-day-month-${id}`}
|
|
||||||
css="margin-right: 10px"
|
|
||||||
isDisabled={runOn.value !== 'day'}
|
|
||||||
data={monthOptions}
|
|
||||||
{...runOnDayMonth}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextInput
|
|
||||||
id={`schedule-run-on-day-number-${id}`}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="31"
|
|
||||||
step="1"
|
|
||||||
isDisabled={runOn.value !== 'day'}
|
|
||||||
{...runOnDayNumber}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
runOnDayNumber.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
value="day"
|
|
||||||
isChecked={runOn.value === 'day'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'day';
|
|
||||||
runOn.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RunOnRadio
|
|
||||||
id={`schedule-run-on-the-${id}`}
|
|
||||||
name={`${prefix}.runOn`}
|
|
||||||
label={
|
|
||||||
<div css="display: flex;align-items: center;">
|
|
||||||
<span
|
|
||||||
id={`radio-schedule-run-on-the-${id}`}
|
|
||||||
css="margin-right: 10px;"
|
|
||||||
>
|
|
||||||
<Trans>The</Trans>
|
|
||||||
</span>
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-the-occurrence-${id}`}
|
|
||||||
isDisabled={runOn.value !== 'the'}
|
|
||||||
data={[
|
|
||||||
{ value: 1, key: 'first', label: t`First` },
|
|
||||||
{
|
|
||||||
value: 2,
|
|
||||||
key: 'second',
|
|
||||||
label: t`Second`,
|
|
||||||
},
|
|
||||||
{ value: 3, key: 'third', label: t`Third` },
|
|
||||||
{
|
|
||||||
value: 4,
|
|
||||||
key: 'fourth',
|
|
||||||
label: t`Fourth`,
|
|
||||||
},
|
|
||||||
{ value: 5, key: 'fifth', label: t`Fifth` },
|
|
||||||
{ value: -1, key: 'last', label: t`Last` },
|
|
||||||
]}
|
|
||||||
{...runOnTheOccurrence}
|
|
||||||
/>
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-the-day-${id}`}
|
|
||||||
isDisabled={runOn.value !== 'the'}
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
value: 'sunday',
|
|
||||||
key: 'sunday',
|
|
||||||
label: t`Sunday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'monday',
|
|
||||||
key: 'monday',
|
|
||||||
label: t`Monday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'tuesday',
|
|
||||||
key: 'tuesday',
|
|
||||||
label: t`Tuesday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'wednesday',
|
|
||||||
key: 'wednesday',
|
|
||||||
label: t`Wednesday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'thursday',
|
|
||||||
key: 'thursday',
|
|
||||||
label: t`Thursday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'friday',
|
|
||||||
key: 'friday',
|
|
||||||
label: t`Friday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'saturday',
|
|
||||||
key: 'saturday',
|
|
||||||
label: t`Saturday`,
|
|
||||||
},
|
|
||||||
{ value: 'day', key: 'day', label: t`Day` },
|
|
||||||
{
|
|
||||||
value: 'weekday',
|
|
||||||
key: 'weekday',
|
|
||||||
label: t`Weekday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'weekendDay',
|
|
||||||
key: 'weekendDay',
|
|
||||||
label: t`Weekend day`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
{...runOnTheDay}
|
|
||||||
/>
|
|
||||||
{frequency === 'year' && (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
id={`of-schedule-run-on-the-month-${id}`}
|
|
||||||
css="margin-left: 10px;"
|
|
||||||
>
|
|
||||||
<Trans>of</Trans>
|
|
||||||
</span>
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-the-month-${id}`}
|
|
||||||
isDisabled={runOn.value !== 'the'}
|
|
||||||
data={monthOptions}
|
|
||||||
{...runOnTheMonth}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
value="the"
|
|
||||||
isChecked={runOn.value === 'the'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'the';
|
|
||||||
runOn.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
fieldId={`schedule-end-${id}`}
|
|
||||||
helperTextInvalid={endMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={!endMeta.touched || !endMeta.error ? 'default' : 'error'}
|
|
||||||
label={t`End`}
|
|
||||||
>
|
|
||||||
<Radio
|
|
||||||
id={`end-never-${id}`}
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
label={t`Never`}
|
|
||||||
value="never"
|
|
||||||
isChecked={end.value === 'never'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'never';
|
|
||||||
end.onChange(event);
|
|
||||||
}}
|
|
||||||
ouiaId={`end-never-radio-button-${id}`}
|
|
||||||
/>
|
|
||||||
<Radio
|
|
||||||
id={`end-after-${id}`}
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
label={t`After number of occurrences`}
|
|
||||||
value="after"
|
|
||||||
isChecked={end.value === 'after'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'after';
|
|
||||||
end.onChange(event);
|
|
||||||
}}
|
|
||||||
ouiaId={`end-after-radio-button-${id}`}
|
|
||||||
/>
|
|
||||||
<Radio
|
|
||||||
id={`end-on-date-${id}`}
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
label={t`On date`}
|
|
||||||
value="onDate"
|
|
||||||
isChecked={end.value === 'onDate'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'onDate';
|
|
||||||
end.onChange(event);
|
|
||||||
}}
|
|
||||||
ouiaId={`end-on-radio-button-${id}`}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
{end?.value === 'after' && (
|
|
||||||
<FormField
|
|
||||||
id={`schedule-occurrences-${id}`}
|
|
||||||
label={t`Occurrences`}
|
|
||||||
name={`${prefix}.occurrences`}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{end?.value === 'onDate' && (
|
|
||||||
<DateTimePicker
|
|
||||||
dateFieldName={`${prefix}.endDate`}
|
|
||||||
timeFieldName={`${prefix}.endTime`}
|
|
||||||
label={t`End date/time`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FrequencyDetailSubform;
|
|
||||||
@@ -1,30 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { arrayOf, string } from 'prop-types';
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||||
|
|
||||||
export default function FrequencySelect({
|
export default function FrequencySelect({ id, onBlur, placeholderText }) {
|
||||||
id,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
placeholderText,
|
|
||||||
children,
|
|
||||||
}) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [frequency, , frequencyHelpers] = useField('freq');
|
||||||
const onSelect = (event, selectedValue) => {
|
|
||||||
if (selectedValue === 'none') {
|
|
||||||
onChange([]);
|
|
||||||
setIsOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = value.indexOf(selectedValue);
|
|
||||||
if (index === -1) {
|
|
||||||
onChange(value.concat(selectedValue));
|
|
||||||
} else {
|
|
||||||
onChange(value.slice(0, index).concat(value.slice(index + 1)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggle = (val) => {
|
const onToggle = (val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
@@ -35,21 +17,26 @@ export default function FrequencySelect({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
variant={SelectVariant.checkbox}
|
onSelect={(e, v) => {
|
||||||
onSelect={onSelect}
|
frequencyHelpers.setValue(v);
|
||||||
selections={value}
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
selections={frequency.value}
|
||||||
placeholderText={placeholderText}
|
placeholderText={placeholderText}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
|
value={frequency.value}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
ouiaId={`frequency-select-${id}`}
|
ouiaId={`frequency-select-${id}`}
|
||||||
|
onBlur={() => frequencyHelpers.setTouched(true)}
|
||||||
>
|
>
|
||||||
{children}
|
<SelectOption value={RRule.MINUTELY}>{t`Minute`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.HOURLY}>{t`Hour`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.DAILY}>{t`Day`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.WEEKLY}>{t`Week`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.MONTHLY}>{t`Month`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.YEARLY}>{t`Year`}</SelectOption>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FrequencySelect.propTypes = {
|
|
||||||
value: arrayOf(string).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { SelectOption, SelectVariant };
|
export { SelectOption, SelectVariant };
|
||||||
|
|||||||
77
awx/ui/src/components/Schedule/shared/MonthandYearForm.js
Normal file
77
awx/ui/src/components/Schedule/shared/MonthandYearForm.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import AnsibleSelect from 'components/AnsibleSelect';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
Checkbox as _Checkbox,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { bysetposOptions, monthOptions } from './scheduleFormHelpers';
|
||||||
|
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const Checkbox = styled(_Checkbox)`
|
||||||
|
:not(:last-of-type) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
function MonthandYearForm({ id }) {
|
||||||
|
const [bySetPos, , bySetPosHelpers] = useField('bysetpos');
|
||||||
|
const [byMonth, , byMonthHelpers] = useField('bymonth');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GroupWrapper
|
||||||
|
fieldId={`schedule-run-on-${id}`}
|
||||||
|
label={<b>{t`Run on a specific month`}</b>}
|
||||||
|
>
|
||||||
|
<Grid hasGutter>
|
||||||
|
{monthOptions.map((month) => (
|
||||||
|
<GridItem key={month.label} span={2} rowSpan={2}>
|
||||||
|
<Checkbox
|
||||||
|
label={month.label}
|
||||||
|
isChecked={byMonth.value?.includes(month.value)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
byMonthHelpers.setValue([...byMonth.value, month.value]);
|
||||||
|
} else {
|
||||||
|
const removed = byMonth.value.filter(
|
||||||
|
(i) => i !== month.value
|
||||||
|
);
|
||||||
|
byMonthHelpers.setValue(removed);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
id={`bymonth-${month.label}`}
|
||||||
|
ouiaId={`bymonth-${month.label}`}
|
||||||
|
name="bymonth"
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</GroupWrapper>
|
||||||
|
<GroupWrapper
|
||||||
|
label={<b>{t`Run on a specific week day at monthly intervals`}</b>}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
id={`schedule-run-on-the-occurrence-${id}`}
|
||||||
|
data={bysetposOptions}
|
||||||
|
{...bySetPos}
|
||||||
|
onChange={(e, v) => {
|
||||||
|
bySetPosHelpers.setValue(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default MonthandYearForm;
|
||||||
45
awx/ui/src/components/Schedule/shared/OrdinalDayForm.js
Normal file
45
awx/ui/src/components/Schedule/shared/OrdinalDayForm.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { FormGroup, TextInput } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function OrdinalDayForm() {
|
||||||
|
const [byMonthDay] = useField('bymonthday');
|
||||||
|
const [byYearDay] = useField('byyearday');
|
||||||
|
return (
|
||||||
|
<GroupWrapper
|
||||||
|
label={<b>{t`On a specific number day`}</b>}
|
||||||
|
name="ordinalDay"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder={t`Run on a day of month`}
|
||||||
|
aria-label={t`Type a numbered day`}
|
||||||
|
type="number"
|
||||||
|
onChange={(value, event) => {
|
||||||
|
byMonthDay.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder={t`Run on a day of year`}
|
||||||
|
aria-label={t`Type a numbered day`}
|
||||||
|
type="number"
|
||||||
|
onChange={(value, event) => {
|
||||||
|
byYearDay.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrdinalDayForm;
|
||||||
67
awx/ui/src/components/Schedule/shared/ScheduleEndForm.js
Normal file
67
awx/ui/src/components/Schedule/shared/ScheduleEndForm.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { FormGroup, Radio } from '@patternfly/react-core';
|
||||||
|
import FormField from 'components/FormField';
|
||||||
|
import DateTimePicker from './DateTimePicker';
|
||||||
|
|
||||||
|
function ScheduleEndForm() {
|
||||||
|
const [endType, , { setValue }] = useField('endingType');
|
||||||
|
const [count] = useField('count');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup name="end" label={t`End`}>
|
||||||
|
<Radio
|
||||||
|
id="endNever"
|
||||||
|
name={t`Never End`}
|
||||||
|
label={t`Never`}
|
||||||
|
value="never"
|
||||||
|
isChecked={endType.value === 'never'}
|
||||||
|
onChange={() => {
|
||||||
|
setValue('never');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
name="Count"
|
||||||
|
id="after"
|
||||||
|
label={t`After number of occurrences`}
|
||||||
|
value="after"
|
||||||
|
isChecked={endType.value === 'after'}
|
||||||
|
onChange={() => {
|
||||||
|
setValue('after');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
name="End Date"
|
||||||
|
label={t`On date`}
|
||||||
|
value="onDate"
|
||||||
|
id="endDate"
|
||||||
|
isChecked={endType.value === 'onDate'}
|
||||||
|
onChange={() => {
|
||||||
|
setValue('onDate');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{endType.value === 'after' && (
|
||||||
|
<FormField
|
||||||
|
label={t`Occurrences`}
|
||||||
|
name="count"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
isRequired
|
||||||
|
{...count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{endType.value === 'onDate' && (
|
||||||
|
<DateTimePicker
|
||||||
|
dateFieldName="endDate"
|
||||||
|
timeFieldName="endTime"
|
||||||
|
label={t`End date/time`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScheduleEndForm;
|
||||||
@@ -18,14 +18,9 @@ import SchedulePromptableFields from './SchedulePromptableFields';
|
|||||||
import ScheduleFormFields from './ScheduleFormFields';
|
import ScheduleFormFields from './ScheduleFormFields';
|
||||||
import UnsupportedScheduleForm from './UnsupportedScheduleForm';
|
import UnsupportedScheduleForm from './UnsupportedScheduleForm';
|
||||||
import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj';
|
import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj';
|
||||||
import buildRuleObj from './buildRuleObj';
|
import ScheduleFormWizard from './ScheduleFormWizard';
|
||||||
import buildRuleSet from './buildRuleSet';
|
import FrequenciesList from './FrequenciesList';
|
||||||
|
// import { validateSchedule } from './scheduleFormHelpers';
|
||||||
const NUM_DAYS_PER_FREQUENCY = {
|
|
||||||
week: 7,
|
|
||||||
month: 31,
|
|
||||||
year: 365,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ScheduleForm({
|
function ScheduleForm({
|
||||||
hasDaysToKeepField,
|
hasDaysToKeepField,
|
||||||
@@ -40,15 +35,16 @@ function ScheduleForm({
|
|||||||
}) {
|
}) {
|
||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||||
|
const [isScheduleWizardOpen, setIsScheduleWizardOpen] = useState(false);
|
||||||
const originalLabels = useRef([]);
|
const originalLabels = useRef([]);
|
||||||
const originalInstanceGroups = useRef([]);
|
const originalInstanceGroups = useRef([]);
|
||||||
|
|
||||||
let rruleError;
|
let rruleError;
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
|
|
||||||
const closestQuarterHour = DateTime.fromMillis(
|
const closestQuarterHour = DateTime.fromMillis(
|
||||||
Math.ceil(now.ts / 900000) * 900000
|
Math.ceil(now.ts / 900000) * 900000
|
||||||
);
|
);
|
||||||
const tomorrow = closestQuarterHour.plus({ days: 1 });
|
|
||||||
const isTemplate =
|
const isTemplate =
|
||||||
resource.type === 'workflow_job_template' ||
|
resource.type === 'workflow_job_template' ||
|
||||||
resource.type === 'job_template';
|
resource.type === 'job_template';
|
||||||
@@ -283,69 +279,10 @@ function ScheduleForm({
|
|||||||
}
|
}
|
||||||
const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO());
|
const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO());
|
||||||
|
|
||||||
const [tomorrowDate] = dateToInputDateTime(tomorrow.toISO());
|
|
||||||
const initialFrequencyOptions = {
|
|
||||||
minute: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
},
|
|
||||||
hour: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
},
|
|
||||||
day: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
},
|
|
||||||
week: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
daysOfWeek: [],
|
|
||||||
},
|
|
||||||
month: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
runOn: 'day',
|
|
||||||
runOnTheOccurrence: 1,
|
|
||||||
runOnTheDay: 'sunday',
|
|
||||||
runOnDayNumber: 1,
|
|
||||||
},
|
|
||||||
year: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
runOn: 'day',
|
|
||||||
runOnTheOccurrence: 1,
|
|
||||||
runOnTheDay: 'sunday',
|
|
||||||
runOnTheMonth: 1,
|
|
||||||
runOnDayMonth: 1,
|
|
||||||
runOnDayNumber: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
description: schedule.description || '',
|
description: schedule.description || '',
|
||||||
frequency: [],
|
frequencies: [],
|
||||||
exceptionFrequency: [],
|
exceptionFrequency: [],
|
||||||
frequencyOptions: initialFrequencyOptions,
|
|
||||||
exceptionOptions: initialFrequencyOptions,
|
|
||||||
name: schedule.name || '',
|
name: schedule.name || '',
|
||||||
startDate: currentDate,
|
startDate: currentDate,
|
||||||
startTime: time,
|
startTime: time,
|
||||||
@@ -367,11 +304,9 @@ function ScheduleForm({
|
|||||||
}
|
}
|
||||||
initialValues.daysToKeep = initialDaysToKeep;
|
initialValues.daysToKeep = initialDaysToKeep;
|
||||||
}
|
}
|
||||||
|
|
||||||
let overriddenValues = {};
|
|
||||||
if (schedule.rrule) {
|
if (schedule.rrule) {
|
||||||
try {
|
try {
|
||||||
overriddenValues = parseRuleObj(schedule);
|
parseRuleObj(schedule);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof UnsupportedRRuleError) {
|
if (error instanceof UnsupportedRRuleError) {
|
||||||
return (
|
return (
|
||||||
@@ -394,89 +329,33 @@ function ScheduleForm({
|
|||||||
if (contentLoading) {
|
if (contentLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
const frequencies = [];
|
||||||
const validate = (values) => {
|
frequencies.push(parseRuleObj(schedule));
|
||||||
const errors = {};
|
|
||||||
|
|
||||||
values.frequency.forEach((freq) => {
|
|
||||||
const options = values.frequencyOptions[freq];
|
|
||||||
const freqErrors = {};
|
|
||||||
|
|
||||||
if (
|
|
||||||
(freq === 'month' || freq === 'year') &&
|
|
||||||
options.runOn === 'day' &&
|
|
||||||
(options.runOnDayNumber < 1 || options.runOnDayNumber > 31)
|
|
||||||
) {
|
|
||||||
freqErrors.runOn = t`Please select a day number between 1 and 31.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.end === 'after' && !options.occurrences) {
|
|
||||||
freqErrors.occurrences = t`Please enter a number of occurrences.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.end === 'onDate') {
|
|
||||||
if (
|
|
||||||
DateTime.fromFormat(
|
|
||||||
`${values.startDate} ${values.startTime}`,
|
|
||||||
'yyyy-LL-dd h:mm a'
|
|
||||||
).toMillis() >=
|
|
||||||
DateTime.fromFormat(
|
|
||||||
`${options.endDate} ${options.endTime}`,
|
|
||||||
'yyyy-LL-dd h:mm a'
|
|
||||||
).toMillis()
|
|
||||||
) {
|
|
||||||
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
DateTime.fromISO(options.endDate)
|
|
||||||
.diff(DateTime.fromISO(values.startDate), 'days')
|
|
||||||
.toObject().days < NUM_DAYS_PER_FREQUENCY[freq]
|
|
||||||
) {
|
|
||||||
const rule = new RRule(
|
|
||||||
buildRuleObj({
|
|
||||||
startDate: values.startDate,
|
|
||||||
startTime: values.startTime,
|
|
||||||
frequency: freq,
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (rule.all().length === 0) {
|
|
||||||
errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
|
||||||
freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Object.keys(freqErrors).length > 0) {
|
|
||||||
if (!errors.frequencyOptions) {
|
|
||||||
errors.frequencyOptions = {};
|
|
||||||
}
|
|
||||||
errors.frequencyOptions[freq] = freqErrors;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
|
|
||||||
errors.exceptionFrequency = t`This schedule has no occurrences due to the selected exceptions.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Config>
|
<Config>
|
||||||
{() => (
|
{() => (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
...initialValues,
|
name: schedule.name || '',
|
||||||
...overriddenValues,
|
description: schedule.description || '',
|
||||||
frequencyOptions: {
|
frequencies: frequencies || [],
|
||||||
...initialValues.frequencyOptions,
|
freq: RRule.DAILY,
|
||||||
...overriddenValues.frequencyOptions,
|
interval: 1,
|
||||||
},
|
wkst: RRule.SU,
|
||||||
exceptionOptions: {
|
byweekday: [],
|
||||||
...initialValues.exceptionOptions,
|
byweekno: [],
|
||||||
...overriddenValues.exceptionOptions,
|
bymonth: [],
|
||||||
},
|
bymonthday: '',
|
||||||
|
byyearday: '',
|
||||||
|
bysetpos: '',
|
||||||
|
until: schedule.until || null,
|
||||||
|
endDate: currentDate,
|
||||||
|
endTime: time,
|
||||||
|
count: 1,
|
||||||
|
endingType: 'never',
|
||||||
|
timezone: schedule.timezone || now.zoneName,
|
||||||
|
startDate: currentDate,
|
||||||
|
startTime: time,
|
||||||
}}
|
}}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
submitSchedule(
|
submitSchedule(
|
||||||
@@ -488,73 +367,90 @@ function ScheduleForm({
|
|||||||
credentials
|
credentials
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
validate={validate}
|
validate={() => {}}
|
||||||
>
|
>
|
||||||
{(formik) => (
|
{(formik) => (
|
||||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
<>
|
||||||
<FormColumnLayout>
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
<ScheduleFormFields
|
<FormColumnLayout>
|
||||||
hasDaysToKeepField={hasDaysToKeepField}
|
<ScheduleFormFields
|
||||||
zoneOptions={zoneOptions}
|
hasDaysToKeepField={hasDaysToKeepField}
|
||||||
zoneLinks={zoneLinks}
|
zoneOptions={zoneOptions}
|
||||||
/>
|
zoneLinks={zoneLinks}
|
||||||
{isWizardOpen && (
|
|
||||||
<SchedulePromptableFields
|
|
||||||
schedule={schedule}
|
|
||||||
credentials={credentials}
|
|
||||||
surveyConfig={surveyConfig}
|
|
||||||
launchConfig={launchConfig}
|
|
||||||
resource={resource}
|
|
||||||
onCloseWizard={() => {
|
|
||||||
setIsWizardOpen(false);
|
|
||||||
}}
|
|
||||||
onSave={() => {
|
|
||||||
setIsWizardOpen(false);
|
|
||||||
setIsSaveDisabled(false);
|
|
||||||
}}
|
|
||||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
|
||||||
labels={originalLabels.current}
|
|
||||||
instanceGroups={originalInstanceGroups.current}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{isWizardOpen && (
|
||||||
<FormSubmitError error={submitError} />
|
<SchedulePromptableFields
|
||||||
<FormFullWidthLayout>
|
schedule={schedule}
|
||||||
<ActionGroup>
|
credentials={credentials}
|
||||||
<Button
|
surveyConfig={surveyConfig}
|
||||||
ouiaId="schedule-form-save-button"
|
launchConfig={launchConfig}
|
||||||
aria-label={t`Save`}
|
resource={resource}
|
||||||
variant="primary"
|
onCloseWizard={() => {
|
||||||
type="button"
|
setIsWizardOpen(false);
|
||||||
onClick={formik.handleSubmit}
|
}}
|
||||||
isDisabled={isSaveDisabled}
|
onSave={() => {
|
||||||
>
|
setIsWizardOpen(false);
|
||||||
{t`Save`}
|
setIsSaveDisabled(false);
|
||||||
</Button>
|
}}
|
||||||
|
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||||
{isTemplate && showPromptButton && (
|
labels={originalLabels.current}
|
||||||
|
instanceGroups={originalInstanceGroups.current}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
<FrequenciesList openWizard={setIsScheduleWizardOpen} />
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
<FormSubmitError error={submitError} />
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
<ActionGroup>
|
||||||
<Button
|
<Button
|
||||||
ouiaId="schedule-form-prompt-button"
|
ouiaId="schedule-form-save-button"
|
||||||
|
aria-label={t`Save`}
|
||||||
|
variant="primary"
|
||||||
|
type="button"
|
||||||
|
onClick={formik.handleSubmit}
|
||||||
|
isDisabled={isSaveDisabled}
|
||||||
|
>
|
||||||
|
{t`Save`}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {}}
|
||||||
|
>{t`Preview occurances`}</Button>
|
||||||
|
|
||||||
|
{isTemplate && showPromptButton && (
|
||||||
|
<Button
|
||||||
|
ouiaId="schedule-form-prompt-button"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
aria-label={t`Prompt`}
|
||||||
|
onClick={() => setIsWizardOpen(true)}
|
||||||
|
>
|
||||||
|
{t`Prompt`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
ouiaId="schedule-form-cancel-button"
|
||||||
|
aria-label={t`Cancel`}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t`Prompt`}
|
onClick={handleCancel}
|
||||||
onClick={() => setIsWizardOpen(true)}
|
|
||||||
>
|
>
|
||||||
{t`Prompt`}
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</ActionGroup>
|
||||||
<Button
|
</FormFullWidthLayout>
|
||||||
ouiaId="schedule-form-cancel-button"
|
</FormColumnLayout>
|
||||||
aria-label={t`Cancel`}
|
</Form>
|
||||||
variant="secondary"
|
{isScheduleWizardOpen && (
|
||||||
type="button"
|
<ScheduleFormWizard
|
||||||
onClick={handleCancel}
|
staticFormFormkik={formik}
|
||||||
>
|
isOpen={isScheduleWizardOpen}
|
||||||
{t`Cancel`}
|
handleSave={() => {}}
|
||||||
</Button>
|
setIsOpen={setIsScheduleWizardOpen}
|
||||||
</ActionGroup>
|
/>
|
||||||
</FormFullWidthLayout>
|
)}
|
||||||
</FormColumnLayout>
|
</>
|
||||||
</Form>
|
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
)}
|
)}
|
||||||
@@ -575,24 +471,3 @@ ScheduleForm.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ScheduleForm;
|
export default ScheduleForm;
|
||||||
|
|
||||||
function scheduleHasInstances(values) {
|
|
||||||
let rangeToCheck = 1;
|
|
||||||
values.frequency.forEach((freq) => {
|
|
||||||
if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) {
|
|
||||||
rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const ruleSet = buildRuleSet(values, true);
|
|
||||||
const startDate = DateTime.fromISO(values.startDate);
|
|
||||||
const endDate = startDate.plus({ days: rangeToCheck });
|
|
||||||
const instances = ruleSet.between(
|
|
||||||
startDate.toJSDate(),
|
|
||||||
endDate.toJSDate(),
|
|
||||||
true,
|
|
||||||
(date, i) => i === 0
|
|
||||||
);
|
|
||||||
|
|
||||||
return instances.length > 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,41 +1,27 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { FormGroup, Title } from '@patternfly/react-core';
|
import { FormGroup } from '@patternfly/react-core';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
|
||||||
import 'styled-components/macro';
|
|
||||||
import FormField from 'components/FormField';
|
import FormField from 'components/FormField';
|
||||||
import { required } from 'util/validators';
|
import { required } from 'util/validators';
|
||||||
import { useConfig } from 'contexts/Config';
|
import { useConfig } from 'contexts/Config';
|
||||||
import Popover from '../../Popover';
|
import Popover from '../../Popover';
|
||||||
import AnsibleSelect from '../../AnsibleSelect';
|
import AnsibleSelect from '../../AnsibleSelect';
|
||||||
import FrequencySelect, { SelectOption } from './FrequencySelect';
|
|
||||||
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
||||||
import { SubFormLayout, FormColumnLayout } from '../../FormLayout';
|
|
||||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
|
||||||
import DateTimePicker from './DateTimePicker';
|
import DateTimePicker from './DateTimePicker';
|
||||||
import sortFrequencies from './sortFrequencies';
|
|
||||||
|
|
||||||
const SelectClearOption = styled(SelectOption)`
|
|
||||||
& > input[type='checkbox'] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ScheduleFormFields({
|
export default function ScheduleFormFields({
|
||||||
hasDaysToKeepField,
|
hasDaysToKeepField,
|
||||||
zoneOptions,
|
zoneOptions,
|
||||||
zoneLinks,
|
zoneLinks,
|
||||||
|
setTimeZone,
|
||||||
}) {
|
}) {
|
||||||
const helpText = getHelpText();
|
const helpText = getHelpText();
|
||||||
const [timezone, timezoneMeta] = useField({
|
const [timezone, timezoneMeta] = useField({
|
||||||
name: 'timezone',
|
name: 'timezone',
|
||||||
validate: required(t`Select a value for this field`),
|
validate: required(t`Select a value for this field`),
|
||||||
});
|
});
|
||||||
const [frequency, frequencyMeta, frequencyHelper] = useField({
|
|
||||||
name: 'frequency',
|
|
||||||
validate: required(t`Select a value for this field`),
|
|
||||||
});
|
|
||||||
const [timezoneMessage, setTimezoneMessage] = useState('');
|
const [timezoneMessage, setTimezoneMessage] = useState('');
|
||||||
const warnLinkedTZ = (event, selectedValue) => {
|
const warnLinkedTZ = (event, selectedValue) => {
|
||||||
if (zoneLinks[selectedValue]) {
|
if (zoneLinks[selectedValue]) {
|
||||||
@@ -46,6 +32,7 @@ export default function ScheduleFormFields({
|
|||||||
setTimezoneMessage('');
|
setTimezoneMessage('');
|
||||||
}
|
}
|
||||||
timezone.onChange(event, selectedValue);
|
timezone.onChange(event, selectedValue);
|
||||||
|
setTimeZone(zoneLinks(selectedValue));
|
||||||
};
|
};
|
||||||
let timezoneValidatedStatus = 'default';
|
let timezoneValidatedStatus = 'default';
|
||||||
if (timezoneMeta.touched && timezoneMeta.error) {
|
if (timezoneMeta.touched && timezoneMeta.error) {
|
||||||
@@ -55,16 +42,6 @@ export default function ScheduleFormFields({
|
|||||||
}
|
}
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] =
|
|
||||||
useField({
|
|
||||||
name: 'exceptionFrequency',
|
|
||||||
validate: required(t`Select a value for this field`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateFrequency = (setFrequency) => (values) => {
|
|
||||||
setFrequency(values.sort(sortFrequencies));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -103,33 +80,7 @@ export default function ScheduleFormFields({
|
|||||||
onChange={warnLinkedTZ}
|
onChange={warnLinkedTZ}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
|
||||||
name="frequency"
|
|
||||||
fieldId="schedule-frequency"
|
|
||||||
helperTextInvalid={frequencyMeta.error}
|
|
||||||
validated={
|
|
||||||
!frequencyMeta.touched || !frequencyMeta.error ? 'default' : 'error'
|
|
||||||
}
|
|
||||||
label={t`Repeat frequency`}
|
|
||||||
>
|
|
||||||
<FrequencySelect
|
|
||||||
id="schedule-frequency"
|
|
||||||
onChange={updateFrequency(frequencyHelper.setValue)}
|
|
||||||
value={frequency.value}
|
|
||||||
placeholderText={
|
|
||||||
frequency.value.length ? t`Select frequency` : t`None (run once)`
|
|
||||||
}
|
|
||||||
onBlur={frequencyHelper.setTouched}
|
|
||||||
>
|
|
||||||
<SelectClearOption value="none">{t`None (run once)`}</SelectClearOption>
|
|
||||||
<SelectOption value="minute">{t`Minute`}</SelectOption>
|
|
||||||
<SelectOption value="hour">{t`Hour`}</SelectOption>
|
|
||||||
<SelectOption value="day">{t`Day`}</SelectOption>
|
|
||||||
<SelectOption value="week">{t`Week`}</SelectOption>
|
|
||||||
<SelectOption value="month">{t`Month`}</SelectOption>
|
|
||||||
<SelectOption value="year">{t`Year`}</SelectOption>
|
|
||||||
</FrequencySelect>
|
|
||||||
</FormGroup>
|
|
||||||
{hasDaysToKeepField ? (
|
{hasDaysToKeepField ? (
|
||||||
<FormField
|
<FormField
|
||||||
id="schedule-days-to-keep"
|
id="schedule-days-to-keep"
|
||||||
@@ -140,68 +91,6 @@ export default function ScheduleFormFields({
|
|||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{frequency.value.length ? (
|
|
||||||
<SubFormLayout>
|
|
||||||
<Title size="md" headingLevel="h4">
|
|
||||||
{t`Frequency Details`}
|
|
||||||
</Title>
|
|
||||||
{frequency.value.map((val) => (
|
|
||||||
<FormColumnLayout key={val} stacked>
|
|
||||||
<FrequencyDetailSubform
|
|
||||||
frequency={val}
|
|
||||||
prefix={`frequencyOptions.${val}`}
|
|
||||||
/>
|
|
||||||
</FormColumnLayout>
|
|
||||||
))}
|
|
||||||
<Title
|
|
||||||
size="md"
|
|
||||||
headingLevel="h4"
|
|
||||||
css="margin-top: var(--pf-c-card--child--PaddingRight)"
|
|
||||||
>{t`Exceptions`}</Title>
|
|
||||||
<FormColumnLayout stacked>
|
|
||||||
<FormGroup
|
|
||||||
name="exceptions"
|
|
||||||
fieldId="exception-frequency"
|
|
||||||
helperTextInvalid={exceptionFrequencyMeta.error}
|
|
||||||
validated={
|
|
||||||
!exceptionFrequencyMeta.touched || !exceptionFrequencyMeta.error
|
|
||||||
? 'default'
|
|
||||||
: 'error'
|
|
||||||
}
|
|
||||||
label={t`Add exceptions`}
|
|
||||||
>
|
|
||||||
<FrequencySelect
|
|
||||||
id="exception-frequency"
|
|
||||||
onChange={updateFrequency(exceptionFrequencyHelper.setValue)}
|
|
||||||
value={exceptionFrequency.value}
|
|
||||||
placeholderText={
|
|
||||||
exceptionFrequency.value.length
|
|
||||||
? t`Select frequency`
|
|
||||||
: t`None`
|
|
||||||
}
|
|
||||||
onBlur={exceptionFrequencyHelper.setTouched}
|
|
||||||
>
|
|
||||||
<SelectClearOption value="none">{t`None`}</SelectClearOption>
|
|
||||||
<SelectOption value="minute">{t`Minute`}</SelectOption>
|
|
||||||
<SelectOption value="hour">{t`Hour`}</SelectOption>
|
|
||||||
<SelectOption value="day">{t`Day`}</SelectOption>
|
|
||||||
<SelectOption value="week">{t`Week`}</SelectOption>
|
|
||||||
<SelectOption value="month">{t`Month`}</SelectOption>
|
|
||||||
<SelectOption value="year">{t`Year`}</SelectOption>
|
|
||||||
</FrequencySelect>
|
|
||||||
</FormGroup>
|
|
||||||
</FormColumnLayout>
|
|
||||||
{exceptionFrequency.value.map((val) => (
|
|
||||||
<FormColumnLayout key={val} stacked>
|
|
||||||
<FrequencyDetailSubform
|
|
||||||
frequency={val}
|
|
||||||
prefix={`exceptionOptions.${val}`}
|
|
||||||
isException
|
|
||||||
/>
|
|
||||||
</FormColumnLayout>
|
|
||||||
))}
|
|
||||||
</SubFormLayout>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
199
awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js
Normal file
199
awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Wizard,
|
||||||
|
WizardContextConsumer,
|
||||||
|
WizardFooter,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
|
import { useField, useFormikContext } from 'formik';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { formatDateString } from 'util/dates';
|
||||||
|
import FrequencySelect from './FrequencySelect';
|
||||||
|
import MonthandYearForm from './MonthandYearForm';
|
||||||
|
import OrdinalDayForm from './OrdinalDayForm';
|
||||||
|
import WeekdayForm from './WeekdayForm';
|
||||||
|
import ScheduleEndForm from './ScheduleEndForm';
|
||||||
|
import parseRuleObj from './parseRuleObj';
|
||||||
|
import { buildDtStartObj } from './buildRuleObj';
|
||||||
|
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function ScheduleFormWizard({ isOpen, setIsOpen }) {
|
||||||
|
const { values, resetForm, initialValues } = useFormikContext();
|
||||||
|
const [freq, freqMeta] = useField('freq');
|
||||||
|
const [{ value: frequenciesValue }] = useField('frequencies');
|
||||||
|
const [interval, , intervalHelpers] = useField('interval');
|
||||||
|
|
||||||
|
const handleSubmit = (goToStepById) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
endingType,
|
||||||
|
endTime,
|
||||||
|
endDate,
|
||||||
|
timezone,
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
frequencies,
|
||||||
|
...rest
|
||||||
|
} = values;
|
||||||
|
if (endingType === 'onDate') {
|
||||||
|
const dt = DateTime.fromFormat(
|
||||||
|
`${endDate} ${endTime}`,
|
||||||
|
'yyyy-MM-dd h:mm a',
|
||||||
|
{
|
||||||
|
zone: timezone,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
rest.until = formatDateString(dt, timezone);
|
||||||
|
|
||||||
|
delete rest.count;
|
||||||
|
}
|
||||||
|
if (endingType === 'never') delete rest.count;
|
||||||
|
|
||||||
|
const rule = new RRule(rest);
|
||||||
|
|
||||||
|
const start = buildDtStartObj({
|
||||||
|
startDate: values.startDate,
|
||||||
|
startTime: values.startTime,
|
||||||
|
timezone: values.timezone,
|
||||||
|
frequency: values.freq,
|
||||||
|
});
|
||||||
|
const newFrequency = parseRuleObj({
|
||||||
|
timezone,
|
||||||
|
frequency: freq.value,
|
||||||
|
rrule: rule.toString(),
|
||||||
|
dtstart: start,
|
||||||
|
});
|
||||||
|
if (goToStepById) {
|
||||||
|
goToStepById(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
...initialValues,
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
startDate: values.startDate,
|
||||||
|
startTime: values.startTime,
|
||||||
|
timezone: values.timezone,
|
||||||
|
frequencies: frequenciesValue[0].frequency.length
|
||||||
|
? [...frequenciesValue, newFrequency]
|
||||||
|
: [newFrequency],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const CustomFooter = (
|
||||||
|
<WizardFooter>
|
||||||
|
<WizardContextConsumer>
|
||||||
|
{({ activeStep, onNext, onBack, goToStepById }) => (
|
||||||
|
<>
|
||||||
|
{activeStep.id === 2 ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(true, goToStepById);
|
||||||
|
}}
|
||||||
|
>{t`Finish and create new`}</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>{t`Finish and close`}</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button variant="primary" onClick={onNext}>{t`Next`}</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="secondary" onClick={onBack}>{t`Back`}</Button>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
...initialValues,
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
startDate: values.startDate,
|
||||||
|
startTime: values.startTime,
|
||||||
|
timezone: values.timezone,
|
||||||
|
frequencies: values.frequencies,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>{t`Cancel`}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</WizardContextConsumer>
|
||||||
|
</WizardFooter>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wizard
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
isOpen={isOpen}
|
||||||
|
footer={CustomFooter}
|
||||||
|
steps={[
|
||||||
|
{
|
||||||
|
key: 'frequency',
|
||||||
|
name: 'Frequency',
|
||||||
|
id: 1,
|
||||||
|
component: (
|
||||||
|
<>
|
||||||
|
<Title size="md" headingLevel="h4">{t`Repeat frequency`}</Title>
|
||||||
|
<GroupWrapper
|
||||||
|
name="freq"
|
||||||
|
fieldId="schedule-frequency"
|
||||||
|
isRequired
|
||||||
|
helperTextInvalid={freqMeta.error}
|
||||||
|
validated={
|
||||||
|
!freqMeta.touched || !freqMeta.error ? 'default' : 'error'
|
||||||
|
}
|
||||||
|
label={<b>{t`Frequency`}</b>}
|
||||||
|
>
|
||||||
|
<FrequencySelect />
|
||||||
|
</GroupWrapper>
|
||||||
|
<GroupWrapper isRequired label={<b>{t`Interval`}</b>}>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
value={interval.value}
|
||||||
|
placeholder={t`Choose an interval for the schedule`}
|
||||||
|
aria-label={t`Choose an interval for the schedule`}
|
||||||
|
onChange={(v) => intervalHelpers.setValue(v)}
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
<WeekdayForm />
|
||||||
|
<MonthandYearForm />
|
||||||
|
<OrdinalDayForm />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'End',
|
||||||
|
key: 'end',
|
||||||
|
id: 2,
|
||||||
|
component: <ScheduleEndForm />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default ScheduleFormWizard;
|
||||||
164
awx/ui/src/components/Schedule/shared/WeekdayForm.js
Normal file
164
awx/ui/src/components/Schedule/shared/WeekdayForm.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Checkbox as _Checkbox,
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { weekdayOptions } from './scheduleFormHelpers';
|
||||||
|
|
||||||
|
const Checkbox = styled(_Checkbox)`
|
||||||
|
:not(:last-of-type) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function WeekdayForm({ id }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField('byweekday');
|
||||||
|
const [weekStartDay, , weekStartDayHelpers] = useField('wkst');
|
||||||
|
const updateDaysOfWeek = (day, checked) => {
|
||||||
|
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
|
||||||
|
daysOfWeekHelpers.setTouched(true);
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
newDaysOfWeek.push(day);
|
||||||
|
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
||||||
|
} else {
|
||||||
|
daysOfWeekHelpers.setValue(
|
||||||
|
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GroupWrapper
|
||||||
|
name="wkst"
|
||||||
|
label={<b>{t`Select the first day of the week`}</b>}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
onSelect={(e, value) => {
|
||||||
|
weekStartDayHelpers.setValue(value);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
onBlur={() => setIsOpen(false)}
|
||||||
|
selections={weekStartDay.value}
|
||||||
|
onToggle={(isopen) => setIsOpen(isopen)}
|
||||||
|
isOpen={isOpen}
|
||||||
|
id={`schedule-run-on-the-day-${id}`}
|
||||||
|
onChange={(e, v) => {
|
||||||
|
weekStartDayHelpers.setValue(v);
|
||||||
|
}}
|
||||||
|
{...weekStartDay}
|
||||||
|
>
|
||||||
|
{weekdayOptions.map(({ key, value, label }) => (
|
||||||
|
<SelectOption key={key} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</GroupWrapper>
|
||||||
|
<GroupWrapper
|
||||||
|
name="byweekday"
|
||||||
|
fieldId={`schedule-days-of-week-${id}`}
|
||||||
|
helperTextInvalid={daysOfWeekMeta.error}
|
||||||
|
validated={
|
||||||
|
!daysOfWeekMeta.touched || !daysOfWeekMeta.error ? 'default' : 'error'
|
||||||
|
}
|
||||||
|
label={<b>{t`On selected day(s) of the week`}</b>}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Sun`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.SU)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.SU, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Sunday`}
|
||||||
|
id={`schedule-days-of-week-sun-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-sun-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Mon`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.MO)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.MO, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Monday`}
|
||||||
|
id={`schedule-days-of-week-mon-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-mon-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Tue`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.TU)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.TU, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Tuesday`}
|
||||||
|
id={`schedule-days-of-week-tue-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-tue-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Wed`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.WE)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.WE, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Wednesday`}
|
||||||
|
id={`schedule-days-of-week-wed-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-wed-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Thu`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.TH)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.TH, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Thursday`}
|
||||||
|
id={`schedule-days-of-week-thu-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-thu-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Fri`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.FR)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.FR, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Friday`}
|
||||||
|
id={`schedule-days-of-week-fri-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-fri-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Sat`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.SA)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.SA, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Saturday`}
|
||||||
|
id={`schedule-days-of-week-sat-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-sat-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default WeekdayForm;
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
|
||||||
import { RRule } from 'rrule';
|
import { RRule } from 'rrule';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { getRRuleDayConstants } from 'util/dates';
|
|
||||||
|
|
||||||
window.RRule = RRule;
|
window.RRule = RRule;
|
||||||
window.DateTime = DateTime;
|
window.DateTime = DateTime;
|
||||||
@@ -22,7 +20,7 @@ export function buildDtStartObj(values) {
|
|||||||
startHour
|
startHour
|
||||||
)}${pad(startMinute)}00`;
|
)}${pad(startMinute)}00`;
|
||||||
const rruleString = values.timezone
|
const rruleString = values.timezone
|
||||||
? `DTSTART;TZID=${values.timezone}:${dateString}`
|
? `DTSTART;TZID=${values.timezone}${dateString}`
|
||||||
: `DTSTART:${dateString}Z`;
|
: `DTSTART:${dateString}Z`;
|
||||||
const rule = RRule.fromString(rruleString);
|
const rule = RRule.fromString(rruleString);
|
||||||
|
|
||||||
@@ -38,7 +36,8 @@ function pad(num) {
|
|||||||
|
|
||||||
export default function buildRuleObj(values, includeStart) {
|
export default function buildRuleObj(values, includeStart) {
|
||||||
const ruleObj = {
|
const ruleObj = {
|
||||||
interval: values.interval,
|
interval: values.interval || 1,
|
||||||
|
freq: values.freq,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeStart) {
|
if (includeStart) {
|
||||||
@@ -49,68 +48,6 @@ export default function buildRuleObj(values, includeStart) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (values.frequency) {
|
|
||||||
case 'none':
|
|
||||||
ruleObj.count = 1;
|
|
||||||
ruleObj.freq = RRule.MINUTELY;
|
|
||||||
break;
|
|
||||||
case 'minute':
|
|
||||||
ruleObj.freq = RRule.MINUTELY;
|
|
||||||
break;
|
|
||||||
case 'hour':
|
|
||||||
ruleObj.freq = RRule.HOURLY;
|
|
||||||
break;
|
|
||||||
case 'day':
|
|
||||||
ruleObj.freq = RRule.DAILY;
|
|
||||||
break;
|
|
||||||
case 'week':
|
|
||||||
ruleObj.freq = RRule.WEEKLY;
|
|
||||||
ruleObj.byweekday = values.daysOfWeek;
|
|
||||||
break;
|
|
||||||
case 'month':
|
|
||||||
ruleObj.freq = RRule.MONTHLY;
|
|
||||||
if (values.runOn === 'day') {
|
|
||||||
ruleObj.bymonthday = values.runOnDayNumber;
|
|
||||||
} else if (values.runOn === 'the') {
|
|
||||||
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
|
|
||||||
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'year':
|
|
||||||
ruleObj.freq = RRule.YEARLY;
|
|
||||||
if (values.runOn === 'day') {
|
|
||||||
ruleObj.bymonth = parseInt(values.runOnDayMonth, 10);
|
|
||||||
ruleObj.bymonthday = values.runOnDayNumber;
|
|
||||||
} else if (values.runOn === 'the') {
|
|
||||||
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
|
|
||||||
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
|
|
||||||
ruleObj.bymonth = parseInt(values.runOnTheMonth, 10);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(t`Frequency did not match an expected value`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.frequency !== 'none') {
|
|
||||||
switch (values.end) {
|
|
||||||
case 'never':
|
|
||||||
break;
|
|
||||||
case 'after':
|
|
||||||
ruleObj.count = values.occurrences;
|
|
||||||
break;
|
|
||||||
case 'onDate': {
|
|
||||||
ruleObj.until = buildDateTime(
|
|
||||||
values.endDate,
|
|
||||||
values.endTime,
|
|
||||||
values.timezone
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(t`End did not match an expected value (${values.end})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ruleObj;
|
return ruleObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { RRule, RRuleSet } from 'rrule';
|
import { RRule, RRuleSet } from 'rrule';
|
||||||
import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
|
import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
|
||||||
|
import { FREQUENCIESCONSTANTS } from './scheduleFormHelpers';
|
||||||
|
|
||||||
window.RRuleSet = RRuleSet;
|
window.RRuleSet = RRuleSet;
|
||||||
|
|
||||||
@@ -12,42 +13,31 @@ export default function buildRuleSet(values, useUTCStart) {
|
|||||||
startDate: values.startDate,
|
startDate: values.startDate,
|
||||||
startTime: values.startTime,
|
startTime: values.startTime,
|
||||||
timezone: values.timezone,
|
timezone: values.timezone,
|
||||||
|
frequency: values.freq,
|
||||||
});
|
});
|
||||||
set.rrule(startRule);
|
set.rrule(startRule);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.frequency.length === 0) {
|
values.frequencies.forEach(({ frequency, rrule }) => {
|
||||||
const rule = buildRuleObj(
|
if (!frequencies.includes(frequency)) {
|
||||||
{
|
|
||||||
startDate: values.startDate,
|
|
||||||
startTime: values.startTime,
|
|
||||||
timezone: values.timezone,
|
|
||||||
frequency: 'none',
|
|
||||||
interval: 1,
|
|
||||||
},
|
|
||||||
useUTCStart
|
|
||||||
);
|
|
||||||
set.rrule(new RRule(rule));
|
|
||||||
}
|
|
||||||
|
|
||||||
frequencies.forEach((frequency) => {
|
|
||||||
if (!values.frequency.includes(frequency)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rule = buildRuleObj(
|
const rule = buildRuleObj(
|
||||||
{
|
{
|
||||||
startDate: values.startDate,
|
startDate: values.startDate,
|
||||||
startTime: values.startTime,
|
startTime: values.startTime,
|
||||||
timezone: values.timezone,
|
timezone: values.timezone,
|
||||||
frequency,
|
freq: FREQUENCIESCONSTANTS[frequency],
|
||||||
...values.frequencyOptions[frequency],
|
rrule,
|
||||||
},
|
},
|
||||||
useUTCStart
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
set.rrule(new RRule(rule));
|
set.rrule(new RRule(rule));
|
||||||
});
|
});
|
||||||
|
|
||||||
frequencies.forEach((frequency) => {
|
values.exceptions?.forEach(({ frequency, rrule }) => {
|
||||||
if (!values.exceptionFrequency?.includes(frequency)) {
|
if (!values.exceptionFrequency?.includes(frequency)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -56,8 +46,8 @@ export default function buildRuleSet(values, useUTCStart) {
|
|||||||
startDate: values.startDate,
|
startDate: values.startDate,
|
||||||
startTime: values.startTime,
|
startTime: values.startTime,
|
||||||
timezone: values.timezone,
|
timezone: values.timezone,
|
||||||
frequency,
|
freq: FREQUENCIESCONSTANTS[frequency],
|
||||||
...values.exceptionOptions[frequency],
|
rrule,
|
||||||
},
|
},
|
||||||
useUTCStart
|
useUTCStart
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ export class UnsupportedRRuleError extends Error {
|
|||||||
|
|
||||||
export default function parseRuleObj(schedule) {
|
export default function parseRuleObj(schedule) {
|
||||||
let values = {
|
let values = {
|
||||||
frequency: [],
|
frequency: '',
|
||||||
frequencyOptions: {},
|
rrules: '',
|
||||||
exceptionFrequency: [],
|
|
||||||
exceptionOptions: {},
|
|
||||||
timezone: schedule.timezone,
|
timezone: schedule.timezone,
|
||||||
};
|
};
|
||||||
|
if (Object.values(schedule).length === 0) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), {
|
const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), {
|
||||||
forceset: true,
|
forceset: true,
|
||||||
});
|
});
|
||||||
@@ -40,25 +42,9 @@ export default function parseRuleObj(schedule) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSingleOccurrence(values)) {
|
|
||||||
values.frequency = [];
|
|
||||||
values.frequencyOptions = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSingleOccurrence(values) {
|
|
||||||
if (values.frequency.length > 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (values.frequency[0] !== 'minute') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const options = values.frequencyOptions.minute;
|
|
||||||
return options.end === 'after' && options.occurrences === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDtstart(schedule, values) {
|
function parseDtstart(schedule, values) {
|
||||||
// TODO: should this rely on DTSTART in rruleset rather than schedule.dtstart?
|
// TODO: should this rely on DTSTART in rruleset rather than schedule.dtstart?
|
||||||
const [startDate, startTime] = dateToInputDateTime(
|
const [startDate, startTime] = dateToInputDateTime(
|
||||||
@@ -81,27 +67,12 @@ const frequencyTypes = {
|
|||||||
[RRule.YEARLY]: 'year',
|
[RRule.YEARLY]: 'year',
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseRrule(rruleString, schedule, values) {
|
function parseRrule(rruleString, schedule) {
|
||||||
const { frequency, options } = parseRule(
|
const { frequency } = parseRule(rruleString, schedule);
|
||||||
rruleString,
|
|
||||||
schedule,
|
|
||||||
values.exceptionFrequency
|
|
||||||
);
|
|
||||||
|
|
||||||
if (values.frequencyOptions[frequency]) {
|
const freq = { frequency, rrule: rruleString };
|
||||||
throw new UnsupportedRRuleError(
|
|
||||||
'Duplicate exception frequency types not supported'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return freq;
|
||||||
...values,
|
|
||||||
frequency: [...values.frequency, frequency].sort(sortFrequencies),
|
|
||||||
frequencyOptions: {
|
|
||||||
...values.frequencyOptions,
|
|
||||||
[frequency]: options,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseExRule(exruleString, schedule, values) {
|
function parseExRule(exruleString, schedule, values) {
|
||||||
@@ -129,20 +100,10 @@ function parseExRule(exruleString, schedule, values) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRule(ruleString, schedule, frequencies) {
|
function parseRule(ruleString, schedule) {
|
||||||
const {
|
const {
|
||||||
origOptions: {
|
origOptions: { count, freq, interval, until, ...rest },
|
||||||
bymonth,
|
|
||||||
bymonthday,
|
|
||||||
bysetpos,
|
|
||||||
byweekday,
|
|
||||||
count,
|
|
||||||
freq,
|
|
||||||
interval,
|
|
||||||
until,
|
|
||||||
},
|
|
||||||
} = RRule.fromString(ruleString);
|
} = RRule.fromString(ruleString);
|
||||||
|
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
const closestQuarterHour = DateTime.fromMillis(
|
const closestQuarterHour = DateTime.fromMillis(
|
||||||
Math.ceil(now.ts / 900000) * 900000
|
Math.ceil(now.ts / 900000) * 900000
|
||||||
@@ -156,17 +117,17 @@ function parseRule(ruleString, schedule, frequencies) {
|
|||||||
endTime: time,
|
endTime: time,
|
||||||
occurrences: 1,
|
occurrences: 1,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
end: 'never',
|
endingType: 'never',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (until) {
|
if (until?.length) {
|
||||||
options.end = 'onDate';
|
options.endingType = 'onDate';
|
||||||
const end = DateTime.fromISO(until.toISOString());
|
const end = DateTime.fromISO(until.toISOString());
|
||||||
const [endDate, endTime] = dateToInputDateTime(end, schedule.timezone);
|
const [endDate, endTime] = dateToInputDateTime(end, schedule.timezone);
|
||||||
options.endDate = endDate;
|
options.endDate = endDate;
|
||||||
options.endTime = endTime;
|
options.endTime = endTime;
|
||||||
} else if (count) {
|
} else if (count) {
|
||||||
options.end = 'after';
|
options.endingType = 'after';
|
||||||
options.occurrences = count;
|
options.occurrences = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,101 +139,10 @@ function parseRule(ruleString, schedule, frequencies) {
|
|||||||
throw new Error(`Unexpected rrule frequency: ${freq}`);
|
throw new Error(`Unexpected rrule frequency: ${freq}`);
|
||||||
}
|
}
|
||||||
const frequency = frequencyTypes[freq];
|
const frequency = frequencyTypes[freq];
|
||||||
if (frequencies.includes(frequency)) {
|
|
||||||
throw new Error(`Duplicate frequency types not supported (${frequency})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freq === RRule.WEEKLY && byweekday) {
|
|
||||||
options.daysOfWeek = byweekday;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freq === RRule.MONTHLY) {
|
|
||||||
options.runOn = 'day';
|
|
||||||
options.runOnTheOccurrence = 1;
|
|
||||||
options.runOnTheDay = 'sunday';
|
|
||||||
options.runOnDayNumber = 1;
|
|
||||||
|
|
||||||
if (bymonthday) {
|
|
||||||
options.runOnDayNumber = bymonthday;
|
|
||||||
}
|
|
||||||
if (bysetpos) {
|
|
||||||
options.runOn = 'the';
|
|
||||||
options.runOnTheOccurrence = bysetpos;
|
|
||||||
options.runOnTheDay = generateRunOnTheDay(byweekday);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freq === RRule.YEARLY) {
|
|
||||||
options.runOn = 'day';
|
|
||||||
options.runOnTheOccurrence = 1;
|
|
||||||
options.runOnTheDay = 'sunday';
|
|
||||||
options.runOnTheMonth = 1;
|
|
||||||
options.runOnDayMonth = 1;
|
|
||||||
options.runOnDayNumber = 1;
|
|
||||||
|
|
||||||
if (bymonthday) {
|
|
||||||
options.runOnDayNumber = bymonthday;
|
|
||||||
options.runOnDayMonth = bymonth;
|
|
||||||
}
|
|
||||||
if (bysetpos) {
|
|
||||||
options.runOn = 'the';
|
|
||||||
options.runOnTheOccurrence = bysetpos;
|
|
||||||
options.runOnTheDay = generateRunOnTheDay(byweekday);
|
|
||||||
options.runOnTheMonth = bymonth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
frequency,
|
frequency,
|
||||||
options,
|
...options,
|
||||||
|
...rest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRunOnTheDay(days = []) {
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
RRule.MO,
|
|
||||||
RRule.TU,
|
|
||||||
RRule.WE,
|
|
||||||
RRule.TH,
|
|
||||||
RRule.FR,
|
|
||||||
RRule.SA,
|
|
||||||
RRule.SU,
|
|
||||||
].every((element) => days.indexOf(element) > -1)
|
|
||||||
) {
|
|
||||||
return 'day';
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
[RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR].every(
|
|
||||||
(element) => days.indexOf(element) > -1
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return 'weekday';
|
|
||||||
}
|
|
||||||
if ([RRule.SA, RRule.SU].every((element) => days.indexOf(element) > -1)) {
|
|
||||||
return 'weekendDay';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.MO) > -1) {
|
|
||||||
return 'monday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.TU) > -1) {
|
|
||||||
return 'tuesday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.WE) > -1) {
|
|
||||||
return 'wednesday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.TH) > -1) {
|
|
||||||
return 'thursday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.FR) > -1) {
|
|
||||||
return 'friday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.SA) > -1) {
|
|
||||||
return 'saturday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.SU) > -1) {
|
|
||||||
return 'sunday';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|||||||
232
awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js
Normal file
232
awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
|
import buildRuleObj from './buildRuleObj';
|
||||||
|
import buildRuleSet from './buildRuleSet';
|
||||||
|
|
||||||
|
// const NUM_DAYS_PER_FREQUENCY = {
|
||||||
|
// week: 7,
|
||||||
|
// month: 31,
|
||||||
|
// year: 365,
|
||||||
|
// };
|
||||||
|
// const validateSchedule = () =>
|
||||||
|
// const errors = {};
|
||||||
|
|
||||||
|
// values.frequencies.forEach((freq) => {
|
||||||
|
// const options = values.frequencyOptions[freq];
|
||||||
|
// const freqErrors = {};
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// (freq === 'month' || freq === 'year') &&
|
||||||
|
// options.runOn === 'day' &&
|
||||||
|
// (options.runOnDayNumber < 1 || options.runOnDayNumber > 31)
|
||||||
|
// ) {
|
||||||
|
// freqErrors.runOn = t`Please select a day number between 1 and 31.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (options.end === 'after' && !options.occurrences) {
|
||||||
|
// freqErrors.occurrences = t`Please enter a number of occurrences.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (options.end === 'onDate') {
|
||||||
|
// if (
|
||||||
|
// DateTime.fromFormat(
|
||||||
|
// `${values.startDate} ${values.startTime}`,
|
||||||
|
// 'yyyy-LL-dd h:mm a'
|
||||||
|
// ).toMillis() >=
|
||||||
|
// DateTime.fromFormat(
|
||||||
|
// `${options.endDate} ${options.endTime}`,
|
||||||
|
// 'yyyy-LL-dd h:mm a'
|
||||||
|
// ).toMillis()
|
||||||
|
// ) {
|
||||||
|
// freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// DateTime.fromISO(options.endDate)
|
||||||
|
// .diff(DateTime.fromISO(values.startDate), 'days')
|
||||||
|
// .toObject().days < NUM_DAYS_PER_FREQUENCY[freq]
|
||||||
|
// ) {
|
||||||
|
// const rule = new RRule(
|
||||||
|
// buildRuleObj({
|
||||||
|
// startDate: values.startDate,
|
||||||
|
// startTime: values.startTime,
|
||||||
|
// frequencies: freq,
|
||||||
|
// ...options,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// if (rule.all().length === 0) {
|
||||||
|
// errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||||
|
// freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (Object.keys(freqErrors).length > 0) {
|
||||||
|
// if (!errors.frequencyOptions) {
|
||||||
|
// errors.frequencyOptions = {};
|
||||||
|
// }
|
||||||
|
// errors.frequencyOptions[freq] = freqErrors;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
|
||||||
|
// errors.exceptionFrequency = t`This schedule has no occurrences due to the
|
||||||
|
// selected exceptions.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ({});
|
||||||
|
// function scheduleHasInstances(values) {
|
||||||
|
// let rangeToCheck = 1;
|
||||||
|
// values.frequencies.forEach((freq) => {
|
||||||
|
// if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) {
|
||||||
|
// rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq];
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const ruleSet = buildRuleSet(values, true);
|
||||||
|
// const startDate = DateTime.fromISO(values.startDate);
|
||||||
|
// const endDate = startDate.plus({ days: rangeToCheck });
|
||||||
|
// const instances = ruleSet.between(
|
||||||
|
// startDate.toJSDate(),
|
||||||
|
// endDate.toJSDate(),
|
||||||
|
// true,
|
||||||
|
// (date, i) => i === 0
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return instances.length > 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const bysetposOptions = [
|
||||||
|
{ value: '', key: 'none', label: 'None' },
|
||||||
|
{ value: 1, key: 'first', label: t`First` },
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
key: 'second',
|
||||||
|
label: t`Second`,
|
||||||
|
},
|
||||||
|
{ value: 3, key: 'third', label: t`Third` },
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
key: 'fourth',
|
||||||
|
label: t`Fourth`,
|
||||||
|
},
|
||||||
|
{ value: 5, key: 'fifth', label: t`Fifth` },
|
||||||
|
{ value: -1, key: 'last', label: t`Last` },
|
||||||
|
];
|
||||||
|
|
||||||
|
const monthOptions = [
|
||||||
|
{
|
||||||
|
key: 'january',
|
||||||
|
value: 1,
|
||||||
|
label: t`January`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'february',
|
||||||
|
value: 2,
|
||||||
|
label: t`February`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'march',
|
||||||
|
value: 3,
|
||||||
|
label: t`March`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'april',
|
||||||
|
value: 4,
|
||||||
|
label: t`April`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'may',
|
||||||
|
value: 5,
|
||||||
|
label: t`May`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'june',
|
||||||
|
value: 6,
|
||||||
|
label: t`June`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'july',
|
||||||
|
value: 7,
|
||||||
|
label: t`July`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'august',
|
||||||
|
value: 8,
|
||||||
|
label: t`August`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'september',
|
||||||
|
value: 9,
|
||||||
|
label: t`September`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'october',
|
||||||
|
value: 10,
|
||||||
|
label: t`October`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'november',
|
||||||
|
value: 11,
|
||||||
|
label: t`November`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'december',
|
||||||
|
value: 12,
|
||||||
|
label: t`December`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const weekdayOptions = [
|
||||||
|
{
|
||||||
|
value: RRule.SU,
|
||||||
|
key: 'sunday',
|
||||||
|
label: t`Sunday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.MO,
|
||||||
|
key: 'monday',
|
||||||
|
label: t`Monday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.TU,
|
||||||
|
key: 'tuesday',
|
||||||
|
label: t`Tuesday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.WE,
|
||||||
|
key: 'wednesday',
|
||||||
|
label: t`Wednesday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.TH,
|
||||||
|
key: 'thursday',
|
||||||
|
label: t`Thursday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.FR,
|
||||||
|
key: 'friday',
|
||||||
|
label: t`Friday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.SA,
|
||||||
|
key: 'saturday',
|
||||||
|
label: t`Saturday`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const FREQUENCIESCONSTANTS = {
|
||||||
|
minute: RRule.MINUTELY,
|
||||||
|
hour: RRule.HOURLY,
|
||||||
|
day: RRule.DAILY,
|
||||||
|
week: RRule.WEEKLY,
|
||||||
|
month: RRule.MONTHLY,
|
||||||
|
year: RRule.YEARLY,
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
monthOptions,
|
||||||
|
weekdayOptions,
|
||||||
|
bysetposOptions,
|
||||||
|
// validateSchedule,
|
||||||
|
FREQUENCIESCONSTANTS,
|
||||||
|
};
|
||||||
@@ -465,7 +465,7 @@
|
|||||||
},
|
},
|
||||||
"created": "2020-05-18T21:53:35.370730Z",
|
"created": "2020-05-18T21:53:35.370730Z",
|
||||||
"modified": "2020-05-18T21:54:05.436400Z",
|
"modified": "2020-05-18T21:54:05.436400Z",
|
||||||
"name": "CyberArk AIM Central Credential Provider Lookup",
|
"name": "CyberArk Central Credential Provider Lookup",
|
||||||
"description": "",
|
"description": "",
|
||||||
"kind": "external",
|
"kind": "external",
|
||||||
"namespace": "aim",
|
"namespace": "aim",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function JobEvent({
|
|||||||
if (lineNumber < 0) {
|
if (lineNumber < 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const canToggle = index === toggleLineIndex;
|
const canToggle = index === toggleLineIndex && !event.isTracebackOnly;
|
||||||
return (
|
return (
|
||||||
<JobEventLine
|
<JobEventLine
|
||||||
onClick={isClickable ? onJobEventClick : undefined}
|
onClick={isClickable ? onJobEventClick : undefined}
|
||||||
@@ -55,7 +55,7 @@ function JobEvent({
|
|||||||
onToggle={onToggleCollapsed}
|
onToggle={onToggleCollapsed}
|
||||||
/>
|
/>
|
||||||
<JobEventLineNumber>
|
<JobEventLineNumber>
|
||||||
{lineNumber}
|
{!event.isTracebackOnly ? lineNumber : ''}
|
||||||
<JobEventEllipsis isCollapsed={isCollapsed && canToggle} />
|
<JobEventEllipsis isCollapsed={isCollapsed && canToggle} />
|
||||||
</JobEventLineNumber>
|
</JobEventLineNumber>
|
||||||
<JobEventLineText
|
<JobEventLineText
|
||||||
|
|||||||
@@ -29,8 +29,11 @@ export function prependTraceback(job, events) {
|
|||||||
start_line: 0,
|
start_line: 0,
|
||||||
};
|
};
|
||||||
const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1);
|
const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1);
|
||||||
if (firstIndex && events[firstIndex]?.stdout) {
|
if (firstIndex > -1) {
|
||||||
const stdoutLines = events[firstIndex].stdout.split('\r\n');
|
if (!events[firstIndex].stdout) {
|
||||||
|
events[firstIndex].isTracebackOnly = true;
|
||||||
|
}
|
||||||
|
const stdoutLines = events[firstIndex].stdout?.split('\r\n') || [];
|
||||||
stdoutLines[0] = tracebackEvent.stdout;
|
stdoutLines[0] = tracebackEvent.stdout;
|
||||||
events[firstIndex].stdout = stdoutLines.join('\r\n');
|
events[firstIndex].stdout = stdoutLines.join('\r\n');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function WorkflowJobTemplateAdd() {
|
|||||||
limit,
|
limit,
|
||||||
job_tags,
|
job_tags,
|
||||||
skip_tags,
|
skip_tags,
|
||||||
|
scm_branch,
|
||||||
...templatePayload
|
...templatePayload
|
||||||
} = values;
|
} = values;
|
||||||
templatePayload.inventory = inventory?.id;
|
templatePayload.inventory = inventory?.id;
|
||||||
@@ -32,6 +33,7 @@ function WorkflowJobTemplateAdd() {
|
|||||||
templatePayload.limit = limit === '' ? null : limit;
|
templatePayload.limit = limit === '' ? null : limit;
|
||||||
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
||||||
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
||||||
|
templatePayload.scm_branch = scm_branch === '' ? null : scm_branch;
|
||||||
const organizationId =
|
const organizationId =
|
||||||
organization?.id || inventory?.summary_fields?.organization.id;
|
organization?.id || inventory?.summary_fields?.organization.id;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ describe('<WorkflowJobTemplateAdd/>', () => {
|
|||||||
job_tags: null,
|
job_tags: null,
|
||||||
limit: null,
|
limit: null,
|
||||||
organization: undefined,
|
organization: undefined,
|
||||||
scm_branch: '',
|
scm_branch: null,
|
||||||
skip_tags: null,
|
skip_tags: null,
|
||||||
webhook_credential: undefined,
|
webhook_credential: undefined,
|
||||||
webhook_service: '',
|
webhook_service: '',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function WorkflowJobTemplateEdit({ template }) {
|
|||||||
limit,
|
limit,
|
||||||
job_tags,
|
job_tags,
|
||||||
skip_tags,
|
skip_tags,
|
||||||
|
scm_branch,
|
||||||
...templatePayload
|
...templatePayload
|
||||||
} = values;
|
} = values;
|
||||||
templatePayload.inventory = inventory?.id || null;
|
templatePayload.inventory = inventory?.id || null;
|
||||||
@@ -38,6 +39,7 @@ function WorkflowJobTemplateEdit({ template }) {
|
|||||||
templatePayload.limit = limit === '' ? null : limit;
|
templatePayload.limit = limit === '' ? null : limit;
|
||||||
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
||||||
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
||||||
|
templatePayload.scm_branch = scm_branch === '' ? null : scm_branch;
|
||||||
|
|
||||||
const formOrgId =
|
const formOrgId =
|
||||||
organization?.id || inventory?.summary_fields?.organization.id || null;
|
organization?.id || inventory?.summary_fields?.organization.id || null;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ __metaclass__ = type
|
|||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
name: controller
|
name: controller
|
||||||
plugin_type: inventory
|
|
||||||
author:
|
author:
|
||||||
- Matthew Jones (@matburt)
|
- Matthew Jones (@matburt)
|
||||||
- Yunfan Zhang (@YunfanZhang42)
|
- Yunfan Zhang (@YunfanZhang42)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
|
|||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
lookup: controller_api
|
name: controller_api
|
||||||
author: John Westcott IV (@john-westcott-iv)
|
author: John Westcott IV (@john-westcott-iv)
|
||||||
short_description: Search the API for objects
|
short_description: Search the API for objects
|
||||||
requirements:
|
requirements:
|
||||||
@@ -74,7 +74,7 @@ EXAMPLES = """
|
|||||||
|
|
||||||
- name: Load the UI settings specifying the connection info
|
- name: Load the UI settings specifying the connection info
|
||||||
set_fact:
|
set_fact:
|
||||||
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/ui' host='controller.example.com',
|
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/ui', host='controller.example.com',
|
||||||
username='admin', password=my_pass_var, verify_ssl=False) }}"
|
username='admin', password=my_pass_var, verify_ssl=False) }}"
|
||||||
|
|
||||||
- name: Report the usernames of all users with admin privs
|
- name: Report the usernames of all users with admin privs
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
|
|||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
lookup: schedule_rrule
|
name: schedule_rrule
|
||||||
author: John Westcott IV (@john-westcott-iv)
|
author: John Westcott IV (@john-westcott-iv)
|
||||||
short_description: Generate an rrule string which can be used for Schedules
|
short_description: Generate an rrule string which can be used for Schedules
|
||||||
requirements:
|
requirements:
|
||||||
@@ -101,39 +101,39 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
class LookupModule(LookupBase):
|
class LookupModule(LookupBase):
|
||||||
frequencies = {
|
|
||||||
'none': rrule.DAILY,
|
|
||||||
'minute': rrule.MINUTELY,
|
|
||||||
'hour': rrule.HOURLY,
|
|
||||||
'day': rrule.DAILY,
|
|
||||||
'week': rrule.WEEKLY,
|
|
||||||
'month': rrule.MONTHLY,
|
|
||||||
}
|
|
||||||
|
|
||||||
weekdays = {
|
|
||||||
'monday': rrule.MO,
|
|
||||||
'tuesday': rrule.TU,
|
|
||||||
'wednesday': rrule.WE,
|
|
||||||
'thursday': rrule.TH,
|
|
||||||
'friday': rrule.FR,
|
|
||||||
'saturday': rrule.SA,
|
|
||||||
'sunday': rrule.SU,
|
|
||||||
}
|
|
||||||
|
|
||||||
set_positions = {
|
|
||||||
'first': 1,
|
|
||||||
'second': 2,
|
|
||||||
'third': 3,
|
|
||||||
'fourth': 4,
|
|
||||||
'last': -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
# plugin constructor
|
# plugin constructor
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if LIBRARY_IMPORT_ERROR:
|
if LIBRARY_IMPORT_ERROR:
|
||||||
raise_from(AnsibleError('{0}'.format(LIBRARY_IMPORT_ERROR)), LIBRARY_IMPORT_ERROR)
|
raise_from(AnsibleError('{0}'.format(LIBRARY_IMPORT_ERROR)), LIBRARY_IMPORT_ERROR)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.frequencies = {
|
||||||
|
'none': rrule.DAILY,
|
||||||
|
'minute': rrule.MINUTELY,
|
||||||
|
'hour': rrule.HOURLY,
|
||||||
|
'day': rrule.DAILY,
|
||||||
|
'week': rrule.WEEKLY,
|
||||||
|
'month': rrule.MONTHLY,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.weekdays = {
|
||||||
|
'monday': rrule.MO,
|
||||||
|
'tuesday': rrule.TU,
|
||||||
|
'wednesday': rrule.WE,
|
||||||
|
'thursday': rrule.TH,
|
||||||
|
'friday': rrule.FR,
|
||||||
|
'saturday': rrule.SA,
|
||||||
|
'sunday': rrule.SU,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_positions = {
|
||||||
|
'first': 1,
|
||||||
|
'second': 2,
|
||||||
|
'third': 3,
|
||||||
|
'fourth': 4,
|
||||||
|
'last': -1,
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_date_time(date_string):
|
def parse_date_time(date_string):
|
||||||
try:
|
try:
|
||||||
@@ -149,14 +149,13 @@ class LookupModule(LookupBase):
|
|||||||
|
|
||||||
return self.get_rrule(frequency, kwargs)
|
return self.get_rrule(frequency, kwargs)
|
||||||
|
|
||||||
@staticmethod
|
def get_rrule(self, frequency, kwargs):
|
||||||
def get_rrule(frequency, kwargs):
|
|
||||||
|
|
||||||
if frequency not in LookupModule.frequencies:
|
if frequency not in self.frequencies:
|
||||||
raise AnsibleError('Frequency of {0} is invalid'.format(frequency))
|
raise AnsibleError('Frequency of {0} is invalid'.format(frequency))
|
||||||
|
|
||||||
rrule_kwargs = {
|
rrule_kwargs = {
|
||||||
'freq': LookupModule.frequencies[frequency],
|
'freq': self.frequencies[frequency],
|
||||||
'interval': kwargs.get('every', 1),
|
'interval': kwargs.get('every', 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,9 +186,9 @@ class LookupModule(LookupBase):
|
|||||||
days = []
|
days = []
|
||||||
for day in kwargs['on_days'].split(','):
|
for day in kwargs['on_days'].split(','):
|
||||||
day = day.strip()
|
day = day.strip()
|
||||||
if day not in LookupModule.weekdays:
|
if day not in self.weekdays:
|
||||||
raise AnsibleError('Parameter on_days must only contain values {0}'.format(', '.join(LookupModule.weekdays.keys())))
|
raise AnsibleError('Parameter on_days must only contain values {0}'.format(', '.join(self.weekdays.keys())))
|
||||||
days.append(LookupModule.weekdays[day])
|
days.append(self.weekdays[day])
|
||||||
|
|
||||||
rrule_kwargs['byweekday'] = days
|
rrule_kwargs['byweekday'] = days
|
||||||
|
|
||||||
@@ -214,13 +213,13 @@ class LookupModule(LookupBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise_from(AnsibleError('on_the parameter must be two words separated by a space'), e)
|
raise_from(AnsibleError('on_the parameter must be two words separated by a space'), e)
|
||||||
|
|
||||||
if weekday not in LookupModule.weekdays:
|
if weekday not in self.weekdays:
|
||||||
raise AnsibleError('Weekday portion of on_the parameter is not valid')
|
raise AnsibleError('Weekday portion of on_the parameter is not valid')
|
||||||
if occurance not in LookupModule.set_positions:
|
if occurance not in self.set_positions:
|
||||||
raise AnsibleError('The first string of the on_the parameter is not valid')
|
raise AnsibleError('The first string of the on_the parameter is not valid')
|
||||||
|
|
||||||
rrule_kwargs['byweekday'] = LookupModule.weekdays[weekday]
|
rrule_kwargs['byweekday'] = self.weekdays[weekday]
|
||||||
rrule_kwargs['bysetpos'] = LookupModule.set_positions[occurance]
|
rrule_kwargs['bysetpos'] = self.set_positions[occurance]
|
||||||
|
|
||||||
my_rule = rrule.rrule(**rrule_kwargs)
|
my_rule = rrule.rrule(**rrule_kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
|
|||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
lookup: schedule_rruleset
|
name: schedule_rruleset
|
||||||
author: John Westcott IV (@john-westcott-iv)
|
author: John Westcott IV (@john-westcott-iv)
|
||||||
short_description: Generate an rruleset string
|
short_description: Generate an rruleset string
|
||||||
requirements:
|
requirements:
|
||||||
@@ -31,7 +31,8 @@ DOCUMENTATION = """
|
|||||||
rules:
|
rules:
|
||||||
description:
|
description:
|
||||||
- Array of rules in the rruleset
|
- Array of rules in the rruleset
|
||||||
type: array
|
type: list
|
||||||
|
elements: dict
|
||||||
required: True
|
required: True
|
||||||
suboptions:
|
suboptions:
|
||||||
frequency:
|
frequency:
|
||||||
@@ -136,40 +137,44 @@ try:
|
|||||||
import pytz
|
import pytz
|
||||||
from dateutil import rrule
|
from dateutil import rrule
|
||||||
except ImportError as imp_exc:
|
except ImportError as imp_exc:
|
||||||
raise_from(AnsibleError('{0}'.format(imp_exc)), imp_exc)
|
LIBRARY_IMPORT_ERROR = imp_exc
|
||||||
|
else:
|
||||||
|
LIBRARY_IMPORT_ERROR = None
|
||||||
|
|
||||||
|
|
||||||
class LookupModule(LookupBase):
|
class LookupModule(LookupBase):
|
||||||
frequencies = {
|
|
||||||
'none': rrule.DAILY,
|
|
||||||
'minute': rrule.MINUTELY,
|
|
||||||
'hour': rrule.HOURLY,
|
|
||||||
'day': rrule.DAILY,
|
|
||||||
'week': rrule.WEEKLY,
|
|
||||||
'month': rrule.MONTHLY,
|
|
||||||
}
|
|
||||||
|
|
||||||
weekdays = {
|
|
||||||
'monday': rrule.MO,
|
|
||||||
'tuesday': rrule.TU,
|
|
||||||
'wednesday': rrule.WE,
|
|
||||||
'thursday': rrule.TH,
|
|
||||||
'friday': rrule.FR,
|
|
||||||
'saturday': rrule.SA,
|
|
||||||
'sunday': rrule.SU,
|
|
||||||
}
|
|
||||||
|
|
||||||
set_positions = {
|
|
||||||
'first': 1,
|
|
||||||
'second': 2,
|
|
||||||
'third': 3,
|
|
||||||
'fourth': 4,
|
|
||||||
'last': -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
# plugin constructor
|
# plugin constructor
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
if LIBRARY_IMPORT_ERROR:
|
||||||
|
raise_from(AnsibleError('{0}'.format(LIBRARY_IMPORT_ERROR)), LIBRARY_IMPORT_ERROR)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.frequencies = {
|
||||||
|
'none': rrule.DAILY,
|
||||||
|
'minute': rrule.MINUTELY,
|
||||||
|
'hour': rrule.HOURLY,
|
||||||
|
'day': rrule.DAILY,
|
||||||
|
'week': rrule.WEEKLY,
|
||||||
|
'month': rrule.MONTHLY,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.weekdays = {
|
||||||
|
'monday': rrule.MO,
|
||||||
|
'tuesday': rrule.TU,
|
||||||
|
'wednesday': rrule.WE,
|
||||||
|
'thursday': rrule.TH,
|
||||||
|
'friday': rrule.FR,
|
||||||
|
'saturday': rrule.SA,
|
||||||
|
'sunday': rrule.SU,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_positions = {
|
||||||
|
'first': 1,
|
||||||
|
'second': 2,
|
||||||
|
'third': 3,
|
||||||
|
'fourth': 4,
|
||||||
|
'last': -1,
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_date_time(date_string):
|
def parse_date_time(date_string):
|
||||||
@@ -188,14 +193,14 @@ class LookupModule(LookupBase):
|
|||||||
# something: [1,2,3] - A list of ints
|
# something: [1,2,3] - A list of ints
|
||||||
return_values = []
|
return_values = []
|
||||||
# If they give us a single int, lets make it a list of ints
|
# If they give us a single int, lets make it a list of ints
|
||||||
if type(rule[field_name]) == int:
|
if isinstance(rule[field_name], int):
|
||||||
rule[field_name] = [rule[field_name]]
|
rule[field_name] = [rule[field_name]]
|
||||||
# If its not a list, we need to split it into a list
|
# If its not a list, we need to split it into a list
|
||||||
if type(rule[field_name]) != list:
|
if isinstance(rule[field_name], list):
|
||||||
rule[field_name] = rule[field_name].split(',')
|
rule[field_name] = rule[field_name].split(',')
|
||||||
for value in rule[field_name]:
|
for value in rule[field_name]:
|
||||||
# If they have a list of strs we want to strip the str incase its space delineated
|
# If they have a list of strs we want to strip the str incase its space delineated
|
||||||
if type(value) == str:
|
if isinstance(value, str):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
# If value happens to be an int (from a list of ints) we need to coerce it into a str for the re.match
|
# If value happens to be an int (from a list of ints) we need to coerce it into a str for the re.match
|
||||||
if not re.match(r"^\d+$", str(value)) or int(value) < min_value or int(value) > max_value:
|
if not re.match(r"^\d+$", str(value)) or int(value) < min_value or int(value) > max_value:
|
||||||
@@ -205,7 +210,7 @@ class LookupModule(LookupBase):
|
|||||||
|
|
||||||
def process_list(self, field_name, rule, valid_list, rule_number):
|
def process_list(self, field_name, rule, valid_list, rule_number):
|
||||||
return_values = []
|
return_values = []
|
||||||
if type(rule[field_name]) != list:
|
if isinstance(rule[field_name], list):
|
||||||
rule[field_name] = rule[field_name].split(',')
|
rule[field_name] = rule[field_name].split(',')
|
||||||
for value in rule[field_name]:
|
for value in rule[field_name]:
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
@@ -260,11 +265,11 @@ class LookupModule(LookupBase):
|
|||||||
frequency = rule.get('frequency', None)
|
frequency = rule.get('frequency', None)
|
||||||
if not frequency:
|
if not frequency:
|
||||||
raise AnsibleError("Rule {0} is missing a frequency".format(rule_number))
|
raise AnsibleError("Rule {0} is missing a frequency".format(rule_number))
|
||||||
if frequency not in LookupModule.frequencies:
|
if frequency not in self.frequencies:
|
||||||
raise AnsibleError('Frequency of rule {0} is invalid {1}'.format(rule_number, frequency))
|
raise AnsibleError('Frequency of rule {0} is invalid {1}'.format(rule_number, frequency))
|
||||||
|
|
||||||
rrule_kwargs = {
|
rrule_kwargs = {
|
||||||
'freq': LookupModule.frequencies[frequency],
|
'freq': self.frequencies[frequency],
|
||||||
'interval': rule.get('interval', 1),
|
'interval': rule.get('interval', 1),
|
||||||
'dtstart': start_date,
|
'dtstart': start_date,
|
||||||
}
|
}
|
||||||
@@ -287,7 +292,7 @@ class LookupModule(LookupBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'bysetpos' in rule:
|
if 'bysetpos' in rule:
|
||||||
rrule_kwargs['bysetpos'] = self.process_list('bysetpos', rule, LookupModule.set_positions, rule_number)
|
rrule_kwargs['bysetpos'] = self.process_list('bysetpos', rule, self.set_positions, rule_number)
|
||||||
|
|
||||||
if 'bymonth' in rule:
|
if 'bymonth' in rule:
|
||||||
rrule_kwargs['bymonth'] = self.process_integer('bymonth', rule, 1, 12, rule_number)
|
rrule_kwargs['bymonth'] = self.process_integer('bymonth', rule, 1, 12, rule_number)
|
||||||
@@ -302,7 +307,7 @@ class LookupModule(LookupBase):
|
|||||||
rrule_kwargs['byweekno'] = self.process_integer('byweekno', rule, 1, 52, rule_number)
|
rrule_kwargs['byweekno'] = self.process_integer('byweekno', rule, 1, 52, rule_number)
|
||||||
|
|
||||||
if 'byweekday' in rule:
|
if 'byweekday' in rule:
|
||||||
rrule_kwargs['byweekday'] = self.process_list('byweekday', rule, LookupModule.weekdays, rule_number)
|
rrule_kwargs['byweekday'] = self.process_list('byweekday', rule, self.weekdays, rule_number)
|
||||||
|
|
||||||
if 'byhour' in rule:
|
if 'byhour' in rule:
|
||||||
rrule_kwargs['byhour'] = self.process_integer('byhour', rule, 0, 23, rule_number)
|
rrule_kwargs['byhour'] = self.process_integer('byhour', rule, 0, 23, rule_number)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ __metaclass__ = type
|
|||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||||
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
|
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
|
||||||
|
from ansible.module_utils.parsing.convert_bool import boolean as strtobool
|
||||||
from ansible.module_utils.six import PY2
|
from ansible.module_utils.six import PY2
|
||||||
from ansible.module_utils.six import raise_from, string_types
|
from ansible.module_utils.six import raise_from, string_types
|
||||||
from ansible.module_utils.six.moves import StringIO
|
from ansible.module_utils.six.moves import StringIO
|
||||||
@@ -11,14 +12,21 @@ from ansible.module_utils.six.moves.urllib.error import HTTPError
|
|||||||
from ansible.module_utils.six.moves.http_cookiejar import CookieJar
|
from ansible.module_utils.six.moves.http_cookiejar import CookieJar
|
||||||
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
|
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
|
||||||
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
|
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
|
||||||
from distutils.version import LooseVersion as Version
|
|
||||||
from socket import getaddrinfo, IPPROTO_TCP
|
from socket import getaddrinfo, IPPROTO_TCP
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
from json import loads, dumps
|
from json import loads, dumps
|
||||||
from os.path import isfile, expanduser, split, join, exists, isdir
|
from os.path import isfile, expanduser, split, join, exists, isdir
|
||||||
from os import access, R_OK, getcwd
|
from os import access, R_OK, getcwd
|
||||||
from distutils.util import strtobool
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ansible.module_utils.compat.version import LooseVersion as Version
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from distutils.version import LooseVersion as Version
|
||||||
|
except ImportError:
|
||||||
|
raise AssertionError('To use this plugin or module with ansible-core 2.11, you need to use Python < 3.12 with distutils.version present')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ options:
|
|||||||
description:
|
description:
|
||||||
- The arguments to pass to the module.
|
- The arguments to pass to the module.
|
||||||
type: str
|
type: str
|
||||||
default: ""
|
|
||||||
forks:
|
forks:
|
||||||
description:
|
description:
|
||||||
- The number of forks to use for this ad hoc execution.
|
- The number of forks to use for this ad hoc execution.
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ options:
|
|||||||
- Maximum time in seconds to wait for a job to finish.
|
- Maximum time in seconds to wait for a job to finish.
|
||||||
- Not specifying means the task will wait until the controller cancels the command.
|
- Not specifying means the task will wait until the controller cancels the command.
|
||||||
type: int
|
type: int
|
||||||
|
default: 0
|
||||||
extends_documentation_fragment: awx.awx.auth
|
extends_documentation_fragment: awx.awx.auth
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ options:
|
|||||||
- The credential type being created.
|
- The credential type being created.
|
||||||
- Can be a built-in credential type such as "Machine", or a custom credential type such as "My Credential Type"
|
- Can be a built-in credential type such as "Machine", or a custom credential type such as "My Credential Type"
|
||||||
- Choices include Amazon Web Services, Ansible Galaxy/Automation Hub API Token, Centrify Vault Credential Provider Lookup,
|
- Choices include Amazon Web Services, Ansible Galaxy/Automation Hub API Token, Centrify Vault Credential Provider Lookup,
|
||||||
Container Registry, CyberArk AIM Central Credential Provider Lookup, CyberArk Conjur Secrets Manager Lookup, Google Compute Engine,
|
Container Registry, CyberArk Central Credential Provider Lookup, CyberArk Conjur Secret Lookup, Google Compute Engine,
|
||||||
GitHub Personal Access Token, GitLab Personal Access Token, GPG Public Key, HashiCorp Vault Secret Lookup, HashiCorp Vault Signed SSH,
|
GitHub Personal Access Token, GitLab Personal Access Token, GPG Public Key, HashiCorp Vault Secret Lookup, HashiCorp Vault Signed SSH,
|
||||||
Insights, Machine, Microsoft Azure Key Vault, Microsoft Azure Resource Manager, Network, OpenShift or Kubernetes API
|
Insights, Machine, Microsoft Azure Key Vault, Microsoft Azure Resource Manager, Network, OpenShift or Kubernetes API
|
||||||
Bearer Token, OpenStack, Red Hat Ansible Automation Platform, Red Hat Satellite 6, Red Hat Virtualization, Source Control,
|
Bearer Token, OpenStack, Red Hat Ansible Automation Platform, Red Hat Satellite 6, Red Hat Virtualization, Source Control,
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ def main():
|
|||||||
name=dict(required=True),
|
name=dict(required=True),
|
||||||
new_name=dict(),
|
new_name=dict(),
|
||||||
image=dict(required=True),
|
image=dict(required=True),
|
||||||
description=dict(default=''),
|
description=dict(),
|
||||||
organization=dict(),
|
organization=dict(),
|
||||||
credential=dict(default=''),
|
credential=dict(),
|
||||||
state=dict(choices=['present', 'absent'], default='present'),
|
state=dict(choices=['present', 'absent'], default='present'),
|
||||||
pull=dict(choices=['always', 'missing', 'never'], default='missing'),
|
pull=dict(choices=['always', 'missing', 'never'], default='missing'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,6 +86,16 @@ options:
|
|||||||
- workflow names to export
|
- workflow names to export
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
applications:
|
||||||
|
description:
|
||||||
|
- OAuth2 application names to export
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
schedules:
|
||||||
|
description:
|
||||||
|
- schedule names to export
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
requirements:
|
requirements:
|
||||||
- "awxkit >= 9.3.0"
|
- "awxkit >= 9.3.0"
|
||||||
notes:
|
notes:
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Maximum time in seconds to wait for a job to finish (server-side).
|
- Maximum time in seconds to wait for a job to finish (server-side).
|
||||||
type: int
|
type: int
|
||||||
|
default: 0
|
||||||
job_slice_count:
|
job_slice_count:
|
||||||
description:
|
description:
|
||||||
- The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1.
|
- The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1.
|
||||||
@@ -287,7 +288,6 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true.
|
- Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true.
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
labels:
|
labels:
|
||||||
description:
|
description:
|
||||||
- The labels applied to this job template
|
- The labels applied to this job template
|
||||||
|
|||||||
@@ -60,12 +60,10 @@ options:
|
|||||||
description:
|
description:
|
||||||
- The branch to use for the SCM resource.
|
- The branch to use for the SCM resource.
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
scm_refspec:
|
scm_refspec:
|
||||||
description:
|
description:
|
||||||
- The refspec to use for the SCM resource.
|
- The refspec to use for the SCM resource.
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
credential:
|
credential:
|
||||||
description:
|
description:
|
||||||
- Name of the credential to use with this SCM resource.
|
- Name of the credential to use with this SCM resource.
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ options:
|
|||||||
- Specify C(extra_vars) for the template.
|
- Specify C(extra_vars) for the template.
|
||||||
required: False
|
required: False
|
||||||
type: dict
|
type: dict
|
||||||
default: {}
|
|
||||||
forks:
|
forks:
|
||||||
description:
|
description:
|
||||||
- Forks applied as a prompt, assuming job template prompts for forks
|
- Forks applied as a prompt, assuming job template prompts for forks
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ options:
|
|||||||
- Note This is a client side search, not an API side search
|
- Note This is a client side search, not an API side search
|
||||||
required: False
|
required: False
|
||||||
type: dict
|
type: dict
|
||||||
|
default: {}
|
||||||
extends_documentation_fragment: awx.awx.auth
|
extends_documentation_fragment: awx.awx.auth
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ options:
|
|||||||
- Optional description of this access token.
|
- Optional description of this access token.
|
||||||
required: False
|
required: False
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
application:
|
application:
|
||||||
description:
|
description:
|
||||||
- The application tied to this token.
|
- The application tied to this token.
|
||||||
|
|||||||
@@ -214,7 +214,8 @@ options:
|
|||||||
type: int
|
type: int
|
||||||
job_slice_count:
|
job_slice_count:
|
||||||
description:
|
description:
|
||||||
- The number of jobs to slice into at runtime, if job template prompts for job slices. Will cause the Job Template to launch a workflow if value is greater than 1.
|
- The number of jobs to slice into at runtime, if job template prompts for job slices.
|
||||||
|
- Will cause the Job Template to launch a workflow if value is greater than 1.
|
||||||
type: int
|
type: int
|
||||||
default: '1'
|
default: '1'
|
||||||
timeout:
|
timeout:
|
||||||
@@ -328,42 +329,46 @@ options:
|
|||||||
- Nodes that will run after this node completes.
|
- Nodes that will run after this node completes.
|
||||||
- List of node identifiers.
|
- List of node identifiers.
|
||||||
type: list
|
type: list
|
||||||
|
elements: dict
|
||||||
suboptions:
|
suboptions:
|
||||||
identifier:
|
identifier:
|
||||||
description:
|
description:
|
||||||
- Identifier of Node that will run after this node completes given this option.
|
- Identifier of Node that will run after this node completes given this option.
|
||||||
elements: str
|
type: str
|
||||||
success_nodes:
|
success_nodes:
|
||||||
description:
|
description:
|
||||||
- Nodes that will run after this node on success.
|
- Nodes that will run after this node on success.
|
||||||
- List of node identifiers.
|
- List of node identifiers.
|
||||||
type: list
|
type: list
|
||||||
|
elements: dict
|
||||||
suboptions:
|
suboptions:
|
||||||
identifier:
|
identifier:
|
||||||
description:
|
description:
|
||||||
- Identifier of Node that will run after this node completes given this option.
|
- Identifier of Node that will run after this node completes given this option.
|
||||||
elements: str
|
type: str
|
||||||
failure_nodes:
|
failure_nodes:
|
||||||
description:
|
description:
|
||||||
- Nodes that will run after this node on failure.
|
- Nodes that will run after this node on failure.
|
||||||
- List of node identifiers.
|
- List of node identifiers.
|
||||||
type: list
|
type: list
|
||||||
|
elements: dict
|
||||||
suboptions:
|
suboptions:
|
||||||
identifier:
|
identifier:
|
||||||
description:
|
description:
|
||||||
- Identifier of Node that will run after this node completes given this option.
|
- Identifier of Node that will run after this node completes given this option.
|
||||||
elements: str
|
type: str
|
||||||
credentials:
|
credentials:
|
||||||
description:
|
description:
|
||||||
- Credentials to be applied to job as launch-time prompts.
|
- Credentials to be applied to job as launch-time prompts.
|
||||||
- List of credential names.
|
- List of credential names.
|
||||||
- Uniqueness is not handled rigorously.
|
- Uniqueness is not handled rigorously.
|
||||||
type: list
|
type: list
|
||||||
|
elements: dict
|
||||||
suboptions:
|
suboptions:
|
||||||
name:
|
name:
|
||||||
description:
|
description:
|
||||||
- Name Credentials to be applied to job as launch-time prompts.
|
- Name Credentials to be applied to job as launch-time prompts.
|
||||||
elements: str
|
type: str
|
||||||
organization:
|
organization:
|
||||||
description:
|
description:
|
||||||
- Name of key for use in model for organizational reference
|
- Name of key for use in model for organizational reference
|
||||||
@@ -379,11 +384,12 @@ options:
|
|||||||
- List of Label names.
|
- List of Label names.
|
||||||
- Uniqueness is not handled rigorously.
|
- Uniqueness is not handled rigorously.
|
||||||
type: list
|
type: list
|
||||||
|
elements: dict
|
||||||
suboptions:
|
suboptions:
|
||||||
name:
|
name:
|
||||||
description:
|
description:
|
||||||
- Name Labels to be applied to job as launch-time prompts.
|
- Name Labels to be applied to job as launch-time prompts.
|
||||||
elements: str
|
type: str
|
||||||
organization:
|
organization:
|
||||||
description:
|
description:
|
||||||
- Name of key for use in model for organizational reference
|
- Name of key for use in model for organizational reference
|
||||||
@@ -399,11 +405,12 @@ options:
|
|||||||
- List of Instance group names.
|
- List of Instance group names.
|
||||||
- Uniqueness is not handled rigorously.
|
- Uniqueness is not handled rigorously.
|
||||||
type: list
|
type: list
|
||||||
|
elements: dict
|
||||||
suboptions:
|
suboptions:
|
||||||
name:
|
name:
|
||||||
description:
|
description:
|
||||||
- Name of Instance groups to be applied to job as launch-time prompts.
|
- Name of Instance groups to be applied to job as launch-time prompts.
|
||||||
elements: str
|
type: str
|
||||||
destroy_current_nodes:
|
destroy_current_nodes:
|
||||||
description:
|
description:
|
||||||
- Set in order to destroy current workflow_nodes on the workflow.
|
- Set in order to destroy current workflow_nodes on the workflow.
|
||||||
@@ -789,6 +796,7 @@ def main():
|
|||||||
allow_simultaneous=dict(type='bool'),
|
allow_simultaneous=dict(type='bool'),
|
||||||
ask_variables_on_launch=dict(type='bool'),
|
ask_variables_on_launch=dict(type='bool'),
|
||||||
ask_labels_on_launch=dict(type='bool', aliases=['ask_labels']),
|
ask_labels_on_launch=dict(type='bool', aliases=['ask_labels']),
|
||||||
|
ask_tags_on_launch=dict(type='bool', aliases=['ask_tags']),
|
||||||
ask_skip_tags_on_launch=dict(type='bool', aliases=['ask_skip_tags']),
|
ask_skip_tags_on_launch=dict(type='bool', aliases=['ask_skip_tags']),
|
||||||
inventory=dict(),
|
inventory=dict(),
|
||||||
limit=dict(),
|
limit=dict(),
|
||||||
@@ -873,6 +881,7 @@ def main():
|
|||||||
'ask_limit_on_launch',
|
'ask_limit_on_launch',
|
||||||
'ask_variables_on_launch',
|
'ask_variables_on_launch',
|
||||||
'ask_labels_on_launch',
|
'ask_labels_on_launch',
|
||||||
|
'ask_tags_on_launch',
|
||||||
'ask_skip_tags_on_launch',
|
'ask_skip_tags_on_launch',
|
||||||
'webhook_service',
|
'webhook_service',
|
||||||
'job_tags',
|
'job_tags',
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ options:
|
|||||||
- Variables to apply at launch time.
|
- Variables to apply at launch time.
|
||||||
- Will only be accepted if job template prompts for vars or has a survey asking for those vars.
|
- Will only be accepted if job template prompts for vars or has a survey asking for those vars.
|
||||||
type: dict
|
type: dict
|
||||||
default: {}
|
|
||||||
inventory:
|
inventory:
|
||||||
description:
|
description:
|
||||||
- Inventory applied as a prompt, if job template prompts for inventory
|
- Inventory applied as a prompt, if job template prompts for inventory
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ def run_module(request, collection_import):
|
|||||||
elif getattr(resource_module, 'TowerLegacyModule', None):
|
elif getattr(resource_module, 'TowerLegacyModule', None):
|
||||||
resource_class = resource_module.TowerLegacyModule
|
resource_class = resource_module.TowerLegacyModule
|
||||||
else:
|
else:
|
||||||
raise ("The module has neither a TowerLegacyModule, ControllerAWXKitModule or a ControllerAPIModule")
|
raise RuntimeError("The module has neither a TowerLegacyModule, ControllerAWXKitModule or a ControllerAPIModule")
|
||||||
|
|
||||||
with mock.patch.object(resource_class, '_load_params', new=mock_load_params):
|
with mock.patch.object(resource_class, '_load_params', new=mock_load_params):
|
||||||
# Call the test utility (like a mock server) instead of issuing HTTP requests
|
# Call the test utility (like a mock server) instead of issuing HTTP requests
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def test_create_project(run_module, admin_user, organization, silence_warning):
|
|||||||
dict(name='foo', organization=organization.name, scm_type='git', scm_url='https://foo.invalid', wait=False, scm_update_cache_timeout=5),
|
dict(name='foo', organization=organization.name, scm_type='git', scm_url='https://foo.invalid', wait=False, scm_update_cache_timeout=5),
|
||||||
admin_user,
|
admin_user,
|
||||||
)
|
)
|
||||||
silence_warning.assert_called_once_with('scm_update_cache_timeout will be ignored since scm_update_on_launch ' 'was not set to true')
|
silence_warning.assert_called_once_with('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true')
|
||||||
|
|
||||||
assert result.pop('changed', None), result
|
assert result.pop('changed', None), result
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def test_delete_same_named_schedule(run_module, project, inventory, admin_user):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_rrule_lookup_plugin(collection_import, freq, kwargs, expect):
|
def test_rrule_lookup_plugin(collection_import, freq, kwargs, expect):
|
||||||
LookupModule = collection_import('plugins.lookup.schedule_rrule').LookupModule
|
LookupModule = collection_import('plugins.lookup.schedule_rrule').LookupModule()
|
||||||
generated_rule = LookupModule.get_rrule(freq, kwargs)
|
generated_rule = LookupModule.get_rrule(freq, kwargs)
|
||||||
assert generated_rule == expect
|
assert generated_rule == expect
|
||||||
rrule_checker = SchedulePreviewSerializer()
|
rrule_checker = SchedulePreviewSerializer()
|
||||||
@@ -92,7 +92,7 @@ def test_rrule_lookup_plugin(collection_import, freq, kwargs, expect):
|
|||||||
|
|
||||||
@pytest.mark.parametrize("freq", ('none', 'minute', 'hour', 'day', 'week', 'month'))
|
@pytest.mark.parametrize("freq", ('none', 'minute', 'hour', 'day', 'week', 'month'))
|
||||||
def test_empty_schedule_rrule(collection_import, freq):
|
def test_empty_schedule_rrule(collection_import, freq):
|
||||||
LookupModule = collection_import('plugins.lookup.schedule_rrule').LookupModule
|
LookupModule = collection_import('plugins.lookup.schedule_rrule').LookupModule()
|
||||||
if freq == 'day':
|
if freq == 'day':
|
||||||
pfreq = 'DAILY'
|
pfreq = 'DAILY'
|
||||||
elif freq == 'none':
|
elif freq == 'none':
|
||||||
@@ -136,7 +136,7 @@ def test_empty_schedule_rrule(collection_import, freq):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_rrule_lookup_plugin_failure(collection_import, freq, kwargs, msg):
|
def test_rrule_lookup_plugin_failure(collection_import, freq, kwargs, msg):
|
||||||
LookupModule = collection_import('plugins.lookup.schedule_rrule').LookupModule
|
LookupModule = collection_import('plugins.lookup.schedule_rrule').LookupModule()
|
||||||
with pytest.raises(AnsibleError) as e:
|
with pytest.raises(AnsibleError) as e:
|
||||||
assert LookupModule.get_rrule(freq, kwargs)
|
assert LookupModule.get_rrule(freq, kwargs)
|
||||||
assert msg in str(e.value)
|
assert msg in str(e.value)
|
||||||
|
|||||||
3
awx_collection/tests/config.yml
Normal file
3
awx_collection/tests/config.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
modules:
|
||||||
|
python_requires: '>3'
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
credential:
|
credential:
|
||||||
description: Credential for Testing Source
|
description: Credential for Testing Source
|
||||||
name: "{{ src_cred_name }}"
|
name: "{{ src_cred_name }}"
|
||||||
credential_type: CyberArk AIM Central Credential Provider Lookup
|
credential_type: CyberArk Central Credential Provider Lookup
|
||||||
inputs:
|
inputs:
|
||||||
url: "https://cyberark.example.com"
|
url: "https://cyberark.example.com"
|
||||||
app_id: "My-App-ID"
|
app_id: "My-App-ID"
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
credential:
|
credential:
|
||||||
description: Credential for Testing Source Change
|
description: Credential for Testing Source Change
|
||||||
name: "{{ src_cred_name }}-2"
|
name: "{{ src_cred_name }}-2"
|
||||||
credential_type: CyberArk AIM Central Credential Provider Lookup
|
credential_type: CyberArk Central Credential Provider Lookup
|
||||||
inputs:
|
inputs:
|
||||||
url: "https://cyberark-prod.example.com"
|
url: "https://cyberark-prod.example.com"
|
||||||
app_id: "My-App-ID"
|
app_id: "My-App-ID"
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
credential:
|
credential:
|
||||||
name: "{{ src_cred_name }}"
|
name: "{{ src_cred_name }}"
|
||||||
organization: Default
|
organization: Default
|
||||||
credential_type: CyberArk AIM Central Credential Provider Lookup
|
credential_type: CyberArk Central Credential Provider Lookup
|
||||||
state: absent
|
state: absent
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
credential:
|
credential:
|
||||||
name: "{{ src_cred_name }}-2"
|
name: "{{ src_cred_name }}-2"
|
||||||
organization: Default
|
organization: Default
|
||||||
credential_type: CyberArk AIM Central Credential Provider Lookup
|
credential_type: CyberArk Central Credential Provider Lookup
|
||||||
state: absent
|
state: absent
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,7 @@
|
|||||||
ask_inventory_on_launch: true
|
ask_inventory_on_launch: true
|
||||||
ask_scm_branch_on_launch: true
|
ask_scm_branch_on_launch: true
|
||||||
ask_limit_on_launch: true
|
ask_limit_on_launch: true
|
||||||
|
ask_tags_on_launch: true
|
||||||
ask_variables_on_launch: true
|
ask_variables_on_launch: true
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
@@ -263,6 +264,7 @@
|
|||||||
ask_inventory_on_launch: true
|
ask_inventory_on_launch: true
|
||||||
ask_scm_branch_on_launch: true
|
ask_scm_branch_on_launch: true
|
||||||
ask_limit_on_launch: true
|
ask_limit_on_launch: true
|
||||||
|
ask_tags_on_launch: true
|
||||||
ask_variables_on_launch: true
|
ask_variables_on_launch: true
|
||||||
register: bad_label_results
|
register: bad_label_results
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
ask_inventory_on_launch: false
|
ask_inventory_on_launch: false
|
||||||
ask_scm_branch_on_launch: false
|
ask_scm_branch_on_launch: false
|
||||||
ask_limit_on_launch: false
|
ask_limit_on_launch: false
|
||||||
|
ask_tags_on_launch: false
|
||||||
ask_variables_on_launch: false
|
ask_variables_on_launch: false
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
|||||||
1
awx_collection/tests/sanity/ignore-2.14.txt
Normal file
1
awx_collection/tests/sanity/ignore-2.14.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
plugins/modules/export.py validate-modules:nonexistent-parameter-documented # needs awxkit to construct argspec
|
||||||
1
awx_collection/tests/sanity/ignore-2.15.txt
Normal file
1
awx_collection/tests/sanity/ignore-2.15.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
plugins/modules/export.py validate-modules:nonexistent-parameter-documented # needs awxkit to construct argspec
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
---
|
---
|
||||||
|
- name: Sanity assertions, that some variables have a non-blank value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- collection_version
|
||||||
|
- collection_package
|
||||||
|
- collection_path
|
||||||
|
|
||||||
- name: Set the collection version in the controller_api.py file
|
- name: Set the collection version in the controller_api.py file
|
||||||
replace:
|
replace:
|
||||||
path: "{{ collection_path }}/plugins/module_utils/controller_api.py"
|
path: "{{ collection_path }}/plugins/module_utils/controller_api.py"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ skip_missing_interpreters = true
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
basepython = python3.9
|
basepython = python3.9
|
||||||
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
|
|
||||||
setenv =
|
setenv =
|
||||||
PYTHONPATH = {toxinidir}:{env:PYTHONPATH:}:.
|
PYTHONPATH = {toxinidir}:{env:PYTHONPATH:}:.
|
||||||
deps =
|
deps =
|
||||||
|
|||||||
22
licenses/aioredis.txt
Normal file
22
licenses/aioredis.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2017 Alexey Popravka
|
||||||
|
Copyright (c) 2021 Sean Stewart
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
29
licenses/hiredis.txt
Normal file
29
licenses/hiredis.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||||
|
Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of Redis nor the names of its contributors may be used
|
||||||
|
to endorse or promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -148,6 +148,24 @@ in the top-level Makefile.
|
|||||||
|
|
||||||
If modifying this library make sure testing with the offline build is performed to confirm it is functionally working.
|
If modifying this library make sure testing with the offline build is performed to confirm it is functionally working.
|
||||||
|
|
||||||
|
### channels-redis
|
||||||
|
|
||||||
|
Due to an upstream bug (linked below), we see `RuntimeError: Event loop is closed` errors with newer versions of `channels-redis`.
|
||||||
|
Upstream is aware of the bug and it is likely to be fixed in the next release according to the issue linked below.
|
||||||
|
For now, we pin to the old version, 3.4.1
|
||||||
|
|
||||||
|
* https://github.com/django/channels_redis/issues/332
|
||||||
|
* https://github.com/ansible/awx/issues/13313
|
||||||
|
|
||||||
|
### hiredis
|
||||||
|
|
||||||
|
The hiredis 2.1.0 release doesn't provide source distribution on PyPI which prevents users to build that python package from the
|
||||||
|
sources.
|
||||||
|
Downgrading to 2.0.0 (which provides source distribution) until the channels-redis issue is fixed or a newer hiredis version is
|
||||||
|
available on PyPi with source distribution.
|
||||||
|
|
||||||
|
* https://github.com/redis/hiredis-py/issues/138
|
||||||
|
|
||||||
## Library Notes
|
## Library Notes
|
||||||
|
|
||||||
### pexpect
|
### pexpect
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ asciichartpy
|
|||||||
asn1
|
asn1
|
||||||
azure-keyvault==1.1.0 # see UPGRADE BLOCKERs
|
azure-keyvault==1.1.0 # see UPGRADE BLOCKERs
|
||||||
channels
|
channels
|
||||||
channels-redis
|
channels-redis==3.4.1 # see UPGRADE BLOCKERs
|
||||||
cryptography
|
cryptography
|
||||||
Cython<3 # Since the bump to PyYAML 5.4.1 this is now a mandatory dep
|
Cython<3 # Since the bump to PyYAML 5.4.1 this is now a mandatory dep
|
||||||
daphne
|
daphne
|
||||||
@@ -26,6 +26,7 @@ djangorestframework==3.13.1
|
|||||||
djangorestframework-yaml
|
djangorestframework-yaml
|
||||||
filelock
|
filelock
|
||||||
GitPython
|
GitPython
|
||||||
|
hiredis==2.0.0 # see UPGRADE BLOCKERs
|
||||||
irc
|
irc
|
||||||
jinja2
|
jinja2
|
||||||
JSON-log-formatter
|
JSON-log-formatter
|
||||||
@@ -38,12 +39,11 @@ psycopg2
|
|||||||
psutil
|
psutil
|
||||||
pygerduty
|
pygerduty
|
||||||
pyparsing==2.4.6 # Upgrading to v3 of pyparsing introduce errors on smart host filtering: Expected 'or' term, found 'or' (at char 15), (line:1, col:16)
|
pyparsing==2.4.6 # Upgrading to v3 of pyparsing introduce errors on smart host filtering: Expected 'or' term, found 'or' (at char 15), (line:1, col:16)
|
||||||
python3-saml==1.13.0
|
|
||||||
python-dsv-sdk
|
python-dsv-sdk
|
||||||
python-tss-sdk==1.0.0
|
python-tss-sdk==1.0.0
|
||||||
python-ldap
|
python-ldap
|
||||||
pyyaml
|
pyyaml
|
||||||
receptorctl==1.2.3
|
receptorctl==1.3.0
|
||||||
schedule==0.6.0
|
schedule==0.6.0
|
||||||
social-auth-core[openidconnect]==4.3.0 # see UPGRADE BLOCKERs
|
social-auth-core[openidconnect]==4.3.0 # see UPGRADE BLOCKERs
|
||||||
social-auth-app-django==5.0.0 # see UPGRADE BLOCKERs
|
social-auth-app-django==5.0.0 # see UPGRADE BLOCKERs
|
||||||
@@ -59,10 +59,8 @@ wheel
|
|||||||
pip==21.2.4 # see UPGRADE BLOCKERs
|
pip==21.2.4 # see UPGRADE BLOCKERs
|
||||||
setuptools # see UPGRADE BLOCKERs
|
setuptools # see UPGRADE BLOCKERs
|
||||||
setuptools_scm[toml] # see UPGRADE BLOCKERs, xmlsec build dep
|
setuptools_scm[toml] # see UPGRADE BLOCKERs, xmlsec build dep
|
||||||
xmlsec==1.3.12 # xmlsec 1.3.13 removed the ability to use lxml 4.7.0 but python3-saml requires lxml 4.7.0 so we need to pin xmlsec
|
|
||||||
lxml>=3.8 # xmlsec build dep
|
|
||||||
pkgconfig>=1.5.1 # xmlsec build dep
|
|
||||||
setuptools-rust >= 0.11.4 # cryptography build dep
|
setuptools-rust >= 0.11.4 # cryptography build dep
|
||||||
|
pkgconfig>=1.5.1 # xmlsec build dep - needed for offline build
|
||||||
|
|
||||||
# Temporarily added to use ansible-runner from git branch, to be removed
|
# Temporarily added to use ansible-runner from git branch, to be removed
|
||||||
# when ansible-runner moves from requirements_git.txt to here
|
# when ansible-runner moves from requirements_git.txt to here
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ adal==1.2.7
|
|||||||
# via msrestazure
|
# via msrestazure
|
||||||
aiohttp==3.8.3
|
aiohttp==3.8.3
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
|
aioredis==1.3.1
|
||||||
|
# via channels-redis
|
||||||
aiosignal==1.3.1
|
aiosignal==1.3.1
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
# via -r /awx_devel/requirements/requirements_git.txt
|
# via -r /awx_devel/requirements/requirements_git.txt
|
||||||
@@ -20,6 +22,7 @@ asn1==2.6.0
|
|||||||
async-timeout==4.0.2
|
async-timeout==4.0.2
|
||||||
# via
|
# via
|
||||||
# aiohttp
|
# aiohttp
|
||||||
|
# aioredis
|
||||||
# redis
|
# redis
|
||||||
attrs==22.1.0
|
attrs==22.1.0
|
||||||
# via
|
# via
|
||||||
@@ -51,11 +54,11 @@ cachetools==5.2.0
|
|||||||
# requests
|
# requests
|
||||||
cffi==1.15.1
|
cffi==1.15.1
|
||||||
# via cryptography
|
# via cryptography
|
||||||
channels==4.0.0
|
channels==3.0.5
|
||||||
# via
|
# via
|
||||||
# -r /awx_devel/requirements/requirements.in
|
# -r /awx_devel/requirements/requirements.in
|
||||||
# channels-redis
|
# channels-redis
|
||||||
channels-redis==4.0.0
|
channels-redis==3.4.1
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
charset-normalizer==2.1.1
|
charset-normalizer==2.1.1
|
||||||
# via
|
# via
|
||||||
@@ -76,8 +79,10 @@ cryptography==38.0.4
|
|||||||
# social-auth-core
|
# social-auth-core
|
||||||
cython==0.29.32
|
cython==0.29.32
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
daphne==4.0.0
|
daphne==3.0.2
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via
|
||||||
|
# -r /awx_devel/requirements/requirements.in
|
||||||
|
# channels
|
||||||
dataclasses==0.6
|
dataclasses==0.6
|
||||||
# via
|
# via
|
||||||
# python-dsv-sdk
|
# python-dsv-sdk
|
||||||
@@ -153,6 +158,10 @@ gitpython==3.1.29
|
|||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
google-auth==2.14.1
|
google-auth==2.14.1
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
|
hiredis==2.0.0
|
||||||
|
# via
|
||||||
|
# -r /awx_devel/requirements/requirements.in
|
||||||
|
# aioredis
|
||||||
hyperlink==21.0.0
|
hyperlink==21.0.0
|
||||||
# via
|
# via
|
||||||
# autobahn
|
# autobahn
|
||||||
@@ -198,15 +207,14 @@ jinja2==3.1.2
|
|||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
json-log-formatter==0.5.1
|
json-log-formatter==0.5.1
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
jsonschema==4.17.1
|
jsonschema==4.17.3
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
kubernetes==25.3.0
|
kubernetes==25.3.0
|
||||||
# via openshift
|
# via openshift
|
||||||
lockfile==0.12.2
|
lockfile==0.12.2
|
||||||
# via python-daemon
|
# via python-daemon
|
||||||
lxml==4.7.0
|
lxml==4.9.1
|
||||||
# via
|
# via
|
||||||
# -r /awx_devel/requirements/requirements.in
|
|
||||||
# python3-saml
|
# python3-saml
|
||||||
# xmlsec
|
# xmlsec
|
||||||
markdown==3.4.1
|
markdown==3.4.1
|
||||||
@@ -315,8 +323,7 @@ python-tss-sdk==1.0.0
|
|||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
python3-openid==3.2.0
|
python3-openid==3.2.0
|
||||||
# via social-auth-core
|
# via social-auth-core
|
||||||
python3-saml==1.13.0
|
# via -r /awx_devel/requirements/requirements_git.txt
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
|
||||||
pytz==2022.6
|
pytz==2022.6
|
||||||
# via
|
# via
|
||||||
# django
|
# django
|
||||||
@@ -331,12 +338,11 @@ pyyaml==6.0
|
|||||||
# djangorestframework-yaml
|
# djangorestframework-yaml
|
||||||
# kubernetes
|
# kubernetes
|
||||||
# receptorctl
|
# receptorctl
|
||||||
receptorctl==1.2.3
|
receptorctl==1.3.0
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
redis==4.3.5
|
redis==4.3.5
|
||||||
# via
|
# via
|
||||||
# -r /awx_devel/requirements/requirements.in
|
# -r /awx_devel/requirements/requirements.in
|
||||||
# channels-redis
|
|
||||||
# django-redis
|
# django-redis
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
# via
|
# via
|
||||||
@@ -435,10 +441,8 @@ websocket-client==1.4.2
|
|||||||
# via kubernetes
|
# via kubernetes
|
||||||
wheel==0.38.4
|
wheel==0.38.4
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
xmlsec==1.3.12
|
xmlsec==1.3.13
|
||||||
# via
|
# via python3-saml
|
||||||
# -r /awx_devel/requirements/requirements.in
|
|
||||||
# python3-saml
|
|
||||||
yarl==1.8.1
|
yarl==1.8.1
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
zipp==3.11.0
|
zipp==3.11.0
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ git+https://github.com/ansible/ansible-runner.git@devel#egg=ansible-runner
|
|||||||
# django-radius has an aggressive pin of future==0.16.0, see https://github.com/robgolding/django-radius/pull/25
|
# django-radius has an aggressive pin of future==0.16.0, see https://github.com/robgolding/django-radius/pull/25
|
||||||
git+https://github.com/ansible/django-radius.git@develop#egg=django-radius
|
git+https://github.com/ansible/django-radius.git@develop#egg=django-radius
|
||||||
git+https://github.com/PythonCharmers/python-future@master#egg=future
|
git+https://github.com/PythonCharmers/python-future@master#egg=future
|
||||||
|
git+https://github.com/ansible/python3-saml.git@devel#egg=python3-saml
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
|
|||||||
python3-psycopg2 \
|
python3-psycopg2 \
|
||||||
python3-setuptools \
|
python3-setuptools \
|
||||||
rsync \
|
rsync \
|
||||||
"rsyslog >= 8.1911.0" \
|
rsyslog-8.2102.0-106.el9 \
|
||||||
subversion \
|
subversion \
|
||||||
sudo \
|
sudo \
|
||||||
vim-minimal \
|
vim-minimal \
|
||||||
|
|||||||
Reference in New Issue
Block a user