mirror of
https://github.com/ansible/awx.git
synced 2024-10-30 22:21:13 +03:00
Merge pull request #3484 from ansible/insights-integration
Insights integration
This commit is contained in:
commit
96183cf9c4
@ -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):
|
||||
|
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
|
||||
}
|
||||
}
|
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>`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user