mirror of
https://github.com/ansible/awx.git
synced 2026-02-23 14:05:59 -03:30
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/requirements"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -2,6 +2,19 @@
|
||||
|
||||
This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/<version>`.
|
||||
|
||||
# (TBD)
|
||||
- Added user interface for the activity stream: https://github.com/ansible/awx/pull/9083
|
||||
- Converted many of the top-level list views (Jobs, Teams, Hosts, Inventories, Projects, and more) to a new, permanent table component for substantially increased responsiveness, usability, maintainability, and other 'ility's: https://github.com/ansible/awx/pull/8970, https://github.com/ansible/awx/pull/9182 and many others!
|
||||
- Added click-to-expand details for job tables
|
||||
- Add search filtering to job output https://github.com/ansible/awx/pull/9208
|
||||
- Added the new migration, update, and "installation in progress" page https://github.com/ansible/awx/pull/9123
|
||||
- Added the user interface for job settings https://github.com/ansible/awx/pull/8661
|
||||
- Runtime errors from jobs are now displayed, along with an explanation for what went wrong, on the output page https://github.com/ansible/awx/pull/9264
|
||||
- You can now cancel a running job from its output and details panel https://github.com/ansible/awx/pull/9199
|
||||
- Fixed a bug where launch prompt inputs were unexpectedly deposited in the url: https://github.com/ansible/awx/pull/9231
|
||||
- Playbook, credential type, and inventory file inputs now support type-ahead and manual type-in! https://github.com/ansible/awx/pull/9120
|
||||
- Added ability to relaunch against failed hosts: https://github.com/ansible/awx/pull/9225
|
||||
|
||||
# 17.0.1 (January 26, 2021)
|
||||
- Fixed pgdocker directory permissions issue with Local Docker installer: https://github.com/ansible/awx/pull/9152
|
||||
- Fixed a bug in the UI which caused toggle settings to not be changed when clicked: https://github.com/ansible/awx/pull/9093
|
||||
|
||||
2
Makefile
2
Makefile
@@ -515,7 +515,7 @@ ui-zuul-lint-and-test:
|
||||
$(NPM_BIN) --prefix awx/ui_next install
|
||||
$(NPM_BIN) run --prefix awx/ui_next lint
|
||||
$(NPM_BIN) run --prefix awx/ui_next prettier-check
|
||||
$(NPM_BIN) run --prefix awx/ui_next test
|
||||
$(NPM_BIN) run --prefix awx/ui_next test -- --coverage --watchAll=false
|
||||
|
||||
|
||||
# Build a pip-installable package into dist/ with a timestamped version number.
|
||||
|
||||
@@ -6,6 +6,7 @@ from multiprocessing import Process
|
||||
from django.conf import settings
|
||||
from django.db import connections
|
||||
from schedule import Scheduler
|
||||
from django_guid.middleware import GuidMiddleware
|
||||
|
||||
from awx.main.dispatch.worker import TaskWorker
|
||||
|
||||
@@ -35,6 +36,7 @@ class Scheduler(Scheduler):
|
||||
# If the database connection has a hiccup, re-establish a new
|
||||
# connection
|
||||
conn.close_if_unusable_or_obsolete()
|
||||
GuidMiddleware.set_guid(GuidMiddleware._generate_guid())
|
||||
self.run_pending()
|
||||
except Exception:
|
||||
logger.exception(
|
||||
|
||||
@@ -16,6 +16,7 @@ from queue import Full as QueueFull, Empty as QueueEmpty
|
||||
from django.conf import settings
|
||||
from django.db import connection as django_connection, connections
|
||||
from django.core.cache import cache as django_cache
|
||||
from django_guid.middleware import GuidMiddleware
|
||||
from jinja2 import Template
|
||||
import psutil
|
||||
|
||||
@@ -445,6 +446,8 @@ class AutoscalePool(WorkerPool):
|
||||
return super(AutoscalePool, self).up()
|
||||
|
||||
def write(self, preferred_queue, body):
|
||||
if 'guid' in body:
|
||||
GuidMiddleware.set_guid(body['guid'])
|
||||
try:
|
||||
# when the cluster heartbeat occurs, clean up internally
|
||||
if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']:
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django_guid.middleware import GuidMiddleware
|
||||
|
||||
from . import pg_bus_conn
|
||||
|
||||
@@ -83,6 +84,9 @@ class task:
|
||||
'kwargs': kwargs,
|
||||
'task': cls.name
|
||||
}
|
||||
guid = GuidMiddleware.get_guid()
|
||||
if guid:
|
||||
obj['guid'] = guid
|
||||
obj.update(**kw)
|
||||
if callable(queue):
|
||||
queue = queue()
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.conf import settings
|
||||
from django.utils.timezone import now as tz_now
|
||||
from django.db import DatabaseError, OperationalError, connection as django_connection
|
||||
from django.db.utils import InterfaceError, InternalError
|
||||
from django_guid.middleware import GuidMiddleware
|
||||
|
||||
import psutil
|
||||
|
||||
@@ -152,6 +153,8 @@ class CallbackBrokerWorker(BaseWorker):
|
||||
|
||||
if body.get('event') == 'EOF':
|
||||
try:
|
||||
if 'guid' in body:
|
||||
GuidMiddleware.set_guid(body['guid'])
|
||||
final_counter = body.get('final_counter', 0)
|
||||
logger.info('Event processing is finished for Job {}, sending notifications'.format(job_identifier))
|
||||
# EOF events are sent when stdout for the running task is
|
||||
@@ -176,6 +179,8 @@ class CallbackBrokerWorker(BaseWorker):
|
||||
handle_success_and_failure_notifications.apply_async([uj.id])
|
||||
except Exception:
|
||||
logger.exception('Worker failed to emit notifications: Job {}'.format(job_identifier))
|
||||
finally:
|
||||
GuidMiddleware.set_guid('')
|
||||
return
|
||||
|
||||
event = cls.create_from_data(**body)
|
||||
|
||||
@@ -6,6 +6,8 @@ import traceback
|
||||
|
||||
from kubernetes.config import kube_config
|
||||
|
||||
from django_guid.middleware import GuidMiddleware
|
||||
|
||||
from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown
|
||||
|
||||
from .base import BaseWorker
|
||||
@@ -52,6 +54,8 @@ class TaskWorker(BaseWorker):
|
||||
uuid = body.get('uuid', '<unknown>')
|
||||
args = body.get('args', [])
|
||||
kwargs = body.get('kwargs', {})
|
||||
if 'guid' in body:
|
||||
GuidMiddleware.set_guid(body.pop('guid'))
|
||||
_call = TaskWorker.resolve_callable(task)
|
||||
if inspect.isclass(_call):
|
||||
# the callable is a class, e.g., RunJob; instantiate and
|
||||
|
||||
@@ -92,8 +92,8 @@ def _openstack_data(cred):
|
||||
},
|
||||
}
|
||||
|
||||
if cred.has_input('project_region_name'):
|
||||
openstack_data['clouds']['devstack']['region_name'] = cred.get_input('project_region_name', default='')
|
||||
if cred.has_input('region'):
|
||||
openstack_data['clouds']['devstack']['region_name'] = cred.get_input('region', default='')
|
||||
|
||||
return openstack_data
|
||||
|
||||
|
||||
@@ -388,12 +388,17 @@ class JobNotificationMixin(object):
|
||||
and a url to the job run."""
|
||||
job_context = {'host_status_counts': {}}
|
||||
summary = None
|
||||
if hasattr(self, 'job_host_summaries'):
|
||||
summary = self.job_host_summaries.first()
|
||||
if summary:
|
||||
from awx.api.serializers import JobHostSummarySerializer
|
||||
summary_data = JobHostSummarySerializer(summary).to_representation(summary)
|
||||
job_context['host_status_counts'] = summary_data
|
||||
try:
|
||||
has_event_property = any([f for f in self.event_class._meta.fields if f.name == 'event'])
|
||||
except NotImplementedError:
|
||||
has_event_property = False
|
||||
if has_event_property:
|
||||
qs = self.get_event_queryset()
|
||||
if qs:
|
||||
event = qs.only('event_data').filter(event='playbook_on_stats').first()
|
||||
if event:
|
||||
summary = event.get_host_status_counts()
|
||||
job_context['host_status_counts'] = summary
|
||||
context = {
|
||||
'job': job_context,
|
||||
'job_friendly_name': self.get_notification_friendly_name(),
|
||||
|
||||
@@ -34,6 +34,7 @@ from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext_lazy as _, gettext_noop
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django_guid.middleware import GuidMiddleware
|
||||
|
||||
# Kubernetes
|
||||
from kubernetes.client.rest import ApiException
|
||||
@@ -839,6 +840,7 @@ class BaseTask(object):
|
||||
self.cleanup_paths = []
|
||||
self.parent_workflow_job_id = None
|
||||
self.host_map = {}
|
||||
self.guid = GuidMiddleware.get_guid()
|
||||
|
||||
def update_model(self, pk, _attempt=0, **updates):
|
||||
"""Reload the model instance from the database and update the
|
||||
@@ -1274,6 +1276,9 @@ class BaseTask(object):
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if 'event_data' in event_data:
|
||||
event_data['event_data']['guid'] = self.guid
|
||||
|
||||
event_data.setdefault(self.event_data_key, self.instance.id)
|
||||
self.dispatcher.dispatch(event_data)
|
||||
self.event_ct += 1
|
||||
@@ -1310,6 +1315,7 @@ class BaseTask(object):
|
||||
event_data = {
|
||||
'event': 'EOF',
|
||||
'final_counter': self.event_ct,
|
||||
'guid': self.guid,
|
||||
}
|
||||
event_data.setdefault(self.event_data_key, self.instance.id)
|
||||
self.dispatcher.dispatch(event_data)
|
||||
|
||||
@@ -8,4 +8,5 @@ clouds:
|
||||
project_name: fooo
|
||||
username: fooo
|
||||
private: true
|
||||
region_name: fooo
|
||||
verify: false
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
{
|
||||
"ansible_all_ipv4_addresses": [
|
||||
"172.17.0.7"
|
||||
],
|
||||
"ansible_all_ipv6_addresses": [
|
||||
"fe80::42:acff:fe11:7"
|
||||
],
|
||||
"ansible_architecture": "x86_64",
|
||||
"ansible_bios_date": "12/01/2006",
|
||||
"ansible_bios_version": "VirtualBox",
|
||||
"ansible_cmdline": {
|
||||
"BOOT_IMAGE": "/boot/vmlinuz64",
|
||||
"base": true,
|
||||
"console": "tty0",
|
||||
"initrd": "/boot/initrd.img",
|
||||
"loglevel": "3",
|
||||
"noembed": true,
|
||||
"nomodeset": true,
|
||||
"norestore": true,
|
||||
"user": "docker",
|
||||
"waitusb": "10:LABEL=boot2docker-data"
|
||||
},
|
||||
"ansible_date_time": {
|
||||
"date": "2016-02-02",
|
||||
"day": "02",
|
||||
"epoch": "1454424257",
|
||||
"hour": "14",
|
||||
"iso8601": "2016-02-02T14:44:17Z",
|
||||
"iso8601_basic": "20160202T144417348424",
|
||||
"iso8601_basic_short": "20160202T144417",
|
||||
"iso8601_micro": "2016-02-02T14:44:17.348496Z",
|
||||
"minute": "44",
|
||||
"month": "02",
|
||||
"second": "17",
|
||||
"time": "14:44:17",
|
||||
"tz": "UTC",
|
||||
"tz_offset": "+0000",
|
||||
"weekday": "Tuesday",
|
||||
"weekday_number": "2",
|
||||
"weeknumber": "05",
|
||||
"year": "2016"
|
||||
},
|
||||
"ansible_default_ipv4": {
|
||||
"address": "172.17.0.7",
|
||||
"alias": "eth0",
|
||||
"broadcast": "global",
|
||||
"gateway": "172.17.0.1",
|
||||
"interface": "eth0",
|
||||
"macaddress": "02:42:ac:11:00:07",
|
||||
"mtu": 1500,
|
||||
"netmask": "255.255.0.0",
|
||||
"network": "172.17.0.0",
|
||||
"type": "ether"
|
||||
},
|
||||
"ansible_default_ipv6": {},
|
||||
"ansible_devices": {
|
||||
"sda": {
|
||||
"holders": [],
|
||||
"host": "",
|
||||
"model": "VBOX HARDDISK",
|
||||
"partitions": {
|
||||
"sda1": {
|
||||
"sectors": "510015555",
|
||||
"sectorsize": 512,
|
||||
"size": "243.19 GB",
|
||||
"start": "1975995"
|
||||
},
|
||||
"sda2": {
|
||||
"sectors": "1975932",
|
||||
"sectorsize": 512,
|
||||
"size": "964.81 MB",
|
||||
"start": "63"
|
||||
}
|
||||
},
|
||||
"removable": "0",
|
||||
"rotational": "0",
|
||||
"scheduler_mode": "deadline",
|
||||
"sectors": "512000000",
|
||||
"sectorsize": "512",
|
||||
"size": "244.14 GB",
|
||||
"support_discard": "0",
|
||||
"vendor": "ATA"
|
||||
},
|
||||
"sr0": {
|
||||
"holders": [],
|
||||
"host": "",
|
||||
"model": "CD-ROM",
|
||||
"partitions": {},
|
||||
"removable": "1",
|
||||
"rotational": "1",
|
||||
"scheduler_mode": "deadline",
|
||||
"sectors": "61440",
|
||||
"sectorsize": "2048",
|
||||
"size": "120.00 MB",
|
||||
"support_discard": "0",
|
||||
"vendor": "VBOX"
|
||||
}
|
||||
},
|
||||
"ansible_distribution": "Ubuntu",
|
||||
"ansible_distribution_major_version": "14",
|
||||
"ansible_distribution_release": "trusty",
|
||||
"ansible_distribution_version": "14.04",
|
||||
"ansible_dns": {
|
||||
"nameservers": [
|
||||
"8.8.8.8"
|
||||
]
|
||||
},
|
||||
"ansible_domain": "",
|
||||
"ansible_env": {
|
||||
"HOME": "/root",
|
||||
"HOSTNAME": "ede894599989",
|
||||
"LANG": "en_US.UTF-8",
|
||||
"LC_ALL": "en_US.UTF-8",
|
||||
"LC_MESSAGES": "en_US.UTF-8",
|
||||
"LESSCLOSE": "/usr/bin/lesspipe %s %s",
|
||||
"LESSOPEN": "| /usr/bin/lesspipe %s",
|
||||
"LS_COLORS": "",
|
||||
"OLDPWD": "/ansible",
|
||||
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"PWD": "/ansible/examples",
|
||||
"SHLVL": "1",
|
||||
"_": "/usr/local/bin/ansible",
|
||||
"container": "docker"
|
||||
},
|
||||
"ansible_eth0": {
|
||||
"active": true,
|
||||
"device": "eth0",
|
||||
"ipv4": {
|
||||
"address": "172.17.0.7",
|
||||
"broadcast": "global",
|
||||
"netmask": "255.255.0.0",
|
||||
"network": "172.17.0.0"
|
||||
},
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "fe80::42:acff:fe11:7",
|
||||
"prefix": "64",
|
||||
"scope": "link"
|
||||
}
|
||||
],
|
||||
"macaddress": "02:42:ac:11:00:07",
|
||||
"mtu": 1500,
|
||||
"promisc": false,
|
||||
"type": "ether"
|
||||
},
|
||||
"ansible_fips": false,
|
||||
"ansible_form_factor": "Other",
|
||||
"ansible_fqdn": "ede894599989",
|
||||
"ansible_hostname": "ede894599989",
|
||||
"ansible_interfaces": [
|
||||
"lo",
|
||||
"eth0"
|
||||
],
|
||||
"ansible_kernel": "4.1.12-boot2docker",
|
||||
"ansible_lo": {
|
||||
"active": true,
|
||||
"device": "lo",
|
||||
"ipv4": {
|
||||
"address": "127.0.0.1",
|
||||
"broadcast": "host",
|
||||
"netmask": "255.0.0.0",
|
||||
"network": "127.0.0.0"
|
||||
},
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "::1",
|
||||
"prefix": "128",
|
||||
"scope": "host"
|
||||
}
|
||||
],
|
||||
"mtu": 65536,
|
||||
"promisc": false,
|
||||
"type": "loopback"
|
||||
},
|
||||
"ansible_lsb": {
|
||||
"codename": "trusty",
|
||||
"description": "Ubuntu 14.04.3 LTS",
|
||||
"id": "Ubuntu",
|
||||
"major_release": "14",
|
||||
"release": "14.04"
|
||||
},
|
||||
"ansible_machine": "x86_64",
|
||||
"ansible_memfree_mb": 3746,
|
||||
"ansible_memory_mb": {
|
||||
"nocache": {
|
||||
"free": 8896,
|
||||
"used": 3638
|
||||
},
|
||||
"real": {
|
||||
"free": 3746,
|
||||
"total": 12534,
|
||||
"used": 8788
|
||||
},
|
||||
"swap": {
|
||||
"cached": 0,
|
||||
"free": 4048,
|
||||
"total": 4048,
|
||||
"used": 0
|
||||
}
|
||||
},
|
||||
"ansible_memtotal_mb": 12534,
|
||||
"ansible_mounts": [
|
||||
{
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"mount": "/etc/resolv.conf",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 201281392640,
|
||||
"size_total": 256895700992,
|
||||
"uuid": "NA"
|
||||
},
|
||||
{
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"mount": "/etc/hostname",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 201281392640,
|
||||
"size_total": 256895700992,
|
||||
"uuid": "NA"
|
||||
},
|
||||
{
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"mount": "/etc/hosts",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 201281392640,
|
||||
"size_total": 256895700992,
|
||||
"uuid": "NA"
|
||||
}
|
||||
],
|
||||
"ansible_nodename": "ede894599989",
|
||||
"ansible_os_family": "Debian",
|
||||
"ansible_pkg_mgr": "apt",
|
||||
"ansible_processor": [
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
|
||||
"GenuineIntel",
|
||||
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz"
|
||||
],
|
||||
"ansible_processor_cores": 8,
|
||||
"ansible_processor_count": 1,
|
||||
"ansible_processor_threads_per_core": 1,
|
||||
"ansible_processor_vcpus": 8,
|
||||
"ansible_product_name": "VirtualBox",
|
||||
"ansible_product_serial": "0",
|
||||
"ansible_product_uuid": "25C5EA5A-1DF1-48D9-A2C6-81227DA153C0",
|
||||
"ansible_product_version": "1.2",
|
||||
"ansible_python_version": "2.7.6",
|
||||
"ansible_selinux": false,
|
||||
"ansible_service_mgr": "upstart",
|
||||
"ansible_ssh_host_key_dsa_public": "AAAAB3NzaC1kc3MAAACBALF0xsM8UMXgSKiWNw4t19wxbxLnxQX742t/dIM0O8YLx+/lIP+Q69Dv5uoVt0zKV39eFziRlCh96qj2KYkGEJ6XfVZFnhpculL2Pv2CPpSwKuQ1vTbDO/xxUrvY+bHpfNJf9Rh69bFEE2pTsjomFPCgp8M0qGaFtwg6czSaeBONAAAAFQCGEfVtj97JiexTVRqgQITYlFp/eQAAAIEAg+S9qWn+AIb3amwVoLL/usQYOPCmZY9RVPzpkjJ6OG+HI4B7cXeauPtNTJwT0f9vGEqzf4mPpmS+aCShj6iwdmJ+cOwR5+SJlNalab3CMBoXKVLbT1J2XWFlK0szKKnoReP96IDbkAkGQ3fkm4jz0z6Wy0u6wOQVNcd4G5cwLZ4AAACAFvBm+H1LwNrwWBjWio+ayhglZ4Y25mLMEn2+dqBz0gLK5szEbft1HMPOWIVHvl6vi3v34pAJHKpxXpkLlNliTn8iw9BzCOrgP4V8sp2/85mxEuCdI1w/QERj9cHu5iS2pZ0cUwDE3pfuuGBB3IEliaJyaapowdrM8lN12jQl11E=",
|
||||
"ansible_ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHiYp4e9RfXpxDcEWpK4EuXPHW9++xcFI9hiB0TYAZgxEF9RIgwfucpPawFk7HIFoNc7EXQMlryilLSbg155KWM=",
|
||||
"ansible_ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAILclD2JaC654azEsAfcHRIOA2Ig9/Qk6MX80i/VCEdSH",
|
||||
"ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDeSUGxZaZsgBsezld0mj3HcbAwx6aykGnejceBjcs6lVwSGMHevofzSXIQDPYBhZoyWNl0PYAHv6AsQ8+3khd2SitUMJAuHSz1ZjgHCCGQP9ijXTKHn+lWCKA8rhLG/dwYwiouoOPZfn1G+erbKO6XiVbELrrf2RadnMGuMinESIOKVj3IunXsaGRMsDOQferOnUf7MvH7xpQnoySyQ1+p4rGruaohWG+Y2cDo7+B2FylPVbrpRDDJkfbt4J96WHx0KOdD0qzOicQP8JqDflqQPJJCWcgrvjQOSe4gXdPB6GZDtBl2qgQRwt1IgizPMm+b7Bwbd2VDe1TeWV2gT/7H",
|
||||
"ansible_swapfree_mb": 4048,
|
||||
"ansible_swaptotal_mb": 4048,
|
||||
"ansible_system": "Linux",
|
||||
"ansible_system_vendor": "innotek GmbH",
|
||||
"ansible_uptime_seconds": 178398,
|
||||
"ansible_user_dir": "/root",
|
||||
"ansible_user_gecos": "root",
|
||||
"ansible_user_gid": 0,
|
||||
"ansible_user_id": "root",
|
||||
"ansible_user_shell": "/bin/bash",
|
||||
"ansible_user_uid": 0,
|
||||
"ansible_userspace_architecture": "x86_64",
|
||||
"ansible_userspace_bits": "64",
|
||||
"ansible_virtualization_role": "guest",
|
||||
"ansible_virtualization_type": "docker",
|
||||
"module_setup": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,697 +0,0 @@
|
||||
[
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "iprdump"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "iprinit"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "iprupdate"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "stopped",
|
||||
"name": "netconsole"
|
||||
},
|
||||
{
|
||||
"source": "sysv",
|
||||
"state": "running",
|
||||
"name": "network"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "arp-ethers.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "auditd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "autovt@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "avahi-daemon.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "blk-availability.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "brandbot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "console-getty.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "console-shell.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "cpupower.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "crond.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.fedoraproject.FirewallD1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.freedesktop.Avahi.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.hostname1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.locale1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.login1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.machine1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.freedesktop.NetworkManager.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dbus-org.freedesktop.nm-dispatcher.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus-org.freedesktop.timedate1.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dbus.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "debug-shell.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "dhcpd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dhcpd6.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dhcrelay.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dm-event.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dnsmasq.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-cmdline.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-initqueue.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-mount.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-mount.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-pivot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-trigger.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-pre-udev.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "dracut-shutdown.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "ebtables.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "emergency.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "firewalld.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "getty@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "halt-local.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-cleanup.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-parse-etc.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-switch-root.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "initrd-udevadm-cleanup-db.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "irqbalance.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "kdump.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "kmod-static-nodes.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "lvm2-lvmetad.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "lvm2-monitor.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "lvm2-pvscan@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "messagebus.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "microcode.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "named-setup-rndc.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "named.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "NetworkManager-dispatcher.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "NetworkManager-wait-online.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "NetworkManager.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "ntpd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "ntpdate.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "openvpn@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-halt.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-kexec.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-poweroff.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-quit-wait.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-quit.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-read-write.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-reboot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-start.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "plymouth-switch-root.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "polkit.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "postfix.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "quotaon.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rc-local.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rdisc.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rescue.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-autorelabel-mark.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-autorelabel.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-configure.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-dmesg.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-domainname.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-import-state.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-loadmodules.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "rhel-readonly.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "rsyslog.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "serial-getty@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "sshd-keygen.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "sshd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "sshd@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-ask-password-console.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-ask-password-plymouth.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-ask-password-wall.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-backlight@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-binfmt.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-fsck-root.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-fsck@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-halt.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-hibernate.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-hostnamed.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-hybrid-sleep.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-initctl.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-journal-flush.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-journald.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-kexec.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-localed.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-logind.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-machined.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-modules-load.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-nspawn@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-poweroff.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-quotacheck.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-random-seed.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "systemd-readahead-collect.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-readahead-done.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "systemd-readahead-drop.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "systemd-readahead-replay.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-reboot.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-remount-fs.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-shutdownd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-suspend.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-sysctl.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-timedated.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-tmpfiles-clean.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-tmpfiles-setup-dev.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-tmpfiles-setup.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-udev-settle.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-udev-trigger.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-udevd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-update-utmp-runlevel.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-update-utmp.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-user-sessions.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "systemd-vconsole-setup.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "teamd@.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "tuned.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "running",
|
||||
"name": "vmtoolsd.service"
|
||||
},
|
||||
{
|
||||
"source": "systemd",
|
||||
"state": "stopped",
|
||||
"name": "wpa_supplicant.service"
|
||||
}
|
||||
]
|
||||
@@ -249,7 +249,7 @@ def test_openstack_client_config_generation_with_project_domain_name(mocker, sou
|
||||
@pytest.mark.parametrize("source,expected", [
|
||||
(None, True), (False, False), (True, True)
|
||||
])
|
||||
def test_openstack_client_config_generation_with_project_region_name(mocker, source, expected, private_data_dir):
|
||||
def test_openstack_client_config_generation_with_region(mocker, source, expected, private_data_dir):
|
||||
update = tasks.RunInventoryUpdate()
|
||||
credential_type = CredentialType.defaults['openstack']()
|
||||
inputs = {
|
||||
@@ -259,7 +259,7 @@ def test_openstack_client_config_generation_with_project_region_name(mocker, sou
|
||||
'project': 'demo-project',
|
||||
'domain': 'my-demo-domain',
|
||||
'project_domain_name': 'project-domain',
|
||||
'project_region_name': 'region-name',
|
||||
'region': 'region-name',
|
||||
}
|
||||
if source is not None:
|
||||
inputs['verify_ssl'] = source
|
||||
@@ -527,7 +527,7 @@ class TestGenericRun():
|
||||
task.instance = Job(pk=1, id=1)
|
||||
task.event_ct = 17
|
||||
task.finished_callback(None)
|
||||
task.dispatcher.dispatch.assert_called_with({'event': 'EOF', 'final_counter': 17, 'job_id': 1})
|
||||
task.dispatcher.dispatch.assert_called_with({'event': 'EOF', 'final_counter': 17, 'job_id': 1, 'guid': None})
|
||||
|
||||
def test_save_job_metadata(self, job, update_model_wrapper):
|
||||
class MockMe():
|
||||
|
||||
@@ -15,6 +15,10 @@ from django.apps import apps
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from django_guid.log_filters import CorrelationId
|
||||
from django_guid.middleware import GuidMiddleware
|
||||
|
||||
from awx import MODE
|
||||
from awx.main.constants import LOGGER_BLOCKLIST
|
||||
from awx.main.utils.common import get_search_fields
|
||||
|
||||
@@ -364,3 +368,14 @@ class SmartFilter(object):
|
||||
return res[0].result
|
||||
|
||||
raise RuntimeError("Parsing the filter_string %s went terribly wrong" % filter_string)
|
||||
|
||||
|
||||
|
||||
class DefaultCorrelationId(CorrelationId):
|
||||
|
||||
def filter(self, record):
|
||||
guid = GuidMiddleware.get_guid() or '-'
|
||||
if MODE == 'development':
|
||||
guid = guid[:8]
|
||||
record.guid = guid
|
||||
return True
|
||||
|
||||
@@ -154,6 +154,9 @@ class LogstashFormatter(LogstashFormatterBase):
|
||||
|
||||
if kind == 'job_events' and raw_data.get('python_objects', {}).get('job_event'):
|
||||
job_event = raw_data['python_objects']['job_event']
|
||||
guid = job_event.event_data.pop('guid', None)
|
||||
if guid:
|
||||
data_for_log['guid'] = guid
|
||||
for field_object in job_event._meta.fields:
|
||||
|
||||
if not field_object.__class__ or not field_object.__class__.__name__:
|
||||
|
||||
@@ -285,6 +285,7 @@ INSTALLED_APPS = [
|
||||
'polymorphic',
|
||||
'taggit',
|
||||
'social_django',
|
||||
'django_guid',
|
||||
'corsheaders',
|
||||
'awx.conf',
|
||||
'awx.main',
|
||||
@@ -828,11 +829,14 @@ LOGGING = {
|
||||
},
|
||||
'dynamic_level_filter': {
|
||||
'()': 'awx.main.utils.filters.DynamicLevelFilter'
|
||||
}
|
||||
},
|
||||
'guid': {
|
||||
'()': 'awx.main.utils.filters.DefaultCorrelationId'
|
||||
},
|
||||
},
|
||||
'formatters': {
|
||||
'simple': {
|
||||
'format': '%(asctime)s %(levelname)-8s %(name)s %(message)s',
|
||||
'format': '%(asctime)s %(levelname)-8s [%(guid)s] %(name)s %(message)s',
|
||||
},
|
||||
'json': {
|
||||
'()': 'awx.main.utils.formatters.LogstashFormatter'
|
||||
@@ -842,7 +846,7 @@ LOGGING = {
|
||||
'format': '%(relativeSeconds)9.3f %(levelname)-8s %(message)s'
|
||||
},
|
||||
'dispatcher': {
|
||||
'format': '%(asctime)s %(levelname)-8s %(name)s PID:%(process)d %(message)s',
|
||||
'format': '%(asctime)s %(levelname)-8s [%(guid)s] %(name)s PID:%(process)d %(message)s',
|
||||
},
|
||||
'job_lifecycle': {
|
||||
'()': 'awx.main.utils.formatters.JobLifeCycleFormatter',
|
||||
@@ -852,7 +856,7 @@ LOGGING = {
|
||||
'console': {
|
||||
'()': 'logging.StreamHandler',
|
||||
'level': 'DEBUG',
|
||||
'filters': ['require_debug_true_or_test'],
|
||||
'filters': ['require_debug_true_or_test', 'guid'],
|
||||
'formatter': 'simple',
|
||||
},
|
||||
'null': {
|
||||
@@ -872,33 +876,33 @@ LOGGING = {
|
||||
'class': 'awx.main.utils.handlers.RSysLogHandler',
|
||||
'formatter': 'json',
|
||||
'address': '/var/run/awx-rsyslog/rsyslog.sock',
|
||||
'filters': ['external_log_enabled', 'dynamic_level_filter'],
|
||||
'filters': ['external_log_enabled', 'dynamic_level_filter', 'guid'],
|
||||
},
|
||||
'tower_warnings': {
|
||||
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter'],
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
|
||||
'filename': os.path.join(LOG_ROOT, 'tower.log'),
|
||||
'formatter':'simple',
|
||||
},
|
||||
'callback_receiver': {
|
||||
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter'],
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
|
||||
'filename': os.path.join(LOG_ROOT, 'callback_receiver.log'),
|
||||
'formatter':'simple',
|
||||
},
|
||||
'dispatcher': {
|
||||
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter'],
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
|
||||
'filename': os.path.join(LOG_ROOT, 'dispatcher.log'),
|
||||
'formatter':'dispatcher',
|
||||
},
|
||||
'wsbroadcast': {
|
||||
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter'],
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
|
||||
'filename': os.path.join(LOG_ROOT, 'wsbroadcast.log'),
|
||||
'formatter':'simple',
|
||||
},
|
||||
@@ -914,7 +918,7 @@ LOGGING = {
|
||||
'task_system': {
|
||||
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter'],
|
||||
'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
|
||||
'filename': os.path.join(LOG_ROOT, 'task_system.log'),
|
||||
'formatter':'simple',
|
||||
},
|
||||
@@ -1094,6 +1098,7 @@ AWX_CALLBACK_PROFILE = False
|
||||
AWX_CLEANUP_PATHS = True
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django_guid.middleware.GuidMiddleware',
|
||||
'awx.main.middleware.TimingMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'awx.main.middleware.MigrationRanCheckMiddleware',
|
||||
@@ -1134,3 +1139,7 @@ BROADCAST_WEBSOCKET_NEW_INSTANCE_POLL_RATE_SECONDS = 10
|
||||
|
||||
# How often websocket process will generate stats
|
||||
BROADCAST_WEBSOCKET_STATS_POLL_RATE_SECONDS = 5
|
||||
|
||||
DJANGO_GUID = {
|
||||
'GUID_HEADER_NAME': 'X-API-Request-Id',
|
||||
}
|
||||
|
||||
@@ -58,12 +58,12 @@ class MetadataView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
from social_django.utils import load_backend, load_strategy
|
||||
complete_url = reverse('social:complete', args=('saml', ))
|
||||
saml_backend = load_backend(
|
||||
load_strategy(request),
|
||||
'saml',
|
||||
redirect_uri=complete_url,
|
||||
)
|
||||
try:
|
||||
saml_backend = load_backend(
|
||||
load_strategy(request),
|
||||
'saml',
|
||||
redirect_uri=complete_url,
|
||||
)
|
||||
metadata, errors = saml_backend.generate_metadata_xml()
|
||||
except Exception as e:
|
||||
logger.exception('unable to generate SAML metadata')
|
||||
|
||||
@@ -18,7 +18,7 @@ div.response-info span.meta {
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img class="logo" src="{% static 'assets/logo-header.svg' %}">
|
||||
<img class="logo" src="{% static 'media/logo-header.svg' %}">
|
||||
</a>
|
||||
<a class="navbar-title" href="{{ request.get_full_path }}">
|
||||
<span> — {{name}}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<link href="{% static 'assets/favicon.ico' %}?v={{tower_version}}" rel="shortcut icon" />
|
||||
<link href="{% static 'favicon.ico' %}?v={{tower_version}}" rel="shortcut icon" />
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url 'api:api_root_view' %}">
|
||||
<img class="logo" src="{% static 'assets/logo-header.svg' %}">
|
||||
<img class="logo" src="{% static 'media/logo-header.svg' %}">
|
||||
<span>{% trans 'REST API' %}</span>
|
||||
</a>
|
||||
<a class="navbar-title" href="{{ request.get_full_path }}">
|
||||
|
||||
@@ -53,6 +53,9 @@ npm --prefix awx/ui_next test -- src/screens/Login/Login.test.jsx
|
||||
|
||||
# start the test watcher and run tests on files that you've changed
|
||||
npm --prefix awx/ui_next run test-watch
|
||||
|
||||
# start the tests and get the coverage report after the tests have completed
|
||||
npm --prefix awx/ui_next run test -- --coverage
|
||||
```
|
||||
#### Note:
|
||||
- Once the test watcher is up and running you can hit `a` to run all the tests.
|
||||
|
||||
40
awx/ui_next/package-lock.json
generated
40
awx/ui_next/package-lock.json
generated
@@ -2121,22 +2121,39 @@
|
||||
"dev": true
|
||||
},
|
||||
"@patternfly/patternfly": {
|
||||
"version": "4.70.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.70.2.tgz",
|
||||
"integrity": "sha512-XKCHnOjx1JThY3s98AJhsApSsGHPvEdlY7r+b18OecqUnmThVGw3nslzYYrwfCGlJ/xQtV5so29SduH2/uhHzA=="
|
||||
"version": "4.80.3",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.80.3.tgz",
|
||||
"integrity": "sha512-YLUk4L6iCBXql92YP6zHg0FdlnEkd5/3V+uz/A3UoBuuDdEoyDpx4M/Tf56R7IXmYiRaHE1mToJHPDYypIlnmw=="
|
||||
},
|
||||
"@patternfly/react-core": {
|
||||
"version": "4.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.84.3.tgz",
|
||||
"integrity": "sha512-VeCv/r09ay6yIER7Eb8Dp5ZhbDu6SCW9smwgUTNp80kt83wIfKvGvQOKg+/7cev/GC6VzfgPHhiS04Jm/N5loA==",
|
||||
"version": "4.90.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.90.2.tgz",
|
||||
"integrity": "sha512-Ld4HCXYrVZR+ycWKvygmOEDyyMpb/pfZFklJiT/9ktSSSU9SfKwH2XmXotgjwBy1lYUoRYJ58xTpV19duouEoA==",
|
||||
"requires": {
|
||||
"@patternfly/react-icons": "^4.7.22",
|
||||
"@patternfly/react-styles": "^4.7.22",
|
||||
"@patternfly/react-tokens": "^4.9.22",
|
||||
"@patternfly/react-icons": "^4.8.4",
|
||||
"@patternfly/react-styles": "^4.7.29",
|
||||
"@patternfly/react-tokens": "^4.9.26",
|
||||
"focus-trap": "6.2.2",
|
||||
"react-dropzone": "9.0.0",
|
||||
"tippy.js": "5.1.2",
|
||||
"tslib": "1.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@patternfly/react-icons": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.8.4.tgz",
|
||||
"integrity": "sha512-nGrZ6F57drim7t5FlFz7YusHVs8xngiayB20oSsJeewppOZ3vJDVhHaiJjsIRMB9PcVOY99QT2wnyiY1eYPVyA=="
|
||||
},
|
||||
"@patternfly/react-styles": {
|
||||
"version": "4.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.7.29.tgz",
|
||||
"integrity": "sha512-eAO9xh2+IQHIBCihmwNuDVCeAWhGXIhbUNJEwZzvevYuH4Pnl0X8YaWoYmM2ZfL8ZdagRTLvjGW+hoZGkUyCBQ=="
|
||||
},
|
||||
"@patternfly/react-tokens": {
|
||||
"version": "4.9.26",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.9.26.tgz",
|
||||
"integrity": "sha512-SSbY6BaUb7ycXhvdBNk2yzOlteRUHnchHSYPBFvhUk+SqpsjQ7j13/ZTok6pcHXrcj3FaL9f82/ep4eTXPzmWg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@patternfly/react-icons": {
|
||||
@@ -2163,6 +2180,11 @@
|
||||
"tslib": "1.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@patternfly/patternfly": {
|
||||
"version": "4.70.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.70.2.tgz",
|
||||
"integrity": "sha512-XKCHnOjx1JThY3s98AJhsApSsGHPvEdlY7r+b18OecqUnmThVGw3nslzYYrwfCGlJ/xQtV5so29SduH2/uhHzA=="
|
||||
},
|
||||
"@patternfly/react-core": {
|
||||
"version": "4.84.4",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.84.4.tgz",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/react": "^2.9.1",
|
||||
"@patternfly/patternfly": "4.70.2",
|
||||
"@patternfly/react-core": "4.84.3",
|
||||
"@patternfly/patternfly": "^4.80.3",
|
||||
"@patternfly/react-core": "^4.90.2",
|
||||
"@patternfly/react-icons": "4.7.22",
|
||||
"@patternfly/react-table": "^4.19.15",
|
||||
"ansi-to-html": "^0.6.11",
|
||||
@@ -58,7 +58,7 @@
|
||||
"start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start",
|
||||
"start-instrumented": "DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start",
|
||||
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
|
||||
"test": "TZ='UTC' react-scripts test --coverage --watchAll=false",
|
||||
"test": "TZ='UTC' react-scripts test --watchAll=false",
|
||||
"test-watch": "TZ='UTC' react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext .js --ext .jsx .",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Tooltip } from '@patternfly/react-core';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { CopyIcon } from '@patternfly/react-icons';
|
||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||
import AlertModal from '../AlertModal';
|
||||
@@ -15,7 +15,7 @@ function CopyButton({
|
||||
isDisabled,
|
||||
onCopyStart,
|
||||
onCopyFinish,
|
||||
helperText,
|
||||
errorMessage,
|
||||
i18n,
|
||||
}) {
|
||||
const { isLoading, error: copyError, request: copyItemToAPI } = useRequest(
|
||||
@@ -33,17 +33,15 @@ function CopyButton({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip content={helperText.tooltip} position="top">
|
||||
<Button
|
||||
id={id}
|
||||
isDisabled={isLoading || isDisabled}
|
||||
aria-label={i18n._(t`Copy`)}
|
||||
variant="plain"
|
||||
onClick={copyItemToAPI}
|
||||
>
|
||||
<CopyIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
id={id}
|
||||
isDisabled={isLoading || isDisabled}
|
||||
aria-label={i18n._(t`Copy`)}
|
||||
variant="plain"
|
||||
onClick={copyItemToAPI}
|
||||
>
|
||||
<CopyIcon />
|
||||
</Button>
|
||||
<AlertModal
|
||||
aria-label={i18n._(t`Copy Error`)}
|
||||
isOpen={error}
|
||||
@@ -51,7 +49,7 @@ function CopyButton({
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={dismissError}
|
||||
>
|
||||
{helperText.errorMessage}
|
||||
{errorMessage}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
</>
|
||||
@@ -62,10 +60,7 @@ CopyButton.propTypes = {
|
||||
copyItem: PropTypes.func.isRequired,
|
||||
onCopyStart: PropTypes.func.isRequired,
|
||||
onCopyFinish: PropTypes.func.isRequired,
|
||||
helperText: PropTypes.shape({
|
||||
tooltip: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import CopyButton from './CopyButton';
|
||||
|
||||
jest.mock('../../api');
|
||||
|
||||
let wrapper;
|
||||
|
||||
describe('<CopyButton/>', () => {
|
||||
test('shold mount properly', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<CopyButton
|
||||
onCopyStart={() => {}}
|
||||
onCopyFinish={() => {}}
|
||||
copyItem={() => {}}
|
||||
helperText={{
|
||||
tooltip: `Copy Template`,
|
||||
errorMessage: `Failed to copy template.`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('should mount properly', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CopyButton
|
||||
onCopyStart={() => {}}
|
||||
onCopyFinish={() => {}}
|
||||
copyItem={() => {}}
|
||||
errorMessage="Failed to copy template."
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('CopyButton').length).toBe(1);
|
||||
});
|
||||
test('should render proper tooltip', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<CopyButton
|
||||
onCopyStart={() => {}}
|
||||
onCopyFinish={() => {}}
|
||||
copyItem={() => {}}
|
||||
helperText={{
|
||||
tooltip: `Copy Template`,
|
||||
errorMessage: `Failed to copy template.`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('Tooltip').prop('content')).toBe('Copy Template');
|
||||
test('should call the correct function on button click', async () => {
|
||||
const copyItem = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CopyButton
|
||||
onCopyStart={() => {}}
|
||||
onCopyFinish={() => {}}
|
||||
copyItem={copyItem}
|
||||
errorMessage="Failed to copy template."
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('button').simulate('click');
|
||||
});
|
||||
expect(copyItem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||
import { RocketIcon } from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import { ActionsTd, ActionItem } from '../PaginatedTable';
|
||||
import LaunchButton from '../LaunchButton';
|
||||
import { LaunchButton, ReLaunchDropDown } from '../LaunchButton';
|
||||
import StatusLabel from '../StatusLabel';
|
||||
import { DetailList, Detail, LaunchedByDetail } from '../DetailList';
|
||||
import ChipGroup from '../ChipGroup';
|
||||
@@ -83,19 +83,31 @@ function JobListItem({
|
||||
job.type !== 'system_job' &&
|
||||
job.summary_fields?.user_capabilities?.start
|
||||
}
|
||||
tooltip={i18n._(t`Relaunch Job`)}
|
||||
tooltip={
|
||||
job.status === 'failed' && job.type === 'job'
|
||||
? i18n._(t`Relaunch using host parameters`)
|
||||
: i18n._(t`Relaunch Job`)
|
||||
}
|
||||
>
|
||||
<LaunchButton resource={job}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={handleRelaunch}
|
||||
aria-label={i18n._(t`Relaunch`)}
|
||||
>
|
||||
<RocketIcon />
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
{job.status === 'failed' && job.type === 'job' ? (
|
||||
<LaunchButton resource={job}>
|
||||
{({ handleRelaunch }) => (
|
||||
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
|
||||
)}
|
||||
</LaunchButton>
|
||||
) : (
|
||||
<LaunchButton resource={job}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={handleRelaunch}
|
||||
aria-label={i18n._(t`Relaunch`)}
|
||||
>
|
||||
<RocketIcon />
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
)}
|
||||
</ActionItem>
|
||||
</ActionsTd>
|
||||
</Tr>
|
||||
|
||||
@@ -53,6 +53,10 @@ describe('<JobListItem />', () => {
|
||||
expect(wrapper.find('LaunchButton').length).toBe(1);
|
||||
});
|
||||
|
||||
test('launch button shown to users with launch capabilities', () => {
|
||||
expect(wrapper.find('LaunchButton').length).toBe(1);
|
||||
});
|
||||
|
||||
test('launch button hidden from users without launch capabilities', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<table>
|
||||
@@ -92,3 +96,83 @@ describe('<JobListItem />', () => {
|
||||
expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<JobListItem with failed job />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/jobs'],
|
||||
});
|
||||
wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<JobListItem
|
||||
job={{ ...mockJob, status: 'failed' }}
|
||||
isSelected
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
</tbody>
|
||||
</table>,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('launch button shown to users with launch capabilities', () => {
|
||||
expect(wrapper.find('LaunchButton').length).toBe(1);
|
||||
});
|
||||
|
||||
test('dropdown should be displayed in case of failed job', () => {
|
||||
expect(wrapper.find('LaunchButton').length).toBe(1);
|
||||
const dropdown = wrapper.find('Dropdown');
|
||||
expect(dropdown).toHaveLength(1);
|
||||
expect(dropdown.find('DropdownItem')).toHaveLength(0);
|
||||
dropdown.find('button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('dropdown should not be rendered for job type different of playbook run', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<JobListItem
|
||||
job={{
|
||||
...mockJob,
|
||||
status: 'failed',
|
||||
type: 'project_update',
|
||||
}}
|
||||
onSelect={() => {}}
|
||||
isSelected
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
expect(wrapper.find('LaunchButton').length).toBe(1);
|
||||
expect(wrapper.find('Dropdown')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('launch button hidden from users without launch capabilities', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<JobListItem
|
||||
job={{
|
||||
...mockJob,
|
||||
status: 'failed',
|
||||
summary_fields: { user_capabilities: { start: false } },
|
||||
}}
|
||||
detailUrl={`/jobs/playbook/${mockJob.id}`}
|
||||
onSelect={() => {}}
|
||||
isSelected={false}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
expect(wrapper.find('LaunchButton').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ function LaunchButton({ resource, i18n, children, history }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRelaunch = async () => {
|
||||
const handleRelaunch = async params => {
|
||||
let readRelaunch;
|
||||
let relaunch;
|
||||
|
||||
@@ -125,7 +125,7 @@ function LaunchButton({ resource, i18n, children, history }) {
|
||||
} else if (resource.type === 'ad_hoc_command') {
|
||||
relaunch = AdHocCommandsAPI.relaunch(resource.id);
|
||||
} else if (resource.type === 'job') {
|
||||
relaunch = JobsAPI.relaunch(resource.id);
|
||||
relaunch = JobsAPI.relaunch(resource.id, params || {});
|
||||
}
|
||||
const { data: job } = await relaunch;
|
||||
history.push(`/jobs/${job.id}/output`);
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('LaunchButton', () => {
|
||||
await act(() => button.prop('onClick')());
|
||||
expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1);
|
||||
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1, {});
|
||||
expect(history.location.pathname).toEqual('/jobs/9000/output');
|
||||
});
|
||||
|
||||
|
||||
96
awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
Normal file
96
awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
DropdownItem,
|
||||
DropdownPosition,
|
||||
DropdownSeparator,
|
||||
DropdownDirection,
|
||||
} from '@patternfly/react-core';
|
||||
import { RocketIcon } from '@patternfly/react-icons';
|
||||
|
||||
function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n }) {
|
||||
const [isOpen, setIsOPen] = useState(false);
|
||||
|
||||
const onToggle = () => {
|
||||
setIsOPen(prev => !prev);
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
<DropdownItem
|
||||
aria-label={i18n._(t`Relaunch on`)}
|
||||
key="relaunch_on"
|
||||
component="div"
|
||||
isPlainText
|
||||
>
|
||||
{i18n._(t`Relaunch on`)}
|
||||
</DropdownItem>,
|
||||
<DropdownSeparator key="separator" />,
|
||||
<DropdownItem
|
||||
key="relaunch_all"
|
||||
aria-label={i18n._(t`Relaunch all hosts`)}
|
||||
component="button"
|
||||
onClick={() => {
|
||||
handleRelaunch({ hosts: 'all' });
|
||||
}}
|
||||
>
|
||||
{i18n._(t`All`)}
|
||||
</DropdownItem>,
|
||||
|
||||
<DropdownItem
|
||||
key="relaunch_failed"
|
||||
aria-label={i18n._(t`Relaunch failed hosts`)}
|
||||
component="button"
|
||||
onClick={() => {
|
||||
handleRelaunch({ hosts: 'failed' });
|
||||
}}
|
||||
>
|
||||
{i18n._(t`Failed hosts`)}
|
||||
</DropdownItem>,
|
||||
];
|
||||
|
||||
if (isPrimary) {
|
||||
return (
|
||||
<Dropdown
|
||||
position={DropdownPosition.left}
|
||||
direction={DropdownDirection.up}
|
||||
isOpen={isOpen}
|
||||
dropdownItems={dropdownItems}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
toggleIndicator={null}
|
||||
onToggle={onToggle}
|
||||
aria-label={i18n._(`relaunch jobs`)}
|
||||
id="relaunch_jobs"
|
||||
isPrimary
|
||||
>
|
||||
{i18n._(t`Relaunch`)}
|
||||
</DropdownToggle>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isPlain
|
||||
position={DropdownPosition.right}
|
||||
isOpen={isOpen}
|
||||
dropdownItems={dropdownItems}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
toggleIndicator={null}
|
||||
onToggle={onToggle}
|
||||
aria-label={i18n._(`relaunch jobs`)}
|
||||
id="relaunch_jobs"
|
||||
>
|
||||
<RocketIcon />
|
||||
</DropdownToggle>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(ReLaunchDropDown);
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import ReLaunchDropDown from './ReLaunchDropDown';
|
||||
|
||||
describe('ReLaunchDropDown', () => {
|
||||
const handleRelaunch = jest.fn();
|
||||
|
||||
test('expected content is rendered on initialization', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('Dropdown')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('dropdown have expected items and callbacks', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
|
||||
);
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(0);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(3);
|
||||
|
||||
wrapper
|
||||
.find('DropdownItem[aria-label="Relaunch failed hosts"]')
|
||||
.simulate('click');
|
||||
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'failed' });
|
||||
|
||||
wrapper
|
||||
.find('DropdownItem[aria-label="Relaunch all hosts"]')
|
||||
.simulate('click');
|
||||
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'all' });
|
||||
});
|
||||
|
||||
test('dropdown isPrimary have expected items and callbacks', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ReLaunchDropDown isPrimary handleRelaunch={handleRelaunch} />
|
||||
);
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(0);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('DropdownItem')).toHaveLength(3);
|
||||
|
||||
wrapper
|
||||
.find('DropdownItem[aria-label="Relaunch failed hosts"]')
|
||||
.simulate('click');
|
||||
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'failed' });
|
||||
|
||||
wrapper
|
||||
.find('DropdownItem[aria-label="Relaunch all hosts"]')
|
||||
.simulate('click');
|
||||
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'all' });
|
||||
});
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export { default } from './LaunchButton';
|
||||
export { default as LaunchButton } from './LaunchButton';
|
||||
export { default as ReLaunchDropDown } from './ReLaunchDropDown';
|
||||
|
||||
@@ -311,7 +311,7 @@ describe('<MultiCredentialsLookup />', () => {
|
||||
});
|
||||
wrapper.update();
|
||||
const typeSelect = wrapper.find('AnsibleSelect');
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
typeSelect.invoke('onChange')({}, 500);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
@@ -29,6 +29,7 @@ function OrganizationLookup({
|
||||
value,
|
||||
history,
|
||||
autoPopulate,
|
||||
isDisabled,
|
||||
}) {
|
||||
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
||||
|
||||
@@ -80,6 +81,7 @@ function OrganizationLookup({
|
||||
label={i18n._(t`Organization`)}
|
||||
>
|
||||
<Lookup
|
||||
isDisabled={isDisabled}
|
||||
id="organization"
|
||||
header={i18n._(t`Organization`)}
|
||||
value={value}
|
||||
@@ -139,6 +141,7 @@ OrganizationLookup.propTypes = {
|
||||
required: bool,
|
||||
value: Organization,
|
||||
autoPopulate: bool,
|
||||
isDisabled: bool,
|
||||
};
|
||||
|
||||
OrganizationLookup.defaultProps = {
|
||||
@@ -148,6 +151,7 @@ OrganizationLookup.defaultProps = {
|
||||
required: false,
|
||||
value: null,
|
||||
autoPopulate: false,
|
||||
isDisabled: false,
|
||||
};
|
||||
|
||||
export { OrganizationLookup as _OrganizationLookup };
|
||||
|
||||
@@ -9,7 +9,7 @@ function LookupErrorMessage({ error, i18n }) {
|
||||
|
||||
return (
|
||||
<div className="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||
{error.message || i18n._(t`An error occured`)}
|
||||
{error.message || i18n._(t`An error occurred`)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import { Button } from '@patternfly/react-core';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
|
||||
const SmallButton = styled(Button)`
|
||||
padding: 3px 8px;
|
||||
font-size: var(--pf-global--FontSize--xs);
|
||||
&& {
|
||||
padding: 3px 8px;
|
||||
font-size: var(--pf-global--FontSize--xs);
|
||||
}
|
||||
`;
|
||||
|
||||
function MultiButtonToggle({ buttons, value, onChange }) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function ActionItem({ column, tooltip, visible, children }) {
|
||||
`}
|
||||
>
|
||||
<Tooltip content={tooltip} position="top">
|
||||
{children}
|
||||
<div>{children}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('<ActionItem />', () => {
|
||||
|
||||
const tooltip = wrapper.find('Tooltip');
|
||||
expect(tooltip.prop('content')).toEqual('a tooltip');
|
||||
expect(tooltip.prop('children')).toEqual('foo');
|
||||
expect(tooltip.prop('children')).toEqual(<div>foo</div>);
|
||||
});
|
||||
|
||||
test('should render null if not visible', async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { TeamsAPI, UsersAPI } from '../../api';
|
||||
import { RolesAPI, TeamsAPI, UsersAPI } from '../../api';
|
||||
import AddResourceRole from '../AddRole/AddResourceRole';
|
||||
import AlertModal from '../AlertModal';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
@@ -26,7 +26,13 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
result: { accessRecords, itemCount, relatedSearchableKeys, searchableKeys },
|
||||
result: {
|
||||
accessRecords,
|
||||
itemCount,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
organizationRoles,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchAccessRecords,
|
||||
@@ -37,6 +43,41 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
||||
apiModel.readAccessList(resource.id, params),
|
||||
apiModel.readAccessOptions(resource.id),
|
||||
]);
|
||||
|
||||
// Eventually this could be expanded to other access lists.
|
||||
// We will need to combine the role ids of all the different level
|
||||
// of resource level roles.
|
||||
|
||||
let orgRoles;
|
||||
if (location.pathname.includes('/organizations')) {
|
||||
const {
|
||||
data: { results: roles },
|
||||
} = await RolesAPI.read({ content_type__isnull: true });
|
||||
const sysAdmin = roles.filter(
|
||||
role => role.name === 'System Administrator'
|
||||
);
|
||||
const sysAud = roles.filter(role => {
|
||||
let auditor;
|
||||
if (role.name === 'System Auditor') {
|
||||
auditor = role.id;
|
||||
}
|
||||
return auditor;
|
||||
});
|
||||
|
||||
orgRoles = Object.values(resource.summary_fields.object_roles).map(
|
||||
opt => {
|
||||
let item;
|
||||
if (opt.name === 'Admin') {
|
||||
item = [`${opt.id}, ${sysAdmin[0].id}`, opt.name];
|
||||
} else if (sysAud[0].id && opt.name === 'Auditor') {
|
||||
item = [`${sysAud[0].id}, ${opt.id}`, opt.name];
|
||||
} else {
|
||||
item = [`${opt.id}`, opt.name];
|
||||
}
|
||||
return item;
|
||||
}
|
||||
);
|
||||
}
|
||||
return {
|
||||
accessRecords: response.data.results,
|
||||
itemCount: response.data.count,
|
||||
@@ -46,8 +87,9 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
||||
searchableKeys: Object.keys(
|
||||
actionsResponse.data.actions?.GET || {}
|
||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||
organizationRoles: orgRoles,
|
||||
};
|
||||
}, [apiModel, location, resource.id]),
|
||||
}, [apiModel, location, resource]),
|
||||
{
|
||||
accessRecords: [],
|
||||
itemCount: 0,
|
||||
@@ -78,6 +120,29 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
||||
fetchItems: fetchAccessRecords,
|
||||
}
|
||||
);
|
||||
const toolbarSearchColumns = [
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First Name`),
|
||||
key: 'first_name__icontains',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last Name`),
|
||||
key: 'last_name__icontains',
|
||||
},
|
||||
];
|
||||
|
||||
if (organizationRoles?.length > 0) {
|
||||
toolbarSearchColumns.push({
|
||||
name: i18n._(t`Roles`),
|
||||
key: `or__roles__in`,
|
||||
options: organizationRoles,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -88,21 +153,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={i18n._(t`Roles`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First Name`),
|
||||
key: 'first_name__icontains',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last Name`),
|
||||
key: 'last_name__icontains',
|
||||
},
|
||||
]}
|
||||
toolbarSearchColumns={toolbarSearchColumns}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../api';
|
||||
import { OrganizationsAPI, TeamsAPI, UsersAPI, RolesAPI } from '../../api';
|
||||
|
||||
import ResourceAccessList from './ResourceAccessList';
|
||||
|
||||
@@ -17,7 +18,24 @@ describe('<ResourceAccessList />', () => {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
summary_fields: {
|
||||
object_roles: {},
|
||||
object_roles: {
|
||||
admin_role: {
|
||||
description: 'Can manage all aspects of the organization',
|
||||
name: 'Admin',
|
||||
id: 2,
|
||||
user_only: true,
|
||||
},
|
||||
execute_role: {
|
||||
description: 'May run any executable resources in the organization',
|
||||
name: 'Execute',
|
||||
id: 3,
|
||||
},
|
||||
project_admin_role: {
|
||||
description: 'Can manage all projects of the organization',
|
||||
name: 'Project Admin',
|
||||
id: 4,
|
||||
},
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
@@ -87,12 +105,24 @@ describe('<ResourceAccessList />', () => {
|
||||
});
|
||||
TeamsAPI.disassociateRole.mockResolvedValue({});
|
||||
UsersAPI.disassociateRole.mockResolvedValue({});
|
||||
RolesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'System Administrator' },
|
||||
{ id: 14, name: 'System Auditor' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/organizations/1/access'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList
|
||||
resource={organization}
|
||||
apiModel={OrganizationsAPI}
|
||||
/>
|
||||
/>,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@@ -168,4 +198,24 @@ describe('<ResourceAccessList />', () => {
|
||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
||||
done();
|
||||
});
|
||||
test('should call api to get org details', async () => {
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
|
||||
expect(
|
||||
wrapper.find('PaginatedDataList').prop('toolbarSearchColumns')
|
||||
).toStrictEqual([
|
||||
{ isDefault: true, key: 'username__icontains', name: 'Username' },
|
||||
{ key: 'first_name__icontains', name: 'First Name' },
|
||||
{ key: 'last_name__icontains', name: 'Last Name' },
|
||||
{
|
||||
key: 'or__roles__in',
|
||||
name: 'Roles',
|
||||
options: [
|
||||
['2, 1', 'Admin'],
|
||||
['3', 'Execute'],
|
||||
['4', 'Project Admin'],
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +56,8 @@ function ResourceAccessListItem({ accessRecord, onRoleDelete, i18n }) {
|
||||
onRoleDelete(role, accessRecord);
|
||||
}}
|
||||
isReadOnly={!role.user_capabilities.unattach}
|
||||
ouiaId={`${role.name}-${role.id}`}
|
||||
closeBtnAriaLabel={i18n._(t`Remove ${role.name} chip`)}
|
||||
>
|
||||
{role.name}
|
||||
</Chip>
|
||||
|
||||
@@ -290,6 +290,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
}
|
||||
}
|
||||
paused={false}
|
||||
preventScrollOnDeactivate={false}
|
||||
>
|
||||
<div
|
||||
className="pf-l-bullseye"
|
||||
|
||||
@@ -98,11 +98,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
>
|
||||
<Chip
|
||||
className=""
|
||||
closeBtnAriaLabel="close"
|
||||
closeBtnAriaLabel="Remove Member chip"
|
||||
component="div"
|
||||
isOverflowChip={false}
|
||||
isReadOnly={false}
|
||||
onClick={[Function]}
|
||||
ouiaId="Member-3"
|
||||
tooltipPosition="top"
|
||||
>
|
||||
Member
|
||||
@@ -164,11 +165,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
>
|
||||
<Chip
|
||||
className=""
|
||||
closeBtnAriaLabel="close"
|
||||
closeBtnAriaLabel="Remove Member chip"
|
||||
component="div"
|
||||
isOverflowChip={false}
|
||||
isReadOnly={false}
|
||||
onClick={[Function]}
|
||||
ouiaId="Member-3"
|
||||
tooltipPosition="top"
|
||||
>
|
||||
Member
|
||||
@@ -253,11 +255,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
>
|
||||
<Chip
|
||||
className=""
|
||||
closeBtnAriaLabel="close"
|
||||
closeBtnAriaLabel="Remove Member chip"
|
||||
component="div"
|
||||
isOverflowChip={false}
|
||||
isReadOnly={false}
|
||||
onClick={[Function]}
|
||||
ouiaId="Member-3"
|
||||
tooltipPosition="top"
|
||||
>
|
||||
Member
|
||||
@@ -688,11 +691,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
>
|
||||
<Chip
|
||||
className=""
|
||||
closeBtnAriaLabel="close"
|
||||
closeBtnAriaLabel="Remove Member chip"
|
||||
component="div"
|
||||
isOverflowChip={false}
|
||||
isReadOnly={false}
|
||||
onClick={[Function]}
|
||||
ouiaId="Member-3"
|
||||
tooltipPosition="top"
|
||||
>
|
||||
Member
|
||||
@@ -840,6 +844,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
isClosable={false}
|
||||
numChips={5}
|
||||
onClick={[Function]}
|
||||
onOverflowChipClick={[Function]}
|
||||
tooltipPosition="top"
|
||||
>
|
||||
<GenerateId
|
||||
@@ -864,12 +869,13 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
>
|
||||
<Chip
|
||||
className=""
|
||||
closeBtnAriaLabel="close"
|
||||
closeBtnAriaLabel="Remove Member chip"
|
||||
component="div"
|
||||
isOverflowChip={false}
|
||||
isReadOnly={false}
|
||||
key=".$3"
|
||||
onClick={[Function]}
|
||||
ouiaId="Member-3"
|
||||
tooltipPosition="top"
|
||||
>
|
||||
<GenerateId
|
||||
@@ -877,7 +883,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
>
|
||||
<div
|
||||
className="pf-c-chip"
|
||||
data-ouia-component-id="OUIA-Generated-Chip-1"
|
||||
data-ouia-component-id="Member-3"
|
||||
data-ouia-component-type="PF4/Chip"
|
||||
data-ouia-safe={true}
|
||||
>
|
||||
@@ -888,19 +894,19 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
Member
|
||||
</span>
|
||||
<Button
|
||||
aria-label="close"
|
||||
aria-label="Remove Member chip"
|
||||
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
|
||||
id="remove_pf-random-id-1"
|
||||
onClick={[Function]}
|
||||
ouiaId="close"
|
||||
ouiaId="Member-3"
|
||||
variant="plain"
|
||||
>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-label="close"
|
||||
aria-label="Remove Member chip"
|
||||
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
|
||||
className="pf-c-button pf-m-plain"
|
||||
data-ouia-component-id="close"
|
||||
data-ouia-component-id="Member-3"
|
||||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe={true}
|
||||
disabled={false}
|
||||
|
||||
@@ -140,10 +140,12 @@ describe('<Sort />', () => {
|
||||
act(() => wrapper.find('Dropdown').invoke('onToggle')(true));
|
||||
wrapper.update();
|
||||
await waitForElement(wrapper, 'Dropdown', el => el.prop('isOpen') === true);
|
||||
wrapper
|
||||
.find('li')
|
||||
.at(0)
|
||||
.prop('onClick')({ target: { innerText: 'Bar' } });
|
||||
act(() =>
|
||||
wrapper
|
||||
.find('li')
|
||||
.at(0)
|
||||
.prop('onClick')({ target: { innerText: 'Bar' } })
|
||||
);
|
||||
wrapper.update();
|
||||
expect(onSort).toBeCalledWith('bar', 'ascending');
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import CredentialChip from '../CredentialChip';
|
||||
import { timeOfDay, formatDateString } from '../../util/dates';
|
||||
|
||||
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api';
|
||||
import LaunchButton from '../LaunchButton';
|
||||
import { LaunchButton } from '../LaunchButton';
|
||||
import Sparkline from '../Sparkline';
|
||||
import { toTitleCase } from '../../util/strings';
|
||||
import CopyButton from '../CopyButton';
|
||||
@@ -177,13 +177,13 @@ function TemplateListItem({
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</ActionItem>
|
||||
<ActionItem visible={template.summary_fields.user_capabilities.copy}>
|
||||
<ActionItem
|
||||
tooltip={i18n._(t`Copy Template`)}
|
||||
visible={template.summary_fields.user_capabilities.copy}
|
||||
>
|
||||
<CopyButton
|
||||
id={`template-action-copy-${template.id}`}
|
||||
helperText={{
|
||||
errorMessage: i18n._(t`Failed to copy template.`),
|
||||
tooltip: i18n._(t`Copy Template`),
|
||||
}}
|
||||
errorMessage={i18n._(t`Failed to copy template.`)}
|
||||
isDisabled={isDisabled}
|
||||
onCopyStart={handleCopyStart}
|
||||
onCopyFinish={handleCopyFinish}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { object } from 'prop-types';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
@@ -6,15 +6,20 @@ import {
|
||||
CredentialsAPI,
|
||||
CredentialInputSourcesAPI,
|
||||
CredentialTypesAPI,
|
||||
OrganizationsAPI,
|
||||
UsersAPI,
|
||||
} from '../../../api';
|
||||
import ContentError from '../../../components/ContentError';
|
||||
import ContentLoading from '../../../components/ContentLoading';
|
||||
import CredentialForm from '../shared/CredentialForm';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { useConfig } from '../../../contexts/Config';
|
||||
|
||||
function CredentialEdit({ credential, me }) {
|
||||
function CredentialEdit({ credential }) {
|
||||
const history = useHistory();
|
||||
const { id: credId } = useParams();
|
||||
const { me = {} } = useConfig();
|
||||
const [isOrgLookupDisabled, setIsOrgLookupDisabled] = useState(false);
|
||||
|
||||
const { error: submitError, request: submitRequest, result } = useRequest(
|
||||
useCallback(
|
||||
@@ -81,8 +86,11 @@ function CredentialEdit({ credential, me }) {
|
||||
// can send only one of org, user, team
|
||||
if (organization?.id) {
|
||||
modifiedData.organization = organization.id;
|
||||
} else if (me?.id) {
|
||||
modifiedData.user = me.id;
|
||||
} else {
|
||||
modifiedData.organization = null;
|
||||
if (me?.id) {
|
||||
modifiedData.user = me.id;
|
||||
}
|
||||
}
|
||||
const [{ data }] = await Promise.all([
|
||||
CredentialsAPI.update(credId, modifiedData),
|
||||
@@ -114,10 +122,22 @@ function CredentialEdit({ credential, me }) {
|
||||
{
|
||||
data: { results },
|
||||
},
|
||||
{
|
||||
data: { count: adminOrgCount },
|
||||
},
|
||||
{
|
||||
data: { count: credentialAdminCount },
|
||||
},
|
||||
] = await Promise.all([
|
||||
CredentialTypesAPI.read({ page_size: 200 }),
|
||||
CredentialsAPI.readInputSources(credId, { page_size: 200 }),
|
||||
CredentialsAPI.readInputSources(credId),
|
||||
UsersAPI.readAdminOfOrganizations(me.id),
|
||||
OrganizationsAPI.read({
|
||||
page_size: 1,
|
||||
role_level: 'credential_admin_role',
|
||||
}),
|
||||
]);
|
||||
setIsOrgLookupDisabled(!(adminOrgCount || credentialAdminCount));
|
||||
const credTypes = data.results;
|
||||
if (data.next && data.next.includes('page=2')) {
|
||||
const {
|
||||
@@ -137,7 +157,7 @@ function CredentialEdit({ credential, me }) {
|
||||
return inputSourcesMap;
|
||||
}, {});
|
||||
return { credentialTypes: creds, loadedInputSources: inputSources };
|
||||
}, [credId]),
|
||||
}, [credId, me.id]),
|
||||
{ credentialTypes: {}, loadedInputSources: {} }
|
||||
);
|
||||
|
||||
@@ -171,6 +191,7 @@ function CredentialEdit({ credential, me }) {
|
||||
credentialTypes={credentialTypes}
|
||||
inputSources={loadedInputSources}
|
||||
submitError={submitError}
|
||||
isOrgLookupDisabled={isOrgLookupDisabled}
|
||||
/>
|
||||
</CardBody>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
CredentialsAPI,
|
||||
CredentialInputSourcesAPI,
|
||||
CredentialTypesAPI,
|
||||
OrganizationsAPI,
|
||||
UsersAPI,
|
||||
} from '../../../api';
|
||||
import CredentialEdit from './CredentialEdit';
|
||||
|
||||
@@ -114,6 +116,25 @@ const mockCredential = {
|
||||
kubernetes: false,
|
||||
};
|
||||
|
||||
UsersAPI.readAdminOfOrganizations.mockResolvedValue({
|
||||
data: {
|
||||
count: 1,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'org',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [{ id: 1 }],
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
@@ -197,6 +218,58 @@ CredentialTypesAPI.read.mockResolvedValue({
|
||||
},
|
||||
injectors: {},
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
type: 'credential_type',
|
||||
url: '/api/v2/credential_types/9/',
|
||||
related: {
|
||||
credentials: '/api/v2/credential_types/9/credentials/',
|
||||
activity_stream: '/api/v2/credential_types/9/activity_stream/',
|
||||
},
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
},
|
||||
created: '2021-02-12T19:13:22.352791Z',
|
||||
modified: '2021-02-12T19:14:15.578773Z',
|
||||
name: 'Google Compute Engine',
|
||||
description: '',
|
||||
kind: 'cloud',
|
||||
namespace: 'gce',
|
||||
managed_by_tower: true,
|
||||
inputs: {
|
||||
fields: [
|
||||
{
|
||||
id: 'username',
|
||||
label: 'Service Account Email Address',
|
||||
type: 'string',
|
||||
help_text:
|
||||
'The email address assigned to the Google Compute Engine service account.',
|
||||
},
|
||||
{
|
||||
id: 'project',
|
||||
label: 'Project',
|
||||
type: 'string',
|
||||
help_text:
|
||||
'The Project ID is the GCE assigned identification. It is often constructed as three words or two words followed by a three-digit number. Examples: project-id-000 and another-project-id',
|
||||
},
|
||||
{
|
||||
id: 'ssh_key_data',
|
||||
label: 'RSA Private Key',
|
||||
type: 'string',
|
||||
format: 'ssh_private_key',
|
||||
secret: true,
|
||||
multiline: true,
|
||||
help_text:
|
||||
'Paste the contents of the PEM file associated with the service account email.',
|
||||
},
|
||||
],
|
||||
required: ['username', 'ssh_key_data'],
|
||||
},
|
||||
injectors: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -258,7 +331,12 @@ describe('<CredentialEdit />', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialEdit credential={mockCredential} />,
|
||||
{
|
||||
context: { router: { history } },
|
||||
context: {
|
||||
router: { history },
|
||||
me: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -325,6 +403,7 @@ describe('<CredentialEdit />', () => {
|
||||
expect(CredentialsAPI.update).toHaveBeenCalledWith(3, {
|
||||
user: 1,
|
||||
name: 'foo',
|
||||
organization: null,
|
||||
description: 'bar',
|
||||
credential_type: '1',
|
||||
inputs: {
|
||||
@@ -354,7 +433,40 @@ describe('<CredentialEdit />', () => {
|
||||
expect(CredentialInputSourcesAPI.destroy).toHaveBeenCalledWith(34);
|
||||
expect(history.location.pathname).toBe('/credentials/3/details');
|
||||
});
|
||||
test('inputs are properly rendered', async () => {
|
||||
history = createMemoryHistory({ initialEntries: ['/credentials'] });
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialEdit
|
||||
credential={{
|
||||
...mockCredential,
|
||||
inputs: {
|
||||
project: 'foo',
|
||||
username: 'foo@ansible.com',
|
||||
ssh_key_data: '$encrypted$',
|
||||
},
|
||||
kind: 'gce',
|
||||
credential_type: 9,
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
context: { router: { history } },
|
||||
}
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#credential-username').prop('value')).toBe(
|
||||
'foo@ansible.com'
|
||||
);
|
||||
expect(wrapper.find('input#credential-project').prop('value')).toBe(
|
||||
'foo'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('textarea#credential-ssh_key_data').prop('value')
|
||||
).toBe('$encrypted$');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initial GET request fails', () => {
|
||||
test('shows error when initial GET request fails', async () => {
|
||||
CredentialTypesAPI.read.mockRejectedValue(new Error());
|
||||
|
||||
@@ -72,16 +72,16 @@ function CredentialListItem({
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</ActionItem>
|
||||
<ActionItem visible={credential.summary_fields.user_capabilities.copy}>
|
||||
<ActionItem
|
||||
tooltip={i18n._(t`Copy Credential`)}
|
||||
visible={credential.summary_fields.user_capabilities.copy}
|
||||
>
|
||||
<CopyButton
|
||||
isDisabled={isDisabled}
|
||||
onCopyStart={handleCopyStart}
|
||||
onCopyFinish={handleCopyFinish}
|
||||
copyItem={copyCredential}
|
||||
helperText={{
|
||||
tooltip: i18n._(t`Copy Credential`),
|
||||
errorMessage: i18n._(t`Failed to copy credential.`),
|
||||
}}
|
||||
errorMessage={i18n._(t`Failed to copy credential.`)}
|
||||
/>
|
||||
</ActionItem>
|
||||
</ActionsTd>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { Formik, useField, useFormikContext } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { arrayOf, func, object, shape } from 'prop-types';
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
@@ -136,6 +136,7 @@ function CredentialFormFields({ i18n, credentialTypes }) {
|
||||
touched={orgMeta.touched}
|
||||
error={orgMeta.error}
|
||||
required={isGalaxyCredential}
|
||||
isDisabled={initialValues.isOrgLookupDisabled}
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="credential-Type"
|
||||
@@ -189,6 +190,7 @@ function CredentialForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitError,
|
||||
isOrgLookupDisabled,
|
||||
...rest
|
||||
}) {
|
||||
const [showExternalTestModal, setShowExternalTestModal] = useState(false);
|
||||
@@ -197,8 +199,9 @@ function CredentialForm({
|
||||
description: credential.description || '',
|
||||
organization: credential?.summary_fields?.organization || null,
|
||||
credential_type: credential?.credential_type || '',
|
||||
inputs: {},
|
||||
inputs: credential?.inputs || {},
|
||||
passwordPrompts: {},
|
||||
isOrgLookupDisabled: isOrgLookupDisabled || false,
|
||||
};
|
||||
|
||||
Object.values(credentialTypes).forEach(credentialType => {
|
||||
@@ -311,18 +314,18 @@ function CredentialForm({
|
||||
);
|
||||
}
|
||||
|
||||
CredentialForm.proptype = {
|
||||
CredentialForm.propTypes = {
|
||||
handleSubmit: func.isRequired,
|
||||
handleCancel: func.isRequired,
|
||||
credentialTypes: shape({}).isRequired,
|
||||
credential: shape({}),
|
||||
inputSources: arrayOf(object),
|
||||
inputSources: shape({}),
|
||||
submitError: shape({}),
|
||||
};
|
||||
|
||||
CredentialForm.defaultProps = {
|
||||
credential: {},
|
||||
inputSources: [],
|
||||
inputSources: {},
|
||||
submitError: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -327,6 +327,27 @@ describe('<CredentialForm />', () => {
|
||||
machineFieldExpects();
|
||||
});
|
||||
|
||||
test('organization lookup should be disabled', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialForm
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
credential={machineCredential}
|
||||
credentialTypes={credentialTypes}
|
||||
isOrgLookupDisabled
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('CredentialFormFields')
|
||||
.find('OrganizationLookup')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should display form fields for source control credential properly', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
|
||||
@@ -168,7 +168,7 @@ function ExternalTestModal({
|
||||
);
|
||||
}
|
||||
|
||||
ExternalTestModal.proptype = {
|
||||
ExternalTestModal.propType = {
|
||||
credential: shape({}),
|
||||
credentialType: shape({}).isRequired,
|
||||
credentialFormValues: shape({}).isRequired,
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"help_text": "OpenStack domains define administrative boundaries. It is only needed for Keystone v3 authentication URLs. Refer to Ansible Tower documentation for common scenarios."
|
||||
},
|
||||
{
|
||||
"id": "project_region_name",
|
||||
"id": "region",
|
||||
"label": "Region Name",
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -118,7 +118,7 @@ function InventoryEdit({ inventory }) {
|
||||
);
|
||||
}
|
||||
|
||||
InventoryEdit.proptype = {
|
||||
InventoryEdit.propType = {
|
||||
inventory: object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ function InventoryListItem({
|
||||
isSelected: bool.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
};
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [isCopying, setIsCopying] = useState(false);
|
||||
|
||||
const copyInventory = useCallback(async () => {
|
||||
await InventoriesAPI.copy(inventory.id, {
|
||||
@@ -38,11 +38,11 @@ function InventoryListItem({
|
||||
}, [inventory.id, inventory.name, fetchInventories]);
|
||||
|
||||
const handleCopyStart = useCallback(() => {
|
||||
setIsDisabled(true);
|
||||
setIsCopying(true);
|
||||
}, []);
|
||||
|
||||
const handleCopyFinish = useCallback(() => {
|
||||
setIsDisabled(false);
|
||||
setIsCopying(false);
|
||||
}, []);
|
||||
|
||||
const labelId = `check-action-${inventory.id}`;
|
||||
@@ -115,7 +115,7 @@ function InventoryListItem({
|
||||
tooltip={i18n._(t`Edit Inventory`)}
|
||||
>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
isDisabled={isCopying}
|
||||
aria-label={i18n._(t`Edit Inventory`)}
|
||||
variant="plain"
|
||||
component={Link}
|
||||
@@ -128,17 +128,18 @@ function InventoryListItem({
|
||||
</ActionItem>
|
||||
<ActionItem
|
||||
visible={inventory.summary_fields.user_capabilities.copy}
|
||||
tooltip={i18n._(t`Copy Inventory`)}
|
||||
tooltip={
|
||||
inventory.has_inventory_sources
|
||||
? i18n._(t`Inventories with sources cannot be copied`)
|
||||
: i18n._(t`Copy Inventory`)
|
||||
}
|
||||
>
|
||||
<CopyButton
|
||||
copyItem={copyInventory}
|
||||
isDisabled={isDisabled}
|
||||
isDisabled={isCopying || inventory.has_inventory_sources}
|
||||
onCopyStart={handleCopyStart}
|
||||
onCopyFinish={handleCopyFinish}
|
||||
helperText={{
|
||||
tooltip: i18n._(t`Copy Inventory`),
|
||||
errorMessage: i18n._(t`Failed to copy inventory.`),
|
||||
}}
|
||||
errorMessage={i18n._(t`Failed to copy inventory.`)}
|
||||
/>
|
||||
</ActionItem>
|
||||
</ActionsTd>
|
||||
|
||||
@@ -138,7 +138,7 @@ function InventoryForm({
|
||||
);
|
||||
}
|
||||
|
||||
InventoryForm.proptype = {
|
||||
InventoryForm.propType = {
|
||||
handleSubmit: func.isRequired,
|
||||
handleCancel: func.isRequired,
|
||||
instanceGroups: shape(),
|
||||
|
||||
@@ -19,7 +19,10 @@ import CredentialChip from '../../../components/CredentialChip';
|
||||
import { VariablesInput as _VariablesInput } from '../../../components/CodeMirrorInput';
|
||||
import DeleteButton from '../../../components/DeleteButton';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import {
|
||||
LaunchButton,
|
||||
ReLaunchDropDown,
|
||||
} from '../../../components/LaunchButton';
|
||||
import StatusIcon from '../../../components/StatusIcon';
|
||||
import { toTitleCase } from '../../../util/strings';
|
||||
import { formatDateString } from '../../../util/dates';
|
||||
@@ -346,7 +349,14 @@ function JobDetail({ job, i18n }) {
|
||||
)}
|
||||
<CardActionsRow>
|
||||
{job.type !== 'system_job' &&
|
||||
job.summary_fields.user_capabilities.start && (
|
||||
job.summary_fields.user_capabilities.start &&
|
||||
(job.status === 'failed' && job.type === 'job' ? (
|
||||
<LaunchButton resource={job}>
|
||||
{({ handleRelaunch }) => (
|
||||
<ReLaunchDropDown isPrimary handleRelaunch={handleRelaunch} />
|
||||
)}
|
||||
</LaunchButton>
|
||||
) : (
|
||||
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button type="submit" onClick={handleRelaunch}>
|
||||
@@ -354,7 +364,7 @@ function JobDetail({ job, i18n }) {
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
)}
|
||||
))}
|
||||
{job.summary_fields.user_capabilities.delete && (
|
||||
<DeleteButton
|
||||
name={job.name}
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
} from '@patternfly/react-icons';
|
||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||
import DeleteButton from '../../../../components/DeleteButton';
|
||||
import LaunchButton from '../../../../components/LaunchButton';
|
||||
import {
|
||||
LaunchButton,
|
||||
ReLaunchDropDown,
|
||||
} from '../../../../components/LaunchButton';
|
||||
|
||||
const BadgeGroup = styled.div`
|
||||
margin-left: 20px;
|
||||
@@ -125,18 +128,32 @@ const OutputToolbar = ({ i18n, job, jobStatus, onDelete, onCancel }) => {
|
||||
|
||||
{job.type !== 'system_job' &&
|
||||
job.summary_fields.user_capabilities?.start && (
|
||||
<Tooltip content={i18n._(t`Relaunch Job`)}>
|
||||
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={handleRelaunch}
|
||||
aria-label={i18n._(t`Relaunch`)}
|
||||
>
|
||||
<RocketIcon />
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
<Tooltip
|
||||
content={
|
||||
job.status === 'failed' && job.type === 'job'
|
||||
? i18n._(t`Relaunch using host parameters`)
|
||||
: i18n._(t`Relaunch Job`)
|
||||
}
|
||||
>
|
||||
{job.status === 'failed' && job.type === 'job' ? (
|
||||
<LaunchButton resource={job}>
|
||||
{({ handleRelaunch }) => (
|
||||
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
|
||||
)}
|
||||
</LaunchButton>
|
||||
) : (
|
||||
<LaunchButton resource={job}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={handleRelaunch}
|
||||
aria-label={i18n._(t`Relaunch`)}
|
||||
>
|
||||
<RocketIcon />
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
@@ -159,16 +159,15 @@ function NotificationTemplateListItem({
|
||||
<div />
|
||||
)}
|
||||
{template.summary_fields.user_capabilities.copy && (
|
||||
<CopyButton
|
||||
copyItem={copyTemplate}
|
||||
isCopyDisabled={isCopyDisabled}
|
||||
onCopyStart={handleCopyStart}
|
||||
onCopyFinish={handleCopyFinish}
|
||||
helperText={{
|
||||
tooltip: i18n._(t`Copy Notification Template`),
|
||||
errorMessage: i18n._(t`Failed to copy template.`),
|
||||
}}
|
||||
/>
|
||||
<Tooltip content={i18n._(t`Copy Notification Template`)}>
|
||||
<CopyButton
|
||||
copyItem={copyTemplate}
|
||||
isCopyDisabled={isCopyDisabled}
|
||||
onCopyStart={handleCopyStart}
|
||||
onCopyFinish={handleCopyFinish}
|
||||
errorMessage={i18n._(t`Failed to copy template.`)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import styled from 'styled-components';
|
||||
import DataListCell from '../../../components/DataListCell';
|
||||
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import Sparkline from '../../../components/Sparkline';
|
||||
import { toTitleCase } from '../../../util/strings';
|
||||
|
||||
|
||||
@@ -143,16 +143,16 @@ function ProjectListItem({
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</ActionItem>
|
||||
<ActionItem visible={project.summary_fields.user_capabilities.copy}>
|
||||
<ActionItem
|
||||
tooltip={i18n._(t`Copy Project`)}
|
||||
visible={project.summary_fields.user_capabilities.copy}
|
||||
>
|
||||
<CopyButton
|
||||
copyItem={copyProject}
|
||||
isDisabled={isDisabled}
|
||||
onCopyStart={handleCopyStart}
|
||||
onCopyFinish={handleCopyFinish}
|
||||
helperText={{
|
||||
tooltip: i18n._(t`Copy Project`),
|
||||
errorMessage: i18n._(t`Failed to copy project.`),
|
||||
}}
|
||||
errorMessage={i18n._(t`Failed to copy project.`)}
|
||||
/>
|
||||
</ActionItem>
|
||||
</ActionsTd>
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from '../../../components/DetailList';
|
||||
import DeleteButton from '../../../components/DeleteButton';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
||||
import { JobTemplatesAPI } from '../../../api';
|
||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
UserDateDetail,
|
||||
} from '../../../components/DetailList';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import Sparkline from '../../../components/Sparkline';
|
||||
import { toTitleCase } from '../../../util/strings';
|
||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
WrenchIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import {
|
||||
WorkflowDispatchContext,
|
||||
WorkflowStateContext,
|
||||
|
||||
@@ -118,7 +118,7 @@ def main():
|
||||
limit=dict(),
|
||||
credential=dict(required=True),
|
||||
module_name=dict(required=True),
|
||||
module_args=dict(default=""),
|
||||
module_args=dict(),
|
||||
forks=dict(type='int'),
|
||||
verbosity=dict(type='int', choices=['0', '1', '2', '3', '4', '5']),
|
||||
extra_vars=dict(type='dict'),
|
||||
|
||||
@@ -76,10 +76,10 @@ from ..module_utils.tower_api import TowerAPIModule
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
description=dict(default=''),
|
||||
description=dict(),
|
||||
input_field_name=dict(required=True),
|
||||
target_credential=dict(required=True),
|
||||
source_credential=dict(default=''),
|
||||
source_credential=dict(),
|
||||
metadata=dict(type="dict"),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
@@ -340,24 +340,24 @@ def main():
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
new_name=dict(),
|
||||
description=dict(default=''),
|
||||
description=dict(),
|
||||
organization=dict(),
|
||||
job_type=dict(choices=['run', 'check']),
|
||||
inventory=dict(),
|
||||
project=dict(),
|
||||
playbook=dict(),
|
||||
credential=dict(default=''),
|
||||
vault_credential=dict(default=''),
|
||||
credential=dict(),
|
||||
vault_credential=dict(),
|
||||
custom_virtualenv=dict(),
|
||||
credentials=dict(type='list', elements='str'),
|
||||
forks=dict(type='int'),
|
||||
limit=dict(default=''),
|
||||
limit=dict(),
|
||||
verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0),
|
||||
extra_vars=dict(type='dict'),
|
||||
job_tags=dict(default=''),
|
||||
job_tags=dict(),
|
||||
force_handlers=dict(type='bool', default=False, aliases=['force_handlers_enabled']),
|
||||
skip_tags=dict(default=''),
|
||||
start_at_task=dict(default=''),
|
||||
skip_tags=dict(),
|
||||
start_at_task=dict(),
|
||||
timeout=dict(type='int', default=0),
|
||||
use_fact_cache=dict(type='bool', aliases=['fact_caching_enabled']),
|
||||
host_config_key=dict(),
|
||||
@@ -399,11 +399,11 @@ def main():
|
||||
credential = module.params.get('credential')
|
||||
vault_credential = module.params.get('vault_credential')
|
||||
credentials = module.params.get('credentials')
|
||||
if vault_credential != '':
|
||||
if vault_credential:
|
||||
if credentials is None:
|
||||
credentials = []
|
||||
credentials.append(vault_credential)
|
||||
if credential != '':
|
||||
if credential:
|
||||
if credentials is None:
|
||||
credentials = []
|
||||
credentials.append(credential)
|
||||
|
||||
@@ -229,8 +229,8 @@ def main():
|
||||
scm_type=dict(choices=['manual', 'git', 'svn', 'insights'], default='manual'),
|
||||
scm_url=dict(),
|
||||
local_path=dict(),
|
||||
scm_branch=dict(default=''),
|
||||
scm_refspec=dict(default=''),
|
||||
scm_branch=dict(),
|
||||
scm_refspec=dict(),
|
||||
credential=dict(aliases=['scm_credential']),
|
||||
scm_clean=dict(type='bool', default=False),
|
||||
scm_delete_on_update=dict(type='bool', default=False),
|
||||
|
||||
@@ -133,7 +133,7 @@ def main():
|
||||
existing_settings = module.get_endpoint('settings/all')['json']
|
||||
|
||||
# Begin a json response
|
||||
json_response = {'changed': False, 'old_values': {}}
|
||||
json_output = {'changed': False, 'old_values': {}, 'new_values': {}}
|
||||
|
||||
# Check any of the settings to see if anything needs to be updated
|
||||
needs_update = False
|
||||
@@ -141,18 +141,29 @@ def main():
|
||||
if a_setting not in existing_settings or existing_settings[a_setting] != new_settings[a_setting]:
|
||||
# At least one thing is different so we need to patch
|
||||
needs_update = True
|
||||
json_response['old_values'][a_setting] = existing_settings[a_setting]
|
||||
json_output['old_values'][a_setting] = existing_settings[a_setting]
|
||||
json_output['new_values'][a_setting] = new_settings[a_setting]
|
||||
|
||||
if module._diff:
|
||||
json_output['diff'] = {
|
||||
'before': json_output['old_values'],
|
||||
'after': json_output['new_values']
|
||||
}
|
||||
|
||||
# If nothing needs an update we can simply exit with the response (as not changed)
|
||||
if not needs_update:
|
||||
module.exit_json(**json_response)
|
||||
module.exit_json(**json_output)
|
||||
|
||||
if module.check_mode and module._diff:
|
||||
json_output['changed'] = True
|
||||
module.exit_json(**json_output)
|
||||
|
||||
# Make the call to update the settings
|
||||
response = module.patch_endpoint('settings/all', **{'data': new_settings})
|
||||
|
||||
if response['status_code'] == 200:
|
||||
# Set the changed response to True
|
||||
json_response['changed'] = True
|
||||
json_output['changed'] = True
|
||||
|
||||
# To deal with the old style values we need to return 'value' in the response
|
||||
new_values = {}
|
||||
@@ -161,11 +172,11 @@ def main():
|
||||
|
||||
# If we were using a name we will just add a value of a string, otherwise we will return an array in values
|
||||
if name is not None:
|
||||
json_response['value'] = new_values[name]
|
||||
json_output['value'] = new_values[name]
|
||||
else:
|
||||
json_response['values'] = new_values
|
||||
json_output['values'] = new_values
|
||||
|
||||
module.exit_json(**json_response)
|
||||
module.exit_json(**json_output)
|
||||
elif 'json' in response and '__all__' in response['json']:
|
||||
module.fail_json(msg=response['json']['__all__'])
|
||||
else:
|
||||
|
||||
@@ -48,6 +48,19 @@
|
||||
- "result is changed"
|
||||
- "result.status == 'successful'"
|
||||
|
||||
- name: Launch an Ad Hoc Command without module argument
|
||||
tower_ad_hoc_command:
|
||||
inventory: "Demo Inventory"
|
||||
credential: "{{ ssh_cred_name }}"
|
||||
module_name: "ping"
|
||||
wait: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- "result.status == 'successful'"
|
||||
|
||||
- name: Check module fails with correct msg
|
||||
tower_ad_hoc_command:
|
||||
inventory: "{{ inv_name }}"
|
||||
|
||||
27
docs/licenses/django-guid.txt
Normal file
27
docs/licenses/django-guid.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2019 Jonas Krüger Svensson
|
||||
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 the authors 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 HOLDERS 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.
|
||||
@@ -13,6 +13,7 @@ django-auth-ldap
|
||||
django-cors-headers
|
||||
django-crum
|
||||
django-extensions>=2.2.9 # https://github.com/ansible/awx/pull/6441
|
||||
django-guid==2.2.0 # pinned to match Django 2.2
|
||||
django-jsonfield==1.2.0 # see UPGRADE BLOCKERs
|
||||
django-oauth-toolkit==1.1.3 # see UPGRADE BLOCKERs
|
||||
django-polymorphic
|
||||
|
||||
@@ -19,7 +19,7 @@ channels-redis==3.1.0 # via -r /awx_devel/requirements/requirements.in
|
||||
channels==2.4.0 # via -r /awx_devel/requirements/requirements.in, channels-redis
|
||||
chardet==3.0.4 # via aiohttp, requests
|
||||
constantly==15.1.0 # via twisted
|
||||
cryptography==2.9.2 # via adal, autobahn, azure-keyvault, pyopenssl, service-identity, social-auth-core
|
||||
cryptography==2.9.2 # via -r /awx_devel/requirements/requirements.in, adal, autobahn, azure-keyvault, pyopenssl, service-identity, social-auth-core
|
||||
daphne==2.4.1 # via -r /awx_devel/requirements/requirements.in, channels
|
||||
defusedxml==0.6.0 # via python3-openid, python3-saml, social-auth-core
|
||||
dictdiffer==0.8.1 # via openshift
|
||||
@@ -27,6 +27,7 @@ django-auth-ldap==2.1.0 # via -r /awx_devel/requirements/requirements.in
|
||||
django-cors-headers==3.2.1 # via -r /awx_devel/requirements/requirements.in
|
||||
django-crum==0.7.5 # via -r /awx_devel/requirements/requirements.in
|
||||
django-extensions==2.2.9 # via -r /awx_devel/requirements/requirements.in
|
||||
django-guid==2.2.0 # via -r /awx_devel/requirements/requirements.in
|
||||
django-jsonfield==1.2.0 # via -r /awx_devel/requirements/requirements.in
|
||||
django-oauth-toolkit==1.1.3 # via -r /awx_devel/requirements/requirements.in
|
||||
django-pglocks==1.0.4 # via -r /awx_devel/requirements/requirements.in
|
||||
@@ -37,7 +38,7 @@ django-redis==4.5.0 # via -r /awx_devel/requirements/requirements.in
|
||||
django-solo==1.1.3 # via -r /awx_devel/requirements/requirements.in
|
||||
django-split-settings==1.0.0 # via -r /awx_devel/requirements/requirements.in
|
||||
django-taggit==1.2.0 # via -r /awx_devel/requirements/requirements.in
|
||||
django==2.2.16 # via -r /awx_devel/requirements/requirements.in, channels, django-auth-ldap, django-cors-headers, django-crum, django-jsonfield, django-oauth-toolkit, django-polymorphic, django-taggit, djangorestframework
|
||||
django==2.2.16 # via -r /awx_devel/requirements/requirements.in, channels, django-auth-ldap, django-cors-headers, django-crum, django-guid, django-jsonfield, django-oauth-toolkit, django-polymorphic, django-taggit, djangorestframework
|
||||
djangorestframework-yaml==1.0.3 # via -r /awx_devel/requirements/requirements.in
|
||||
djangorestframework==3.12.1 # via -r /awx_devel/requirements/requirements.in
|
||||
docutils==0.16 # via python-daemon
|
||||
|
||||
@@ -21,4 +21,4 @@ backports.tempfile # support in unit tests for py32+ tempfile.TemporaryDirector
|
||||
mockldap
|
||||
sdb
|
||||
gprof2dot
|
||||
atomicwrites==1.1.5
|
||||
atomicwrites==1.4.0
|
||||
|
||||
Reference in New Issue
Block a user