Merge pull request #42 from ansible/devel

Rebase
This commit is contained in:
Sean Sullivan
2021-02-20 05:58:30 -06:00
committed by GitHub
75 changed files with 947 additions and 4141 deletions

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
---
version: 2
updates:
- package-ecosystem: "pip"
directory: "/requirements"
schedule:
interval: "monthly"

View File

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

View File

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

View File

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

View File

@@ -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']:

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

@@ -8,4 +8,5 @@ clouds:
project_name: fooo
username: fooo
private: true
region_name: fooo
verify: false

View File

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

View File

@@ -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"
}
]

View File

@@ -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():

View File

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

View File

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

View File

@@ -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',
}

View File

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

View File

@@ -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>&nbsp;&mdash; {{name}}</span>

View File

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

View File

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

View File

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

View File

@@ -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 .",

View File

@@ -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,
};

View File

@@ -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);
});
});

View File

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

View File

@@ -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);
});
});

View File

@@ -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`);

View File

@@ -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');
});

View 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);

View File

@@ -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' });
});
});

View File

@@ -1 +1,2 @@
export { default } from './LaunchButton';
export { default as LaunchButton } from './LaunchButton';
export { default as ReLaunchDropDown } from './ReLaunchDropDown';

View File

@@ -311,7 +311,7 @@ describe('<MultiCredentialsLookup />', () => {
});
wrapper.update();
const typeSelect = wrapper.find('AnsibleSelect');
act(() => {
await act(async () => {
typeSelect.invoke('onChange')({}, 500);
});
wrapper.update();

View File

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

View File

@@ -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>
);
}

View File

@@ -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 }) {

View File

@@ -14,7 +14,7 @@ export default function ActionItem({ column, tooltip, visible, children }) {
`}
>
<Tooltip content={tooltip} position="top">
{children}
<div>{children}</div>
</Tooltip>
</div>
);

View File

@@ -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 () => {

View File

@@ -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`),

View File

@@ -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'],
],
},
]);
});
});

View File

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

View File

@@ -290,6 +290,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
}
}
paused={false}
preventScrollOnDeactivate={false}
>
<div
className="pf-l-bullseye"

View File

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

View File

@@ -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');
});

View File

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

View File

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

View File

@@ -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());

View File

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

View File

@@ -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,
};

View File

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

View File

@@ -168,7 +168,7 @@ function ExternalTestModal({
);
}
ExternalTestModal.proptype = {
ExternalTestModal.propType = {
credential: shape({}),
credentialType: shape({}).isRequired,
credentialFormValues: shape({}).isRequired,

View File

@@ -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"
},

View File

@@ -118,7 +118,7 @@ function InventoryEdit({ inventory }) {
);
}
InventoryEdit.proptype = {
InventoryEdit.propType = {
inventory: object.isRequired,
};

View File

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

View File

@@ -138,7 +138,7 @@ function InventoryForm({
);
}
InventoryForm.proptype = {
InventoryForm.propType = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
instanceGroups: shape(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'),

View File

@@ -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'),
)

View File

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

View File

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

View File

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

View File

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

View 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.

View File

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

View File

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

View File

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