Merge branch 'downstream' into devel

This commit is contained in:
Ryan Petrello 2019-04-26 08:02:04 -04:00
commit d11dfd0a2b
No known key found for this signature in database
GPG Key ID: F2AA5F2122351777
40 changed files with 1133 additions and 1185 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
e2ePipeline()

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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