mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 18:09:57 -03:30
Merge branch 'downstream' into devel
This commit is contained in:
commit
d11dfd0a2b
@ -31,7 +31,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import PermissionDenied, ParseError
|
||||
from rest_framework.exceptions import APIException, PermissionDenied, ParseError, NotFound
|
||||
from rest_framework.parsers import FormParser
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer
|
||||
@ -1613,17 +1613,58 @@ class HostActivityStreamList(SubListAPIView):
|
||||
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
|
||||
|
||||
|
||||
class BadGateway(APIException):
|
||||
status_code = status.HTTP_502_BAD_GATEWAY
|
||||
default_detail = ''
|
||||
default_code = 'bad_gateway'
|
||||
|
||||
|
||||
class GatewayTimeout(APIException):
|
||||
status_code = status.HTTP_504_GATEWAY_TIMEOUT
|
||||
default_detail = ''
|
||||
default_code = 'gateway_timeout'
|
||||
|
||||
|
||||
class HostInsights(GenericAPIView):
|
||||
|
||||
model = models.Host
|
||||
serializer_class = serializers.EmptySerializer
|
||||
|
||||
def _extract_insights_creds(self, credential):
|
||||
return (credential.get_input('username', default=''), credential.get_input('password', default=''))
|
||||
def _call_insights_api(self, url, session, headers):
|
||||
try:
|
||||
res = session.get(url, headers=headers, timeout=120)
|
||||
except requests.exceptions.SSLError:
|
||||
raise BadGateway(_('SSLError while trying to connect to {}').format(url))
|
||||
except requests.exceptions.Timeout:
|
||||
raise GatewayTimeout(_('Request to {} timed out.').format(url))
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise BadGateway(_('Unknown exception {} while trying to GET {}').format(e, url))
|
||||
|
||||
def _get_insights(self, url, username, password):
|
||||
if res.status_code == 401:
|
||||
raise BadGateway(
|
||||
_('Unauthorized access. Please check your Insights Credential username and password.'))
|
||||
elif res.status_code != 200:
|
||||
raise BadGateway(
|
||||
_(
|
||||
'Failed to access the Insights API at URL {}.'
|
||||
' Server responded with {} status code and message {}'
|
||||
).format(url, res.status_code, res.content)
|
||||
)
|
||||
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
raise BadGateway(
|
||||
_('Expected JSON response from Insights at URL {}'
|
||||
' but instead got {}').format(url, res.content))
|
||||
|
||||
def _get_session(self, username, password):
|
||||
session = requests.Session()
|
||||
session.auth = requests.auth.HTTPBasicAuth(username, password)
|
||||
|
||||
return session
|
||||
|
||||
def _get_headers(self):
|
||||
license = get_license(show_key=False).get('license_type', 'UNLICENSED')
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
@ -1633,47 +1674,84 @@ class HostInsights(GenericAPIView):
|
||||
license
|
||||
)
|
||||
}
|
||||
return session.get(url, headers=headers, timeout=120)
|
||||
|
||||
def get_insights(self, url, username, password):
|
||||
return headers
|
||||
|
||||
def _get_platform_info(self, host, session, headers):
|
||||
url = '{}/api/inventory/v1/hosts?insights_id={}'.format(
|
||||
settings.INSIGHTS_URL_BASE, host.insights_system_id)
|
||||
res = self._call_insights_api(url, session, headers)
|
||||
try:
|
||||
res = self._get_insights(url, username, password)
|
||||
except requests.exceptions.SSLError:
|
||||
return (dict(error=_('SSLError while trying to connect to {}').format(url)), status.HTTP_502_BAD_GATEWAY)
|
||||
except requests.exceptions.Timeout:
|
||||
return (dict(error=_('Request to {} timed out.').format(url)), status.HTTP_504_GATEWAY_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return (dict(error=_('Unknown exception {} while trying to GET {}').format(e, url)), status.HTTP_502_BAD_GATEWAY)
|
||||
res['results'][0]['id']
|
||||
except (IndexError, KeyError):
|
||||
raise NotFound(
|
||||
_('Could not translate Insights system ID {}'
|
||||
' into an Insights platform ID.').format(host.insights_system_id))
|
||||
|
||||
if res.status_code == 401:
|
||||
return (dict(error=_('Unauthorized access. Please check your Insights Credential username and password.')), status.HTTP_502_BAD_GATEWAY)
|
||||
elif res.status_code != 200:
|
||||
return (dict(error=_(
|
||||
'Failed to gather reports and maintenance plans from Insights API at URL {}. Server responded with {} status code and message {}'
|
||||
).format(url, res.status_code, res.content)), status.HTTP_502_BAD_GATEWAY)
|
||||
return res['results'][0]
|
||||
|
||||
try:
|
||||
filtered_insights_content = filter_insights_api_response(res.json())
|
||||
return (dict(insights_content=filtered_insights_content), status.HTTP_200_OK)
|
||||
except ValueError:
|
||||
return (dict(error=_('Expected JSON response from Insights but instead got {}').format(res.content)), status.HTTP_502_BAD_GATEWAY)
|
||||
def _get_reports(self, platform_id, session, headers):
|
||||
url = '{}/api/insights/v1/system/{}/reports/'.format(
|
||||
settings.INSIGHTS_URL_BASE, platform_id)
|
||||
|
||||
return self._call_insights_api(url, session, headers)
|
||||
|
||||
def _get_remediations(self, platform_id, session, headers):
|
||||
url = '{}/api/remediations/v1/remediations?system={}'.format(
|
||||
settings.INSIGHTS_URL_BASE, platform_id)
|
||||
|
||||
remediations = []
|
||||
|
||||
# Iterate over all of the pages of content.
|
||||
while url:
|
||||
data = self._call_insights_api(url, session, headers)
|
||||
remediations.extend(data['data'])
|
||||
|
||||
url = data['links']['next'] # Will be `None` if this is the last page.
|
||||
|
||||
return remediations
|
||||
|
||||
def _get_insights(self, host, session, headers):
|
||||
platform_info = self._get_platform_info(host, session, headers)
|
||||
platform_id = platform_info['id']
|
||||
reports = self._get_reports(platform_id, session, headers)
|
||||
remediations = self._get_remediations(platform_id, session, headers)
|
||||
|
||||
return {
|
||||
'insights_content': filter_insights_api_response(platform_info, reports, remediations)
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
host = self.get_object()
|
||||
cred = None
|
||||
|
||||
if host.insights_system_id is None:
|
||||
return Response(dict(error=_('This host is not recognized as an Insights host.')), status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
dict(error=_('This host is not recognized as an Insights host.')),
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
if host.inventory and host.inventory.insights_credential:
|
||||
cred = host.inventory.insights_credential
|
||||
else:
|
||||
return Response(dict(error=_('The Insights Credential for "{}" was not found.').format(host.inventory.name)), status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
dict(error=_('The Insights Credential for "{}" was not found.').format(host.inventory.name)),
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
url = settings.INSIGHTS_URL_BASE + '/r/insights/v3/systems/{}/reports/'.format(host.insights_system_id)
|
||||
(username, password) = self._extract_insights_creds(cred)
|
||||
(msg, err_code) = self.get_insights(url, username, password)
|
||||
return Response(msg, status=err_code)
|
||||
username = cred.get_input('username', default='')
|
||||
password = cred.get_input('password', default='')
|
||||
session = self._get_session(username, password)
|
||||
headers = self._get_headers()
|
||||
|
||||
data = self._get_insights(host, session, headers)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
def handle_exception(self, exc):
|
||||
# Continue supporting the slightly different way we have handled error responses on this view.
|
||||
response = super().handle_exception(exc)
|
||||
response.data['error'] = response.data.pop('detail')
|
||||
return response
|
||||
|
||||
|
||||
class GroupList(ListCreateAPIView):
|
||||
|
||||
@ -213,7 +213,7 @@ def copy_tables(since, full_path):
|
||||
main_jobevent.uuid,
|
||||
main_jobevent.parent_uuid,
|
||||
main_jobevent.event,
|
||||
main_jobevent.event_data::json->'task_action',
|
||||
main_jobevent.event_data::json->'task_action' AS task_action,
|
||||
main_jobevent.failed,
|
||||
main_jobevent.changed,
|
||||
main_jobevent.playbook,
|
||||
@ -225,7 +225,7 @@ def copy_tables(since, full_path):
|
||||
main_jobevent.host_name
|
||||
FROM main_jobevent
|
||||
WHERE main_jobevent.created > {}
|
||||
ORDER BY main_jobevent.id ASC) to stdout'''.format(since.strftime("'%Y-%m-%d %H:%M:%S'"))
|
||||
ORDER BY main_jobevent.id ASC) TO STDOUT WITH CSV HEADER'''.format(since.strftime("'%Y-%m-%d %H:%M:%S'"))
|
||||
_copy_table(table='events', query=events_query, path=full_path)
|
||||
|
||||
unified_job_query = '''COPY (SELECT main_unifiedjob.id,
|
||||
@ -250,7 +250,7 @@ def copy_tables(since, full_path):
|
||||
WHERE main_unifiedjob.created > {} AND
|
||||
main_unifiedjob.polymorphic_ctype_id = django_content_type.id AND
|
||||
main_unifiedjob.launch_type != 'sync'
|
||||
ORDER BY main_unifiedjob.id ASC) to stdout'''.format(since.strftime("'%Y-%m-%d %H:%M:%S'"))
|
||||
ORDER BY main_unifiedjob.id ASC) TO STDOUT WITH CSV HEADER'''.format(since.strftime("'%Y-%m-%d %H:%M:%S'"))
|
||||
_copy_table(table='unified_jobs', query=unified_job_query, path=full_path)
|
||||
|
||||
unified_job_template_query = '''COPY (SELECT main_unifiedjobtemplate.id,
|
||||
@ -270,7 +270,7 @@ def copy_tables(since, full_path):
|
||||
main_unifiedjobtemplate.status
|
||||
FROM main_unifiedjobtemplate, django_content_type
|
||||
WHERE main_unifiedjobtemplate.polymorphic_ctype_id = django_content_type.id
|
||||
ORDER BY main_unifiedjobtemplate.id ASC) to stdout'''.format(since.strftime("'%Y-%m-%d %H:%M:%S'"))
|
||||
ORDER BY main_unifiedjobtemplate.id ASC) TO STDOUT WITH CSV HEADER'''.format(since.strftime("'%Y-%m-%d %H:%M:%S'"))
|
||||
_copy_table(table='unified_job_template', query=unified_job_template_query, path=full_path)
|
||||
return
|
||||
|
||||
|
||||
@ -44,6 +44,9 @@ INSTANCE_INFO = Info('awx_instance', 'Info about each node in a Tower system', [
|
||||
INSTANCE_LAUNCH_TYPE = Gauge('awx_instance_launch_type_total', 'Type of Job launched', ['node', 'launch_type',])
|
||||
INSTANCE_STATUS = Gauge('awx_instance_status_total', 'Status of Job launched', ['node', 'status',])
|
||||
|
||||
LICENSE_INSTANCE_TOTAL = Gauge('awx_license_instance_total', 'Total number of managed hosts provided by your license')
|
||||
LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining managed hosts provided by your license')
|
||||
|
||||
|
||||
def metrics():
|
||||
license_info = get_license(show_key=False)
|
||||
@ -54,13 +57,15 @@ def metrics():
|
||||
'tower_version': get_awx_version(),
|
||||
'ansible_version': get_ansible_version(),
|
||||
'license_type': license_info.get('license_type', 'UNLICENSED'),
|
||||
'free_instances': str(license_info.get('free instances', 0)),
|
||||
'license_expiry': str(license_info.get('time_remaining', 0)),
|
||||
'pendo_tracking': settings.PENDO_TRACKING_STATE,
|
||||
'external_logger_enabled': str(settings.LOG_AGGREGATOR_ENABLED),
|
||||
'external_logger_type': getattr(settings, 'LOG_AGGREGATOR_TYPE', 'None')
|
||||
})
|
||||
|
||||
LICENSE_INSTANCE_TOTAL.set(str(license_info.get('available_instances', 0)))
|
||||
LICENSE_INSTANCE_FREE.set(str(license_info.get('free_instances', 0)))
|
||||
|
||||
current_counts = counts(None)
|
||||
|
||||
ORG_COUNT.set(current_counts['organization'])
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -4,6 +4,11 @@ import os
|
||||
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
with open(os.path.join(dir_path, 'insights.json')) as data_file:
|
||||
TEST_INSIGHTS_PLANS = json.loads(data_file.read())
|
||||
with open(os.path.join(dir_path, 'insights_hosts.json')) as data_file:
|
||||
TEST_INSIGHTS_HOSTS = json.load(data_file)
|
||||
|
||||
with open(os.path.join(dir_path, 'insights.json')) as data_file:
|
||||
TEST_INSIGHTS_PLANS = json.load(data_file)
|
||||
|
||||
with open(os.path.join(dir_path, 'insights_remediations.json')) as data_file:
|
||||
TEST_INSIGHTS_REMEDIATIONS = json.load(data_file)['data']
|
||||
|
||||
13
awx/main/tests/data/insights_hosts.json
Normal file
13
awx/main/tests/data/insights_hosts.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"total": 1,
|
||||
"count": 1,
|
||||
"page": 1,
|
||||
"per_page": 50,
|
||||
"results": [
|
||||
{
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"insights_id": "22222222-2222-2222-2222-222222222222",
|
||||
"updated": "2019-03-19T21:59:09.213151-04:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
awx/main/tests/data/insights_remediations.json
Normal file
33
awx/main/tests/data/insights_remediations.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "9197ba55-0abc-4028-9bbe-269e530f8bd5",
|
||||
"name": "Fix Critical CVEs",
|
||||
"created_by": {
|
||||
"username": "jharting@redhat.com",
|
||||
"first_name": "Jozef",
|
||||
"last_name": "Hartinger"
|
||||
},
|
||||
"created_at": "2018-12-05T08:19:36.641Z",
|
||||
"updated_by": {
|
||||
"username": "jharting@redhat.com",
|
||||
"first_name": "Jozef",
|
||||
"last_name": "Hartinger"
|
||||
},
|
||||
"updated_at": "2018-12-05T08:19:36.641Z",
|
||||
"issue_count": 0,
|
||||
"system_count": 0,
|
||||
"needs_reboot": true
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"count": 0,
|
||||
"total": 0
|
||||
},
|
||||
"links": {
|
||||
"first": null,
|
||||
"last": null,
|
||||
"next": null,
|
||||
"previous": null
|
||||
}
|
||||
}
|
||||
@ -28,6 +28,8 @@ EXPECTED_VALUES = {
|
||||
'awx_instance_cpu':0.0,
|
||||
'awx_instance_memory':0.0,
|
||||
'awx_instance_info':1.0,
|
||||
'awx_license_instance_total':0,
|
||||
'awx_license_instance_free':0,
|
||||
}
|
||||
|
||||
|
||||
|
||||
135
awx/main/tests/functional/api/test_host_insights.py
Normal file
135
awx/main/tests/functional/api/test_host_insights.py
Normal file
@ -0,0 +1,135 @@
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestHostInsights:
|
||||
def test_insights_bad_host(self, get, hosts, user, mocker):
|
||||
mocker.patch.object(requests.Session, 'get')
|
||||
|
||||
host = hosts(host_count=1)[0]
|
||||
|
||||
url = reverse('api:host_insights', kwargs={'pk': host.pk})
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert response.data['error'] == 'This host is not recognized as an Insights host.'
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_insights_host_missing_from_insights(self, get, hosts, insights_credential, user, mocker):
|
||||
class Response:
|
||||
status_code = 200
|
||||
content = "{'results': []}"
|
||||
|
||||
def json(self):
|
||||
return {'results': []}
|
||||
|
||||
mocker.patch.object(requests.Session, 'get', return_value=Response())
|
||||
|
||||
host = hosts(host_count=1)[0]
|
||||
host.insights_system_id = '123e4567-e89b-12d3-a456-426655440000'
|
||||
host.inventory.insights_credential = insights_credential
|
||||
host.inventory.save()
|
||||
host.save()
|
||||
|
||||
url = reverse('api:host_insights', kwargs={'pk': host.pk})
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert response.data['error'] == (
|
||||
'Could not translate Insights system ID 123e4567-e89b-12d3-a456-426655440000'
|
||||
' into an Insights platform ID.')
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_insights_no_credential(self, get, hosts, user, mocker):
|
||||
mocker.patch.object(requests.Session, 'get')
|
||||
|
||||
host = hosts(host_count=1)[0]
|
||||
host.insights_system_id = '123e4567-e89b-12d3-a456-426655440000'
|
||||
host.save()
|
||||
|
||||
url = reverse('api:host_insights', kwargs={'pk': host.pk})
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert response.data['error'] == 'The Insights Credential for "test-inv" was not found.'
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.parametrize("status_code, exception, error, message", [
|
||||
(502, requests.exceptions.SSLError, 'SSLError while trying to connect to https://myexample.com/whocares/me/', None,),
|
||||
(504, requests.exceptions.Timeout, 'Request to https://myexample.com/whocares/me/ timed out.', None,),
|
||||
(502, requests.exceptions.RequestException, 'booo!', 'Unknown exception booo! while trying to GET https://myexample.com/whocares/me/'),
|
||||
])
|
||||
def test_insights_exception(self, get, hosts, insights_credential, user, mocker, status_code, exception, error, message):
|
||||
mocker.patch.object(requests.Session, 'get', side_effect=exception(error))
|
||||
|
||||
host = hosts(host_count=1)[0]
|
||||
host.insights_system_id = '123e4567-e89b-12d3-a456-426655440000'
|
||||
host.inventory.insights_credential = insights_credential
|
||||
host.inventory.save()
|
||||
host.save()
|
||||
|
||||
url = reverse('api:host_insights', kwargs={'pk': host.pk})
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert response.data['error'] == message or error
|
||||
assert response.status_code == status_code
|
||||
|
||||
def test_insights_unauthorized(self, get, hosts, insights_credential, user, mocker):
|
||||
Response = namedtuple('Response', 'status_code content')
|
||||
mocker.patch.object(requests.Session, 'get', return_value=Response(401, 'mock 401 err msg'))
|
||||
|
||||
host = hosts(host_count=1)[0]
|
||||
host.insights_system_id = '123e4567-e89b-12d3-a456-426655440000'
|
||||
host.inventory.insights_credential = insights_credential
|
||||
host.inventory.save()
|
||||
host.save()
|
||||
|
||||
url = reverse('api:host_insights', kwargs={'pk': host.pk})
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert response.data['error'] == (
|
||||
"Unauthorized access. Please check your Insights Credential username and password.")
|
||||
assert response.status_code == 502
|
||||
|
||||
def test_insights_bad_status(self, get, hosts, insights_credential, user, mocker):
|
||||
Response = namedtuple('Response', 'status_code content')
|
||||
mocker.patch.object(requests.Session, 'get', return_value=Response(500, 'mock 500 err msg'))
|
||||
|
||||
host = hosts(host_count=1)[0]
|
||||
host.insights_system_id = '123e4567-e89b-12d3-a456-426655440000'
|
||||
host.inventory.insights_credential = insights_credential
|
||||
host.inventory.save()
|
||||
host.save()
|
||||
|
||||
url = reverse('api:host_insights', kwargs={'pk': host.pk})
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert response.data['error'].startswith("Failed to access the Insights API at URL")
|
||||
assert "Server responded with 500 status code and message mock 500 err msg" in response.data['error']
|
||||
assert response.status_code == 502
|
||||
|
||||
def test_insights_bad_json(self, get, hosts, insights_credential, user, mocker):
|
||||
class Response:
|
||||
status_code = 200
|
||||
content = 'booo!'
|
||||
|
||||
def json(self):
|
||||
raise ValueError("we do not care what this is")
|
||||
|
||||
mocker.patch.object(requests.Session, 'get', return_value=Response())
|
||||
|
||||
host = hosts(host_count=1)[0]
|
||||
host.insights_system_id = '123e4567-e89b-12d3-a456-426655440000'
|
||||
host.inventory.insights_credential = insights_credential
|
||||
host.inventory.save()
|
||||
host.save()
|
||||
|
||||
url = reverse('api:host_insights', kwargs={'pk': host.pk})
|
||||
response = get(url, user('admin', True))
|
||||
|
||||
assert response.data['error'].startswith("Expected JSON response from Insights at URL")
|
||||
assert 'insights_id=123e4567-e89b-12d3-a456-426655440000' in response.data['error']
|
||||
assert response.data['error'].endswith("but instead got booo!")
|
||||
assert response.status_code == 502
|
||||
@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import pytest
|
||||
import requests
|
||||
from copy import deepcopy
|
||||
from unittest import mock
|
||||
|
||||
@ -11,13 +9,9 @@ from awx.api.views import (
|
||||
ApiVersionRootView,
|
||||
JobTemplateLabelList,
|
||||
InventoryInventorySourcesUpdate,
|
||||
HostInsights,
|
||||
JobTemplateSurveySpec
|
||||
)
|
||||
|
||||
from awx.main.models import (
|
||||
Host,
|
||||
)
|
||||
from awx.main.views import handle_error
|
||||
|
||||
from rest_framework.test import APIRequestFactory
|
||||
@ -122,103 +116,6 @@ class TestInventoryInventorySourcesUpdate:
|
||||
assert response.data == expected
|
||||
|
||||
|
||||
class TestHostInsights():
|
||||
|
||||
@pytest.fixture
|
||||
def patch_parent(self, mocker):
|
||||
mocker.patch('awx.api.generics.GenericAPIView')
|
||||
|
||||
@pytest.mark.parametrize("status_code, exception, error, message", [
|
||||
(502, requests.exceptions.SSLError, 'SSLError while trying to connect to https://myexample.com/whocares/me/', None,),
|
||||
(504, requests.exceptions.Timeout, 'Request to https://myexample.com/whocares/me/ timed out.', None,),
|
||||
(502, requests.exceptions.RequestException, 'booo!', 'Unknown exception booo! while trying to GET https://myexample.com/whocares/me/'),
|
||||
])
|
||||
def test_get_insights_request_exception(self, patch_parent, mocker, status_code, exception, error, message):
|
||||
view = HostInsights()
|
||||
mocker.patch.object(view, '_get_insights', side_effect=exception(error))
|
||||
|
||||
(msg, code) = view.get_insights('https://myexample.com/whocares/me/', 'ignore', 'ignore')
|
||||
assert code == status_code
|
||||
assert msg['error'] == message or error
|
||||
|
||||
def test_get_insights_non_200(self, patch_parent, mocker):
|
||||
view = HostInsights()
|
||||
Response = namedtuple('Response', 'status_code content')
|
||||
mocker.patch.object(view, '_get_insights', return_value=Response(500, 'mock 500 err msg'))
|
||||
|
||||
(msg, code) = view.get_insights('https://myexample.com/whocares/me/', 'ignore', 'ignore')
|
||||
assert msg['error'] == (
|
||||
'Failed to gather reports and maintenance plans from Insights API at URL'
|
||||
' https://myexample.com/whocares/me/. Server responded with 500 status code '
|
||||
'and message mock 500 err msg')
|
||||
|
||||
def test_get_insights_401(self, patch_parent, mocker):
|
||||
view = HostInsights()
|
||||
Response = namedtuple('Response', 'status_code content')
|
||||
mocker.patch.object(view, '_get_insights', return_value=Response(401, ''))
|
||||
|
||||
(msg, code) = view.get_insights('https://myexample.com/whocares/me/', 'ignore', 'ignore')
|
||||
assert msg['error'] == 'Unauthorized access. Please check your Insights Credential username and password.'
|
||||
|
||||
def test_get_insights_malformed_json_content(self, patch_parent, mocker):
|
||||
view = HostInsights()
|
||||
|
||||
class Response():
|
||||
status_code = 200
|
||||
content = 'booo!'
|
||||
|
||||
def json(self):
|
||||
raise ValueError('we do not care what this is')
|
||||
|
||||
mocker.patch.object(view, '_get_insights', return_value=Response())
|
||||
|
||||
(msg, code) = view.get_insights('https://myexample.com/whocares/me/', 'ignore', 'ignore')
|
||||
assert msg['error'] == 'Expected JSON response from Insights but instead got booo!'
|
||||
assert code == 502
|
||||
|
||||
#def test_get_not_insights_host(self, patch_parent, mocker, mock_response_new):
|
||||
#def test_get_not_insights_host(self, patch_parent, mocker):
|
||||
def test_get_not_insights_host(self, mocker):
|
||||
|
||||
view = HostInsights()
|
||||
|
||||
host = Host()
|
||||
host.insights_system_id = None
|
||||
|
||||
mocker.patch.object(view, 'get_object', return_value=host)
|
||||
|
||||
resp = view.get(None)
|
||||
|
||||
assert resp.data['error'] == 'This host is not recognized as an Insights host.'
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_get_no_credential(self, patch_parent, mocker):
|
||||
view = HostInsights()
|
||||
|
||||
class MockInventory():
|
||||
insights_credential = None
|
||||
name = 'inventory_name_here'
|
||||
|
||||
class MockHost():
|
||||
insights_system_id = 'insights_system_id_value'
|
||||
inventory = MockInventory()
|
||||
|
||||
mocker.patch.object(view, 'get_object', return_value=MockHost())
|
||||
|
||||
resp = view.get(None)
|
||||
|
||||
assert resp.data['error'] == 'The Insights Credential for "inventory_name_here" was not found.'
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_get_insights_user_agent(self, patch_parent, mocker):
|
||||
with mock.patch.object(requests.Session, 'get') as get:
|
||||
HostInsights()._get_insights('https://example.org', 'joe', 'example')
|
||||
assert get.call_count == 1
|
||||
args, kwargs = get.call_args_list[0]
|
||||
assert args == ('https://example.org',)
|
||||
assert re.match(r'AWX [^\s]+ \(open\)', kwargs['headers']['User-Agent'])
|
||||
|
||||
|
||||
class TestSurveySpecValidation:
|
||||
|
||||
def test_create_text_encrypted(self):
|
||||
|
||||
@ -3,22 +3,25 @@
|
||||
|
||||
|
||||
from awx.main.utils.insights import filter_insights_api_response
|
||||
from awx.main.tests.data.insights import TEST_INSIGHTS_PLANS
|
||||
from awx.main.tests.data.insights import TEST_INSIGHTS_HOSTS, TEST_INSIGHTS_PLANS, TEST_INSIGHTS_REMEDIATIONS
|
||||
|
||||
|
||||
def test_filter_insights_api_response():
|
||||
actual = filter_insights_api_response(TEST_INSIGHTS_PLANS)
|
||||
actual = filter_insights_api_response(
|
||||
TEST_INSIGHTS_HOSTS['results'][0], TEST_INSIGHTS_PLANS, TEST_INSIGHTS_REMEDIATIONS)
|
||||
|
||||
assert actual['last_check_in'] == '2017-07-21T07:07:29.000Z'
|
||||
assert len(actual['reports']) == 9
|
||||
assert actual['reports'][0]['maintenance_actions'][0]['maintenance_plan']['name'] == "RHEL Demo Infrastructure"
|
||||
assert actual['reports'][0]['maintenance_actions'][0]['maintenance_plan']['maintenance_id'] == 29315
|
||||
assert actual['reports'][0]['rule']['severity'] == 'ERROR'
|
||||
assert actual['reports'][0]['rule']['description'] == 'Remote code execution vulnerability in libresolv via crafted DNS response (CVE-2015-7547)'
|
||||
assert actual['reports'][0]['rule']['category'] == 'Security'
|
||||
assert actual['reports'][0]['rule']['summary'] == ("A critical security flaw in the `glibc` library was found. "
|
||||
"It allows an attacker to crash an application built against "
|
||||
"that library or, potentially, execute arbitrary code with "
|
||||
"privileges of the user running the application.")
|
||||
assert actual['reports'][0]['rule']['ansible_fix'] is False
|
||||
assert actual['last_check_in'] == '2019-03-19T21:59:09.213151-04:00'
|
||||
assert len(actual['reports']) == 5
|
||||
assert len(actual['reports'][0]['maintenance_actions']) == 1
|
||||
assert actual['reports'][0]['maintenance_actions'][0]['name'] == "Fix Critical CVEs"
|
||||
rule = actual['reports'][0]['rule']
|
||||
|
||||
assert rule['severity'] == 'WARN'
|
||||
assert rule['description'] == (
|
||||
"Kernel vulnerable to side-channel attacks in modern microprocessors (CVE-2017-5715/Spectre)")
|
||||
assert rule['category'] == 'Security'
|
||||
assert rule['summary'] == (
|
||||
"A vulnerability was discovered in modern microprocessors supported by the kernel,"
|
||||
" whereby an unprivileged attacker can use this flaw to bypass restrictions to gain read"
|
||||
" access to privileged memory.\nThe issue was reported as [CVE-2017-5715 / Spectre]"
|
||||
"(https://access.redhat.com/security/cve/CVE-2017-5715).\n")
|
||||
|
||||
@ -2,42 +2,46 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
|
||||
def filter_insights_api_response(json):
|
||||
new_json = {}
|
||||
'''
|
||||
'last_check_in',
|
||||
'reports.[].rule.severity',
|
||||
'reports.[].rule.description',
|
||||
'reports.[].rule.category',
|
||||
'reports.[].rule.summary',
|
||||
'reports.[].rule.ansible_fix',
|
||||
'reports.[].rule.ansible',
|
||||
'reports.[].maintenance_actions.[].maintenance_plan.name',
|
||||
'reports.[].maintenance_actions.[].maintenance_plan.maintenance_id',
|
||||
'''
|
||||
# Old Insights API -> New API
|
||||
#
|
||||
# last_check_in is missing entirely, is now provided by a different endpoint
|
||||
# reports[] -> []
|
||||
# reports[].rule.{description,summary} -> [].rule.{description,summary}
|
||||
# reports[].rule.category -> [].rule.category.name
|
||||
# reports[].rule.severity (str) -> [].rule.total_risk (int)
|
||||
|
||||
if 'last_check_in' in json:
|
||||
new_json['last_check_in'] = json['last_check_in']
|
||||
if 'reports' in json:
|
||||
new_json['reports'] = []
|
||||
for rep in json['reports']:
|
||||
new_report = {
|
||||
'rule': {},
|
||||
'maintenance_actions': []
|
||||
}
|
||||
if 'rule' in rep:
|
||||
for k in ['severity', 'description', 'category', 'summary', 'ansible_fix', 'ansible',]:
|
||||
if k in rep['rule']:
|
||||
new_report['rule'][k] = rep['rule'][k]
|
||||
# reports[].rule.{ansible,ansible_fix} appears to be unused
|
||||
# reports[].maintenance_actions[] missing entirely, is now provided
|
||||
# by a different Insights endpoint
|
||||
|
||||
|
||||
def filter_insights_api_response(platform_info, reports, remediations):
|
||||
severity_mapping = {
|
||||
1: 'INFO',
|
||||
2: 'WARN',
|
||||
3: 'ERROR',
|
||||
4: 'CRITICAL'
|
||||
}
|
||||
|
||||
new_json = {
|
||||
'platform_id': platform_info['id'],
|
||||
'last_check_in': platform_info.get('updated'),
|
||||
'reports': [],
|
||||
}
|
||||
for rep in reports:
|
||||
new_report = {
|
||||
'rule': {},
|
||||
'maintenance_actions': remediations
|
||||
}
|
||||
rule = rep.get('rule') or {}
|
||||
for k in ['description', 'summary']:
|
||||
if k in rule:
|
||||
new_report['rule'][k] = rule[k]
|
||||
if 'category' in rule:
|
||||
new_report['rule']['category'] = rule['category']['name']
|
||||
if rule.get('total_risk') in severity_mapping:
|
||||
new_report['rule']['severity'] = severity_mapping[rule['total_risk']]
|
||||
|
||||
new_json['reports'].append(new_report)
|
||||
|
||||
for action in rep.get('maintenance_actions', []):
|
||||
new_action = {'maintenance_plan': {}}
|
||||
if 'maintenance_plan' in action:
|
||||
for k in ['name', 'maintenance_id']:
|
||||
if k in action['maintenance_plan']:
|
||||
new_action['maintenance_plan'][k] = action['maintenance_plan'][k]
|
||||
new_report['maintenance_actions'].append(new_action)
|
||||
|
||||
new_json['reports'].append(new_report)
|
||||
return new_json
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
@ -9,8 +11,11 @@ from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def save_playbook(self, proj_path, plan, content):
|
||||
fname = '{}-{}.yml'.format(plan.get('name', None) or 'insights-plan', plan['maintenance_id'])
|
||||
def save_playbook(self, proj_path, remediation, content):
|
||||
name = remediation.get('name', None) or 'insights-remediation'
|
||||
name = re.sub(r'[^\w\s-]', '', name).strip().lower()
|
||||
name = re.sub(r'[-\s]+', '-', name)
|
||||
fname = '{}-{}.yml'.format(name, remediation['id'])
|
||||
file_path = os.path.join(proj_path, fname)
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(content)
|
||||
@ -18,9 +23,8 @@ class ActionModule(ActionBase):
|
||||
def is_stale(self, proj_path, etag):
|
||||
file_path = os.path.join(proj_path, '.version')
|
||||
try:
|
||||
f = open(file_path, 'r')
|
||||
version = f.read()
|
||||
f.close()
|
||||
with open(file_path, 'r') as f:
|
||||
version = f.read()
|
||||
return version != etag
|
||||
except IOError:
|
||||
return True
|
||||
@ -31,7 +35,6 @@ class ActionModule(ActionBase):
|
||||
f.write(etag)
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
|
||||
self._supports_check_mode = False
|
||||
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
@ -53,35 +56,10 @@ class ActionModule(ActionBase):
|
||||
license
|
||||
)
|
||||
}
|
||||
url = '/api/remediations/v1/remediations'
|
||||
while url:
|
||||
res = session.get('{}{}'.format(insights_url, url), headers=headers, timeout=120)
|
||||
|
||||
|
||||
url = '{}/r/insights/v3/maintenance?ansible=true'.format(insights_url)
|
||||
|
||||
res = session.get(url, headers=headers, timeout=120)
|
||||
|
||||
if res.status_code != 200:
|
||||
result['failed'] = True
|
||||
result['msg'] = (
|
||||
'Expected {} to return a status code of 200 but returned status '
|
||||
'code "{}" instead with content "{}".'.format(url, res.status_code, res.content)
|
||||
)
|
||||
return result
|
||||
|
||||
if 'ETag' in res.headers:
|
||||
version = res.headers['ETag']
|
||||
if version.startswith('"') and version.endswith('"'):
|
||||
version = version[1:-1]
|
||||
else:
|
||||
version = "ETAG_NOT_FOUND"
|
||||
|
||||
if not self.is_stale(proj_path, version):
|
||||
result['changed'] = False
|
||||
result['version'] = version
|
||||
return result
|
||||
|
||||
for item in res.json():
|
||||
url = '{}/r/insights/v3/maintenance/{}/playbook'.format(insights_url, item['maintenance_id'])
|
||||
res = session.get(url, timeout=120)
|
||||
if res.status_code != 200:
|
||||
result['failed'] = True
|
||||
result['msg'] = (
|
||||
@ -89,7 +67,37 @@ class ActionModule(ActionBase):
|
||||
'code "{}" instead with content "{}".'.format(url, res.status_code, res.content)
|
||||
)
|
||||
return result
|
||||
self.save_playbook(proj_path, item, res.content)
|
||||
|
||||
# FIXME: ETags are (maybe?) not yet supported in the new
|
||||
# API, and even if they are we'll need to put some thought
|
||||
# into how to deal with them in combination with pagination.
|
||||
if 'ETag' in res.headers:
|
||||
version = res.headers['ETag']
|
||||
if version.startswith('"') and version.endswith('"'):
|
||||
version = version[1:-1]
|
||||
else:
|
||||
version = "ETAG_NOT_FOUND"
|
||||
|
||||
if not self.is_stale(proj_path, version):
|
||||
result['changed'] = False
|
||||
result['version'] = version
|
||||
return result
|
||||
|
||||
url = res.json()['links']['next'] # will be None if we're on the last page
|
||||
|
||||
for item in res.json()['data']:
|
||||
playbook_url = '{}/api/remediations/v1/remediations/{}/playbook'.format(
|
||||
insights_url, item['id'])
|
||||
res = session.get(playbook_url, timeout=120)
|
||||
if res.status_code != 200:
|
||||
result['failed'] = True
|
||||
result['msg'] = (
|
||||
'Expected {} to return a status code of 200 but returned status '
|
||||
'code "{}" instead with content "{}".'.format(
|
||||
playbook_url, res.status_code, res.content)
|
||||
)
|
||||
return result
|
||||
self.save_playbook(proj_path, item, res.content)
|
||||
|
||||
self.write_version(proj_path, version)
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ function (data, $scope, moment, $state, InventoryData, InsightsService,
|
||||
InventoryData.summary_fields.insights_credential && InventoryData.summary_fields.insights_credential.id) ?
|
||||
InventoryData.summary_fields.insights_credential.id : null;
|
||||
$scope.canRemediate = CanRemediate;
|
||||
$scope.platformId = $scope.reports_dataset.platform_id;
|
||||
}
|
||||
|
||||
function filter(str){
|
||||
@ -40,7 +41,7 @@ function (data, $scope, moment, $state, InventoryData, InsightsService,
|
||||
};
|
||||
|
||||
$scope.viewDataInInsights = function(){
|
||||
window.open(`https://access.redhat.com/insights/inventory?machine=${$scope.$parent.host.insights_system_id}`, '_blank');
|
||||
window.open(`https://cloud.redhat.com/insights/inventory/${$scope.platformId}/insights`, '_blank');
|
||||
};
|
||||
|
||||
$scope.remediateInventory = function(inv_id, insights_credential){
|
||||
|
||||
@ -7,10 +7,10 @@
|
||||
export default function(){
|
||||
return function(plan) {
|
||||
if(plan === null || plan === undefined){
|
||||
return "PLAN: Not Available <a href='https://access.redhat.com/insights/info/' target='_blank'>CREATE A NEW PLAN IN INSIGHTS</a>";
|
||||
return "PLAN: Not Available <a href='https://cloud.redhat.com/insights/remediations/' target='_blank'>CREATE A NEW PLAN IN INSIGHTS</a>";
|
||||
} else {
|
||||
let name = (plan.maintenance_plan.name === null) ? "Unnamed Plan" : plan.maintenance_plan.name;
|
||||
return `<a href="https://access.redhat.com/insights/planner/${plan.maintenance_plan.maintenance_id}" target="_blank">${name} (${plan.maintenance_plan.maintenance_id})</a>`;
|
||||
let name = (plan.name === null) ? "Unnamed Plan" : plan.name;
|
||||
return `<a href="https://cloud.redhat.com/insights/remediations/${plan.id}" target="_blank">${name} (${plan.id})</a>`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,10 +7,10 @@
|
||||
export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath',
|
||||
'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n',
|
||||
'ConfigData', 'resolvedModels', 'scmCredentialType',
|
||||
'ConfigData', 'resolvedModels', 'scmCredentialType', 'insightsCredentialType',
|
||||
function($scope, $location, $stateParams, GenerateForm, ProjectsForm, Rest,
|
||||
Alert, ProcessErrors, GetBasePath, GetProjectPath, GetChoices, Wait, $state,
|
||||
CreateSelect2, i18n, ConfigData, resolvedModels, scmCredentialType) {
|
||||
CreateSelect2, i18n, ConfigData, resolvedModels, scmCredentialType, insightsCredentialType) {
|
||||
|
||||
let form = ProjectsForm(),
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
@ -191,9 +191,13 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
$scope.lookupCredential = function(){
|
||||
// Perform a lookup on the credential_type. Git, Mercurial, and Subversion
|
||||
// all use SCM as their credential type.
|
||||
let lookupCredentialType = scmCredentialType;
|
||||
if ($scope.scm_type.value === 'insights') {
|
||||
lookupCredentialType = insightsCredentialType;
|
||||
}
|
||||
$state.go('.credential', {
|
||||
credential_search: {
|
||||
credential_type: scmCredentialType,
|
||||
credential_type: lookupCredentialType,
|
||||
page_size: '5',
|
||||
page: '1'
|
||||
}
|
||||
|
||||
@ -8,12 +8,12 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
'Alert', 'ProcessErrors', 'GenerateForm', 'Prompt', 'isNotificationAdmin',
|
||||
'GetBasePath', 'GetProjectPath', 'Authorization', 'GetChoices', 'Empty',
|
||||
'Wait', 'ProjectUpdate', '$state', 'CreateSelect2', 'ToggleNotification',
|
||||
'i18n', 'OrgAdminLookup', 'ConfigData', 'scmCredentialType',
|
||||
'i18n', 'OrgAdminLookup', 'ConfigData', 'scmCredentialType', 'insightsCredentialType',
|
||||
function($scope, $rootScope, $stateParams, ProjectsForm, Rest, Alert,
|
||||
ProcessErrors, GenerateForm, Prompt, isNotificationAdmin, GetBasePath,
|
||||
GetProjectPath, Authorization, GetChoices, Empty, Wait, ProjectUpdate,
|
||||
$state, CreateSelect2, ToggleNotification, i18n, OrgAdminLookup,
|
||||
ConfigData, scmCredentialType) {
|
||||
ConfigData, scmCredentialType, insightsCredentialType) {
|
||||
|
||||
let form = ProjectsForm(),
|
||||
defaultUrl = GetBasePath('projects') + $stateParams.project_id + '/',
|
||||
@ -310,10 +310,13 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
$scope.lookupCredential = function(){
|
||||
// Perform a lookup on the credential_type. Git, Mercurial, and Subversion
|
||||
// all use SCM as their credential type.
|
||||
|
||||
let lookupCredentialType = scmCredentialType;
|
||||
if ($scope.scm_type.value === 'insights') {
|
||||
lookupCredentialType = insightsCredentialType;
|
||||
}
|
||||
$state.go('.credential', {
|
||||
credential_search: {
|
||||
credential_type: scmCredentialType,
|
||||
credential_type: lookupCredentialType,
|
||||
page_size: '5',
|
||||
page: '1'
|
||||
}
|
||||
|
||||
@ -36,7 +36,23 @@ function ResolveScmCredentialType (GetBasePath, Rest, ProcessErrors) {
|
||||
});
|
||||
}
|
||||
|
||||
function ResolveInsightsCredentialType (GetBasePath, Rest, ProcessErrors) {
|
||||
Rest.setUrl(GetBasePath('credential_types') + '?name=Insights');
|
||||
|
||||
return Rest.get()
|
||||
.then(({ data }) => {
|
||||
return data.results[0].id;
|
||||
})
|
||||
.catch(({ data, status }) => {
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get credential type data: ' + status
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ResolveScmCredentialType.$inject = ['GetBasePath', 'Rest', 'ProcessErrors'];
|
||||
ResolveInsightsCredentialType.$inject = ['GetBasePath', 'Rest', 'ProcessErrors'];
|
||||
|
||||
|
||||
export default
|
||||
@ -70,6 +86,7 @@ angular.module('Projects', [])
|
||||
const stateIndex = res.states.findIndex(s => s.name === projectsAddName);
|
||||
|
||||
res.states[stateIndex].resolve.scmCredentialType = ResolveScmCredentialType;
|
||||
res.states[stateIndex].resolve.insightsCredentialType = ResolveInsightsCredentialType;
|
||||
|
||||
return res;
|
||||
});
|
||||
@ -113,6 +130,7 @@ angular.module('Projects', [])
|
||||
const stateIndex = res.states.findIndex(s => s.name === projectsEditName);
|
||||
|
||||
res.states[stateIndex].resolve.scmCredentialType = ResolveScmCredentialType;
|
||||
res.states[stateIndex].resolve.insightsCredentialType = ResolveInsightsCredentialType;
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
exports.command = function logout () {
|
||||
const logoutButton = '.at-Layout-topNav i.fa-power-off';
|
||||
this
|
||||
.waitForElementVisible(logoutButton)
|
||||
.click(logoutButton)
|
||||
.findThenClick(logoutButton, 'css')
|
||||
.waitForElementPresent('#login-button');
|
||||
};
|
||||
|
||||
1
awx/ui/test/e2e/e2e-pipeline.groovy
Normal file
1
awx/ui/test/e2e/e2e-pipeline.groovy
Normal file
@ -0,0 +1 @@
|
||||
e2ePipeline()
|
||||
@ -62,7 +62,7 @@ module.exports = {
|
||||
this
|
||||
.waitForElementVisible('#alert-modal-msg')
|
||||
.expect.element('#alert-modal-msg').text.contain(application.name);
|
||||
this.click('#alert_ok_btn');
|
||||
this.findThenClick('#alert_ok_btn', 'css');
|
||||
this.waitForElementNotVisible('#alert-modal-msg');
|
||||
},
|
||||
delete (name) {
|
||||
|
||||
@ -20,6 +20,7 @@ const store = {
|
||||
lastName: `last-admin-${testID}`,
|
||||
password: `admin-${testID}`,
|
||||
username: `admin-${testID}`,
|
||||
usernameDefault: `admin-${testID}`,
|
||||
type: 'administrator',
|
||||
},
|
||||
auditor: {
|
||||
@ -28,6 +29,7 @@ const store = {
|
||||
lastName: `last-auditor-${testID}`,
|
||||
password: `auditor-${testID}`,
|
||||
username: `auditor-${testID}`,
|
||||
usernameDefault: `auditor-${testID}`,
|
||||
type: 'auditor',
|
||||
},
|
||||
user: {
|
||||
@ -36,12 +38,20 @@ const store = {
|
||||
lastName: `last-${testID}`,
|
||||
password: `${testID}`,
|
||||
username: `user-${testID}`,
|
||||
usernameDefault: `user-${testID}`,
|
||||
type: 'normal',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
before: (client, done) => {
|
||||
// generate a unique username on each attempt.
|
||||
const uniqueUser = uuid().substr(0, 8);
|
||||
Object.keys(store).forEach(key => {
|
||||
if ('username' in store[key]) {
|
||||
store[key].username = `${store[key].usernameDefault}-${uniqueUser}`;
|
||||
}
|
||||
});
|
||||
const resources = [
|
||||
getOrganization(store.organization.name),
|
||||
getAuditor(store.auditor.username),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* Websocket tests. These tests verify that items like the sparkline (colored box rows which
|
||||
* display job status) and other status icons update correctly as the jobs progress.
|
||||
*/
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
getInventorySource,
|
||||
@ -160,13 +161,14 @@ module.exports = {
|
||||
.to.be.present.before(AWX_E2E_TIMEOUT_ASYNC);
|
||||
},
|
||||
'Test pending deletion of inventories': client => {
|
||||
getInventorySource('test-pending-delete');
|
||||
const uniqueID = uuid().substr(0, 8);
|
||||
getInventorySource(`test-pending-delete-${uniqueID}`);
|
||||
client
|
||||
.useCss()
|
||||
.navigateTo(`${AWX_E2E_URL}/#/inventories`, false)
|
||||
.waitForElementVisible('.SmartSearch-input')
|
||||
.clearValue('.SmartSearch-input')
|
||||
.setValue('.SmartSearch-input', ['test-pending-delete', client.Keys.ENTER])
|
||||
.setValue('.SmartSearch-input', [`test-pending-delete-${uniqueID}`, client.Keys.ENTER])
|
||||
.pause(AWX_E2E_TIMEOUT_SHORT) // helps prevent flake
|
||||
.findThenClick('.fa-trash-o', 'css')
|
||||
.waitForElementVisible('#prompt_action_btn')
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
getInventorySource,
|
||||
getJobTemplate,
|
||||
@ -7,15 +9,12 @@ import {
|
||||
|
||||
import {
|
||||
AWX_E2E_URL,
|
||||
AWX_E2E_TIMEOUT_LONG
|
||||
} from '../settings';
|
||||
|
||||
let data;
|
||||
const spinny = "//*[contains(@class, 'spinny')]";
|
||||
const workflowSelector = "//a[text()='test-actions-workflow-template']";
|
||||
const workflowVisualizerBtn = "//button[contains(@id, 'workflow_job_template_workflow_visualizer_btn')]";
|
||||
const workflowSearchBar = "//input[contains(@class, 'SmartSearch-input')]";
|
||||
const workflowText = 'name.iexact:"test-actions-workflow-template"';
|
||||
|
||||
const startNodeId = '1';
|
||||
let initialJobNodeId;
|
||||
@ -26,10 +25,6 @@ let leafNodeId;
|
||||
const nodeAdd = "//*[contains(@class, 'WorkflowChart-nodeAddIcon')]";
|
||||
const nodeRemove = "//*[contains(@class, 'WorkflowChart-nodeRemoveIcon')]";
|
||||
|
||||
// one of the jobs or projects or inventories
|
||||
const testActionsJob = "//div[contains(@class, 'List-tableCell') and contains(text(), 'test-actions-job')]";
|
||||
const testActionsJobText = 'name.iexact:"test-actions-job-template"';
|
||||
|
||||
// search bar for visualizer templates
|
||||
const jobSearchBar = "//*[contains(@id, 'workflow-jobs-list')]//input[contains(@class, 'SmartSearch-input')]";
|
||||
|
||||
@ -49,51 +44,50 @@ const deleteConfirmation = "//button[@ng-click='confirmDeleteNode()']";
|
||||
|
||||
const xPathNodeById = (id) => `//*[@id='node-${id}']`;
|
||||
const xPathLinkById = (sourceId, targetId) => `//*[@id='link-${sourceId}-${targetId}']//*[contains(@class, 'WorkflowChart-linkPath')]`;
|
||||
const xPathNodeByName = (name) => `//*[contains(@class, "WorkflowChart-nameText") and contains(text(), "${name}")]/..`;
|
||||
|
||||
module.exports = {
|
||||
before: (client, done) => {
|
||||
// Ensure deterministic state on retries
|
||||
const testID = uuid().substr(0, 8);
|
||||
const namespace = `test-actions-${testID}`;
|
||||
const resources = [
|
||||
getInventorySource('test-actions'),
|
||||
getJobTemplate('test-actions'),
|
||||
getProject('test-actions'),
|
||||
getWorkflowTemplate('test-actions'),
|
||||
getInventorySource(namespace),
|
||||
getJobTemplate(namespace),
|
||||
getProject(namespace),
|
||||
getWorkflowTemplate(namespace),
|
||||
];
|
||||
|
||||
Promise.all(resources)
|
||||
.then(([source, template, project, workflow]) => {
|
||||
data = { source, template, project, workflow };
|
||||
.then(([inventory, template, project, workflow]) => {
|
||||
data = { inventory, template, project, workflow };
|
||||
client
|
||||
.login()
|
||||
.waitForAngular()
|
||||
.resizeWindow(1200, 1000)
|
||||
.navigateTo(`${AWX_E2E_URL}/#/templates`, false)
|
||||
.useXpath()
|
||||
.waitForElementVisible(workflowSearchBar)
|
||||
.setValue(workflowSearchBar, [`name.iexact:"${data.workflow.name}"`])
|
||||
.click('//*[contains(@class, "SmartSearch-searchButton")]')
|
||||
.waitForSpinny(true)
|
||||
.click(`//a[text()="${namespace}-workflow-template"]`)
|
||||
.waitForElementVisible(workflowVisualizerBtn)
|
||||
.click(workflowVisualizerBtn)
|
||||
.waitForSpinny(true);
|
||||
client.waitForElementVisible(xPathNodeByName(`${namespace}-job`));
|
||||
// Grab the ids of the nodes
|
||||
client.getAttribute(xPathNodeByName(`${namespace}-job`), 'id', (res) => {
|
||||
initialJobNodeId = res.value.split('-')[1];
|
||||
});
|
||||
client.getAttribute(xPathNodeByName(`${namespace}-pro`), 'id', (res) => {
|
||||
initialProjectNodeId = res.value.split('-')[1];
|
||||
});
|
||||
client.getAttribute(xPathNodeByName(`${namespace}-inv`), 'id', (res) => {
|
||||
initialInventoryNodeId = res.value.split('-')[1];
|
||||
});
|
||||
done();
|
||||
});
|
||||
client
|
||||
.login()
|
||||
.waitForAngular()
|
||||
.resizeWindow(1200, 1000)
|
||||
.navigateTo(`${AWX_E2E_URL}/#/templates`, false)
|
||||
.useXpath()
|
||||
.waitForElementVisible(workflowSearchBar)
|
||||
.setValue(workflowSearchBar, [workflowText])
|
||||
.click('//*[contains(@class, "SmartSearch-searchButton")]')
|
||||
.waitForSpinny(true)
|
||||
.click('//*[contains(@class, "SmartSearch-clearAll")]')
|
||||
.waitForSpinny(true)
|
||||
.setValue(workflowSearchBar, [workflowText])
|
||||
.click('//*[contains(@class, "SmartSearch-searchButton")]')
|
||||
.waitForSpinny(true)
|
||||
.click(workflowSelector)
|
||||
.waitForSpinny(true)
|
||||
.click(workflowVisualizerBtn);
|
||||
client.waitForElementVisible('//*[contains(@class, "WorkflowChart-nameText") and contains(text(), "test-actions-job")]/..');
|
||||
|
||||
// Grab the ids of the nodes
|
||||
client.getAttribute('//*[contains(@class, "WorkflowChart-nameText") and contains(text(), "test-actions-job")]/..', 'id', (res) => {
|
||||
initialJobNodeId = res.value.split('-')[1];
|
||||
});
|
||||
client.getAttribute('//*[contains(@class, "WorkflowChart-nameText") and contains(text(), "test-actions-project")]/..', 'id', (res) => {
|
||||
initialProjectNodeId = res.value.split('-')[1];
|
||||
});
|
||||
client.getAttribute('//*[contains(@class, "WorkflowChart-nameText") and contains(text(), "test-actions-inventory")]/..', 'id', (res) => {
|
||||
initialInventoryNodeId = res.value.split('-')[1];
|
||||
});
|
||||
},
|
||||
'verify that workflow visualizer new root node can only be set to always': client => {
|
||||
client
|
||||
@ -143,9 +137,9 @@ module.exports = {
|
||||
client
|
||||
.waitForElementVisible(jobSearchBar)
|
||||
.clearValue(jobSearchBar)
|
||||
.setValue(jobSearchBar, [testActionsJobText, client.Keys.ENTER])
|
||||
.setValue(jobSearchBar, [`name.iexact:"${data.template.name}"`, client.Keys.ENTER])
|
||||
.pause(1000)
|
||||
.findThenClick(testActionsJob)
|
||||
.findThenClick(`//div[contains(@class, "List-tableCell") and contains(text(), "${data.template.name}")]`)
|
||||
.pause(1000)
|
||||
.waitForElementNotVisible(spinny)
|
||||
.findThenClick(edgeTypeDropdownBar)
|
||||
@ -174,9 +168,9 @@ module.exports = {
|
||||
client
|
||||
.waitForElementVisible(jobSearchBar)
|
||||
.clearValue(jobSearchBar)
|
||||
.setValue(jobSearchBar, [testActionsJobText, client.Keys.ENTER])
|
||||
.setValue(jobSearchBar, [`name.iexact:"${data.template.name}"`, client.Keys.ENTER])
|
||||
.pause(1000)
|
||||
.findThenClick(testActionsJob)
|
||||
.findThenClick(`//div[contains(@class, "List-tableCell") and contains(text(), "${data.template.name}")]`)
|
||||
.pause(1000)
|
||||
.waitForElementNotVisible(spinny)
|
||||
.findThenClick(edgeTypeDropdownBar)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Thomas Kluyver and contributors
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
docs/licenses/azure-mgmt-cdn.txt
Normal file
21
docs/licenses/azure-mgmt-cdn.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
docs/licenses/azure-mgmt-cosmosdb.txt
Normal file
21
docs/licenses/azure-mgmt-cosmosdb.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
docs/licenses/azure-mgmt-devtestlabs.txt
Normal file
21
docs/licenses/azure-mgmt-devtestlabs.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
docs/licenses/azure-mgmt-hdinsight.txt
Normal file
21
docs/licenses/azure-mgmt-hdinsight.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
docs/licenses/azure-mgmt-loganalytics.txt
Normal file
21
docs/licenses/azure-mgmt-loganalytics.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
docs/licenses/azure-mgmt-redis.txt
Normal file
21
docs/licenses/azure-mgmt-redis.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
docs/licenses/azure-mgmt-servicebus.txt
Normal file
21
docs/licenses/azure-mgmt-servicebus.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,20 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Łukasz Langa and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -1,82 +0,0 @@
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization ("Licensee") accessing and otherwise using this software in source or binary form and its associated documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License Agreement, BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use the Software alone or in any derivative version, provided, however, that the BeOpen Python License is retained in the Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS" basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all respects by the law of the State of California, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between BeOpen and Licensee. This License Agreement does not grant permission to use BeOpen trademarks or trade names in a trademark sense to endorse or promote products or services of Licensee, or any third party. As an exception, the "BeOpen Python" logos available at http://www.pythonlabs.com/logos.html may be used according to the permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee agrees to be bound by the terms and conditions of this License Agreement.
|
||||
|
||||
|
||||
CNRI OPEN SOURCE LICENSE AGREEMENT (for Python 1.6b1)
|
||||
|
||||
IMPORTANT: PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.
|
||||
|
||||
BY CLICKING ON "ACCEPT" WHERE INDICATED BELOW, OR BY COPYING, INSTALLING OR OTHERWISE USING PYTHON 1.6, beta 1 SOFTWARE, YOU ARE DEEMED TO HAVE AGREED TO THE TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT.
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 1.6, beta 1 software in source or binary form and its associated documentation, as released at the www.python.org Internet site on August 4, 2000 ("Python 1.6b1").
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 1.6b1 alone or in any derivative version, provided, however, that CNRIs License Agreement is retained in Python 1.6b1, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
Alternately, in lieu of CNRIs License Agreement, Licensee may substitute the following text (omitting the quotes): "Python 1.6, beta 1, is made available subject to the terms and conditions in CNRIs License Agreement. This Agreement may be located on the Internet using the following unique, persistent identifier (known as a handle): 1895.22/1011. This Agreement may also be obtained from a proxy server on the Internet using the URL:http://hdl.handle.net/1895.22/1011".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on or incorporates Python 1.6b1 or any part thereof, and wants to make the derivative work available to the public as provided herein, then Licensee hereby agrees to indicate in any such work the nature of the modifications made to Python 1.6b1.
|
||||
|
||||
4. CNRI is making Python 1.6b1 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6b1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING PYTHON 1.6b1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by and interpreted in all respects by the law of the State of Virginia, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying, installing or otherwise using Python 1.6b1, Licensee agrees to be bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
and
|
||||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -1,25 +0,0 @@
|
||||
Copyright 2012-2016 Dmitry Shachnev <mitya57@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. 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.
|
||||
3. Neither the name of the University 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 REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -1,4 +1,4 @@
|
||||
ansible-runner==1.3.3
|
||||
ansible-runner==1.3.4
|
||||
appdirs==1.4.2
|
||||
asgi-amqp==1.1.3
|
||||
asgiref==1.1.2
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#
|
||||
adal==1.2.1 # via msrestazure
|
||||
amqp==2.3.2 # via kombu
|
||||
ansible-runner==1.3.3
|
||||
ansible-runner==1.3.4
|
||||
appdirs==1.4.2
|
||||
argparse==1.4.0 # via uwsgitop
|
||||
asgi-amqp==1.1.3
|
||||
|
||||
@ -1,32 +1,40 @@
|
||||
# Azure
|
||||
# azure deps from https://github.com/ansible/ansible/blob/stable-2.7/packaging/requirements/requirements-azure.txt
|
||||
# azure deps from https://github.com/ansible/ansible/blob/stable-2.8/packaging/requirements/requirements-azure.txt
|
||||
packaging
|
||||
azure-cli-core==2.0.35
|
||||
azure-cli-nspkg==3.0.2
|
||||
azure-common==1.1.11
|
||||
azure-mgmt-batch==4.1.0
|
||||
azure-mgmt-compute==2.1.0
|
||||
azure-mgmt-containerinstance==0.4.0
|
||||
azure-mgmt-authorization==0.51.1
|
||||
azure-mgmt-batch==5.0.1
|
||||
azure-mgmt-cdn==3.0.0
|
||||
azure-mgmt-compute==4.4.0
|
||||
azure-mgmt-containerinstance==1.4.0
|
||||
azure-mgmt-containerregistry==2.0.0
|
||||
azure-mgmt-containerservice==3.0.1
|
||||
azure-mgmt-dns==1.2.0
|
||||
azure-mgmt-keyvault==0.40.0
|
||||
azure-mgmt-containerservice==4.4.0
|
||||
azure-mgmt-dns==2.1.0
|
||||
azure-mgmt-keyvault==1.1.0
|
||||
azure-mgmt-marketplaceordering==0.1.0
|
||||
azure-mgmt-monitor==0.5.2
|
||||
azure-mgmt-network==1.7.1
|
||||
azure-mgmt-network==2.3.0
|
||||
azure-mgmt-nspkg==2.0.0
|
||||
azure-mgmt-rdbms==1.2.0
|
||||
azure-mgmt-resource==1.2.2
|
||||
azure-mgmt-sql==0.7.1
|
||||
azure-mgmt-storage==1.5.0
|
||||
azure-mgmt-redis==5.0.0
|
||||
azure-mgmt-resource==2.1.0
|
||||
azure-mgmt-rdbms==1.4.1
|
||||
azure-mgmt-servicebus==0.5.3
|
||||
azure-mgmt-sql==0.10.0
|
||||
azure-mgmt-storage==3.1.0
|
||||
azure-mgmt-trafficmanager==0.50.0
|
||||
azure-mgmt-web==0.32.0
|
||||
azure-mgmt-web==0.41.0
|
||||
azure-nspkg==2.0.0
|
||||
azure-storage==0.35.1
|
||||
msrest==0.4.29
|
||||
msrestazure==0.4.31
|
||||
msrest==0.6.1
|
||||
msrestazure==0.5.0
|
||||
azure-keyvault==1.0.0a1
|
||||
azure-graphrbac==0.40.0
|
||||
azure-mgmt-cosmosdb==0.5.2
|
||||
azure-mgmt-hdinsight==0.1.0
|
||||
azure-mgmt-devtestlabs==3.0.0
|
||||
azure-mgmt-loganalytics==0.2.0
|
||||
# AWS
|
||||
boto==2.47.0 # last which does not break ec2 scripts
|
||||
boto3==1.6.2
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
#
|
||||
# pip-compile --output-file requirements/requirements_ansible.txt requirements/requirements_ansible.in
|
||||
#
|
||||
adal==0.5.0 # via msrestazure
|
||||
appdirs==1.4.3 # via openstacksdk, os-client-config
|
||||
adal==1.2.1 # via msrestazure
|
||||
appdirs==1.4.3 # via openstacksdk
|
||||
applicationinsights==0.11.1 # via azure-cli-core
|
||||
argcomplete==1.9.4 # via azure-cli-core, knack
|
||||
asn1crypto==0.24.0 # via cryptography
|
||||
@ -14,23 +14,31 @@ azure-cli-nspkg==3.0.2
|
||||
azure-common==1.1.11
|
||||
azure-graphrbac==0.40.0
|
||||
azure-keyvault==1.0.0a1
|
||||
azure-mgmt-batch==4.1.0
|
||||
azure-mgmt-compute==2.1.0
|
||||
azure-mgmt-containerinstance==0.4.0
|
||||
azure-mgmt-authorization==0.51.1
|
||||
azure-mgmt-batch==5.0.1
|
||||
azure-mgmt-cdn==3.0.0
|
||||
azure-mgmt-compute==4.4.0
|
||||
azure-mgmt-containerinstance==1.4.0
|
||||
azure-mgmt-containerregistry==2.0.0
|
||||
azure-mgmt-containerservice==3.0.1
|
||||
azure-mgmt-dns==1.2.0
|
||||
azure-mgmt-keyvault==0.40.0
|
||||
azure-mgmt-containerservice==4.4.0
|
||||
azure-mgmt-cosmosdb==0.5.2
|
||||
azure-mgmt-devtestlabs==3.0.0
|
||||
azure-mgmt-dns==2.1.0
|
||||
azure-mgmt-hdinsight==0.1.0
|
||||
azure-mgmt-keyvault==1.1.0
|
||||
azure-mgmt-loganalytics==0.2.0
|
||||
azure-mgmt-marketplaceordering==0.1.0
|
||||
azure-mgmt-monitor==0.5.2
|
||||
azure-mgmt-network==1.7.1
|
||||
azure-mgmt-network==2.3.0
|
||||
azure-mgmt-nspkg==2.0.0
|
||||
azure-mgmt-rdbms==1.2.0
|
||||
azure-mgmt-resource==1.2.2
|
||||
azure-mgmt-sql==0.7.1
|
||||
azure-mgmt-storage==1.5.0
|
||||
azure-mgmt-rdbms==1.4.1
|
||||
azure-mgmt-redis==5.0.0
|
||||
azure-mgmt-resource==2.1.0
|
||||
azure-mgmt-servicebus==0.5.3
|
||||
azure-mgmt-sql==0.10.0
|
||||
azure-mgmt-storage==3.1.0
|
||||
azure-mgmt-trafficmanager==0.50.0
|
||||
azure-mgmt-web==0.32.0
|
||||
azure-mgmt-web==0.41.0
|
||||
azure-nspkg==2.0.0
|
||||
azure-storage==0.35.1
|
||||
backports.ssl-match-hostname==3.5.0.1
|
||||
@ -43,13 +51,11 @@ certifi==2018.1.18 # via msrest, requests
|
||||
cffi==1.11.5 # via bcrypt, cryptography, pynacl
|
||||
chardet==3.0.4 # via requests
|
||||
colorama==0.3.9 # via azure-cli-core, knack
|
||||
configparser==3.5.0 # via entrypoints
|
||||
cryptography==2.6.1 # via adal, azure-keyvault, azure-storage, paramiko, pyopenssl, requests-kerberos, requests-ntlm, secretstorage
|
||||
decorator==4.2.1 # via openstacksdk
|
||||
deprecation==2.0 # via openstacksdk
|
||||
docutils==0.14 # via botocore
|
||||
dogpile.cache==0.6.5 # via openstacksdk
|
||||
entrypoints==0.2.3 # via keyring
|
||||
enum34==1.1.6; python_version < '3' # via cryptography, knack, msrest, ovirt-engine-sdk-python
|
||||
futures==3.2.0; python_version < '3' # via openstacksdk, s3transfer
|
||||
google-auth==1.6.2
|
||||
@ -62,13 +68,12 @@ jinja2==2.10.1
|
||||
jmespath==0.9.3 # via azure-cli-core, boto3, botocore, knack, openstacksdk
|
||||
jsonpatch==1.21 # via openstacksdk
|
||||
jsonpointer==2.0 # via jsonpatch
|
||||
keyring==15.1.0 # via msrestazure
|
||||
keystoneauth1==3.11.2 # via openstacksdk, os-client-config
|
||||
knack==0.3.3 # via azure-cli-core
|
||||
lxml==4.1.1 # via ncclient, pyvmomi
|
||||
monotonic==1.4 # via humanfriendly
|
||||
msrest==0.4.29
|
||||
msrestazure==0.4.31
|
||||
msrest==0.6.1
|
||||
msrestazure==0.5.0
|
||||
munch==2.2.0 # via openstacksdk
|
||||
ncclient==0.6.3
|
||||
netaddr==0.7.19
|
||||
@ -106,9 +111,7 @@ requests==2.20.0
|
||||
requestsexceptions==1.4.0 # via openstacksdk, os-client-config
|
||||
rsa==4.0 # via google-auth
|
||||
s3transfer==0.1.13 # via boto3
|
||||
secretstorage==2.3.1 # via keyring
|
||||
selectors2==2.0.1 # via ncclient
|
||||
|
||||
six==1.11.0 # via azure-cli-core, bcrypt, cryptography, google-auth, isodate, keystoneauth1, knack, munch, ncclient, ntlm-auth, openstacksdk, ovirt-engine-sdk-python, packaging, pynacl, pyopenssl, python-dateutil, pyvmomi, pywinrm, stevedore
|
||||
stevedore==1.28.0 # via keystoneauth1
|
||||
tabulate==0.7.7 # via azure-cli-core, knack
|
||||
|
||||
@ -10,8 +10,6 @@ SOSREPORT_TOWER_COMMANDS = [
|
||||
"awx-manage list_instances", # tower cluster configuration
|
||||
"awx-manage run_dispatcher --status", # tower dispatch worker status
|
||||
"supervisorctl status", # tower process status
|
||||
"rabbitmqctl status",
|
||||
"rabbitmqctl cluster_status",
|
||||
"/var/lib/awx/venv/awx/bin/pip freeze", # pip package list
|
||||
"/var/lib/awx/venv/awx/bin/pip freeze -l", # pip package list without globally-installed packages
|
||||
"/var/lib/awx/venv/ansible/bin/pip freeze", # pip package list
|
||||
@ -30,7 +28,6 @@ SOSREPORT_TOWER_DIRS = [
|
||||
"/etc/nginx/",
|
||||
"/var/log/tower",
|
||||
"/var/log/nginx",
|
||||
"/var/log/rabbitmq",
|
||||
"/var/log/supervisor",
|
||||
"/var/log/syslog",
|
||||
"/var/log/udev",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user