diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py
index 34fd367970..0e937da67c 100644
--- a/awx/api/views/__init__.py
+++ b/awx/api/views/__init__.py
@@ -4303,7 +4303,7 @@ class NotificationTemplateTest(GenericAPIView):
msg = "Tower Notification Test {} {}".format(obj.id, settings.TOWER_URL_BASE)
if obj.notification_type in ('email', 'pagerduty'):
body = "Ansible Tower Test Notification {} {}".format(obj.id, settings.TOWER_URL_BASE)
- elif obj.notification_type == 'webhook':
+ elif obj.notification_type in ('webhook', 'grafana'):
body = '{{"body": "Ansible Tower Test Notification {} {}"}}'.format(obj.id, settings.TOWER_URL_BASE)
else:
body = {"body": "Ansible Tower Test Notification {} {}".format(obj.id, settings.TOWER_URL_BASE)}
diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py
index 71a68b2a9f..f731a03ce0 100644
--- a/awx/main/models/notifications.py
+++ b/awx/main/models/notifications.py
@@ -270,21 +270,19 @@ class JobNotificationMixin(object):
'elapsed', 'job_explanation', 'execution_node', 'controller_node', 'allow_simultaneous',
'scm_revision', 'diff_mode', 'job_slice_number', 'job_slice_count', 'custom_virtualenv',
'approval_status', 'approval_node_name', 'workflow_url',
- {'host_status_counts': ['skipped', 'ok', 'changed', 'failures', 'dark']},
- {'playbook_counts': ['play_count', 'task_count']},
+ {'host_status_counts': ['skipped', 'ok', 'changed', 'failed', 'failures', 'dark'
+ 'processed', 'rescued', 'ignored']},
{'summary_fields': [{'inventory': ['id', 'name', 'description', 'has_active_failures',
'total_hosts', 'hosts_with_active_failures', 'total_groups',
'has_inventory_sources',
'total_inventory_sources', 'inventory_sources_with_failures',
'organization_id', 'kind']},
{'project': ['id', 'name', 'description', 'status', 'scm_type']},
- {'project_update': ['id', 'name', 'description', 'status', 'failed']},
{'job_template': ['id', 'name', 'description']},
{'unified_job_template': ['id', 'name', 'description', 'unified_job_type']},
{'instance_group': ['name', 'id']},
{'created_by': ['id', 'username', 'first_name', 'last_name']},
- {'labels': ['count', 'results']},
- {'source_workflow_job': ['description', 'elapsed', 'failed', 'id', 'name', 'status']}]}]
+ {'labels': ['count', 'results']}]}]
@classmethod
def context_stub(cls):
@@ -303,7 +301,7 @@ class JobNotificationMixin(object):
'finished': False,
'force_handlers': False,
'forks': 0,
- 'host_status_counts': {'skipped': 1, 'ok': 5, 'changed': 3, 'failures': 0, 'dark': 0},
+ 'host_status_counts': {'skipped': 1, 'ok': 5, 'changed': 3, 'failures': 0, 'dark': 0, 'failed': False, 'processed': 0, 'rescued': 0},
'id': 42,
'job_explanation': 'Sample job explanation',
'job_slice_count': 1,
@@ -314,7 +312,6 @@ class JobNotificationMixin(object):
'limit': 'bar_limit',
'modified': datetime.datetime(2018, 12, 13, 6, 4, 0, 0, tzinfo=datetime.timezone.utc),
'name': 'Stub JobTemplate',
- 'playbook_counts': {'play_count': 5, 'task_count': 10},
'playbook': 'ping.yml',
'scm_revision': '',
'skip_tags': '',
@@ -347,18 +344,10 @@ class JobNotificationMixin(object):
'name': 'Stub project',
'scm_type': 'git',
'status': 'successful'},
- 'project_update': {'id': 5, 'name': 'Stub Project Update', 'description': 'Project Update',
- 'status': 'running', 'failed': False},
'unified_job_template': {'description': 'Sample unified job template description',
'id': 39,
'name': 'Stub Job Template',
- 'unified_job_type': 'job'},
- 'source_workflow_job': {'description': 'Sample workflow job description',
- 'elapsed': 0.000,
- 'failed': False,
- 'id': 88,
- 'name': 'Stub WorkflowJobTemplate',
- 'status': 'running'}},
+ 'unified_job_type': 'job'}},
'timeout': 0,
'type': 'job',
'url': '/api/v2/jobs/13/',
@@ -392,10 +381,20 @@ class JobNotificationMixin(object):
The context will contain whitelisted content retrieved from a serialized job object
(see JobNotificationMixin.JOB_FIELDS_WHITELIST), the job's friendly name,
and a url to the job run."""
- context = {'job': {},
- 'job_friendly_name': self.get_notification_friendly_name(),
- 'url': self.get_ui_url(),
- 'job_metadata': json.dumps(self.notification_data(), indent=4)}
+ 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
+ context = {
+ 'job': job_context,
+ 'job_friendly_name': self.get_notification_friendly_name(),
+ 'url': self.get_ui_url(),
+ 'job_metadata': json.dumps(self.notification_data(), indent=4)
+ }
def build_context(node, fields, whitelisted_fields):
for safe_field in whitelisted_fields:
diff --git a/awx/main/notifications/grafana_backend.py b/awx/main/notifications/grafana_backend.py
index 58137f27aa..8fbedf58d3 100644
--- a/awx/main/notifications/grafana_backend.py
+++ b/awx/main/notifications/grafana_backend.py
@@ -2,6 +2,7 @@
# All Rights Reserved.
import datetime
+import json
import logging
import requests
import dateutil.parser as dp
@@ -23,6 +24,33 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase):
recipient_parameter = "grafana_url"
sender_parameter = None
+ DEFAULT_BODY = "{{ job_metadata }}"
+ default_messages = {
+ "started": {
+ "body": DEFAULT_BODY, "message": CustomNotificationBase.DEFAULT_MSG
+ },
+ "success": {
+ "body": DEFAULT_BODY, "message": CustomNotificationBase.DEFAULT_MSG
+ },
+ "error": {
+ "body": DEFAULT_BODY, "message": CustomNotificationBase.DEFAULT_MSG
+ },
+ "workflow_approval": {
+ "running": {
+ "message": CustomNotificationBase.DEFAULT_APPROVAL_RUNNING_MSG, "body": None
+ },
+ "approved": {
+ "message": CustomNotificationBase.DEFAULT_APPROVAL_APPROVED_MSG, "body": None
+ },
+ "timed_out": {
+ "message": CustomNotificationBase.DEFAULT_APPROVAL_TIMEOUT_MSG, "body": None
+ },
+ "denied": {
+ "message": CustomNotificationBase.DEFAULT_APPROVAL_DENIED_MSG, "body": None
+ }
+ }
+ }
+
def __init__(self, grafana_key,dashboardId=None, panelId=None, annotation_tags=None, grafana_no_verify_ssl=False, isRegion=True,
fail_silently=False, **kwargs):
super(GrafanaBackend, self).__init__(fail_silently=fail_silently)
@@ -34,6 +62,13 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase):
self.isRegion = isRegion
def format_body(self, body):
+ # expect body to be a string representing a dict
+ try:
+ potential_body = json.loads(body)
+ if isinstance(potential_body, dict):
+ body = potential_body
+ except json.JSONDecodeError:
+ body = {}
return body
def send_messages(self, messages):
@@ -41,14 +76,16 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase):
for m in messages:
grafana_data = {}
grafana_headers = {}
- try:
- epoch=datetime.datetime.utcfromtimestamp(0)
- grafana_data['time'] = int((dp.parse(m.body['started']).replace(tzinfo=None) - epoch).total_seconds() * 1000)
- grafana_data['timeEnd'] = int((dp.parse(m.body['finished']).replace(tzinfo=None) - epoch).total_seconds() * 1000)
- except ValueError:
- logger.error(smart_text(_("Error converting time {} or timeEnd {} to int.").format(m.body['started'],m.body['finished'])))
- if not self.fail_silently:
- raise Exception(smart_text(_("Error converting time {} and/or timeEnd {} to int.").format(m.body['started'],m.body['finished'])))
+ if 'started' in m.body:
+ try:
+ epoch=datetime.datetime.utcfromtimestamp(0)
+ grafana_data['time'] = grafana_data['timeEnd'] = int((dp.parse(m.body['started']).replace(tzinfo=None) - epoch).total_seconds() * 1000)
+ if m.body.get('finished'):
+ grafana_data['timeEnd'] = int((dp.parse(m.body['finished']).replace(tzinfo=None) - epoch).total_seconds() * 1000)
+ except ValueError:
+ logger.error(smart_text(_("Error converting time {} or timeEnd {} to int.").format(m.body['started'],m.body['finished'])))
+ if not self.fail_silently:
+ raise Exception(smart_text(_("Error converting time {} and/or timeEnd {} to int.").format(m.body['started'],m.body['finished'])))
grafana_data['isRegion'] = self.isRegion
grafana_data['dashboardId'] = self.dashboardId
grafana_data['panelId'] = self.panelId
diff --git a/awx/main/tests/functional/models/test_notifications.py b/awx/main/tests/functional/models/test_notifications.py
index 02a2b28e83..57fd4cca91 100644
--- a/awx/main/tests/functional/models/test_notifications.py
+++ b/awx/main/tests/functional/models/test_notifications.py
@@ -23,8 +23,11 @@ class TestJobNotificationMixin(object):
'finished': bool,
'force_handlers': bool,
'forks': int,
- 'host_status_counts': {'skipped': int, 'ok': int, 'changed': int,
- 'failures': int, 'dark': int},
+ 'host_status_counts': {
+ 'skipped': int, 'ok': int, 'changed': int,
+ 'failures': int, 'dark': int, 'processed': int,
+ 'rescued': int, 'failed': bool
+ },
'id': int,
'job_explanation': str,
'job_slice_count': int,
@@ -36,7 +39,6 @@ class TestJobNotificationMixin(object):
'modified': datetime.datetime,
'name': str,
'playbook': str,
- 'playbook_counts': {'play_count': int, 'task_count': int},
'scm_revision': str,
'skip_tags': str,
'start_at_task': str,
@@ -68,17 +70,10 @@ class TestJobNotificationMixin(object):
'name': str,
'scm_type': str,
'status': str},
- 'project_update': {'id': int, 'name': str, 'description': str, 'status': str, 'failed': bool},
'unified_job_template': {'description': str,
'id': int,
'name': str,
- 'unified_job_type': str},
- 'source_workflow_job': {'description': str,
- 'elapsed': float,
- 'failed': bool,
- 'id': int,
- 'name': str,
- 'status': str}},
+ 'unified_job_type': str}},
'timeout': int,
'type': str,
diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json
index 941399b8f9..7c8b645522 100644
--- a/awx/ui_next/package-lock.json
+++ b/awx/ui_next/package-lock.json
@@ -4135,36 +4135,51 @@
"dev": true
},
"@patternfly/patternfly": {
- "version": "2.56.3",
- "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-2.56.3.tgz",
- "integrity": "sha512-merUreEz4ul84s+OXwJ27AtMtcBdzExDX+Xn/T84OD6OAgG8iU1x1dnWdFBeEFKOlTpKxwjXxS/Zc8tSxpfRBw=="
+ "version": "2.66.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-2.66.0.tgz",
+ "integrity": "sha512-fZMr2q9LZhVtKAEcDJ4rzcCGC6iN93mEQPoLlv2T9td5Hba1bLw8Bpgp5fdTm95Fv/++AY0PsdUPZUzh1cx7Sg=="
},
"@patternfly/react-core": {
- "version": "3.135.0",
- "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-3.135.0.tgz",
- "integrity": "sha512-5UotimA2VUiYWt/v8j4x/z8MMyvbgXi7z7BKPzicIiKGwmREJCgQ+kf0eAqGUg/YjFLKcDKncoBrPNqVZ8Ykpg==",
+ "version": "3.140.11",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-3.140.11.tgz",
+ "integrity": "sha512-841DeN5BTuUS02JfVXAAVJYtWY0HWc4ewqMD32Xog2MAR/pn74jzjnQOSQr4LUyVrH5QufB68SK4Alm2+IUzSw==",
"requires": {
- "@patternfly/react-icons": "^3.14.39",
- "@patternfly/react-styles": "^3.6.27",
- "@patternfly/react-tokens": "^2.7.25",
+ "@patternfly/react-icons": "^3.15.3",
+ "@patternfly/react-styles": "^3.7.4",
+ "@patternfly/react-tokens": "^2.8.4",
"emotion": "^9.2.9",
"exenv": "^1.2.2",
"focus-trap-react": "^4.0.1",
"tippy.js": "5.1.2"
+ },
+ "dependencies": {
+ "@patternfly/react-icons": {
+ "version": "3.15.4",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-3.15.4.tgz",
+ "integrity": "sha512-tOVirISoZDIn0bWYFctGN9B7Q8wQ19FaK4XIUD2sgIDRBzDbe9JWuqdef7ogJFF78eQnZNsWOci6nhvVCVF/zA==",
+ "requires": {
+ "@fortawesome/free-brands-svg-icons": "^5.8.1"
+ }
+ },
+ "@patternfly/react-tokens": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-2.8.4.tgz",
+ "integrity": "sha512-GlLyutls0bG39Nwl/sv2FUkicwyRNrXQFso+e7Y4470+VOUtSsVSdQz+rTjgPxQ38olKPsSZdtEjqN9o2PbDiw=="
+ }
}
},
"@patternfly/react-icons": {
- "version": "3.14.39",
- "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-3.14.39.tgz",
- "integrity": "sha512-/1hhKEFRtvBYNa8BFRurqHdlUYYzdovmllwtEWcxye5lffDC1Ghco1NGQzjm0FtzkxX1hPFvw04HRR2jBBG8xQ==",
+ "version": "3.15.4",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-3.15.4.tgz",
+ "integrity": "sha512-tOVirISoZDIn0bWYFctGN9B7Q8wQ19FaK4XIUD2sgIDRBzDbe9JWuqdef7ogJFF78eQnZNsWOci6nhvVCVF/zA==",
"requires": {
"@fortawesome/free-brands-svg-icons": "^5.8.1"
}
},
"@patternfly/react-styles": {
- "version": "3.6.27",
- "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-3.6.27.tgz",
- "integrity": "sha512-XGI+lR0/QIqkGnGHcnzbynR9JImTftDztsXBgaB0FoDu/DmPY4EABgtsF9HvfoAjsIg8KxRs32tqoXN3yN0TWg==",
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-3.7.4.tgz",
+ "integrity": "sha512-D+wu0OIfWVgxWNShQhTK9cadw+KdMCoBYR8gbWjV9Q1aCsCEV/aL/x1nMyyaUQ3c2dqizHhujDG4z9jUZCmCcw==",
"requires": {
"camel-case": "^3.0.0",
"css": "^2.2.3",
@@ -4174,9 +4189,9 @@
}
},
"@patternfly/react-tokens": {
- "version": "2.7.25",
- "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-2.7.25.tgz",
- "integrity": "sha512-04hRDWt07pyjLUO1VN9QbrPpQMJzjd+nQYp8vgoe6+mYBzw+D4banJeudZ1oTFii9hWV+mLEu6aiwPtTigPM1Q=="
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-2.8.4.tgz",
+ "integrity": "sha512-GlLyutls0bG39Nwl/sv2FUkicwyRNrXQFso+e7Y4470+VOUtSsVSdQz+rTjgPxQ38olKPsSZdtEjqN9o2PbDiw=="
},
"@types/babel__core": {
"version": "7.1.1",
@@ -7047,9 +7062,9 @@
}
},
"csstype": {
- "version": "2.6.8",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz",
- "integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA=="
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz",
+ "integrity": "sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q=="
},
"currently-unhandled": {
"version": "0.4.1",
@@ -9367,8 +9382,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -9389,14 +9403,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -9411,20 +9423,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -9541,8 +9550,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -9554,7 +9562,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -9569,7 +9576,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -9577,14 +9583,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -9603,7 +9607,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -9684,8 +9687,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -9697,7 +9699,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -9783,8 +9784,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -9820,7 +9820,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -9840,7 +9839,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -9884,14 +9882,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -15374,9 +15370,9 @@
}
},
"react": {
- "version": "16.10.2",
- "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz",
- "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==",
+ "version": "16.13.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz",
+ "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -15389,20 +15385,20 @@
"integrity": "sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag=="
},
"react-dom": {
- "version": "16.10.2",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz",
- "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==",
+ "version": "16.13.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.0.tgz",
+ "integrity": "sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
- "scheduler": "^0.16.2"
+ "scheduler": "^0.19.0"
},
"dependencies": {
"scheduler": {
- "version": "0.16.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz",
- "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.0.tgz",
+ "integrity": "sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json
index bc844ca9e4..cdd79cd2d7 100644
--- a/awx/ui_next/package.json
+++ b/awx/ui_next/package.json
@@ -58,10 +58,10 @@
},
"dependencies": {
"@lingui/react": "^2.7.2",
- "@patternfly/patternfly": "^2.56.3",
- "@patternfly/react-core": "^3.135.0",
- "@patternfly/react-icons": "^3.14.39",
- "@patternfly/react-tokens": "^2.7.25",
+ "@patternfly/patternfly": "^2.66.0",
+ "@patternfly/react-core": "^3.140.11",
+ "@patternfly/react-icons": "^3.15.4",
+ "@patternfly/react-tokens": "^2.8.4",
"ansi-to-html": "^0.6.11",
"axios": "^0.18.1",
"codemirror": "^5.47.0",
@@ -72,9 +72,9 @@
"html-entities": "^1.2.1",
"js-yaml": "^3.13.1",
"prop-types": "^15.6.2",
- "react": "^16.10.2",
+ "react": "^16.13.0",
"react-codemirror2": "^6.0.0",
- "react-dom": "^16.10.2",
+ "react-dom": "^16.13.0",
"react-router-dom": "^5.1.2",
"react-virtualized": "^9.21.1",
"styled-components": "^4.2.0"
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
index 83533f1168..f561a6f74e 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
@@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react';
import { string, node, number } from 'prop-types';
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from '@components/DetailList';
+import MultiButtonToggle from '@components/MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
import CodeMirrorInput from './CodeMirrorInput';
-import YamlJsonToggle from './YamlJsonToggle';
import { JSON_MODE, YAML_MODE } from './constants';
function getValueAsMode(value, mode) {
@@ -50,8 +50,9 @@ function VariablesDetail({ value, label, rows }) {
- {
try {
setCurrentValue(getValueAsMode(currentValue, newMode));
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx
index 05548f8aa8..5ffdaeb1e7 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx
@@ -31,12 +31,12 @@ describe('', () => {
const wrapper = shallow(
);
- wrapper.find('YamlJsonToggle').invoke('onChange')('javascript');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
const input = wrapper.find('Styled(CodeMirrorInput)');
expect(input.prop('mode')).toEqual('javascript');
expect(input.prop('value')).toEqual('{\n "foo": "bar"\n}');
- wrapper.find('YamlJsonToggle').invoke('onChange')('yaml');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('yaml');
const input2 = wrapper.find('Styled(CodeMirrorInput)');
expect(input2.prop('mode')).toEqual('yaml');
expect(input2.prop('value')).toEqual('foo: bar\n');
@@ -53,7 +53,7 @@ describe('', () => {
);
act(() => {
- wrapper.find('YamlJsonToggle').invoke('onChange')('javascript');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
});
wrapper.setProps({
value: '---bar: baz',
@@ -73,7 +73,7 @@ describe('', () => {
test('should default empty json to "{}"', () => {
const wrapper = mount();
act(() => {
- wrapper.find('YamlJsonToggle').invoke('onChange')('javascript');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
});
wrapper.setProps({ value: '' });
const input = wrapper.find('Styled(CodeMirrorInput)');
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
index 3c72e56084..c8049328a8 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
@@ -6,9 +6,9 @@ import { useField } from 'formik';
import styled from 'styled-components';
import { Split, SplitItem } from '@patternfly/react-core';
import { CheckboxField, FieldTooltip } from '@components/FormField';
+import MultiButtonToggle from '@components/MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
import CodeMirrorInput from './CodeMirrorInput';
-import YamlJsonToggle from './YamlJsonToggle';
import { JSON_MODE, YAML_MODE } from './constants';
const FieldHeader = styled.div`
@@ -43,8 +43,9 @@ function VariablesField({
{tooltip && }
- {
try {
const newVal =
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
index faddd2bc13..cf7758b53e 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
@@ -1,21 +1,16 @@
import React, { useState } from 'react';
import { string, func, bool, number } from 'prop-types';
-import { Button, Split, SplitItem } from '@patternfly/react-core';
+import { Split, SplitItem } from '@patternfly/react-core';
import styled from 'styled-components';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
+import MultiButtonToggle from '@components/MultiButtonToggle';
import CodeMirrorInput from './CodeMirrorInput';
-import ButtonGroup from './ButtonGroup';
import { JSON_MODE, YAML_MODE } from './constants';
function formatJson(jsonString) {
return JSON.stringify(JSON.parse(jsonString), null, 2);
}
-const SmallButton = styled(Button)`
- padding: 3px 8px;
- font-size: var(--pf-global--FontSize--xs);
-`;
-
const SplitItemRight = styled(SplitItem)`
margin-bottom: 5px;
`;
@@ -47,40 +42,22 @@ function VariablesInput(props) {
-
- {
- if (mode === YAML_MODE) {
- return;
- }
- try {
- onChange(jsonToYaml(value));
- setMode(YAML_MODE);
- } catch (err) {
- onError(err.message);
- }
- }}
- variant={mode === YAML_MODE ? 'primary' : 'secondary'}
- >
- YAML
-
- {
+ {
+ try {
if (mode === JSON_MODE) {
- return;
- }
- try {
+ onChange(jsonToYaml(value));
+ } else {
onChange(yamlToJson(value));
- setMode(JSON_MODE);
- } catch (err) {
- onError(err.message);
}
- }}
- variant={mode === JSON_MODE ? 'primary' : 'secondary'}
- >
- JSON
-
-
+ setMode(newMode);
+ } catch (err) {
+ onError(err.message);
+ }
+ }}
+ />
{
- if (mode !== newMode) {
- onChange(newMode);
- }
- };
-
- return (
-
- setMode(YAML_MODE)}
- variant={mode === YAML_MODE ? 'primary' : 'secondary'}
- >
- YAML
-
- setMode(JSON_MODE)}
- variant={mode === JSON_MODE ? 'primary' : 'secondary'}
- >
- JSON
-
-
- );
-}
-YamlJsonToggle.propTypes = {
- mode: oneOf([YAML_MODE, JSON_MODE]).isRequired,
- onChange: func.isRequired,
-};
-
-export default YamlJsonToggle;
diff --git a/awx/ui_next/src/components/CodeMirrorInput/ButtonGroup.jsx b/awx/ui_next/src/components/MultiButtonToggle/ButtonGroup.jsx
similarity index 100%
rename from awx/ui_next/src/components/CodeMirrorInput/ButtonGroup.jsx
rename to awx/ui_next/src/components/MultiButtonToggle/ButtonGroup.jsx
diff --git a/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx
new file mode 100644
index 0000000000..9c72cfbab3
--- /dev/null
+++ b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { func, string } from 'prop-types';
+import styled from 'styled-components';
+import { Button } from '@patternfly/react-core';
+import ButtonGroup from './ButtonGroup';
+
+const SmallButton = styled(Button)`
+ padding: 3px 8px;
+ font-size: var(--pf-global--FontSize--xs);
+`;
+
+function MultiButtonToggle({ buttons, value, onChange }) {
+ const setValue = newValue => {
+ if (value !== newValue) {
+ onChange(newValue);
+ }
+ };
+
+ return (
+
+ {buttons &&
+ buttons.map(([buttonValue, buttonLabel]) => (
+ setValue(buttonValue)}
+ variant={buttonValue === value ? 'primary' : 'secondary'}
+ >
+ {buttonLabel}
+
+ ))}
+
+ );
+}
+
+const buttonsPropType = {
+ isRequired: ({ buttons }) => {
+ if (!buttons) {
+ return new Error(
+ `The prop buttons is marked as required in MultiButtonToggle, but its value is '${buttons}'`
+ );
+ }
+ // We expect this data structure to look like:
+ // [[value(unrestricted type), label(string)], [value(unrestricted type), label(string)], ...]
+ if (
+ !Array.isArray(buttons) ||
+ buttons.length < 2 ||
+ buttons.reduce(
+ (prevVal, button) => prevVal || typeof button[1] !== 'string',
+ false
+ )
+ ) {
+ return new Error(
+ `Invalid prop buttons supplied to MultiButtonToggle. Validation failed.`
+ );
+ }
+
+ return null;
+ },
+};
+
+MultiButtonToggle.propTypes = {
+ buttons: buttonsPropType.isRequired,
+ value: string.isRequired,
+ onChange: func.isRequired,
+};
+
+export default MultiButtonToggle;
diff --git a/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.test.jsx b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.test.jsx
new file mode 100644
index 0000000000..15c65e3a29
--- /dev/null
+++ b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.test.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import MultiButtonToggle from './MultiButtonToggle';
+
+describe('', () => {
+ let wrapper;
+ const onChange = jest.fn();
+ beforeAll(() => {
+ wrapper = mount(
+
+ );
+ });
+ afterAll(() => {
+ wrapper.unmount();
+ });
+ it('should render buttons successfully', () => {
+ const buttons = wrapper.find('Button');
+ expect(buttons.length).toBe(2);
+ expect(buttons.at(0).props().variant).toBe('primary');
+ expect(buttons.at(1).props().variant).toBe('secondary');
+ });
+ it('should call onChange function when button clicked', () => {
+ const buttons = wrapper.find('Button');
+ buttons.at(1).simulate('click');
+ expect(onChange).toHaveBeenCalledWith('json');
+ });
+});
diff --git a/awx/ui_next/src/components/MultiButtonToggle/index.js b/awx/ui_next/src/components/MultiButtonToggle/index.js
new file mode 100644
index 0000000000..9332082446
--- /dev/null
+++ b/awx/ui_next/src/components/MultiButtonToggle/index.js
@@ -0,0 +1 @@
+export { default } from './MultiButtonToggle';
diff --git a/awx_collection/README.md b/awx_collection/README.md
index 7e1022892e..f91396e59c 100644
--- a/awx_collection/README.md
+++ b/awx_collection/README.md
@@ -37,7 +37,7 @@ To use this collection in AWX, you should create a custom virtual environment in
to set the job template `extra_vars` to include `ansible_python_interpreter`
to be the Python in that virtual environment.
-## Running Tests
+## Running Unit Tests
Tests to verify compatibility with the most recent AWX code are
in `awx_collection/test/awx`. These tests require that Python packages
@@ -71,6 +71,30 @@ pip install -e .
PYTHONPATH=awx_collection:$PYTHONPATH py.test awx_collection/test/awx/
```
+## Running Integration tests Tests
+
+The integration tests require a virtualenv with `ansible` >= 2.9 and `tower_cli`.
+The collection must first be installed, which can be done using `make install_collection`.
+You also need a configuration file at `~/.tower_cli.cfg` or
+`/etc/tower/tower_cli.cfg` with the credentials for accessing tower. This can
+be populated using `tower-cli`:
+
+```
+tower-cli config host $HOST
+tower-cli config username $USERNAME
+tower-cli config password $PASSWORD
+# This tells the tower-cli not to veriffy the ssl certs in the tower, if your tower has good certs you should leave this to true
+tower-cli config verify_ssl false
+```
+
+Finally you can run the tests:
+
+```
+# ansible-test must be run from the directory in which the collection is installed
+cd ~/.ansible/collections/ansible_collections/awx/awx/
+ansible-test integration
+```
+
## Building
The build target `make build_collection` will template out a `galaxy.yml` file
diff --git a/awx_collection/tests/integration/targets/tower_credential/tasks/main.yml b/awx_collection/tests/integration/targets/tower_credential/tasks/main.yml
new file mode 100644
index 0000000000..7f4e7d3848
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_credential/tasks/main.yml
@@ -0,0 +1,539 @@
+---
+- name: create a tempdir for an SSH key
+ local_action: shell mktemp -d
+ register: tempdir
+
+- name: Generate a local SSH key
+ local_action: "shell ssh-keygen -b 2048 -t rsa -f {{ tempdir.stdout }}/id_rsa -q -N 'passphrase'"
+
+- name: Read the generated key
+ set_fact:
+ ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}"
+
+- name: Create a User-specific credential
+ tower_credential:
+ name: SSH Credential
+ organization: Default
+ user: admin
+ state: present
+ kind: ssh
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a User-specific credential
+ tower_credential:
+ name: SSH Credential
+ organization: Default
+ user: admin
+ state: absent
+ kind: ssh
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid SSH credential
+ tower_credential:
+ name: SSH Credential
+ organization: Default
+ state: present
+ kind: ssh
+ description: An example SSH credential
+ username: joe
+ password: secret
+ become_method: sudo
+ become_username: superuser
+ become_password: supersecret
+ ssh_key_data: "{{ ssh_key_data }}"
+ ssh_key_unlock: "passphrase"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid SSH credential from lookup source
+ tower_credential:
+ name: SSH Credential from lookup source
+ organization: Default
+ state: present
+ kind: ssh
+ description: An example SSH credential from lookup source
+ username: joe
+ password: secret
+ become_method: sudo
+ become_username: superuser
+ become_password: supersecret
+ ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}"
+ ssh_key_unlock: "passphrase"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid SSH credential from file source
+ tower_credential:
+ name: SSH Credential from file source
+ organization: Default
+ state: present
+ kind: ssh
+ description: An example SSH credential from file source
+ username: joe
+ password: secret
+ become_method: sudo
+ become_username: superuser
+ become_password: supersecret
+ ssh_key_data: "{{ tempdir.stdout }}/id_rsa"
+ ssh_key_unlock: "passphrase"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+ - "result is not failed"
+ - "'ssh_key_data should be a string, not a path to a file.' in result.deprecations[0].msg"
+
+- name: Create an invalid SSH credential (passphrase required)
+ tower_credential:
+ name: SSH Credential
+ organization: Default
+ state: present
+ kind: ssh
+ username: joe
+ ssh_key_data: "{{ ssh_key_data }}"
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - "result is failed"
+ - "'must be set when SSH key is encrypted' in result.msg"
+
+- name: Create an invalid SSH credential (Organization not found)
+ tower_credential:
+ name: SSH Credential
+ organization: Missing Organization
+ state: present
+ kind: ssh
+ username: joe
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - "result is failed"
+ - "'The requested object could not be found' in result.msg"
+
+- name: Delete an SSH credential
+ tower_credential:
+ name: SSH Credential
+ organization: Default
+ state: absent
+ kind: ssh
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid Vault credential
+ tower_credential:
+ name: Vault Credential
+ organization: Default
+ state: present
+ kind: vault
+ description: An example Vault credential
+ vault_password: secret-vault
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid Vault credential w/ kind=ssh (deprecated)
+ tower_credential:
+ name: Vault Credential
+ organization: Default
+ state: present
+ kind: ssh
+ description: An example Vault credential
+ vault_password: secret-vault
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Vault credential
+ tower_credential:
+ name: Vault Credential
+ organization: Default
+ state: absent
+ kind: vault
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid Network credential
+ tower_credential:
+ name: Network Credential
+ organization: Default
+ state: present
+ kind: net
+ username: joe
+ password: secret
+ authorize: true
+ authorize_password: authorize-me
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Network credential
+ tower_credential:
+ name: Network Credential
+ organization: Default
+ state: absent
+ kind: net
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid SCM credential
+ tower_credential:
+ name: SCM Credential
+ organization: Default
+ state: present
+ kind: scm
+ username: joe
+ password: secret
+ ssh_key_data: "{{ ssh_key_data }}"
+ ssh_key_unlock: "passphrase"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete an SCM credential
+ tower_credential:
+ name: SCM Credential
+ organization: Default
+ state: absent
+ kind: scm
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid AWS credential
+ tower_credential:
+ name: AWS Credential
+ organization: Default
+ state: present
+ kind: aws
+ username: joe
+ password: secret
+ security_token: aws-token
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete an AWS credential
+ tower_credential:
+ name: AWS Credential
+ organization: Default
+ state: absent
+ kind: aws
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid VMWare credential
+ tower_credential:
+ name: VMWare Credential
+ organization: Default
+ state: present
+ kind: vmware
+ host: https://example.org
+ username: joe
+ password: secret
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete an VMWare credential
+ tower_credential:
+ name: VMWare Credential
+ organization: Default
+ state: absent
+ kind: vmware
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid Satellite6 credential
+ tower_credential:
+ name: Satellite6 Credential
+ organization: Default
+ state: present
+ kind: satellite6
+ host: https://example.org
+ username: joe
+ password: secret
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Satellite6 credential
+ tower_credential:
+ name: Satellite6 Credential
+ organization: Default
+ state: absent
+ kind: satellite6
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid CloudForms credential
+ tower_credential:
+ name: CloudForms Credential
+ organization: Default
+ state: present
+ kind: cloudforms
+ host: https://example.org
+ username: joe
+ password: secret
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a CloudForms credential
+ tower_credential:
+ name: CloudForms Credential
+ organization: Default
+ state: absent
+ kind: cloudforms
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid GCE credential
+ tower_credential:
+ name: GCE Credential
+ organization: Default
+ state: present
+ kind: gce
+ username: joe
+ project: ABC123
+ ssh_key_data: "{{ ssh_key_data }}"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a GCE credential
+ tower_credential:
+ name: GCE Credential
+ organization: Default
+ state: absent
+ kind: gce
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid AzureRM credential
+ tower_credential:
+ name: AzureRM Credential
+ organization: Default
+ state: present
+ kind: azure_rm
+ username: joe
+ password: secret
+ subscription: some-subscription
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid AzureRM credential with a tenant
+ tower_credential:
+ name: AzureRM Credential
+ organization: Default
+ state: present
+ kind: azure_rm
+ client: some-client
+ secret: some-secret
+ tenant: some-tenant
+ subscription: some-subscription
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete an AzureRM credential
+ tower_credential:
+ name: AzureRM Credential
+ organization: Default
+ state: absent
+ kind: azure_rm
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid OpenStack credential
+ tower_credential:
+ name: OpenStack Credential
+ organization: Default
+ state: present
+ kind: openstack
+ host: https://keystone.example.org
+ username: joe
+ password: secret
+ project: tenant123
+ domain: some-domain
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a OpenStack credential
+ tower_credential:
+ name: OpenStack Credential
+ organization: Default
+ state: absent
+ kind: openstack
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid RHV credential
+ tower_credential:
+ name: RHV Credential
+ organization: Default
+ state: present
+ kind: rhv
+ host: https://example.org
+ username: joe
+ password: secret
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete an RHV credential
+ tower_credential:
+ name: RHV Credential
+ organization: Default
+ state: absent
+ kind: rhv
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid Insights credential
+ tower_credential:
+ name: Insights Credential
+ organization: Default
+ state: present
+ kind: insights
+ username: joe
+ password: secret
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete an Insights credential
+ tower_credential:
+ name: Insights Credential
+ organization: Default
+ state: absent
+ kind: insights
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a valid Tower-to-Tower credential
+ tower_credential:
+ name: Tower Credential
+ organization: Default
+ state: present
+ kind: tower
+ host: https://tower.example.org
+ username: joe
+ password: secret
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Tower-to-Tower credential
+ tower_credential:
+ name: Tower Credential
+ organization: Default
+ state: absent
+ kind: tower
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Check module fails with correct msg
+ tower_credential:
+ name: test-credential
+ description: Credential Description
+ kind: ssh
+ organization: test-non-existing-org
+ state: present
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='Failed to update credential, organization not found: The requested object could not be found.'"
diff --git a/awx_collection/tests/integration/targets/tower_credential_type/tasks/main.yml b/awx_collection/tests/integration/targets/tower_credential_type/tasks/main.yml
new file mode 100644
index 0000000000..9b205e0ba3
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_credential_type/tasks/main.yml
@@ -0,0 +1,23 @@
+---
+- name: Add Tower credential type
+ tower_credential_type:
+ description: Credential type for Test
+ name: test-credential-type
+ kind: cloud
+ inputs: {"fields": [{"type": "string", "id": "username", "label": "Username"}, {"secret": true, "type": "string", "id": "password", "label": "Password"}], "required": ["username", "password"]}
+ injectors: {"extra_vars": {"test": "foo"}}
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Remove a Tower credential type
+ tower_credential_type:
+ name: test-credential-type
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
diff --git a/awx_collection/tests/integration/targets/tower_group/tasks/main.yml b/awx_collection/tests/integration/targets/tower_group/tasks/main.yml
new file mode 100644
index 0000000000..a742021bba
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_group/tasks/main.yml
@@ -0,0 +1,48 @@
+---
+- name: Generate an inventory name
+ set_fact:
+ inv_name: "inv-for-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
+
+- name: Create an Inventory
+ tower_inventory:
+ name: "{{ inv_name }}"
+ organization: Default
+ state: present
+
+- name: Create a Group
+ tower_group:
+ name: Some Group
+ inventory: "{{ inv_name }}"
+ state: present
+ variables:
+ foo: bar
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Group
+ tower_group:
+ name: Some Group
+ inventory: "{{ inv_name }}"
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Check module fails with correct msg
+ tower_group:
+ name: test-group
+ description: Group Description
+ inventory: test-non-existing-inventory
+ state: present
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='Failed to update the group, inventory not found: The requested object could not be found.' or
+ result.msg =='The inventories test-non-existing-inventory was not found on the Tower server'"
diff --git a/awx_collection/tests/integration/targets/tower_host/tasks/main.yml b/awx_collection/tests/integration/targets/tower_host/tasks/main.yml
new file mode 100644
index 0000000000..90689be20c
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_host/tasks/main.yml
@@ -0,0 +1,48 @@
+---
+- name: Generate an inventory name
+ set_fact:
+ inv_name: "inv-for-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
+
+- name: Create an Inventory
+ tower_inventory:
+ name: "{{ inv_name }}"
+ organization: Default
+ state: present
+
+- name: Create a Host
+ tower_host:
+ name: "some-host"
+ inventory: "{{ inv_name }}"
+ state: present
+ variables:
+ foo: bar
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Host
+ tower_host:
+ name: "some-host"
+ inventory: "{{ inv_name }}"
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Check module fails with correct msg
+ tower_host:
+ name: test-host
+ description: Host Description
+ inventory: test-non-existing-inventory
+ state: present
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='The inventories test-non-existing-inventory was not found on the Tower server' or
+ result.msg =='Failed to update host, inventory not found: The requested object could not be found.'"
diff --git a/awx_collection/tests/integration/targets/tower_inventory/tasks/main.yml b/awx_collection/tests/integration/targets/tower_inventory/tasks/main.yml
new file mode 100644
index 0000000000..6430afe427
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_inventory/tasks/main.yml
@@ -0,0 +1,79 @@
+---
+- name: Clean up any pre-existing test Inventory
+ tower_inventory:
+ name: my-inventory
+ organization: Default
+ state: absent
+ ignore_errors: true
+
+
+- name: Create an Inventory
+ tower_inventory:
+ name: my-inventory
+ organization: Default
+ state: present
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Test Inventory module idempotency
+ tower_inventory:
+ name: my-inventory
+ organization: Default
+ state: present
+ register: result
+
+- assert:
+ that:
+ - "result is not changed"
+
+- name: Fail Change Regular to Smart
+ tower_inventory:
+ name: my-inventory
+ organization: Default
+ kind: smart
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result is failed"
+
+- name: Delete an Inventory
+ tower_inventory:
+ name: my-inventory
+ organization: Default
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Non-Existent Inventory
+ tower_inventory:
+ name: my-inventory
+ organization: Default
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is not changed"
+
+- name: Check module fails with correct msg
+ tower_inventory:
+ name: test-inventory
+ description: Inventory Description
+ organization: test-non-existing-org
+ state: present
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result is not changed"
+ - "result.msg =='Failed to update inventory, organization not found: The requested object could not be found.'
+ or result.msg =='The organizations test-non-existing-org was not found on the Tower server'"
diff --git a/awx_collection/tests/integration/targets/tower_inventory_source/tasks/main.yml b/awx_collection/tests/integration/targets/tower_inventory_source/tasks/main.yml
new file mode 100644
index 0000000000..5cd545bf6c
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_inventory_source/tasks/main.yml
@@ -0,0 +1,49 @@
+---
+- name: Add a Tower credential
+ tower_credential:
+ description: Credentials for Openstack Test project
+ name: openstack-test-credential
+ kind: openstack
+ organization: Default
+ project: Test
+ username: admin
+ host: https://example.org:5000
+ password: passw0rd
+ domain: test
+
+- name: Add a Tower inventory
+ tower_inventory:
+ description: Test inventory
+ organization: Default
+ name: openstack-test-inventory
+
+- name: Create a source inventory
+ tower_inventory_source:
+ name: "source-test-inventory {{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
+ description: Source for Test inventory
+ inventory: openstack-test-inventory
+ credential: openstack-test-credential
+ overwrite: true
+ update_on_launch: true
+ source_vars:
+ private: false
+ source: openstack
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete the source inventory
+ tower_inventory_source:
+ name: "{{ result.name }}"
+ description: Source for Test inventory
+ inventory: openstack-test-inventory
+ credential: openstack-test-credential
+ overwrite: true
+ update_on_launch: true
+ source_vars:
+ private: false
+ source: openstack
+ state: absent
+ ignore_errors: true
diff --git a/awx_collection/tests/integration/targets/tower_job_cancel/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_cancel/tasks/main.yml
new file mode 100644
index 0000000000..d949610d98
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_job_cancel/tasks/main.yml
@@ -0,0 +1,40 @@
+---
+- name: Launch a Job Template
+ tower_job_launch:
+ job_template: "Demo Job Template"
+ register: job
+
+- assert:
+ that:
+ - "job is changed"
+
+- name: Cancel the job
+ tower_job_cancel:
+ job_id: "{{ job.id }}"
+ register: results
+
+- assert:
+ that:
+ - results is changed
+
+- name: Cancel an already canceled job (assert failure)
+ tower_job_cancel:
+ job_id: "{{ job.id }}"
+ fail_if_not_running: true
+ register: results
+ ignore_errors: true
+
+- assert:
+ that:
+ - results is failed
+
+- name: Check module fails with correct msg
+ tower_job_cancel:
+ job_id: 9999999999
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='Unable to cancel job_id/9999999999: The requested object could not be found.'
+ or result.msg =='Unable to find job with id 9999999999'"
diff --git a/awx_collection/tests/integration/targets/tower_job_launch/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_launch/tasks/main.yml
new file mode 100644
index 0000000000..cd64fee9d0
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_job_launch/tasks/main.yml
@@ -0,0 +1,102 @@
+---
+- name: Launch a Job Template
+ tower_job_launch:
+ job_template: "Demo Job Template"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+ - "result.status == 'pending'"
+
+- name: Wait for a job template to complete
+ tower_job_wait:
+ job_id: "{{ result.id }}"
+ max_interval: 10
+ timeout: 120
+ register: result
+
+- assert:
+ that:
+ - "result is not changed"
+ - "result.status == 'successful'"
+
+- name: Check module fails with correct msg
+ tower_job_launch:
+ job_template: "Non Existing Job Template"
+ inventory: "Test Inventory"
+ credential: "Test Credential"
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='Unable to launch job, job_template/Non Existing Job Template was not found: The requested object could not be found.'
+ or result.msg == 'The inventories Test Inventory was not found on the Tower server'"
+
+- name: Create a Job Template for testing prompt on launch
+ tower_job_template:
+ name: "Demo Job Template - ask inventory and credential"
+ project: Demo Project
+ playbook: hello_world.yml
+ job_type: run
+ ask_credential: true
+ ask_inventory: true
+ state: present
+ register: result
+
+- name: Launch job template with inventory and credential for prompt on launch
+ tower_job_launch:
+ job_template: "Demo Job Template - ask inventory and credential"
+ inventory: "Demo Inventory"
+ credential: "Demo Credential"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+ - "result.status == 'pending'"
+
+- name: Create a project for testing extra_vars
+ tower_project:
+ name: test-playbooks
+ organization: Default
+ scm_type: git
+ scm_url: https://github.com/ansible/test-playbooks
+
+- name: Create a Job Template for testing extra_vars
+ tower_job_template:
+ name: "Demo Job Template - extra_vars"
+ project: test-playbooks
+ playbook: debug.yml
+ job_type: run
+ state: present
+ inventory: "Demo Inventory"
+ extra_vars:
+ foo: bar
+ register: result
+
+- name: Launch job template with inventory and credential for prompt on launch
+ tower_job_launch:
+ job_template: "Demo Job Template - extra_vars"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Get the job
+ tower_job_list:
+ query: {"id": "{{result.id}}"}
+ register: result
+
+- assert:
+ that:
+ - '{"foo": "bar"} | to_json in result.results[0].extra_vars'
+
+- name: Delete the job
+ tower_project:
+ name: "Demo Job Template - extra_vars"
+ organization: Default
+ state: absent
+ ignore_errors: true
diff --git a/awx_collection/tests/integration/targets/tower_job_list/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_list/tasks/main.yml
new file mode 100644
index 0000000000..a883f28d87
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_job_list/tasks/main.yml
@@ -0,0 +1,38 @@
+---
+- name: Launch a Job Template
+ tower_job_launch:
+ job_template: "Demo Job Template"
+ register: job
+
+- assert:
+ that:
+ - "job is changed"
+ - "job.status == 'pending'"
+
+- name: List jobs w/ a matching primary key
+ tower_job_list:
+ query: {"id": "{{ job.id }}"}
+ register: matching_jobs
+
+- assert:
+ that:
+ - "{{ matching_jobs.count }} == 1"
+
+- name: List failed jobs (which don't exist)
+ tower_job_list:
+ status: failed
+ query: {"id": "{{ job.id }}"}
+ register: successful_jobs
+
+- assert:
+ that:
+ - "{{ successful_jobs.count }} == 0"
+
+- name: Get ALL result pages!
+ tower_job_list:
+ all_pages: true
+ register: all_page_query
+
+- assert:
+ that:
+ - 'not all_page_query.next'
diff --git a/awx_collection/tests/integration/targets/tower_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_template/tasks/main.yml
new file mode 100644
index 0000000000..ccd295c0d5
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_job_template/tasks/main.yml
@@ -0,0 +1,31 @@
+---
+- name: Create an SCM Credential
+ tower_credential:
+ name: SCM Credential for JT
+ organization: Default
+ kind: scm
+
+- name: Create a Demo Project
+ tower_project:
+ name: Job Template Test Project
+ organization: Default
+ state: present
+ scm_type: git
+ scm_url: https://github.com/ansible/ansible-tower-samples.git
+ scm_credential: SCM Credential for JT
+ register: result
+
+- name: Create a Job Template
+ tower_job_template:
+ name: "hello-world {{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
+ project: Job Template Test Project
+ inventory: Demo Inventory
+ playbook: hello_world.yml
+ credential: Demo Credential
+ job_type: run
+ state: present
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
diff --git a/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml
new file mode 100644
index 0000000000..589ea8629f
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml
@@ -0,0 +1,25 @@
+---
+- name: Launch a Job Template
+ tower_job_launch:
+ job_template: "Demo Job Template"
+ register: job
+
+- assert:
+ that:
+ - "job is changed"
+ - "job.status == 'pending'"
+
+- name: Wait for the Job to finish
+ tower_job_wait:
+ job_id: "{{ job.id }}"
+ timeout: 60
+
+- name: Check module fails with correct msg
+ tower_job_wait:
+ job_id: "99999999"
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='Unable to wait, no job_id 99999999 found: The requested object could not be found.'"
diff --git a/awx_collection/tests/integration/targets/tower_label/tasks/main.yml b/awx_collection/tests/integration/targets/tower_label/tasks/main.yml
new file mode 100644
index 0000000000..76b65c62e7
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_label/tasks/main.yml
@@ -0,0 +1,20 @@
+---
+- name: Create a Label
+ tower_label:
+ name: important
+ organization: Default
+ state: present
+
+- name: Check module fails with correct msg
+ tower_label:
+ name: "Test Label"
+ organization: "Non existing org"
+ state: present
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg == 'Failed to update label, organization not found: The requested object could not be found.'"
+
+# TODO: Deleting labels doesn't seem to work currently
diff --git a/awx_collection/tests/integration/targets/tower_notification/tasks/main.yml b/awx_collection/tests/integration/targets/tower_notification/tasks/main.yml
new file mode 100644
index 0000000000..277fee7a81
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_notification/tasks/main.yml
@@ -0,0 +1,206 @@
+---
+- name: Create Slack notification
+ tower_notification:
+ name: notification1
+ organization: Default
+ notification_type: slack
+ token: a_token
+ channels:
+ - general
+ state: present
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Delete Slack notification
+ tower_notification:
+ name: notification1
+ organization: Default
+ notification_type: slack
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Add webhook notification
+ tower_notification:
+ name: notification2
+ organization: Default
+ notification_type: webhook
+ url: http://www.example.com/hook
+ headers:
+ X-Custom-Header: value123
+ state: present
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Delete webhook notification
+ tower_notification:
+ name: notification2
+ organization: Default
+ notification_type: webhook
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Add email notification
+ tower_notification:
+ name: notification3
+ organization: Default
+ notification_type: email
+ username: user
+ password: s3cr3t
+ sender: tower@example.com
+ recipients:
+ - user1@example.com
+ host: smtp.example.com
+ port: 25
+ use_tls: false
+ use_ssl: false
+ state: present
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Delete email notification
+ tower_notification:
+ name: notification3
+ organization: Default
+ notification_type: email
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Add twilio notification
+ tower_notification:
+ name: notification4
+ organization: Default
+ notification_type: twilio
+ account_token: a_token
+ account_sid: a_sid
+ from_number: '+15551112222'
+ to_numbers:
+ - '+15553334444'
+ state: present
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Delete twilio notification
+ tower_notification:
+ name: notification4
+ organization: Default
+ notification_type: twilio
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Add PagerDuty notification
+ tower_notification:
+ name: notification5
+ organization: Default
+ notification_type: pagerduty
+ token: a_token
+ subdomain: sub
+ client_name: client
+ service_key: a_key
+ state: present
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Delete PagerDuty notification
+ tower_notification:
+ name: notification5
+ organization: Default
+ notification_type: pagerduty
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Add HipChat notification
+ tower_notification:
+ name: notification6
+ organization: Default
+ notification_type: hipchat
+ token: a_token
+ message_from: user1
+ api_url: https://hipchat.example.com
+ color: red
+ rooms:
+ - room-A
+ notify: true
+ state: present
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Delete HipChat notification
+ tower_notification:
+ name: notification6
+ organization: Default
+ notification_type: hipchat
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Add IRC notification
+ tower_notification:
+ name: notification7
+ organization: Default
+ notification_type: irc
+ nickname: tower
+ password: s3cr3t
+ targets:
+ - user1
+ port: 8080
+ server: irc.example.com
+ use_ssl: false
+ state: present
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Delete IRC notification
+ tower_notification:
+ name: notification7
+ organization: Default
+ notification_type: irc
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - result is changed
diff --git a/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml b/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml
new file mode 100644
index 0000000000..b7e16935ef
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml
@@ -0,0 +1,110 @@
+---
+- name: Generate an org name
+ set_fact:
+ org_name: "org-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
+
+- name: Make sure {{ org_name }} is not there
+ tower_organization:
+ name: "{{ org_name }}"
+ state: absent
+ register: result
+
+- name: "Create a new organization"
+ tower_organization:
+ name: "{{ org_name }}"
+ register: result
+
+- assert:
+ that: "result is changed"
+
+- name: "Make sure making the same org is not a change"
+ tower_organization:
+ name: "{{ org_name }}"
+ register: result
+
+- assert:
+ that:
+ - "result is not changed"
+
+- name: "Try adding a bad custom_virtualenv"
+ tower_organization:
+ name: "{{ org_name }}"
+ custom_virtualenv: "/does/not/exit"
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result is failed"
+
+- name: "Pass in all parameters"
+ tower_organization:
+ name: "{{ org_name }}"
+ description: "A description"
+ custom_virtualenv: ""
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: "Change the description"
+ tower_organization:
+ name: "{{ org_name }}"
+ description: "A new description"
+ custom_virtualenv: ""
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: "Remove the organization"
+ tower_organization:
+ name: "{{ org_name }}"
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: "Remove a missing organization"
+ tower_organization:
+ name: "{{ org_name }}"
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is not changed"
+
+# Test behaviour common to all tower modules
+- name: Check that SSL is available
+ tower_organization:
+ name: Default
+ register: result
+
+- assert:
+ that: result is not changed
+
+- name: Check that SSL is available and verify_ssl is enabled (task must fail)
+ tower_organization:
+ name: Default
+ validate_certs: true
+ environment:
+ TOWER_CERTIFICATE: /dev/null # force check failure
+ ignore_errors: true
+ register: check_ssl_is_used
+
+- name: Check that connection failed
+ assert:
+ that:
+ - check_ssl_is_used is failed
+
+- name: Check that verify_ssl is disabled (task must not fail)
+ tower_organization:
+ name: Default
+ validate_certs: false
+ environment:
+ TOWER_CERTIFICATE: /dev/null # should not fail because verify_ssl is disabled
diff --git a/awx_collection/tests/integration/targets/tower_project/tasks/main.yml b/awx_collection/tests/integration/targets/tower_project/tasks/main.yml
new file mode 100644
index 0000000000..c6d5d3230f
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_project/tasks/main.yml
@@ -0,0 +1,115 @@
+---
+- name: Delete old git project from any previous test runs
+ tower_project:
+ name: "git project"
+ organization: Default
+ state: absent
+ ignore_errors: true
+
+- name: Create an SCM Credential
+ tower_credential:
+ name: SCM Credential for Project
+ organization: Default
+ kind: scm
+
+- name: Create a git project without credentials without waiting
+ tower_project:
+ name: "git project"
+ organization: Default
+ scm_type: git
+ scm_url: https://github.com/ansible/test-playbooks
+ wait: false
+ register: result
+
+- assert:
+ that:
+ - result is changed
+
+- name: Recreate the project to validate not changed
+ tower_project:
+ name: "git project"
+ organization: Default
+ scm_type: git
+ scm_url: https://github.com/ansible/test-playbooks
+ wait: true
+ register: result
+
+- assert:
+ that:
+ - result is not changed
+
+- name: Create organizations
+ tower_organization:
+ name: TestOrg
+
+- name: Create credential
+ tower_credential:
+ kind: scm
+ name: TestCred
+ organization: TestOrg
+
+ register: new_credentials
+
+- name: Generate random project name appender
+ set_fact:
+ project_name_rand: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
+
+- name: Create a new test project in check_mode
+ tower_project:
+ name: "TestProject {{ project_name_rand }}"
+ organization: TestOrg
+ scm_type: git
+ scm_url: https://github.com/ansible/test-playbooks
+ scm_credential: TestCred
+ check_mode: true
+
+- name: Create a new test project
+ tower_project:
+ name: "TestProject {{ project_name_rand }}"
+ organization: TestOrg
+ scm_type: git
+ scm_url: https://github.com/ansible/test-playbooks
+ scm_credential: TestCred
+ register: result
+
+# If this fails it may be because the check_mode task actually already created
+# the project, or it could be because the module actually failed somehow
+- assert:
+ that:
+ - "result is changed"
+
+- name: Check module fails with correct msg when given non-existing org as param
+ tower_project:
+ name: "TestProject {{ project_name_rand }}"
+ organization: Non Existing Org
+ scm_type: git
+ scm_url: https://github.com/ansible/test-playbooks
+ scm_credential: TestCred
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg == 'The organizations Non Existing Org was not found on the Tower server' or
+ result.msg == 'Failed to update project, organization not found: Non Existing Org'"
+
+- name: Check module fails with correct msg when given non-existing credential as param
+ tower_project:
+ name: "TestProject {{ project_name_rand }}"
+ organization: TestOrg
+ scm_type: git
+ scm_url: https://github.com/ansible/test-playbooks
+ scm_credential: Non Existing Credential
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='The credentials Non Existing Credential was not found on the Tower server' or
+ result.msg =='Failed to update project, credential not found: Non Existing Credential'"
+
+- name: Delete the test project
+ tower_project:
+ name: "TestProject {{ project_name_rand }}"
+ organization: TestOrg
+ state: absent
diff --git a/awx_collection/tests/integration/targets/tower_project_manual/tasks/create_project_dir.yml b/awx_collection/tests/integration/targets/tower_project_manual/tasks/create_project_dir.yml
new file mode 100644
index 0000000000..7f5b3b49c2
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_project_manual/tasks/create_project_dir.yml
@@ -0,0 +1,58 @@
+---
+- name: get tower host variable
+ shell: tower-cli config host | cut -d ' ' -f2
+ register: host
+
+- name: get tower username variable
+ shell: tower-cli config username | cut -d ' ' -f2
+ register: username
+
+- name: get tower password variable
+ shell: tower-cli config password | cut -d ' ' -f2
+ register: password
+
+- name: Fetch project_base_dir
+ uri:
+ url: "{{ host.stdout }}/api/v2/config/"
+ user: "{{ username.stdout }}"
+ password: "{{ password.stdout }}"
+ validate_certs: false
+ return_content: true
+ force_basic_auth: true
+ register: awx_config
+
+- tower_inventory:
+ name: localhost
+ organization: Default
+
+- tower_host:
+ name: localhost
+ inventory: localhost
+ variables:
+ ansible_connection: local
+
+- name: create an unused SSH / Machine credential
+ tower_credential:
+ name: dummy
+ kind: ssh
+ ssh_key_data: |
+ -----BEGIN EC PRIVATE KEY-----
+ MHcCAQEEIIUl6R1xgzR6siIUArz4XBPtGZ09aetma2eWf1v3uYymoAoGCCqGSM49
+ AwEHoUQDQgAENJNjgeZDAh/+BY860s0yqrLDprXJflY0GvHIr7lX3ieCtrzOMCVU
+ QWzw35pc5tvuP34SSi0ZE1E+7cVMDDOF3w==
+ -----END EC PRIVATE KEY-----
+ organization: Default
+
+- name: Disable bubblewrap
+ command: tower-cli setting modify AWX_PROOT_ENABLED false
+
+- block:
+ - name: Create a directory for manual project
+ vars:
+ project_base_dir: "{{ awx_config.json.project_base_dir }}"
+ command: tower-cli ad_hoc launch --wait --inventory localhost
+ --credential dummy --module-name command
+ --module-args "mkdir -p {{ project_base_dir }}/{{ project_dir_name }}"
+ always:
+ - name: enable bubblewrap
+ command: tower-cli setting modify AWX_PROOT_ENABLED true
diff --git a/awx_collection/tests/integration/targets/tower_project_manual/tasks/main.yml b/awx_collection/tests/integration/targets/tower_project_manual/tasks/main.yml
new file mode 100644
index 0000000000..8718219efb
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_project_manual/tasks/main.yml
@@ -0,0 +1,37 @@
+---
+- name: generate random string for project
+ set_fact:
+ rand_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
+- name: Generate manual project dir name
+ set_fact:
+ project_name: "manual project {{ rand_string }}"
+
+- name: Generate manual project dir name
+ set_fact:
+ project_dir_name: "proj_{{ rand_string }}"
+
+- name: create a project directory for manual project
+ import_tasks: create_project_dir.yml
+
+- name: Create a manual project
+ tower_project:
+ name: "{{ project_name }}"
+ organization: Default
+ scm_type: manual
+ local_path: "{{ project_dir_name }}"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a manual project
+ tower_project:
+ name: "{{ project_name }}"
+ organization: Default
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
diff --git a/awx_collection/tests/integration/targets/tower_receive/tasks/main.yml b/awx_collection/tests/integration/targets/tower_receive/tasks/main.yml
new file mode 100644
index 0000000000..e592a2bb3a
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_receive/tasks/main.yml
@@ -0,0 +1,18 @@
+---
+- name: Export all Tower assets
+ tower_receive:
+ all: true
+ register: result
+
+- assert:
+ that:
+ - "result is successful"
+
+- name: Extract names from output
+ set_fact:
+ object_names: "{{ result.assets | map(attribute='name') | list }}"
+
+- assert:
+ that:
+ - "result is successful"
+ - "'Default' in object_names"
diff --git a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml
new file mode 100644
index 0000000000..f35e3d5846
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml
@@ -0,0 +1,40 @@
+---
+- name: Create a User
+ tower_user:
+ first_name: Joe
+ last_name: User
+ username: joe
+ password: "{{ 65535 | random | to_uuid }}"
+ email: joe@example.org
+ state: present
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Add Joe to the update role of the default Project
+ tower_role:
+ user: joe
+ role: update
+ project: Demo Project
+ state: "{{ item }}"
+ register: result
+ with_items:
+ - "present"
+ - "absent"
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a User
+ tower_user:
+ username: joe
+ email: joe@example.org
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
diff --git a/awx_collection/tests/integration/targets/tower_send/tasks/main.yml b/awx_collection/tests/integration/targets/tower_send/tasks/main.yml
new file mode 100644
index 0000000000..ac5ba84bbc
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_send/tasks/main.yml
@@ -0,0 +1,81 @@
+---
+- name: Test no parameters
+ tower_send:
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result is failed"
+
+- name: Create user json
+ set_fact:
+ user:
+ - username: "jowestco"
+ first_name: "John"
+ last_name: "Westcott"
+ asset_type: "user"
+ email: "john.westcott.iv@redhat.com"
+
+- name: Test a new import of asset
+ tower_send:
+ assets: "{{ user | to_json() }}"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Test an existing import of asset
+ tower_send:
+ assets: "{{ user | to_json() }}"
+ register: result
+
+- assert:
+ that:
+ - "result is successful"
+ - "result is not changed"
+
+- name: Change an existing asset
+ tower_send:
+ assets: "{{ user | combine({'last_name': 'Westcott IV'}) | to_json() }}"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Ensure the organization is not created
+ tower_organization:
+ name: "Red Hat"
+ state: absent
+
+- name: Create organization json
+ set_fact:
+ organization:
+ - asset_type: organization
+ name: "Red Hat"
+
+- name: Create temp file
+ tempfile:
+ state: file
+ register: my_temp_file
+
+- name: Drop down a file to import
+ copy:
+ dest: "{{ my_temp_file.path }}"
+ content: "{{ organization | to_nice_json() }}"
+
+- name: Create org via files
+ tower_send:
+ files: "{{ my_temp_file.path }}"
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Remove Temp File
+ file:
+ path: "{{ my_temp_file.path }}"
+ state: absent
diff --git a/awx_collection/tests/integration/targets/tower_settings/tasks/main.yml b/awx_collection/tests/integration/targets/tower_settings/tasks/main.yml
new file mode 100644
index 0000000000..a02ca673de
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_settings/tasks/main.yml
@@ -0,0 +1,76 @@
+---
+- name: Set the value of AWX_PROOT_SHOW_PATHS to a baseline
+ tower_settings:
+ name: AWX_PROOT_SHOW_PATHS
+ value: '["/var/lib/awx/projects/"]'
+
+- name: Set the value of AWX_PROOT_SHOW_PATHS to get an error back from Tower
+ tower_settings:
+ settings:
+ AWX_PROOT_SHOW_PATHS:
+ 'not': 'a valid'
+ 'tower': 'setting'
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result is failed"
+
+- name: Set the value of AWX_PROOT_SHOW_PATHS
+ tower_settings:
+ name: AWX_PROOT_SHOW_PATHS
+ value: '["/var/lib/awx/projects/", "/tmp"]'
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Attempt to set the value of AWX_PROOT_BASE_PATH to what it already is
+ tower_settings:
+ name: AWX_PROOT_BASE_PATH
+ value: /tmp
+ register: result
+
+- debug:
+ msg: "{{ result }}"
+
+- assert:
+ that:
+ - "result is not changed"
+
+- name: Apply a single setting via settings
+ tower_settings:
+ name: AWX_PROOT_SHOW_PATHS
+ value: '["/var/lib/awx/projects/", "/var/tmp"]'
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Apply multiple setting via settings with no change
+ tower_settings:
+ settings:
+ AWX_PROOT_BASE_PATH: /tmp
+ AWX_PROOT_SHOW_PATHS: ["/var/lib/awx/projects/", "/var/tmp"]
+ register: result
+
+- debug:
+ msg: "{{ result }}"
+
+- assert:
+ that:
+ - "result is not changed"
+
+- name: Apply multiple setting via settings with change
+ tower_settings:
+ settings:
+ AWX_PROOT_BASE_PATH: /tmp
+ AWX_PROOT_SHOW_PATHS: []
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
diff --git a/awx_collection/tests/integration/targets/tower_team/tasks/main.yml b/awx_collection/tests/integration/targets/tower_team/tasks/main.yml
new file mode 100644
index 0000000000..a7cd6ad16d
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_team/tasks/main.yml
@@ -0,0 +1,49 @@
+---
+- name: Attempt to add a Tower team to a non-existant Organization
+ tower_team:
+ name: Test Team
+ organization: Missing Organization
+ state: present
+ register: result
+ ignore_errors: true
+
+- name: Assert a meaningful error was provided for the failed Tower team creation
+ assert:
+ that:
+ - result is failed
+ - "result.msg =='Failed to update team, organization not found: The requested object could not be found.' or
+ result.msg =='The organizations Missing Organization was not found on the Tower server'"
+
+- name: Create a Tower team
+ tower_team:
+ name: Test Team
+ organization: Default
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Tower team
+ tower_team:
+ name: Test Team
+ organization: Default
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Check module fails with correct msg
+ tower_team:
+ name: Test Team
+ organization: Non Existing Org
+ state: present
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='Failed to update team, organization not found: The requested object could not be found.' or
+ result.msg =='The organizations Non Existing Org was not found on the Tower server'"
diff --git a/awx_collection/tests/integration/targets/tower_user/tasks/main.yml b/awx_collection/tests/integration/targets/tower_user/tasks/main.yml
new file mode 100644
index 0000000000..6360733e8f
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_user/tasks/main.yml
@@ -0,0 +1,95 @@
+---
+- name: Create a User
+ tower_user:
+ first_name: Joe
+ last_name: User
+ username: joe
+ password: "{{ 65535 | random | to_uuid }}"
+ email: joe@example.org
+ state: present
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a User
+ tower_user:
+ username: joe
+ email: joe@example.org
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create an Auditor
+ tower_user:
+ first_name: Joe
+ last_name: Auditor
+ username: joe
+ password: "{{ 65535 | random | to_uuid }}"
+ email: joe@example.org
+ state: present
+ auditor: true
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete an Auditor
+ tower_user:
+ username: joe
+ email: joe@example.org
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Create a Superuser
+ tower_user:
+ first_name: Joe
+ last_name: Super
+ username: joe
+ password: "{{ 65535 | random | to_uuid }}"
+ email: joe@example.org
+ state: present
+ superuser: true
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a Superuser
+ tower_user:
+ username: joe
+ email: joe@example.org
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Test tower SSL parameter
+ tower_user:
+ first_name: Joe
+ last_name: User
+ username: joe
+ password: "{{ 65535 | random | to_uuid }}"
+ email: joe@example.org
+ state: present
+ validate_certs: true
+ tower_host: http://foo.invalid
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - "'Unable to resolve tower_host' in result.msg or
+ 'Can not verify ssl with non-https protocol' in result.exception"
diff --git a/awx_collection/tests/integration/targets/tower_workflow_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_template/tasks/main.yml
new file mode 100644
index 0000000000..201c452890
--- /dev/null
+++ b/awx_collection/tests/integration/targets/tower_workflow_template/tasks/main.yml
@@ -0,0 +1,81 @@
+---
+- name: Create an SCM Credential
+ tower_credential:
+ name: SCM Credential for JT
+ organization: Default
+ kind: scm
+
+- name: Create a Demo Project
+ tower_project:
+ name: Job Template Test Project
+ organization: Default
+ state: present
+ scm_type: git
+ scm_url: https://github.com/ansible/ansible-tower-samples.git
+ scm_credential: SCM Credential for JT
+ register: result
+
+- name: Create a Job Template
+ tower_job_template:
+ name: my-job-1
+ project: Job Template Test Project
+ inventory: Demo Inventory
+ playbook: hello_world.yml
+ credential: Demo Credential
+ job_type: run
+ state: present
+
+- name: Create a second Job Template
+ tower_job_template:
+ name: my-job-2
+ project: Job Template Test Project
+ inventory: Demo Inventory
+ playbook: hello_world.yml
+ credential: Demo Credential
+ job_type: run
+ state: present
+
+- name: Add a Survey to second Job Template
+ tower_job_template:
+ name: my-job-2
+ project: Job Template Test Project
+ inventory: Demo Inventory
+ playbook: hello_world.yml
+ credential: Demo Credential
+ job_type: run
+ state: present
+ survey_enabled: true
+ survey_spec: '{"spec": [{"index": 0, "question_name": "my question?", "default": "mydef", "variable": "myvar", "type": "text", "required": false}], "description": "test", "name": "test"}'
+
+
+- name: Create a workflow job template
+ tower_workflow_template:
+ name: my-workflow
+ schema: '[{"success": [{"job_template": "my-job-1"}], "job_template": "my-job-2"}]'
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Delete a workflow job template
+ tower_workflow_template:
+ name: my-workflow
+ state: absent
+ register: result
+
+- assert:
+ that:
+ - "result is changed"
+
+- name: Check module fails with correct msg
+ tower_workflow_template:
+ name: my-workflow
+ organization: Non Existing Organization
+ schema: '[{"success": [{"job_template": "my-job-1"}], "job_template": "my-job-2"}]'
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - "result.msg =='Failed to update organization source,organization not found: The requested object could not be found.'"